diff --git a/example/server/internal/client.go b/example/server/internal/client.go index a5bb0e6..ff9d5a4 100644 --- a/example/server/internal/client.go +++ b/example/server/internal/client.go @@ -7,8 +7,125 @@ import ( "github.com/caos/oidc/pkg/op" ) -var clients = map[string]*Client{} +var ( + //we use the default login UI and pass the (auth request) id + defaultLoginURL = func(id string) string { + return "/login/username?authRequestID=" + id + } + //clients to be used by the storage interface + clients = map[string]*Client{} +) + +//Client represents the internal model of an OAuth/OIDC client +//this could also be your database model +type Client struct { + id string + secret string + redirectURIs []string + applicationType op.ApplicationType + authMethod oidc.AuthMethod + loginURL func(string) string + responseTypes []oidc.ResponseType + grantTypes []oidc.GrantType + accessTokenType op.AccessTokenType + devMode bool + idTokenUserinfoClaimsAssertion bool + clockSkew time.Duration +} + +//GetID must return the client_id +func (c *Client) GetID() string { + return c.id +} + +//RedirectURIs must return the registered redirect_uris for Code and Implicit Flow +func (c *Client) RedirectURIs() []string { + return c.redirectURIs +} + +//PostLogoutRedirectURIs must return the registered post_logout_redirect_uris for signouts +func (c *Client) PostLogoutRedirectURIs() []string { + return []string{} +} + +//ApplicationType must return the type of the client (app, native, user agent) +func (c *Client) ApplicationType() op.ApplicationType { + return c.applicationType +} + +//AuthMethod must return the authentication method (client_secret_basic, client_secret_post, none, private_key_jwt) +func (c *Client) AuthMethod() oidc.AuthMethod { + return c.authMethod +} + +//ResponseTypes must return all allowed response types (code, id_token token, id_token) +//these must match with the allowed grant types +func (c *Client) ResponseTypes() []oidc.ResponseType { + return c.responseTypes +} + +//GrantTypes must return all allowed grant types (authorization_code, refresh_token, urn:ietf:params:oauth:grant-type:jwt-bearer) +func (c *Client) GrantTypes() []oidc.GrantType { + return c.grantTypes +} + +//LoginURL will be called to redirect the user (agent) to the login UI +//you could implement some logic here to redirect the users to different login UIs depending on the client +func (c *Client) LoginURL(id string) string { + return c.loginURL(id) +} + +//AccessTokenType must return the type of access token the client uses (Bearer (opaque) or JWT) +func (c *Client) AccessTokenType() op.AccessTokenType { + return c.accessTokenType +} + +//IDTokenLifetime must return the lifetime of the client's id_tokens +func (c *Client) IDTokenLifetime() time.Duration { + return 1 * time.Hour +} + +//DevMode enables the use of non-compliant configs such as redirect_uris (e.g. http schema for user agent client) +func (c *Client) DevMode() bool { + return c.devMode +} + +//RestrictAdditionalIdTokenScopes allows specifying which custom scopes shall be asserted into the id_token +func (c *Client) RestrictAdditionalIdTokenScopes() func(scopes []string) []string { + return func(scopes []string) []string { + return scopes + } +} + +//RestrictAdditionalAccessTokenScopes allows specifying which custom scopes shall be asserted into the JWT access_token +func (c *Client) RestrictAdditionalAccessTokenScopes() func(scopes []string) []string { + return func(scopes []string) []string { + return scopes + } +} + +//IsScopeAllowed enables Client specific custom scopes validation +//in this example we allow the CustomScope for all clients +func (c *Client) IsScopeAllowed(scope string) bool { + return scope == CustomScope +} + +//IDTokenUserinfoClaimsAssertion allows specifying if claims of scope profile, email, phone and address are asserted into the id_token +//even if an access token if issued which violates the OIDC Core spec +//(5.4. Requesting Claims using Scope Values: https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims) +//some clients though require that e.g. email is always in the id_token when requested even if an access_token is issued +func (c *Client) IDTokenUserinfoClaimsAssertion() bool { + return c.idTokenUserinfoClaimsAssertion +} + +//ClockSkew enables clients to instruct the OP to apply a clock skew on the various times and expirations +//(subtract from issued_at, add to expiration, ...) +func (c *Client) ClockSkew() time.Duration { + return c.clockSkew +} + +//RegisterClients enables you to register clients for the example func RegisterClients(registerClients ...*Client) { for _, client := range registerClients { clients[client.id] = client @@ -33,7 +150,7 @@ func NativeClient(id string, redirectURIs ...string) *Client { redirectURIs: redirectURIs, applicationType: op.ApplicationTypeNative, authMethod: oidc.AuthMethodNone, - defaultLoginURL: defaultLoginURL, + loginURL: defaultLoginURL, responseTypes: []oidc.ResponseType{oidc.ResponseTypeCode}, grantTypes: []oidc.GrantType{oidc.GrantTypeCode, oidc.GrantTypeRefreshToken}, accessTokenType: 0, @@ -45,18 +162,21 @@ func NativeClient(id string, redirectURIs ...string) *Client { //WebClient will create a client of type web, which will always use PKCE and allow the use of refresh tokens //user-defined redirectURIs may include: -// - http://localhost without port specification (e.g. http://localhost/auth/callback) +// - http://localhost with port specification (e.g. http://localhost:9999/auth/callback) //(the example will be used as default, if none is provided) func WebClient(id, secret string, redirectURIs ...string) *Client { - return &Client{ - id: id, - secret: secret, - redirectURIs: []string{ + if len(redirectURIs) == 0 { + redirectURIs = []string{ "http://localhost:9999/auth/callback", - }, + } + } + return &Client{ + id: id, + secret: secret, + redirectURIs: redirectURIs, applicationType: op.ApplicationTypeWeb, authMethod: oidc.AuthMethodBasic, - defaultLoginURL: defaultLoginURL, + loginURL: defaultLoginURL, responseTypes: []oidc.ResponseType{oidc.ResponseTypeCode}, grantTypes: []oidc.GrantType{oidc.GrantTypeCode, oidc.GrantTypeRefreshToken}, accessTokenType: 0, @@ -65,88 +185,3 @@ func WebClient(id, secret string, redirectURIs ...string) *Client { clockSkew: 0, } } - -type Client struct { - id string - secret string - redirectURIs []string - applicationType op.ApplicationType - authMethod oidc.AuthMethod - defaultLoginURL func(string) string - responseTypes []oidc.ResponseType - grantTypes []oidc.GrantType - accessTokenType op.AccessTokenType - devMode bool - idTokenUserinfoClaimsAssertion bool - clockSkew time.Duration -} - -func (c *Client) GetID() string { - return c.id -} - -func (c *Client) RedirectURIs() []string { - return c.redirectURIs -} - -func (c *Client) PostLogoutRedirectURIs() []string { - return []string{} -} - -func (c *Client) ApplicationType() op.ApplicationType { - return c.applicationType -} - -func (c *Client) AuthMethod() oidc.AuthMethod { - return c.authMethod -} - -func (c *Client) ResponseTypes() []oidc.ResponseType { - return c.responseTypes -} - -func (c *Client) GrantTypes() []oidc.GrantType { - return c.grantTypes -} - -func (c *Client) LoginURL(id string) string { - //we use the default login UI and pass the (auth request) id, - //but you could implement some logic here to redirect the users to different login UIs depending on the client - return c.defaultLoginURL(id) -} - -func (c *Client) AccessTokenType() op.AccessTokenType { - return c.accessTokenType -} - -func (c *Client) IDTokenLifetime() time.Duration { - return 1 * time.Hour -} - -func (c *Client) DevMode() bool { - return c.devMode -} - -func (c *Client) RestrictAdditionalIdTokenScopes() func(scopes []string) []string { - return func(scopes []string) []string { - return scopes - } -} - -func (c *Client) RestrictAdditionalAccessTokenScopes() func(scopes []string) []string { - return func(scopes []string) []string { - return scopes - } -} - -func (c *Client) IsScopeAllowed(scope string) bool { - return false -} - -func (c *Client) IDTokenUserinfoClaimsAssertion() bool { - return c.idTokenUserinfoClaimsAssertion -} - -func (c *Client) ClockSkew() time.Duration { - return c.clockSkew -} diff --git a/example/server/internal/oidc.go b/example/server/internal/oidc.go index e514d85..1b3bf52 100644 --- a/example/server/internal/oidc.go +++ b/example/server/internal/oidc.go @@ -25,13 +25,13 @@ type AuthRequest struct { ApplicationID string CallbackURI string TransferState string - Prompt []Prompt + Prompt []string UiLocales []language.Tag LoginHint string MaxAuthAge *time.Duration UserID string Scopes []string - ResponseType OIDCResponseType + ResponseType oidc.ResponseType Nonce string CodeChallenge *OIDCCodeChallenge @@ -80,7 +80,7 @@ func (a *AuthRequest) GetRedirectURI() string { } func (a *AuthRequest) GetResponseType() oidc.ResponseType { - return ResponseTypeToOIDC(a.ResponseType) + return a.ResponseType } func (a *AuthRequest) GetResponseMode() oidc.ResponseMode { @@ -103,54 +103,20 @@ func (a *AuthRequest) Done() bool { return a.passwordChecked //this example only uses password for authentication } -type Prompt int32 - -const ( - PromptUnspecified Prompt = iota - PromptNone - PromptLogin - PromptConsent - PromptSelectAccount -) - -func PromptToInternal(oidcPrompt oidc.SpaceDelimitedArray) []Prompt { - prompts := make([]Prompt, len(oidcPrompt)) +func PromptToInternal(oidcPrompt oidc.SpaceDelimitedArray) []string { + prompts := make([]string, len(oidcPrompt)) for _, oidcPrompt := range oidcPrompt { switch oidcPrompt { - case oidc.PromptNone: - prompts = append(prompts, PromptNone) - case oidc.PromptLogin: - prompts = append(prompts, PromptLogin) - case oidc.PromptConsent: - prompts = append(prompts, PromptConsent) - case oidc.PromptSelectAccount: - prompts = append(prompts, PromptSelectAccount) + case oidc.PromptNone, + oidc.PromptLogin, + oidc.PromptConsent, + oidc.PromptSelectAccount: + prompts = append(prompts, oidcPrompt) } } return prompts } -type OIDCResponseType int32 - -const ( - OIDCResponseTypeCode OIDCResponseType = iota - OIDCResponseTypeIDToken - OIDCResponseTypeIDTokenToken -) - -func ResponseTypeToInternal(responseType oidc.ResponseType) OIDCResponseType { - switch responseType { - case oidc.ResponseTypeCode: - return OIDCResponseTypeCode - case oidc.ResponseTypeIDTokenOnly: - return OIDCResponseTypeIDToken - case oidc.ResponseTypeIDToken: - return OIDCResponseTypeIDTokenToken - default: - return OIDCResponseTypeCode - } -} - func MaxAgeToInternal(maxAge *uint) *time.Duration { if maxAge == nil { return nil @@ -159,13 +125,6 @@ func MaxAgeToInternal(maxAge *uint) *time.Duration { return &dur } -type AuthRequestOIDC struct { - Scopes []string - ResponseType interface{} - Nonce string - CodeChallenge *OIDCCodeChallenge -} - func authRequestToInternal(authReq *oidc.AuthRequest, userID string) *AuthRequest { return &AuthRequest{ CreationDate: time.Now(), @@ -178,7 +137,7 @@ func authRequestToInternal(authReq *oidc.AuthRequest, userID string) *AuthReques MaxAuthAge: MaxAgeToInternal(authReq.MaxAge), UserID: userID, Scopes: authReq.Scopes, - ResponseType: ResponseTypeToInternal(authReq.ResponseType), + ResponseType: authReq.ResponseType, Nonce: authReq.Nonce, CodeChallenge: &OIDCCodeChallenge{ Challenge: authReq.CodeChallenge, @@ -206,19 +165,6 @@ func CodeChallengeToOIDC(challenge *OIDCCodeChallenge) *oidc.CodeChallenge { } } -func ResponseTypeToOIDC(responseType OIDCResponseType) oidc.ResponseType { - switch responseType { - case OIDCResponseTypeCode: - return oidc.ResponseTypeCode - case OIDCResponseTypeIDTokenToken: - return oidc.ResponseTypeIDToken - case OIDCResponseTypeIDToken: - return oidc.ResponseTypeIDTokenOnly - default: - return oidc.ResponseTypeCode - } -} - //RefreshTokenRequestFromBusiness will simply wrap the internal RefreshToken to implement the op.RefreshTokenRequest interface func RefreshTokenRequestFromBusiness(token *RefreshToken) op.RefreshTokenRequest { return &RefreshTokenRequest{token} diff --git a/example/server/internal/storage.go b/example/server/internal/storage.go index a50d256..e6dedcf 100644 --- a/example/server/internal/storage.go +++ b/example/server/internal/storage.go @@ -23,6 +23,7 @@ type storage struct { tokens map[string]*Token clients map[string]*Client users map[string]*User + services map[string]Service refreshTokens map[string]*RefreshToken signingKey signingKey } @@ -33,11 +34,6 @@ type signingKey struct { Key *rsa.PrivateKey } -//TODO: describe -var defaultLoginURL = func(id string) string { - return "/login/username?authRequestID=" + id -} - func NewStorage() *storage { key, _ := rsa.GenerateKey(rand.Reader, 2048) return &storage{ @@ -378,13 +374,28 @@ func (s *storage) GetPrivateClaimsFromScopes(ctx context.Context, userID, client //GetKeyByIDAndUserID implements the op.Storage interface //it will be called to validate the signatures of a JWT (JWT Profile Grant and Authentication) func (s *storage) GetKeyByIDAndUserID(ctx context.Context, keyID, userID string) (*jose.JSONWebKey, error) { - return nil, fmt.Errorf("example does not yet support authentication with JWT") //TODO: implement + service, ok := s.services[userID] + if !ok { + return nil, fmt.Errorf("user not found") + } + key, ok := service.keys[keyID] + return &jose.JSONWebKey{ + KeyID: keyID, + Use: "sig", + Key: key, + }, nil } //ValidateJWTProfileScopes implements the op.Storage interface //it will be called to validate the scopes of a JWT Profile Authorization Grant request func (s *storage) ValidateJWTProfileScopes(ctx context.Context, userID string, scopes []string) ([]string, error) { - return nil, fmt.Errorf("example does not yet support authentication with JWT") //TODO: implement + allowedScopes := make([]string, 0) + for _, scope := range scopes { + if scope == oidc.ScopeOpenID { + allowedScopes = append(allowedScopes, scope) + } + } + return allowedScopes, nil } //Health implements the op.Storage interface diff --git a/example/server/internal/user.go b/example/server/internal/user.go index 8d33f0f..19b5d1f 100644 --- a/example/server/internal/user.go +++ b/example/server/internal/user.go @@ -1,6 +1,10 @@ package internal -import "golang.org/x/text/language" +import ( + "crypto/rsa" + + "golang.org/x/text/language" +) type User struct { id string @@ -14,3 +18,7 @@ type User struct { phoneVerified bool preferredLanguage language.Tag } + +type Service struct { + keys map[string]*rsa.PublicKey +} diff --git a/example/server/op.go b/example/server/op.go index 956e9a3..f075a7b 100644 --- a/example/server/op.go +++ b/example/server/op.go @@ -99,7 +99,7 @@ func newOP(ctx context.Context, storage op.Storage, port string, key [32]byte) ( AuthMethodPost: true, //enables additional authentication by using private_key_jwt - AuthMethodPrivateKeyJWT: false, //TODO: implement and set to true + AuthMethodPrivateKeyJWT: true, //enables refresh_token grant use GrantTypeRefreshToken: true,