diff --git a/handlers.go b/handlers.go index 2afa8d3..e5e18e3 100644 --- a/handlers.go +++ b/handlers.go @@ -287,7 +287,7 @@ func (m *MockOIDC) Userinfo(rw http.ResponseWriter, req *http.Request) { return } - resp, err := session.User.userinfo(session.Scopes) + resp, err := session.User.Userinfo(session.Scopes) if err != nil { internalServerError(rw, err.Error()) return diff --git a/queue.go b/queue.go index aa585ba..acb4166 100644 --- a/queue.go +++ b/queue.go @@ -6,7 +6,7 @@ import "sync" // call to the authorize endpoint type UserQueue struct { sync.Mutex - Queue []*User + Queue []User } // CodeQueue manages the queue of codes returned for each @@ -18,14 +18,14 @@ type CodeQueue struct { // Push adds a User to the Queue to be set in subsequent calls to the // `authorization_endpoint` -func (q *UserQueue) Push(user *User) { +func (q *UserQueue) Push(user User) { q.Lock() defer q.Unlock() q.Queue = append(q.Queue, user) } // Pop a User from the Queue. If empty, return `DefaultUser()` -func (q *UserQueue) Pop() *User { +func (q *UserQueue) Pop() User { q.Lock() defer q.Unlock() @@ -33,7 +33,7 @@ func (q *UserQueue) Pop() *User { return DefaultUser() } - var user *User + var user User user, q.Queue = q.Queue[0], q.Queue[1:] return user } diff --git a/session.go b/session.go index 32fc6a5..797782f 100644 --- a/session.go +++ b/session.go @@ -13,7 +13,7 @@ type Session struct { SessionID string Scopes []string OIDCNonce string - User *User + User User Granted bool } @@ -23,6 +23,13 @@ type SessionStore struct { CodeQueue *CodeQueue } +// IDTokenClaims are the mandatory claims any User.Claims implementation +// should use in their jwt.Claims building. +type IDTokenClaims struct { + Nonce string `json:"nonce,omitempty"` + *jwt.StandardClaims +} + // NewSessionStore initializes the SessionStore for this server func NewSessionStore() *SessionStore { return &SessionStore{ @@ -32,7 +39,7 @@ func NewSessionStore() *SessionStore { } // NewSession creates a new Session for a User -func (ss *SessionStore) NewSession(scope string, nonce string, user *User) (*Session, error) { +func (ss *SessionStore) NewSession(scope string, nonce string, user User) (*Session, error) { sessionID, err := ss.CodeQueue.Pop() if err != nil { return nil, err @@ -84,6 +91,21 @@ func (s *Session) RefreshToken(config *Config, kp *Keypair, now time.Time) (stri return kp.SignJWT(claims) } +// IDToken returns the JWT token with the appropriate claims for a user +// based on the scopes set. +func (s *Session) IDToken(config *Config, kp *Keypair, now time.Time) (string, error) { + base := &IDTokenClaims{ + StandardClaims: s.standardClaims(config, config.AccessTTL, now), + Nonce: s.OIDCNonce, + } + claims, err := s.User.Claims(s.Scopes, base) + if err != nil { + return "", err + } + + return kp.SignJWT(claims) +} + func (s *Session) standardClaims(config *Config, ttl time.Duration, now time.Time) *jwt.StandardClaims { return &jwt.StandardClaims{ Audience: config.ClientID, @@ -92,29 +114,6 @@ func (s *Session) standardClaims(config *Config, ttl time.Duration, now time.Tim IssuedAt: now.Unix(), Issuer: config.Issuer, NotBefore: now.Unix(), - Subject: s.User.ID, + Subject: s.User.ID(), } } - -type idTokenClaims struct { - PreferredUsername string `json:"preferred_username,omitempty"` - Email string `json:"email,omitempty"` - Phone string `json:"phone_number,omitempty"` - Address string `json:"address,omitempty"` - Groups []string `json:"groups,omitempty"` - EmailVerified bool `json:"email_verified,omitempty"` - Nonce string `json:"nonce,omitempty"` - jwt.StandardClaims -} - -// IDToken returns the JWT token with the appropriate claims for a user -// based on the scopes set. -func (s *Session) IDToken(config *Config, kp *Keypair, now time.Time) (string, error) { - idClaims := &idTokenClaims{ - Nonce: s.OIDCNonce, - StandardClaims: *s.standardClaims(config, config.AccessTTL, now), - } - s.User.populateClaims(s.Scopes, idClaims) - - return kp.SignJWT(idClaims) -} diff --git a/session_test.go b/session_test.go index e9d230e..bb3718a 100644 --- a/session_test.go +++ b/session_test.go @@ -62,7 +62,7 @@ func TestSession_AccessToken(t *testing.T) { assert.Equal(t, dummySession.SessionID, claims["jti"]) assert.Equal(t, dummyConfig.ClientID, claims["aud"]) assert.Equal(t, dummyConfig.Issuer, claims["iss"]) - assert.Equal(t, dummySession.User.ID, claims["sub"]) + assert.Equal(t, dummySession.User.ID(), claims["sub"]) } func TestSession_RefreshToken(t *testing.T) { @@ -81,7 +81,7 @@ func TestSession_RefreshToken(t *testing.T) { assert.Equal(t, dummySession.SessionID, claims["jti"]) assert.Equal(t, dummyConfig.ClientID, claims["aud"]) assert.Equal(t, dummyConfig.Issuer, claims["iss"]) - assert.Equal(t, dummySession.User.ID, claims["sub"]) + assert.Equal(t, dummySession.User.ID(), claims["sub"]) } func TestSession_IDToken(t *testing.T) { @@ -100,9 +100,9 @@ func TestSession_IDToken(t *testing.T) { assert.Equal(t, dummySession.SessionID, claims["jti"]) assert.Equal(t, dummyConfig.ClientID, claims["aud"]) assert.Equal(t, dummyConfig.Issuer, claims["iss"]) - assert.Equal(t, dummySession.User.ID, claims["sub"]) + assert.Equal(t, dummySession.User.ID(), claims["sub"]) - u := dummySession.User + u := dummySession.User.(*mockoidc.MockUser) assert.Equal(t, u.PreferredUsername, claims["preferred_username"]) assert.Equal(t, u.Address, claims["address"]) assert.Equal(t, u.Phone, claims["phone_number"]) @@ -123,8 +123,8 @@ func TestSessionStore_GetSessionByID(t *testing.T) { _, err := ss.NewSession(scope, oidcNonce, user) assert.NoError(t, err) - user2 := &mockoidc.User{ - ID: "DifferentUserId", + user2 := &mockoidc.MockUser{ + Subject: "DifferentUserId", Email: "different.user@example.com", Phone: "555-555-5555", PreferredUsername: "Jon Diff", @@ -155,8 +155,8 @@ func TestSessionStore_GetSessionFromToken(t *testing.T) { _, err := ss.NewSession(scope, oidcNonce, user) assert.NoError(t, err) - user2 := &mockoidc.User{ - ID: "DifferentUserId", + user2 := &mockoidc.MockUser{ + Subject: "DifferentUserId", Email: "different.user@example.com", Phone: "555-555-5555", PreferredUsername: "Jon Diff", diff --git a/user.go b/user.go index 6a08cbf..0e99e8e 100644 --- a/user.go +++ b/user.go @@ -1,12 +1,30 @@ package mockoidc -import "encoding/json" +import ( + "encoding/json" + + "github.com/dgrijalva/jwt-go" +) // User represents a mock user that the server will grant Oauth tokens for. // Calls to the `authorization_endpoint` will pop any mock Users added to the // `UserQueue`. Otherwise `DefaultUser()` is returned. -type User struct { - ID string +type User interface { + // Unique ID for the User. This will be the Subject claim + ID() string + + // Userinfo returns the Userinfo JSON representation of a User with data + // appropriate for the passed scope []string. + Userinfo([]string) ([]byte, error) + + // Claims returns the ID Token Claims for a User with data appropriate for + // the passed scope []string. It builds off the passed BaseIDTokenClaims. + Claims([]string, *IDTokenClaims) (jwt.Claims, error) +} + +// MockUser is a default implementation of the User interface +type MockUser struct { + Subject string Email string EmailVerified bool PreferredUsername string @@ -15,11 +33,11 @@ type User struct { Groups []string } -// DefaultUser returns a default User that is set in `authorization_endpoint` -// if the UserQueue is empty. -func DefaultUser() *User { - return &User{ - ID: "1234567890", +// DefaultUser returns a default MockUser that is set in +// `authorization_endpoint` if the UserQueue is empty. +func DefaultUser() *MockUser { + return &MockUser{ + Subject: "1234567890", Email: "jane.doe@example.com", PreferredUsername: "jane.doe", Phone: "555-987-6543", @@ -29,7 +47,7 @@ func DefaultUser() *User { } } -type userinfo struct { +type mockUserinfo struct { Email string `json:"email,omitempty"` PreferredUsername string `json:"preferred_username,omitempty"` Phone string `json:"phone,omitempty"` @@ -37,10 +55,14 @@ type userinfo struct { Groups []string `json:"groups,omitempty"` } -func (u *User) userinfo(scopes []string) ([]byte, error) { - user := u.scopedClone(scopes) +func (u *MockUser) ID() string { + return u.Subject +} + +func (u *MockUser) Userinfo(scope []string) ([]byte, error) { + user := u.scopedClone(scope) - ui := &userinfo{ + info := &mockUserinfo{ Email: user.Email, PreferredUsername: user.PreferredUsername, Phone: user.Phone, @@ -48,23 +70,36 @@ func (u *User) userinfo(scopes []string) ([]byte, error) { Groups: user.Groups, } - return json.Marshal(ui) + return json.Marshal(info) +} + +type mockClaims struct { + *IDTokenClaims + Email string `json:"email,omitempty"` + EmailVerified bool `json:"email_verified,omitempty"` + PreferredUsername string `json:"preferred_username,omitempty"` + Phone string `json:"phone_number,omitempty"` + Address string `json:"address,omitempty"` + Groups []string `json:"groups,omitempty"` } -func (u *User) populateClaims(scopes []string, claims *idTokenClaims) { - user := u.scopedClone(scopes) +func (u *MockUser) Claims(scope []string, claims *IDTokenClaims) (jwt.Claims, error) { + user := u.scopedClone(scope) - claims.PreferredUsername = user.PreferredUsername - claims.Address = user.Address - claims.Phone = user.Phone - claims.Email = user.Email - claims.EmailVerified = user.EmailVerified - claims.Groups = user.Groups + return &mockClaims{ + IDTokenClaims: claims, + Email: user.Email, + EmailVerified: user.EmailVerified, + PreferredUsername: user.PreferredUsername, + Phone: user.Phone, + Address: user.Address, + Groups: user.Groups, + }, nil } -func (u *User) scopedClone(scopes []string) *User { - clone := &User{ - ID: u.ID, +func (u *MockUser) scopedClone(scopes []string) *MockUser { + clone := &MockUser{ + Subject: u.Subject, } for _, scope := range scopes { switch scope {