{server,storage}: add LoggedIn flag to AuthRequest and improve storage docs

Currently, whether or not a user has authenticated themselves through
a connector is indicated by a pointer being nil or non-nil. Instead
add an explicit flag that marks this.
This commit is contained in:
Eric Chiang 2016-09-14 16:38:12 -07:00 committed by Eric Chiang
parent 03ad99464f
commit 82a55cf785
4 changed files with 76 additions and 38 deletions

View file

@ -264,7 +264,8 @@ func (s *Server) finalizeLogin(identity connector.Identity, authReqID, connector
} }
updater := func(a storage.AuthRequest) (storage.AuthRequest, error) { updater := func(a storage.AuthRequest) (storage.AuthRequest, error) {
a.Claims = &claims a.LoggedIn = true
a.Claims = claims
a.ConnectorID = connectorID a.ConnectorID = connectorID
a.ConnectorData = identity.ConnectorData a.ConnectorData = identity.ConnectorData
return a, nil return a, nil
@ -282,7 +283,7 @@ func (s *Server) handleApproval(w http.ResponseWriter, r *http.Request) {
s.renderError(w, http.StatusInternalServerError, errServerError, "") s.renderError(w, http.StatusInternalServerError, errServerError, "")
return return
} }
if authReq.Claims == nil { if !authReq.LoggedIn {
log.Printf("Auth request does not have an identity for approval") log.Printf("Auth request does not have an identity for approval")
s.renderError(w, http.StatusInternalServerError, errServerError, "") s.renderError(w, http.StatusInternalServerError, errServerError, "")
return return
@ -341,7 +342,7 @@ func (s *Server) sendCodeResponse(w http.ResponseWriter, r *http.Request, authRe
ConnectorID: authReq.ConnectorID, ConnectorID: authReq.ConnectorID,
Nonce: authReq.Nonce, Nonce: authReq.Nonce,
Scopes: authReq.Scopes, Scopes: authReq.Scopes,
Claims: *authReq.Claims, Claims: authReq.Claims,
Expiry: s.now().Add(time.Minute * 5), Expiry: s.now().Add(time.Minute * 5),
RedirectURI: authReq.RedirectURI, RedirectURI: authReq.RedirectURI,
} }
@ -358,7 +359,7 @@ func (s *Server) sendCodeResponse(w http.ResponseWriter, r *http.Request, authRe
} }
q.Set("code", code.ID) q.Set("code", code.ID)
case responseTypeToken: case responseTypeToken:
idToken, expiry, err := s.newIDToken(authReq.ClientID, *authReq.Claims, authReq.Scopes, authReq.Nonce) idToken, expiry, err := s.newIDToken(authReq.ClientID, authReq.Claims, authReq.Scopes, authReq.Nonce)
if err != nil { if err != nil {
log.Printf("failed to create ID token: %v", err) log.Printf("failed to create ID token: %v", err)
tokenErr(w, errServerError, "", http.StatusInternalServerError) tokenErr(w, errServerError, "", http.StatusInternalServerError)

View file

@ -118,9 +118,11 @@ type AuthRequest struct {
// attempts. // attempts.
ForceApprovalPrompt bool `json:"forceApprovalPrompt,omitempty"` ForceApprovalPrompt bool `json:"forceApprovalPrompt,omitempty"`
LoggedIn bool `json:"loggedIn"`
// The identity of the end user. Generally nil until the user authenticates // The identity of the end user. Generally nil until the user authenticates
// with a backend. // with a backend.
Claims *Claims `json:"claims,omitempty"` Claims Claims `json:"claims,omitempty"`
// The connector used to login the user. Set when the user authenticates. // The connector used to login the user. Set when the user authenticates.
ConnectorID string `json:"connectorID,omitempty"` ConnectorID string `json:"connectorID,omitempty"`
ConnectorData []byte `json:"connectorData,omitempty"` ConnectorData []byte `json:"connectorData,omitempty"`
@ -145,13 +147,11 @@ func toStorageAuthRequest(req AuthRequest) storage.AuthRequest {
Nonce: req.Nonce, Nonce: req.Nonce,
State: req.State, State: req.State,
ForceApprovalPrompt: req.ForceApprovalPrompt, ForceApprovalPrompt: req.ForceApprovalPrompt,
LoggedIn: req.LoggedIn,
ConnectorID: req.ConnectorID, ConnectorID: req.ConnectorID,
ConnectorData: req.ConnectorData, ConnectorData: req.ConnectorData,
Expiry: req.Expiry, Expiry: req.Expiry,
} Claims: toStorageClaims(req.Claims),
if req.Claims != nil {
i := toStorageClaims(*req.Claims)
a.Claims = &i
} }
return a return a
} }
@ -172,14 +172,12 @@ func (cli *client) fromStorageAuthRequest(a storage.AuthRequest) AuthRequest {
RedirectURI: a.RedirectURI, RedirectURI: a.RedirectURI,
Nonce: a.Nonce, Nonce: a.Nonce,
State: a.State, State: a.State,
LoggedIn: a.LoggedIn,
ForceApprovalPrompt: a.ForceApprovalPrompt, ForceApprovalPrompt: a.ForceApprovalPrompt,
ConnectorID: a.ConnectorID, ConnectorID: a.ConnectorID,
ConnectorData: a.ConnectorData, ConnectorData: a.ConnectorData,
Expiry: a.Expiry, Expiry: a.Expiry,
} Claims: fromStorageClaims(a.Claims),
if a.Claims != nil {
i := fromStorageClaims(*a.Claims)
req.Claims = &i
} }
return req return req
} }

