user api: accept bearer tokens with multiple audiences

This commit is contained in:
Eric Chiang 2016-08-01 13:57:58 -07:00
parent 1e0ee1e435
commit 8669167b42
3 changed files with 148 additions and 41 deletions

View file

@ -2,7 +2,6 @@ package server
import ( import (
"encoding/json" "encoding/json"
"errors"
"net/http" "net/http"
"net/url" "net/url"
"strconv" "strconv"
@ -262,18 +261,32 @@ func (s *UserMgmtServer) getCreds(r *http.Request, requiresAdmin bool) (api.Cred
return api.Creds{}, api.ErrorUnauthorized return api.Creds{}, api.ErrorUnauthorized
} }
clientID, ok, err := claims.StringClaim("aud") // The "aud" claim is allowed to be both a list of clients or a single client. Check for both cases.
if err != nil { clientIDs, ok, err := claims.StringsClaim("aud")
log.Errorf("userMgmtServer: GetCreds err: %q", err) if err != nil || !ok {
return api.Creds{}, err clientID, ok, err := claims.StringClaim("aud")
if err != nil {
log.Errorf("userMgmtServer: GetCreds failed to parse 'aud' claim: %q", err)
return api.Creds{}, api.ErrorUnauthorized
}
if !ok || clientID == "" {
return api.Creds{}, api.ErrorUnauthorized
}
clientIDs = []string{clientID}
} }
if !ok || clientID == "" { if len(clientIDs) == 0 {
return api.Creds{}, errors.New("no aud(client ID) claim") log.Errorf("userMgmtServer: GetCreds err: no client in audience")
return api.Creds{}, api.ErrorUnauthorized
} }
verifier := s.jwtvFactory(clientID) // Verify that the JWT is signed by this server, has the correct issuer, hasn't expired, etc.
// While we don't actualy care which client the token was issued for (we'll check that later),
// go-oidc doesn't provide any methods which don't require passing a client ID.
//
// TODO(ericchiang): Add a verifier to go-oidc that doesn't require a client ID.
verifier := s.jwtvFactory(clientIDs[0])
if err := verifier.Verify(jwt); err != nil { if err := verifier.Verify(jwt); err != nil {
log.Errorf("userMgmtServer: GetCreds err: %q", err) log.Errorf("userMgmtServer: GetCreds err: failed to verify token %q", err)
return api.Creds{}, api.ErrorUnauthorized return api.Creds{}, api.ErrorUnauthorized
} }
@ -295,18 +308,32 @@ func (s *UserMgmtServer) getCreds(r *http.Request, requiresAdmin bool) (api.Cred
return api.Creds{}, err return api.Creds{}, err
} }
isAdmin, err := s.cm.IsDexAdmin(clientID) i := 0
if err != nil { for _, clientID := range clientIDs {
log.Errorf("userMgmtServer: GetCreds err: %q", err) // Make sure the client actually exists.
return api.Creds{}, err isAdmin, err := s.cm.IsDexAdmin(clientID)
if err != nil {
log.Errorf("userMgmtServer: GetCreds err: failed to get client %v", err)
return api.Creds{}, err
}
// If the endpoint requires an admin client, filter out clients which are not admins.
if requiresAdmin && !isAdmin {
continue
}
clientIDs[i] = clientID
i++
} }
if requiresAdmin && !isAdmin {
clientIDs = clientIDs[:i]
if len(clientIDs) == 0 {
return api.Creds{}, api.ErrorForbidden return api.Creds{}, api.ErrorForbidden
} }
return api.Creds{ return api.Creds{
ClientID: clientID, ClientIDs: clientIDs,
User: usr, User: usr,
}, nil }, nil
} }

View file

