forked from mystiq/dex
{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:
parent
03ad99464f
commit
82a55cf785
4 changed files with 76 additions and 38 deletions
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue