Skip to content

Commit

Permalink
Make User an interface
Browse files Browse the repository at this point in the history
  • Loading branch information
Nick Meves committed Feb 6, 2021
1 parent 1c31e27 commit e9aac79
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 63 deletions.
2 changes: 1 addition & 1 deletion handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions queue.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -18,22 +18,22 @@ 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()

if len(q.Queue) == 0 {
return DefaultUser()
}

var user *User
var user User
user, q.Queue = q.Queue[0], q.Queue[1:]
return user
}
Expand Down
51 changes: 25 additions & 26 deletions session.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ type Session struct {
SessionID string
Scopes []string
OIDCNonce string
User *User
User User
Granted bool
}

Expand All @@ -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{
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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)
}
16 changes: 8 additions & 8 deletions session_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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) {
Expand All @@ -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"])
Expand All @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
83 changes: 59 additions & 24 deletions user.go
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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",
Expand All @@ -29,42 +47,59 @@ 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"`
Address string `json:"address,omitempty"`
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,
Address: user.Address,
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 {
Expand Down

0 comments on commit e9aac79

Please sign in to comment.