@ -98,8 +98,9 @@ type Emailer interface {
} }
type Creds struct { type Creds struct {
ClientID string // IDTokens can be issued for multiple clients.
User user.User ClientIDs []string
User user.User
} }
// TODO(ericchiang): Don't pass a dbMap. See #385. // TODO(ericchiang): Don't pass a dbMap. See #385.
@ -144,6 +145,22 @@ func (u *UsersAPI) DisableUser(creds Creds, userID string, disable bool) (schema
}, nil }, nil
} }
// validRedirectURL finds the first client for which the redirect URL is valid. If found it returns the client_id of the client.
func validRedirectURL(clientManager *clientmanager.ClientManager, redirectURL url.URL, clientIDs []string) (string, error) {
// Find the first client with a valid redirectURL.
for _, clientID := range clientIDs {
metadata, err := clientManager.Metadata(clientID)
if err != nil {
return "", mapError(err)
}
if _, err := client.ValidRedirectURL(&redirectURL, metadata.RedirectURIs); err == nil {
return clientID, nil
}
}
return "", ErrorInvalidRedirectURL
}
func (u *UsersAPI) CreateUser(creds Creds, usr schema.User, redirURL url.URL) (schema.UserCreateResponse, error) { func (u *UsersAPI) CreateUser(creds Creds, usr schema.User, redirURL url.URL) (schema.UserCreateResponse, error) {
log.Infof("userAPI: CreateUser") log.Infof("userAPI: CreateUser")
if !u.Authorize(creds) { if !u.Authorize(creds) {
@ -155,14 +172,9 @@ func (u *UsersAPI) CreateUser(creds Creds, usr schema.User, redirURL url.URL) (s
return schema.UserCreateResponse{}, mapError(err) return schema.UserCreateResponse{}, mapError(err)
} }
metadata, err := u.clientManager.Metadata(creds.ClientID) clientID, err := validRedirectURL(u.clientManager, redirURL, creds.ClientIDs)
if err != nil { if err != nil {
return schema.UserCreateResponse{}, mapError(err) return schema.UserCreateResponse{}, err
}
validRedirURL, err := client.ValidRedirectURL(&redirURL, metadata.RedirectURIs)
if err != nil {
return schema.UserCreateResponse{}, ErrorInvalidRedirectURL
} }
id, err := u.userManager.CreateUser(schemaUserToUser(usr), user.Password(hash), u.localConnectorID) id, err := u.userManager.CreateUser(schemaUserToUser(usr), user.Password(hash), u.localConnectorID)
@ -177,7 +189,7 @@ func (u *UsersAPI) CreateUser(creds Creds, usr schema.User, redirURL url.URL) (s
usr = userToSchemaUser(userUser) usr = userToSchemaUser(userUser)
url, err := u.emailer.SendInviteEmail(usr.Email, validRedirURL, creds.ClientID) url, err := u.emailer.SendInviteEmail(usr.Email, redirURL, clientID)
// An email is sent only if we don't get a link and there's no error. // An email is sent only if we don't get a link and there's no error.
emailSent := err == nil && url == nil emailSent := err == nil && url == nil
@ -200,14 +212,9 @@ func (u *UsersAPI) ResendEmailInvitation(creds Creds, userID string, redirURL ur
return schema.ResendEmailInvitationResponse{}, ErrorUnauthorized return schema.ResendEmailInvitationResponse{}, ErrorUnauthorized
} }
metadata, err := u.clientManager.Metadata(creds.ClientID) clientID, err := validRedirectURL(u.clientManager, redirURL, creds.ClientIDs)
if err != nil { if err != nil {
return schema.ResendEmailInvitationResponse{}, mapError(err) return schema.ResendEmailInvitationResponse{}, err
}
validRedirURL, err := client.ValidRedirectURL(&redirURL, metadata.RedirectURIs)
if err != nil {
return schema.ResendEmailInvitationResponse{}, ErrorInvalidRedirectURL
} }
// Retrieve user to check if it's already created // Retrieve user to check if it's already created
@ -221,7 +228,7 @@ func (u *UsersAPI) ResendEmailInvitation(creds Creds, userID string, redirURL ur
return schema.ResendEmailInvitationResponse{}, ErrorVerifiedEmail return schema.ResendEmailInvitationResponse{}, ErrorVerifiedEmail
} }
url, err := u.emailer.SendInviteEmail(userUser.Email, validRedirURL, creds.ClientID) url, err := u.emailer.SendInviteEmail(userUser.Email, redirURL, clientID)
// An email is sent only if we don't get a link and there's no error. // An email is sent only if we don't get a link and there's no error.
emailSent := err == nil && url == nil emailSent := err == nil && url == nil

View file

@ -51,15 +51,16 @@ func (t *testEmailer) sendEmail(email string, redirectURL url.URL, clientID stri
} }
var ( var (
clock = clockwork.NewFakeClock() clock = clockwork.NewFakeClock()
goodClientID = "client.example.com" goodClientID = "client.example.com"
nonAdminClientID = "user.example.com"
goodCreds = Creds{ goodCreds = Creds{
User: user.User{ User: user.User{
ID: "ID-1", ID: "ID-1",
Admin: true, Admin: true,
}, },
ClientID: goodClientID, ClientIDs: []string{goodClientID},
} }
badCreds = Creds{ badCreds = Creds{
@ -68,13 +69,21 @@ var (
}, },
} }
credsWithMultipleAudiences = Creds{
User: user.User{
ID: "ID-1",
Admin: true,
},
ClientIDs: []string{nonAdminClientID, goodClientID},
}
disabledCreds = Creds{ disabledCreds = Creds{
User: user.User{ User: user.User{
ID: "ID-1", ID: "ID-1",
Admin: true, Admin: true,
Disabled: true, Disabled: true,
}, },
ClientID: goodClientID, ClientIDs: []string{goodClientID},
} }
resetPasswordURL = url.URL{ resetPasswordURL = url.URL{
@ -87,6 +96,11 @@ var (
Host: goodClientID, Host: goodClientID,
Path: "/callback", Path: "/callback",
} }
validRedirURL2 = url.URL{
Scheme: "http",
Host: nonAdminClientID,
Path: "/callback",
}
) )
func makeTestFixtures() (*UsersAPI, *testEmailer) { func makeTestFixtures() (*UsersAPI, *testEmailer) {
@ -169,6 +183,17 @@ func makeTestFixtures() (*UsersAPI, *testEmailer) {
}, },
}, },
} }
ci2 := client.Client{
Credentials: oidc.ClientCredentials{
ID: nonAdminClientID,
Secret: base64.URLEncoding.EncodeToString([]byte("anothersecret")),
},
Metadata: oidc.ClientMetadata{
RedirectURIs: []url.URL{
validRedirURL2,
},
},
}
clientIDGenerator := func(hostport string) (string, error) { clientIDGenerator := func(hostport string) (string, error) {
return hostport, nil return hostport, nil
@ -176,7 +201,7 @@ func makeTestFixtures() (*UsersAPI, *testEmailer) {
secGen := func() ([]byte, error) { secGen := func() ([]byte, error) {
return []byte("secret"), nil return []byte("secret"), nil
} }
clientRepo, err := db.NewClientRepoFromClients(dbMap, []client.LoadableClient{{Client: ci}}) clientRepo, err := db.NewClientRepoFromClients(dbMap, []client.LoadableClient{{Client: ci}, {Client: ci2}})
if err != nil { if err != nil {
panic("Failed to create client manager: " + err.Error()) panic("Failed to create client manager: " + err.Error())
} }
@ -223,6 +248,10 @@ func TestGetUser(t *testing.T) {
id: "NO_ID", id: "NO_ID",
wantErr: ErrorResourceNotFound, wantErr: ErrorResourceNotFound,
}, },
{
creds: credsWithMultipleAudiences,
id: "ID-1",
},
} }
for i, tt := range tests { for i, tt := range tests {
@ -318,6 +347,7 @@ func TestCreateUser(t *testing.T) {
cantEmail bool cantEmail bool
wantResponse schema.UserCreateResponse wantResponse schema.UserCreateResponse
wantClientID string
wantErr error wantErr error
}{ }{
{ {
@ -340,6 +370,29 @@ func TestCreateUser(t *testing.T) {
CreatedAt: clock.Now().Format(time.RFC3339), CreatedAt: clock.Now().Format(time.RFC3339),
}, },
}, },
wantClientID: goodClientID,
},
{
creds: credsWithMultipleAudiences,
usr: schema.User{
Email: "newuser01@example.com",
DisplayName: "New User",
EmailVerified: true,
Admin: false,
},
redirURL: validRedirURL,
wantResponse: schema.UserCreateResponse{
EmailSent: true,
User: &schema.User{
Email: "newuser01@example.com",
DisplayName: "New User",
EmailVerified: true,
Admin: false,
CreatedAt: clock.Now().Format(time.RFC3339),
},
},
wantClientID: goodClientID,
}, },
{ {
creds: goodCreds, creds: goodCreds,
@ -362,6 +415,7 @@ func TestCreateUser(t *testing.T) {
}, },
ResetPasswordLink: resetPasswordURL.String(), ResetPasswordLink: resetPasswordURL.String(),
}, },
wantClientID: goodClientID,
}, },
{ {
creds: goodCreds, creds: goodCreds,
@ -397,6 +451,7 @@ func TestCreateUser(t *testing.T) {
if tt.wantErr != nil { if tt.wantErr != nil {
if err != tt.wantErr { if err != tt.wantErr {
t.Errorf("case %d: want=%q, got=%q", i, tt.wantErr, err) t.Errorf("case %d: want=%q, got=%q", i, tt.wantErr, err)
continue
} }
tok := "" tok := ""
@ -420,11 +475,13 @@ func TestCreateUser(t *testing.T) {
} }
if err != nil { if err != nil {
t.Errorf("case %d: want nil err, got: %q ", i, err) t.Errorf("case %d: want nil err, got: %q ", i, err)
continue
} }
newID := response.User.Id newID := response.User.Id
if newID == "" { if newID == "" {
t.Errorf("case %d: expected non-empty newID", i) t.Errorf("case %d: expected non-empty newID", i)
continue
} }
tt.wantResponse.User.Id = newID tt.wantResponse.User.Id = newID
@ -436,7 +493,7 @@ func TestCreateUser(t *testing.T) {
wantEmalier := testEmailer{ wantEmalier := testEmailer{
cantEmail: tt.cantEmail, cantEmail: tt.cantEmail,
lastEmail: tt.usr.Email, lastEmail: tt.usr.Email,
lastClientID: tt.creds.ClientID, lastClientID: tt.wantClientID,
lastRedirectURL: tt.redirURL, lastRedirectURL: tt.redirURL,
lastWasInvite: true, lastWasInvite: true,
} }
@ -497,6 +554,7 @@ func TestResendEmailInvitation(t *testing.T) {
wantResponse schema.ResendEmailInvitationResponse wantResponse schema.ResendEmailInvitationResponse
wantErr error wantErr error
wantClientID string
}{ }{
{ {
creds: goodCreds, creds: goodCreds,
@ -507,6 +565,7 @@ func TestResendEmailInvitation(t *testing.T) {
wantResponse: schema.ResendEmailInvitationResponse{ wantResponse: schema.ResendEmailInvitationResponse{
EmailSent: true, EmailSent: true,
}, },
wantClientID: goodClientID,
}, },
{ {
creds: goodCreds, creds: goodCreds,
@ -519,6 +578,20 @@ func TestResendEmailInvitation(t *testing.T) {
EmailSent: false, EmailSent: false,
ResetPasswordLink: resetPasswordURL.String(), ResetPasswordLink: resetPasswordURL.String(),
}, },
wantClientID: goodClientID,
},
{
creds: credsWithMultipleAudiences,
userID: "ID-1",
email: "id1@example.com",
redirURL: validRedirURL,
cantEmail: true,
wantResponse: schema.ResendEmailInvitationResponse{
EmailSent: false,
ResetPasswordLink: resetPasswordURL.String(),
},
wantClientID: goodClientID,
}, },
{ {
creds: badCreds, creds: badCreds,
@ -576,7 +649,7 @@ func TestResendEmailInvitation(t *testing.T) {
wantEmailer := testEmailer{ wantEmailer := testEmailer{
cantEmail: tt.cantEmail, cantEmail: tt.cantEmail,
lastEmail: tt.email, lastEmail: tt.email,
lastClientID: tt.creds.ClientID, lastClientID: tt.wantClientID,
lastRedirectURL: tt.redirURL, lastRedirectURL: tt.redirURL,
lastWasInvite: true, lastWasInvite: true,
} }