View file

@ -70,28 +70,41 @@ type Storage interface {
DeleteRefresh(id string) error DeleteRefresh(id string) error
// Update functions are assumed to be a performed within a single object transaction. // Update functions are assumed to be a performed within a single object transaction.
//
// updaters may be called multiple times.
UpdateClient(id string, updater func(old Client) (Client, error)) error UpdateClient(id string, updater func(old Client) (Client, error)) error
UpdateKeys(updater func(old Keys) (Keys, error)) error UpdateKeys(updater func(old Keys) (Keys, error)) error
UpdateAuthRequest(id string, updater func(a AuthRequest) (AuthRequest, error)) error UpdateAuthRequest(id string, updater func(a AuthRequest) (AuthRequest, error)) error
// TODO(ericchiang): Add a GarbageCollect(now time.Time) method so conformance tests
// can test implementations.
} }
// Client is an OAuth2 client. // Client represents an OAuth2 client.
// //
// For further reading see: // For further reading see:
// * Trusted peers: https://developers.google.com/identity/protocols/CrossClientAuth // * Trusted peers: https://developers.google.com/identity/protocols/CrossClientAuth
// * Public clients: https://developers.google.com/api-client-library/python/auth/installed-app // * Public clients: https://developers.google.com/api-client-library/python/auth/installed-app
type Client struct { type Client struct {
ID string `json:"id" yaml:"id"` // Client ID and secret used to identify the client.
Secret string `json:"secret" yaml:"secret"` ID string `json:"id" yaml:"id"`
Secret string `json:"secret" yaml:"secret"`
// A registered set of redirect URIs. When redirecting from dex to the client, the URI
// requested to redirect to MUST match one of these values, unless the client is "public".
RedirectURIs []string `json:"redirectURIs" yaml:"redirectURIs"` RedirectURIs []string `json:"redirectURIs" yaml:"redirectURIs"`
// TrustedPeers are a list of peers which can issue tokens on this client's behalf. // TrustedPeers are a list of peers which can issue tokens on this client's behalf using
// the dynamic "oauth2:server:client_id:(client_id)" scope. If a peer makes such a request,
// this client's ID will appear as the ID Token's audience.
//
// Clients inherently trust themselves. // Clients inherently trust themselves.
TrustedPeers []string `json:"trustedPeers" yaml:"trustedPeers"` TrustedPeers []string `json:"trustedPeers" yaml:"trustedPeers"`
// Public clients must use either use a redirectURL 127.0.0.1:X or "urn:ietf:wg:oauth:2.0:oob" // Public clients must use either use a redirectURL 127.0.0.1:X or "urn:ietf:wg:oauth:2.0:oob"
Public bool `json:"public" yaml:"public"` Public bool `json:"public" yaml:"public"`
// Name and LogoURL used when displaying this client to the end user.
Name string `json:"name" yaml:"name"` Name string `json:"name" yaml:"name"`
LogoURL string `json:"logoURL" yaml:"logoURL"` LogoURL string `json:"logoURL" yaml:"logoURL"`
} }
@ -109,53 +122,79 @@ type Claims struct {
// AuthRequest represents a OAuth2 client authorization request. It holds the state // AuthRequest represents a OAuth2 client authorization request. It holds the state
// of a single auth flow up to the point that the user authorizes the client. // of a single auth flow up to the point that the user authorizes the client.
type AuthRequest struct { type AuthRequest struct {
ID string // ID used to identify the authorization request.
ID string
// ID of the client requesting authorization from a user.
ClientID string ClientID string
// Values parsed from the initial request. These describe the resources the client is
// requesting as well as values describing the form of the response.
ResponseTypes []string ResponseTypes []string
Scopes []string Scopes []string
RedirectURI string RedirectURI string
Nonce string
Nonce string State string
State string
// The client has indicated that the end user must be shown an approval prompt // The client has indicated that the end user must be shown an approval prompt
// on all requests. The server cannot cache their initial action for subsequent // on all requests. The server cannot cache their initial action for subsequent
// attempts. // attempts.
ForceApprovalPrompt bool ForceApprovalPrompt bool
Expiry time.Time
// Has the user proved their identity through a backing identity provider?
//
// If false, the following fields are invalid.
LoggedIn bool
// The identity of the end user. Generally nil until the user authenticates // The identity of the end user. Generally nil until the user authenticates
// with a backend. // with a backend.
Claims *Claims Claims Claims
// The connector used to login the user and any data the connector wishes to persists. // The connector used to login the user and any data the connector wishes to persists.
// Set when the user authenticates. // Set when the user authenticates.
ConnectorID string ConnectorID string
ConnectorData []byte ConnectorData []byte
Expiry time.Time
} }
// AuthCode represents a code which can be exchanged for an OAuth2 token response. // AuthCode represents a code which can be exchanged for an OAuth2 token response.
//
// This value is created once an end user has authorized a client, the server has
// redirect the end user back to the client, but the client hasn't exchanged the
// code for an access_token and id_token.
type AuthCode struct { type AuthCode struct {
// Actual string returned as the "code" value.
ID string ID string
ClientID string // The client this code value is valid for. When exchanging the code for a
// token response, the client must use its client_secret to authenticate.
ClientID string
// As part of the OAuth2 spec when a client makes a token request it MUST
// present the same redirect_uri as the initial redirect. This values is saved
// to make this check.
//
// https://tools.ietf.org/html/rfc6749#section-4.1.3
RedirectURI string RedirectURI string
ConnectorID string // If provided by the client in the initial request, the provider MUST create
ConnectorData []byte // a ID Token with this nonce in the JWT payload.
Nonce string Nonce string
// Scopes authorized by the end user for the client.
Scopes []string Scopes []string
Claims Claims // Authentication data provided by an upstream source.
ConnectorID string
ConnectorData []byte
Claims Claims
Expiry time.Time Expiry time.Time
} }
// RefreshToken is an OAuth2 refresh token. // RefreshToken is an OAuth2 refresh token which allows a client to request new
// tokens on the end user's behalf.
type RefreshToken struct { type RefreshToken struct {
// The actual refresh token. // The actual refresh token.
RefreshToken string RefreshToken string
@ -163,17 +202,19 @@ type RefreshToken struct {
// Client this refresh token is valid for. // Client this refresh token is valid for.
ClientID string ClientID string
// Authentication data provided by an upstream source.
ConnectorID string ConnectorID string
ConnectorData []byte ConnectorData []byte
Claims Claims
// Scopes present in the initial request. Refresh requests may specify a set // Scopes present in the initial request. Refresh requests may specify a set
// of scopes different from the initial request when refreshing a token, // of scopes different from the initial request when refreshing a token,
// however those scopes must be encompassed by this set. // however those scopes must be encompassed by this set.
Scopes []string Scopes []string
// Nonce value supplied during the initial redirect. This is required to be part
// of the claims of any future id_token generated by the client.
Nonce string Nonce string
Claims Claims
} }
// VerificationKey is a rotated signing key which can still be used to verify // VerificationKey is a rotated signing key which can still be used to verify
@ -188,6 +229,7 @@ type Keys struct {
// Key for creating and verifying signatures. These may be nil. // Key for creating and verifying signatures. These may be nil.
SigningKey *jose.JSONWebKey SigningKey *jose.JSONWebKey
SigningKeyPub *jose.JSONWebKey SigningKeyPub *jose.JSONWebKey
// Old signing keys which have been rotated but can still be used to validate // Old signing keys which have been rotated but can still be used to validate
// existing signatures. // existing signatures.
VerificationKeys []VerificationKey VerificationKeys []VerificationKey

View file

@ -35,7 +35,7 @@ func testUpdateAuthRequest(t *testing.T, s storage.Storage) {
t.Fatalf("failed creating auth request: %v", err) t.Fatalf("failed creating auth request: %v", err)
} }
if err := s.UpdateAuthRequest(a.ID, func(old storage.AuthRequest) (storage.AuthRequest, error) { if err := s.UpdateAuthRequest(a.ID, func(old storage.AuthRequest) (storage.AuthRequest, error) {
old.Claims = &identity old.Claims = identity
old.ConnectorID = "connID" old.ConnectorID = "connID"
return old, nil return old, nil
}); err != nil { }); err != nil {
@ -46,11 +46,8 @@ func testUpdateAuthRequest(t *testing.T, s storage.Storage) {
if err != nil { if err != nil {
t.Fatalf("failed to get auth req: %v", err) t.Fatalf("failed to get auth req: %v", err)
} }
if got.Claims == nil { if !reflect.DeepEqual(got.Claims, identity) {
t.Fatalf("no identity in auth request") t.Fatalf("update failed, wanted identity=%#v got %#v", identity, got.Claims)
}
if !reflect.DeepEqual(*got.Claims, identity) {
t.Fatalf("update failed, wanted identity=%#v got %#v", identity, *got.Claims)
} }
} }