diff options
Diffstat (limited to 'internal/oidc')
| -rw-r--r-- | internal/oidc/client.go | 272 | ||||
| -rw-r--r-- | internal/oidc/client_test.go | 221 | ||||
| -rw-r--r-- | internal/oidc/config.go | 6 | ||||
| -rw-r--r-- | internal/oidc/const.go | 10 | ||||
| -rw-r--r-- | internal/oidc/const_test.go | 12 | ||||
| -rw-r--r-- | internal/oidc/discovery.go | 237 | ||||
| -rw-r--r-- | internal/oidc/discovery_test.go | 24 | ||||
| -rw-r--r-- | internal/oidc/provider.go | 15 | ||||
| -rw-r--r-- | internal/oidc/provider_test.go | 52 | ||||
| -rw-r--r-- | internal/oidc/store.go | 6 | ||||
| -rw-r--r-- | internal/oidc/store_test.go | 63 | ||||
| -rw-r--r-- | internal/oidc/types.go | 347 | ||||
| -rw-r--r-- | internal/oidc/types_test.go | 4 |
13 files changed, 936 insertions, 333 deletions
diff --git a/internal/oidc/client.go b/internal/oidc/client.go index 41d302d03..71b43d867 100644 --- a/internal/oidc/client.go +++ b/internal/oidc/client.go @@ -1,8 +1,10 @@ package oidc import ( + "github.com/go-crypt/crypt/algorithm" "github.com/ory/fosite" "github.com/ory/x/errorsx" + "gopkg.in/square/go-jose.v2" "github.com/authelia/authelia/v4/internal/authentication" "github.com/authelia/authelia/v4/internal/authorization" @@ -11,8 +13,8 @@ import ( ) // NewClient creates a new Client. -func NewClient(config schema.OpenIDConnectClientConfiguration) (client *Client) { - client = &Client{ +func NewClient(config schema.OpenIDConnectClientConfiguration) (client Client) { + base := &BaseClient{ ID: config.ID, Description: config.Description, Secret: config.Secret, @@ -40,14 +42,165 @@ func NewClient(config schema.OpenIDConnectClientConfiguration) (client *Client) } for _, mode := range config.ResponseModes { - client.ResponseModes = append(client.ResponseModes, fosite.ResponseModeType(mode)) + base.ResponseModes = append(base.ResponseModes, fosite.ResponseModeType(mode)) + } + + if config.TokenEndpointAuthMethod != "" && config.TokenEndpointAuthMethod != "auto" { + client = &FullClient{ + BaseClient: base, + TokenEndpointAuthMethod: config.TokenEndpointAuthMethod, + } + } else { + client = base } return client } +// GetID returns the ID. +func (c *BaseClient) GetID() string { + return c.ID +} + +// GetDescription returns the Description. +func (c *BaseClient) GetDescription() string { + if c.Description == "" { + c.Description = c.GetID() + } + + return c.Description +} + +// GetSecret returns the Secret. +func (c *BaseClient) GetSecret() algorithm.Digest { + return c.Secret +} + +// GetSectorIdentifier returns the SectorIdentifier for this client. +func (c *BaseClient) GetSectorIdentifier() string { + return c.SectorIdentifier +} + +// GetHashedSecret returns the Secret. +func (c *BaseClient) GetHashedSecret() (secret []byte) { + if c.Secret == nil { + return []byte(nil) + } + + return []byte(c.Secret.Encode()) +} + +// GetRedirectURIs returns the RedirectURIs. +func (c *BaseClient) GetRedirectURIs() (redirectURIs []string) { + return c.RedirectURIs +} + +// GetGrantTypes returns the GrantTypes. +func (c *BaseClient) GetGrantTypes() fosite.Arguments { + if len(c.GrantTypes) == 0 { + return fosite.Arguments{"authorization_code"} + } + + return c.GrantTypes +} + +// GetResponseTypes returns the ResponseTypes. +func (c *BaseClient) GetResponseTypes() fosite.Arguments { + if len(c.ResponseTypes) == 0 { + return fosite.Arguments{"code"} + } + + return c.ResponseTypes +} + +// GetScopes returns the Scopes. +func (c *BaseClient) GetScopes() fosite.Arguments { + return c.Scopes +} + +// GetAudience returns the Audience. +func (c *BaseClient) GetAudience() fosite.Arguments { + return c.Audience +} + +// GetResponseModes returns the valid response modes for this client. +// +// Implements the fosite.ResponseModeClient. +func (c *BaseClient) GetResponseModes() []fosite.ResponseModeType { + return c.ResponseModes +} + +// GetUserinfoSigningAlgorithm returns the UserinfoSigningAlgorithm. +func (c *BaseClient) GetUserinfoSigningAlgorithm() string { + if c.UserinfoSigningAlgorithm == "" { + c.UserinfoSigningAlgorithm = SigningAlgorithmNone + } + + return c.UserinfoSigningAlgorithm +} + +// GetPAREnforcement returns EnforcePAR. +func (c *BaseClient) GetPAREnforcement() bool { + return c.EnforcePAR +} + +// GetPKCEEnforcement returns EnforcePKCE. +func (c *BaseClient) GetPKCEEnforcement() bool { + return c.EnforcePKCE +} + +// GetPKCEChallengeMethodEnforcement returns EnforcePKCEChallengeMethod. +func (c *BaseClient) GetPKCEChallengeMethodEnforcement() bool { + return c.EnforcePKCEChallengeMethod +} + +// GetPKCEChallengeMethod returns PKCEChallengeMethod. +func (c *BaseClient) GetPKCEChallengeMethod() string { + return c.PKCEChallengeMethod +} + +// GetAuthorizationPolicy returns Policy. +func (c *BaseClient) GetAuthorizationPolicy() authorization.Level { + return c.Policy +} + +// GetConsentPolicy returns Consent. +func (c *BaseClient) GetConsentPolicy() ClientConsent { + return c.Consent +} + +// GetConsentResponseBody returns the proper consent response body for this session.OIDCWorkflowSession. +func (c *BaseClient) GetConsentResponseBody(consent *model.OAuth2ConsentSession) ConsentGetResponseBody { + body := ConsentGetResponseBody{ + ClientID: c.ID, + ClientDescription: c.Description, + PreConfiguration: c.Consent.Mode == ClientConsentModePreConfigured, + } + + if consent != nil { + body.Scopes = consent.RequestedScopes + body.Audience = consent.RequestedAudience + } + + return body +} + +// IsPublic returns the value of the Public property. +func (c *BaseClient) IsPublic() bool { + return c.Public +} + +// IsAuthenticationLevelSufficient returns if the provided authentication.Level is sufficient for the client of the AutheliaClient. +func (c *BaseClient) IsAuthenticationLevelSufficient(level authentication.Level) bool { + if level == authentication.NotAuthenticated { + return false + } + + return authorization.IsAuthLevelSufficient(level, c.Policy) +} + // ValidatePKCEPolicy is a helper function to validate PKCE policy constraints on a per-client basis. -func (c *Client) ValidatePKCEPolicy(r fosite.Requester) (err error) { +func (c *BaseClient) ValidatePKCEPolicy(r fosite.Requester) (err error) { form := r.GetRequestForm() if c.EnforcePKCE { @@ -70,7 +223,7 @@ func (c *Client) ValidatePKCEPolicy(r fosite.Requester) (err error) { } // ValidatePARPolicy is a helper function to validate additional policy constraints on a per-client basis. -func (c *Client) ValidatePARPolicy(r fosite.Requester, prefix string) (err error) { +func (c *BaseClient) ValidatePARPolicy(r fosite.Requester, prefix string) (err error) { if c.EnforcePAR { if !IsPushedAuthorizedRequest(r, prefix) { switch requestURI := r.GetRequestForm().Get(FormParameterRequestURI); requestURI { @@ -87,7 +240,7 @@ func (c *Client) ValidatePARPolicy(r fosite.Requester, prefix string) (err error // ValidateResponseModePolicy is an additional check to the response mode parameter to ensure if it's omitted that the // default response mode for the fosite.AuthorizeRequester is permitted. -func (c *Client) ValidateResponseModePolicy(r fosite.AuthorizeRequester) (err error) { +func (c *BaseClient) ValidateResponseModePolicy(r fosite.AuthorizeRequester) (err error) { if r.GetResponseMode() != fosite.ResponseModeDefault { return nil } @@ -109,91 +262,52 @@ func (c *Client) ValidateResponseModePolicy(r fosite.AuthorizeRequester) (err er return errorsx.WithStack(fosite.ErrUnsupportedResponseMode.WithHintf(`The request omitted the response_mode making the default response_mode "%s" based on the other authorization request parameters but registered OAuth 2.0 client doesn't support this response_mode`, m)) } -// IsAuthenticationLevelSufficient returns if the provided authentication.Level is sufficient for the client of the AutheliaClient. -func (c *Client) IsAuthenticationLevelSufficient(level authentication.Level) bool { - if level == authentication.NotAuthenticated { - return false - } - - return authorization.IsAuthLevelSufficient(level, c.Policy) -} - -// GetSectorIdentifier returns the SectorIdentifier for this client. -func (c *Client) GetSectorIdentifier() string { - return c.SectorIdentifier -} - -// GetConsentResponseBody returns the proper consent response body for this session.OIDCWorkflowSession. -func (c *Client) GetConsentResponseBody(consent *model.OAuth2ConsentSession) ConsentGetResponseBody { - body := ConsentGetResponseBody{ - ClientID: c.ID, - ClientDescription: c.Description, - PreConfiguration: c.Consent.Mode == ClientConsentModePreConfigured, - } - - if consent != nil { - body.Scopes = consent.RequestedScopes - body.Audience = consent.RequestedAudience - } - - return body +// GetRequestURIs is an array of request_uri values that are pre-registered by the RP for use at the OP. Servers MAY +// cache the contents of the files referenced by these URIs and not retrieve them at the time they are used in a request. +// OPs can require that request_uri values used be pre-registered with the require_request_uri_registration +// discovery parameter. +func (c *FullClient) GetRequestURIs() []string { + return c.RequestURIs } -// GetID returns the ID. -func (c *Client) GetID() string { - return c.ID +// GetJSONWebKeys returns the JSON Web Key Set containing the public key used by the client to authenticate. +func (c *FullClient) GetJSONWebKeys() *jose.JSONWebKeySet { + return c.JSONWebKeys } -// GetHashedSecret returns the Secret. -func (c *Client) GetHashedSecret() (secret []byte) { - if c.Secret == nil { - return []byte(nil) - } - - return []byte(c.Secret.Encode()) +// GetJSONWebKeysURI returns the URL for lookup of JSON Web Key Set containing the +// public key used by the client to authenticate. +func (c *FullClient) GetJSONWebKeysURI() string { + return c.JSONWebKeysURI } -// GetRedirectURIs returns the RedirectURIs. -func (c *Client) GetRedirectURIs() (redirectURIs []string) { - return c.RedirectURIs +// GetRequestObjectSigningAlgorithm returns the JWS [JWS] alg algorithm [JWA] that MUST be used for signing Request +// Objects sent to the OP. All Request Objects from this Client MUST be rejected, if not signed with this algorithm. +func (c *FullClient) GetRequestObjectSigningAlgorithm() string { + return c.RequestObjectSigningAlgorithm } -// GetGrantTypes returns the GrantTypes. -func (c *Client) GetGrantTypes() fosite.Arguments { - if len(c.GrantTypes) == 0 { - return fosite.Arguments{"authorization_code"} +// GetTokenEndpointAuthMethod returns the requested Client Authentication Method for the Token Endpoint. The options are +// client_secret_post, client_secret_basic, client_secret_jwt, private_key_jwt, and none. +func (c *FullClient) GetTokenEndpointAuthMethod() string { + if c.TokenEndpointAuthMethod == "" { + if c.Public { + c.TokenEndpointAuthMethod = ClientAuthMethodNone + } else { + c.TokenEndpointAuthMethod = ClientAuthMethodClientSecretPost + } } - return c.GrantTypes + return c.TokenEndpointAuthMethod } -// GetResponseTypes returns the ResponseTypes. -func (c *Client) GetResponseTypes() fosite.Arguments { - if len(c.ResponseTypes) == 0 { - return fosite.Arguments{"code"} +// GetTokenEndpointAuthSigningAlgorithm returns the JWS [JWS] alg algorithm [JWA] that MUST be used for signing the JWT +// [JWT] used to authenticate the Client at the Token Endpoint for the private_key_jwt and client_secret_jwt +// authentication methods. +func (c *FullClient) GetTokenEndpointAuthSigningAlgorithm() string { + if c.TokenEndpointAuthSigningAlgorithm == "" { + c.TokenEndpointAuthSigningAlgorithm = SigningAlgorithmRSAWithSHA256 } - return c.ResponseTypes -} - -// GetScopes returns the Scopes. -func (c *Client) GetScopes() fosite.Arguments { - return c.Scopes -} - -// IsPublic returns the value of the Public property. -func (c *Client) IsPublic() bool { - return c.Public -} - -// GetAudience returns the Audience. -func (c *Client) GetAudience() fosite.Arguments { - return c.Audience -} - -// GetResponseModes returns the valid response modes for this client. -// -// Implements the fosite.ResponseModeClient. -func (c *Client) GetResponseModes() []fosite.ResponseModeType { - return c.ResponseModes + return c.TokenEndpointAuthSigningAlgorithm } diff --git a/internal/oidc/client_test.go b/internal/oidc/client_test.go index 1546644c2..a4be63346 100644 --- a/internal/oidc/client_test.go +++ b/internal/oidc/client_test.go @@ -7,6 +7,7 @@ import ( "github.com/ory/fosite" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "gopkg.in/square/go-jose.v2" "github.com/authelia/authelia/v4/internal/authentication" "github.com/authelia/authelia/v4/internal/authorization" @@ -15,36 +16,136 @@ import ( ) func TestNewClient(t *testing.T) { - blankConfig := schema.OpenIDConnectClientConfiguration{} - blankClient := NewClient(blankConfig) - assert.Equal(t, "", blankClient.ID) - assert.Equal(t, "", blankClient.Description) - assert.Equal(t, "", blankClient.Description) - assert.Len(t, blankClient.ResponseModes, 0) - - exampleConfig := schema.OpenIDConnectClientConfiguration{ - ID: "myapp", - Description: "My App", - Policy: "two_factor", - Secret: MustDecodeSecret("$plaintext$abcdef"), - RedirectURIs: []string{"https://google.com/callback"}, + config := schema.OpenIDConnectClientConfiguration{} + client := NewClient(config) + assert.Equal(t, "", client.GetID()) + assert.Equal(t, "", client.GetDescription()) + assert.Len(t, client.GetResponseModes(), 0) + assert.Len(t, client.GetResponseTypes(), 1) + assert.Equal(t, "", client.GetSectorIdentifier()) + + bclient, ok := client.(*BaseClient) + require.True(t, ok) + assert.Equal(t, "", bclient.UserinfoSigningAlgorithm) + assert.Equal(t, SigningAlgorithmNone, client.GetUserinfoSigningAlgorithm()) + + _, ok = client.(*FullClient) + assert.False(t, ok) + + config = schema.OpenIDConnectClientConfiguration{ + ID: myclient, + Description: myclientdesc, + Policy: twofactor, + Secret: MustDecodeSecret(badsecret), + RedirectURIs: []string{examplecom}, Scopes: schema.DefaultOpenIDConnectClientConfiguration.Scopes, ResponseTypes: schema.DefaultOpenIDConnectClientConfiguration.ResponseTypes, GrantTypes: schema.DefaultOpenIDConnectClientConfiguration.GrantTypes, ResponseModes: schema.DefaultOpenIDConnectClientConfiguration.ResponseModes, } - exampleClient := NewClient(exampleConfig) - assert.Equal(t, "myapp", exampleClient.ID) - require.Len(t, exampleClient.ResponseModes, 3) - assert.Equal(t, fosite.ResponseModeFormPost, exampleClient.ResponseModes[0]) - assert.Equal(t, fosite.ResponseModeQuery, exampleClient.ResponseModes[1]) - assert.Equal(t, fosite.ResponseModeFragment, exampleClient.ResponseModes[2]) - assert.Equal(t, authorization.TwoFactor, exampleClient.Policy) + client = NewClient(config) + assert.Equal(t, myclient, client.GetID()) + require.Len(t, client.GetResponseModes(), 1) + assert.Equal(t, fosite.ResponseModeFormPost, client.GetResponseModes()[0]) + assert.Equal(t, authorization.TwoFactor, client.GetAuthorizationPolicy()) + + config = schema.OpenIDConnectClientConfiguration{ + TokenEndpointAuthMethod: ClientAuthMethodClientSecretBasic, + } + + client = NewClient(config) + + fclient, ok := client.(*FullClient) + + var niljwks *jose.JSONWebKeySet + + require.True(t, ok) + assert.Equal(t, "", fclient.UserinfoSigningAlgorithm) + assert.Equal(t, ClientAuthMethodClientSecretBasic, fclient.TokenEndpointAuthMethod) + assert.Equal(t, ClientAuthMethodClientSecretBasic, fclient.GetTokenEndpointAuthMethod()) + assert.Equal(t, SigningAlgorithmNone, client.GetUserinfoSigningAlgorithm()) + assert.Equal(t, "", fclient.TokenEndpointAuthSigningAlgorithm) + assert.Equal(t, SigningAlgorithmRSAWithSHA256, fclient.GetTokenEndpointAuthSigningAlgorithm()) + assert.Equal(t, "", fclient.RequestObjectSigningAlgorithm) + assert.Equal(t, "", fclient.GetRequestObjectSigningAlgorithm()) + assert.Equal(t, "", fclient.JSONWebKeysURI) + assert.Equal(t, "", fclient.GetJSONWebKeysURI()) + assert.Equal(t, niljwks, fclient.JSONWebKeys) + assert.Equal(t, niljwks, fclient.GetJSONWebKeys()) + assert.Equal(t, []string(nil), fclient.RequestURIs) + assert.Equal(t, []string(nil), fclient.GetRequestURIs()) +} + +func TestBaseClient_ValidatePARPolicy(t *testing.T) { + testCases := []struct { + name string + client *BaseClient + have *fosite.Request + expected string + }{ + { + "ShouldNotEnforcePAR", + &BaseClient{ + EnforcePAR: false, + }, + &fosite.Request{}, + "", + }, + { + "ShouldEnforcePARAndErrorWithoutCorrectRequestURI", + &BaseClient{ + EnforcePAR: true, + }, + &fosite.Request{ + Form: map[string][]string{ + FormParameterRequestURI: {"https://google.com"}, + }, + }, + "invalid_request", + }, + { + "ShouldEnforcePARAndErrorWithEmptyRequestURI", + &BaseClient{ + EnforcePAR: true, + }, + &fosite.Request{ + Form: map[string][]string{ + FormParameterRequestURI: {""}, + }, + }, + "invalid_request", + }, + { + "ShouldEnforcePARAndNotErrorWithCorrectRequestURI", + &BaseClient{ + EnforcePAR: true, + }, + &fosite.Request{ + Form: map[string][]string{ + FormParameterRequestURI: {urnPARPrefix + "abc"}, + }, + }, + "", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := tc.client.ValidatePARPolicy(tc.have, urnPARPrefix) + + switch tc.expected { + case "": + assert.NoError(t, err) + default: + assert.EqualError(t, err, tc.expected) + } + }) + } } func TestIsAuthenticationLevelSufficient(t *testing.T) { - c := Client{} + c := &FullClient{BaseClient: &BaseClient{}} c.Policy = authorization.Bypass assert.False(t, c.IsAuthenticationLevelSufficient(authentication.NotAuthenticated)) @@ -68,7 +169,7 @@ func TestIsAuthenticationLevelSufficient(t *testing.T) { } func TestClient_GetConsentResponseBody(t *testing.T) { - c := Client{} + c := &FullClient{BaseClient: &BaseClient{}} consentRequestBody := c.GetConsentResponseBody(nil) assert.Equal(t, "", consentRequestBody.ClientID) @@ -76,56 +177,56 @@ func TestClient_GetConsentResponseBody(t *testing.T) { assert.Equal(t, []string(nil), consentRequestBody.Scopes) assert.Equal(t, []string(nil), consentRequestBody.Audience) - c.ID = "myclient" - c.Description = "My Client" + c.ID = myclient + c.Description = myclientdesc consent := &model.OAuth2ConsentSession{ - RequestedAudience: []string{"https://example.com"}, - RequestedScopes: []string{"openid", "groups"}, + RequestedAudience: []string{examplecom}, + RequestedScopes: []string{ScopeOpenID, ScopeGroups}, } - expectedScopes := []string{"openid", "groups"} - expectedAudiences := []string{"https://example.com"} + expectedScopes := []string{ScopeOpenID, ScopeGroups} + expectedAudiences := []string{examplecom} consentRequestBody = c.GetConsentResponseBody(consent) - assert.Equal(t, "myclient", consentRequestBody.ClientID) - assert.Equal(t, "My Client", consentRequestBody.ClientDescription) + assert.Equal(t, myclient, consentRequestBody.ClientID) + assert.Equal(t, myclientdesc, consentRequestBody.ClientDescription) assert.Equal(t, expectedScopes, consentRequestBody.Scopes) assert.Equal(t, expectedAudiences, consentRequestBody.Audience) } func TestClient_GetAudience(t *testing.T) { - c := Client{} + c := &FullClient{BaseClient: &BaseClient{}} audience := c.GetAudience() assert.Len(t, audience, 0) - c.Audience = []string{"https://example.com"} + c.Audience = []string{examplecom} audience = c.GetAudience() require.Len(t, audience, 1) - assert.Equal(t, "https://example.com", audience[0]) + assert.Equal(t, examplecom, audience[0]) } func TestClient_GetScopes(t *testing.T) { - c := Client{} + c := &FullClient{BaseClient: &BaseClient{}} scopes := c.GetScopes() assert.Len(t, scopes, 0) - c.Scopes = []string{"openid"} + c.Scopes = []string{ScopeOpenID} scopes = c.GetScopes() require.Len(t, scopes, 1) - assert.Equal(t, "openid", scopes[0]) + assert.Equal(t, ScopeOpenID, scopes[0]) } func TestClient_GetGrantTypes(t *testing.T) { - c := Client{} + c := &FullClient{BaseClient: &BaseClient{}} grantTypes := c.GetGrantTypes() require.Len(t, grantTypes, 1) - assert.Equal(t, "authorization_code", grantTypes[0]) + assert.Equal(t, GrantTypeAuthorizationCode, grantTypes[0]) c.GrantTypes = []string{"device_code"} @@ -135,55 +236,55 @@ func TestClient_GetGrantTypes(t *testing.T) { } func TestClient_Hashing(t *testing.T) { - c := Client{} + c := &FullClient{BaseClient: &BaseClient{}} hashedSecret := c.GetHashedSecret() assert.Equal(t, []byte(nil), hashedSecret) - c.Secret = MustDecodeSecret("$plaintext$a_bad_secret") + c.Secret = MustDecodeSecret(badsecret) assert.True(t, c.Secret.MatchBytes([]byte("a_bad_secret"))) } func TestClient_GetHashedSecret(t *testing.T) { - c := Client{} + c := &FullClient{BaseClient: &BaseClient{}} hashedSecret := c.GetHashedSecret() assert.Equal(t, []byte(nil), hashedSecret) - c.Secret = MustDecodeSecret("$plaintext$a_bad_secret") + c.Secret = MustDecodeSecret(badsecret) hashedSecret = c.GetHashedSecret() - assert.Equal(t, []byte("$plaintext$a_bad_secret"), hashedSecret) + assert.Equal(t, []byte(badsecret), hashedSecret) } func TestClient_GetID(t *testing.T) { - c := Client{} + c := &FullClient{BaseClient: &BaseClient{}} id := c.GetID() assert.Equal(t, "", id) - c.ID = "myid" + c.ID = myclient id = c.GetID() - assert.Equal(t, "myid", id) + assert.Equal(t, myclient, id) } func TestClient_GetRedirectURIs(t *testing.T) { - c := Client{} + c := &FullClient{BaseClient: &BaseClient{}} redirectURIs := c.GetRedirectURIs() require.Len(t, redirectURIs, 0) - c.RedirectURIs = []string{"https://example.com/oauth2/callback"} + c.RedirectURIs = []string{examplecom} redirectURIs = c.GetRedirectURIs() require.Len(t, redirectURIs, 1) - assert.Equal(t, "https://example.com/oauth2/callback", redirectURIs[0]) + assert.Equal(t, examplecom, redirectURIs[0]) } func TestClient_GetResponseModes(t *testing.T) { - c := Client{} + c := &FullClient{BaseClient: &BaseClient{}} responseModes := c.GetResponseModes() require.Len(t, responseModes, 0) @@ -202,18 +303,18 @@ func TestClient_GetResponseModes(t *testing.T) { } func TestClient_GetResponseTypes(t *testing.T) { - c := Client{} + c := &FullClient{BaseClient: &BaseClient{}} responseTypes := c.GetResponseTypes() require.Len(t, responseTypes, 1) - assert.Equal(t, "code", responseTypes[0]) + assert.Equal(t, ResponseTypeAuthorizationCodeFlow, responseTypes[0]) - c.ResponseTypes = []string{"code", "id_token"} + c.ResponseTypes = []string{ResponseTypeAuthorizationCodeFlow, ResponseTypeImplicitFlowIDToken} responseTypes = c.GetResponseTypes() require.Len(t, responseTypes, 2) - assert.Equal(t, "code", responseTypes[0]) - assert.Equal(t, "id_token", responseTypes[1]) + assert.Equal(t, ResponseTypeAuthorizationCodeFlow, responseTypes[0]) + assert.Equal(t, ResponseTypeImplicitFlowIDToken, responseTypes[1]) } func TestNewClientPKCE(t *testing.T) { @@ -290,9 +391,9 @@ func TestNewClientPKCE(t *testing.T) { t.Run(tc.name, func(t *testing.T) { client := NewClient(tc.have) - assert.Equal(t, tc.expectedEnforcePKCE, client.EnforcePKCE) - assert.Equal(t, tc.expectedEnforcePKCEChallengeMethod, client.EnforcePKCEChallengeMethod) - assert.Equal(t, tc.expected, client.PKCEChallengeMethod) + assert.Equal(t, tc.expectedEnforcePKCE, client.GetPKCEEnforcement()) + assert.Equal(t, tc.expectedEnforcePKCEChallengeMethod, client.GetPKCEChallengeMethodEnforcement()) + assert.Equal(t, tc.expected, client.GetPKCEChallengeMethod()) if tc.r != nil { err := client.ValidatePKCEPolicy(tc.r) @@ -355,7 +456,7 @@ func TestNewClientPAR(t *testing.T) { t.Run(tc.name, func(t *testing.T) { client := NewClient(tc.have) - assert.Equal(t, tc.expected, client.EnforcePAR) + assert.Equal(t, tc.expected, client.GetPAREnforcement()) if tc.r != nil { err := client.ValidatePARPolicy(tc.r, urnPARPrefix) @@ -437,7 +538,7 @@ func TestNewClientResponseModes(t *testing.T) { } func TestClient_IsPublic(t *testing.T) { - c := Client{} + c := &FullClient{BaseClient: &BaseClient{}} assert.False(t, c.IsPublic()) diff --git a/internal/oidc/config.go b/internal/oidc/config.go index 1cc7bf098..1db4e757a 100644 --- a/internal/oidc/config.go +++ b/internal/oidc/config.go @@ -169,12 +169,6 @@ type LifespanConfig struct { RefreshToken time.Duration } -const ( - PromptNone = none - PromptLogin = "login" - PromptConsent = "consent" -) - // LoadHandlers reloads the handlers based on the current configuration. func (c *Config) LoadHandlers(store *Store, strategy jwt.Signer) { validator := openid.NewOpenIDConnectRequestValidator(strategy, c) diff --git a/internal/oidc/const.go b/internal/oidc/const.go index db8c3a23d..01670e317 100644 --- a/internal/oidc/const.go +++ b/internal/oidc/const.go @@ -69,15 +69,12 @@ const ( GrantTypeImplicit = implicit GrantTypeRefreshToken = "refresh_token" GrantTypeAuthorizationCode = "authorization_code" - GrantTypePassword = "password" - GrantTypeClientCredentials = "client_credentials" ) // Client Auth Method strings. const ( ClientAuthMethodClientSecretBasic = "client_secret_basic" ClientAuthMethodClientSecretPost = "client_secret_post" - ClientAuthMethodClientSecretJWT = "client_secret_jwt" ClientAuthMethodNone = "none" ) @@ -117,6 +114,13 @@ const ( FormParameterCodeChallengeMethod = "code_challenge_method" ) +const ( + PromptNone = none + PromptLogin = "login" + PromptConsent = "consent" + // PromptCreate = "create" // This prompt value is currently unused. +) + // Endpoints. const ( EndpointAuthorization = "authorization" diff --git a/internal/oidc/const_test.go b/internal/oidc/const_test.go new file mode 100644 index 000000000..b5e3d915b --- /dev/null +++ b/internal/oidc/const_test.go @@ -0,0 +1,12 @@ +package oidc + +const ( + myclient = "myclient" + myclientdesc = "My Client" + onefactor = "one_factor" + twofactor = "two_factor" + examplecom = "https://example.com" + examplecomsid = "example.com" + badsecret = "$plaintext$a_bad_secret" + badhmac = "asbdhaaskmdlkamdklasmdlkams" +) diff --git a/internal/oidc/discovery.go b/internal/oidc/discovery.go index 996e5e3f5..e57bad146 100644 --- a/internal/oidc/discovery.go +++ b/internal/oidc/discovery.go @@ -5,70 +5,76 @@ import ( ) // NewOpenIDConnectWellKnownConfiguration generates a new OpenIDConnectWellKnownConfiguration. -func NewOpenIDConnectWellKnownConfiguration(c *schema.OpenIDConnectConfiguration, clients map[string]*Client) (config OpenIDConnectWellKnownConfiguration) { +func NewOpenIDConnectWellKnownConfiguration(c *schema.OpenIDConnectConfiguration) (config OpenIDConnectWellKnownConfiguration) { config = OpenIDConnectWellKnownConfiguration{ - CommonDiscoveryOptions: CommonDiscoveryOptions{ - SubjectTypesSupported: []string{ - SubjectTypePublic, + OAuth2WellKnownConfiguration: OAuth2WellKnownConfiguration{ + CommonDiscoveryOptions: CommonDiscoveryOptions{ + SubjectTypesSupported: []string{ + SubjectTypePublic, + SubjectTypePairwise, + }, + ResponseTypesSupported: []string{ + ResponseTypeAuthorizationCodeFlow, + ResponseTypeImplicitFlowIDToken, + ResponseTypeImplicitFlowToken, + ResponseTypeImplicitFlowBoth, + ResponseTypeHybridFlowIDToken, + ResponseTypeHybridFlowToken, + ResponseTypeHybridFlowBoth, + }, + GrantTypesSupported: []string{ + GrantTypeAuthorizationCode, + GrantTypeImplicit, + GrantTypeRefreshToken, + }, + ResponseModesSupported: []string{ + ResponseModeFormPost, + ResponseModeQuery, + ResponseModeFragment, + }, + ScopesSupported: []string{ + ScopeOfflineAccess, + ScopeOpenID, + ScopeProfile, + ScopeGroups, + ScopeEmail, + }, + ClaimsSupported: []string{ + ClaimAuthenticationMethodsReference, + ClaimAudience, + ClaimAuthorizedParty, + ClaimClientIdentifier, + ClaimExpirationTime, + ClaimIssuedAt, + ClaimIssuer, + ClaimJWTID, + ClaimRequestedAt, + ClaimSubject, + ClaimAuthenticationTime, + ClaimNonce, + ClaimPreferredEmail, + ClaimEmailVerified, + ClaimEmailAlts, + ClaimGroups, + ClaimPreferredUsername, + ClaimFullName, + }, + TokenEndpointAuthMethodsSupported: []string{ + ClientAuthMethodClientSecretBasic, + ClientAuthMethodClientSecretPost, + ClientAuthMethodNone, + }, }, - ResponseTypesSupported: []string{ - ResponseTypeAuthorizationCodeFlow, - ResponseTypeImplicitFlowIDToken, - ResponseTypeImplicitFlowToken, - ResponseTypeImplicitFlowBoth, - ResponseTypeHybridFlowIDToken, - ResponseTypeHybridFlowToken, - ResponseTypeHybridFlowBoth, + OAuth2DiscoveryOptions: OAuth2DiscoveryOptions{ + CodeChallengeMethodsSupported: []string{ + PKCEChallengeMethodSHA256, + }, }, - GrantTypesSupported: []string{ - GrantTypeAuthorizationCode, - GrantTypeImplicit, - GrantTypeRefreshToken, - }, - ResponseModesSupported: []string{ - ResponseModeFormPost, - ResponseModeQuery, - ResponseModeFragment, - }, - ScopesSupported: []string{ - ScopeOfflineAccess, - ScopeOpenID, - ScopeProfile, - ScopeGroups, - ScopeEmail, - }, - ClaimsSupported: []string{ - ClaimAuthenticationMethodsReference, - ClaimAudience, - ClaimAuthorizedParty, - ClaimClientIdentifier, - ClaimExpirationTime, - ClaimIssuedAt, - ClaimIssuer, - ClaimJWTID, - ClaimRequestedAt, - ClaimSubject, - ClaimAuthenticationTime, - ClaimNonce, - ClaimPreferredEmail, - ClaimEmailVerified, - ClaimEmailAlts, - ClaimGroups, - ClaimPreferredUsername, - ClaimFullName, - }, - TokenEndpointAuthMethodsSupported: []string{ - ClientAuthMethodClientSecretBasic, - ClientAuthMethodClientSecretPost, - ClientAuthMethodClientSecretJWT, - ClientAuthMethodNone, - }, - }, - OAuth2DiscoveryOptions: OAuth2DiscoveryOptions{ - CodeChallengeMethodsSupported: []string{ - PKCEChallengeMethodSHA256, + OAuth2PushedAuthorizationDiscoveryOptions: &OAuth2PushedAuthorizationDiscoveryOptions{ + RequirePushedAuthorizationRequests: c.PAR.Enforce, }, }, + OpenIDConnectDiscoveryOptions: OpenIDConnectDiscoveryOptions{ IDTokenSigningAlgValuesSupported: []string{ SigningAlgorithmRSAWithSHA256, @@ -77,35 +83,110 @@ func NewOpenIDConnectWellKnownConfiguration(c *schema.OpenIDConnectConfiguration SigningAlgorithmNone, SigningAlgorithmRSAWithSHA256, }, - RequestObjectSigningAlgValuesSupported: []string{ - SigningAlgorithmNone, - SigningAlgorithmRSAWithSHA256, - }, }, - PushedAuthorizationDiscoveryOptions: PushedAuthorizationDiscoveryOptions{ - RequirePushedAuthorizationRequests: c.PAR.Enforce, + OpenIDConnectFrontChannelLogoutDiscoveryOptions: &OpenIDConnectFrontChannelLogoutDiscoveryOptions{}, + OpenIDConnectBackChannelLogoutDiscoveryOptions: &OpenIDConnectBackChannelLogoutDiscoveryOptions{}, + OpenIDConnectPromptCreateDiscoveryOptions: &OpenIDConnectPromptCreateDiscoveryOptions{ + PromptValuesSupported: []string{ + PromptNone, + PromptConsent, + }, }, } - var pairwise, public bool + if c.EnablePKCEPlainChallenge { + config.CodeChallengeMethodsSupported = append(config.CodeChallengeMethodsSupported, PKCEChallengeMethodPlain) + } + + return config +} + +// Copy the values of the OAuth2WellKnownConfiguration and return it as a new struct. +func (opts OAuth2WellKnownConfiguration) Copy() (optsCopy OAuth2WellKnownConfiguration) { + optsCopy = OAuth2WellKnownConfiguration{ + CommonDiscoveryOptions: opts.CommonDiscoveryOptions, + OAuth2DiscoveryOptions: opts.OAuth2DiscoveryOptions, + } - for _, client := range clients { - if pairwise && public { - break - } + if opts.OAuth2DeviceAuthorizationGrantDiscoveryOptions != nil { + optsCopy.OAuth2DeviceAuthorizationGrantDiscoveryOptions = &OAuth2DeviceAuthorizationGrantDiscoveryOptions{} + *optsCopy.OAuth2DeviceAuthorizationGrantDiscoveryOptions = *opts.OAuth2DeviceAuthorizationGrantDiscoveryOptions + } - if client.SectorIdentifier != "" { - pairwise = true - } + if opts.OAuth2MutualTLSClientAuthenticationDiscoveryOptions != nil { + optsCopy.OAuth2MutualTLSClientAuthenticationDiscoveryOptions = &OAuth2MutualTLSClientAuthenticationDiscoveryOptions{} + *optsCopy.OAuth2MutualTLSClientAuthenticationDiscoveryOptions = *opts.OAuth2MutualTLSClientAuthenticationDiscoveryOptions } - if pairwise { - config.SubjectTypesSupported = append(config.SubjectTypesSupported, SubjectTypePairwise) + if opts.OAuth2IssuerIdentificationDiscoveryOptions != nil { + optsCopy.OAuth2IssuerIdentificationDiscoveryOptions = &OAuth2IssuerIdentificationDiscoveryOptions{} + *optsCopy.OAuth2IssuerIdentificationDiscoveryOptions = *opts.OAuth2IssuerIdentificationDiscoveryOptions } - if c.EnablePKCEPlainChallenge { - config.CodeChallengeMethodsSupported = append(config.CodeChallengeMethodsSupported, PKCEChallengeMethodPlain) + if opts.OAuth2JWTIntrospectionResponseDiscoveryOptions != nil { + optsCopy.OAuth2JWTIntrospectionResponseDiscoveryOptions = &OAuth2JWTIntrospectionResponseDiscoveryOptions{} + *optsCopy.OAuth2JWTIntrospectionResponseDiscoveryOptions = *opts.OAuth2JWTIntrospectionResponseDiscoveryOptions } - return config + if opts.OAuth2JWTSecuredAuthorizationRequestDiscoveryOptions != nil { + optsCopy.OAuth2JWTSecuredAuthorizationRequestDiscoveryOptions = &OAuth2JWTSecuredAuthorizationRequestDiscoveryOptions{} + *optsCopy.OAuth2JWTSecuredAuthorizationRequestDiscoveryOptions = *opts.OAuth2JWTSecuredAuthorizationRequestDiscoveryOptions + } + + if opts.OAuth2PushedAuthorizationDiscoveryOptions != nil { + optsCopy.OAuth2PushedAuthorizationDiscoveryOptions = &OAuth2PushedAuthorizationDiscoveryOptions{} + *optsCopy.OAuth2PushedAuthorizationDiscoveryOptions = *opts.OAuth2PushedAuthorizationDiscoveryOptions + } + + return optsCopy +} + +// Copy the values of the OpenIDConnectWellKnownConfiguration and return it as a new struct. +func (opts OpenIDConnectWellKnownConfiguration) Copy() (optsCopy OpenIDConnectWellKnownConfiguration) { + optsCopy = OpenIDConnectWellKnownConfiguration{ + OAuth2WellKnownConfiguration: opts.OAuth2WellKnownConfiguration.Copy(), + OpenIDConnectDiscoveryOptions: opts.OpenIDConnectDiscoveryOptions, + } + + if opts.OpenIDConnectFrontChannelLogoutDiscoveryOptions != nil { + optsCopy.OpenIDConnectFrontChannelLogoutDiscoveryOptions = &OpenIDConnectFrontChannelLogoutDiscoveryOptions{} + *optsCopy.OpenIDConnectFrontChannelLogoutDiscoveryOptions = *opts.OpenIDConnectFrontChannelLogoutDiscoveryOptions + } + + if opts.OpenIDConnectBackChannelLogoutDiscoveryOptions != nil { + optsCopy.OpenIDConnectBackChannelLogoutDiscoveryOptions = &OpenIDConnectBackChannelLogoutDiscoveryOptions{} + *optsCopy.OpenIDConnectBackChannelLogoutDiscoveryOptions = *opts.OpenIDConnectBackChannelLogoutDiscoveryOptions + } + + if opts.OpenIDConnectSessionManagementDiscoveryOptions != nil { + optsCopy.OpenIDConnectSessionManagementDiscoveryOptions = &OpenIDConnectSessionManagementDiscoveryOptions{} + *optsCopy.OpenIDConnectSessionManagementDiscoveryOptions = *opts.OpenIDConnectSessionManagementDiscoveryOptions + } + + if opts.OpenIDConnectRPInitiatedLogoutDiscoveryOptions != nil { + optsCopy.OpenIDConnectRPInitiatedLogoutDiscoveryOptions = &OpenIDConnectRPInitiatedLogoutDiscoveryOptions{} + *optsCopy.OpenIDConnectRPInitiatedLogoutDiscoveryOptions = *opts.OpenIDConnectRPInitiatedLogoutDiscoveryOptions + } + + if opts.OpenIDConnectPromptCreateDiscoveryOptions != nil { + optsCopy.OpenIDConnectPromptCreateDiscoveryOptions = &OpenIDConnectPromptCreateDiscoveryOptions{} + *optsCopy.OpenIDConnectPromptCreateDiscoveryOptions = *opts.OpenIDConnectPromptCreateDiscoveryOptions + } + + if opts.OpenIDConnectClientInitiatedBackChannelAuthFlowDiscoveryOptions != nil { + optsCopy.OpenIDConnectClientInitiatedBackChannelAuthFlowDiscoveryOptions = &OpenIDConnectClientInitiatedBackChannelAuthFlowDiscoveryOptions{} + *optsCopy.OpenIDConnectClientInitiatedBackChannelAuthFlowDiscoveryOptions = *opts.OpenIDConnectClientInitiatedBackChannelAuthFlowDiscoveryOptions + } + + if opts.OpenIDConnectJWTSecuredAuthorizationResponseModeDiscoveryOptions != nil { + optsCopy.OpenIDConnectJWTSecuredAuthorizationResponseModeDiscoveryOptions = &OpenIDConnectJWTSecuredAuthorizationResponseModeDiscoveryOptions{} + *optsCopy.OpenIDConnectJWTSecuredAuthorizationResponseModeDiscoveryOptions = *opts.OpenIDConnectJWTSecuredAuthorizationResponseModeDiscoveryOptions + } + + if opts.OpenIDFederationDiscoveryOptions != nil { + optsCopy.OpenIDFederationDiscoveryOptions = &OpenIDFederationDiscoveryOptions{} + *optsCopy.OpenIDFederationDiscoveryOptions = *opts.OpenIDFederationDiscoveryOptions + } + + return optsCopy } diff --git a/internal/oidc/discovery_test.go b/internal/oidc/discovery_test.go index 63ad18b65..34ca1f732 100644 --- a/internal/oidc/discovery_test.go +++ b/internal/oidc/discovery_test.go @@ -13,51 +13,51 @@ func TestNewOpenIDConnectWellKnownConfiguration(t *testing.T) { desc string pkcePlainChallenge bool enforcePAR bool - clients map[string]*Client + clients map[string]Client expectCodeChallengeMethodsSupported, expectSubjectTypesSupported []string }{ { desc: "ShouldHaveChallengeMethodsS256ANDSubjectTypesSupportedPublic", pkcePlainChallenge: false, - clients: map[string]*Client{"a": {}}, + clients: map[string]Client{"a": &BaseClient{}}, expectCodeChallengeMethodsSupported: []string{PKCEChallengeMethodSHA256}, - expectSubjectTypesSupported: []string{SubjectTypePublic}, + expectSubjectTypesSupported: []string{SubjectTypePublic, SubjectTypePairwise}, }, { desc: "ShouldHaveChallengeMethodsS256PlainANDSubjectTypesSupportedPublic", pkcePlainChallenge: true, - clients: map[string]*Client{"a": {}}, + clients: map[string]Client{"a": &BaseClient{}}, expectCodeChallengeMethodsSupported: []string{PKCEChallengeMethodSHA256, PKCEChallengeMethodPlain}, - expectSubjectTypesSupported: []string{SubjectTypePublic}, + expectSubjectTypesSupported: []string{SubjectTypePublic, SubjectTypePairwise}, }, { desc: "ShouldHaveChallengeMethodsS256ANDSubjectTypesSupportedPublicPairwise", pkcePlainChallenge: false, - clients: map[string]*Client{"a": {SectorIdentifier: "yes"}}, + clients: map[string]Client{"a": &BaseClient{SectorIdentifier: "yes"}}, expectCodeChallengeMethodsSupported: []string{PKCEChallengeMethodSHA256}, expectSubjectTypesSupported: []string{SubjectTypePublic, SubjectTypePairwise}, }, { desc: "ShouldHaveChallengeMethodsS256PlainANDSubjectTypesSupportedPublicPairwise", pkcePlainChallenge: true, - clients: map[string]*Client{"a": {SectorIdentifier: "yes"}}, + clients: map[string]Client{"a": &BaseClient{SectorIdentifier: "yes"}}, expectCodeChallengeMethodsSupported: []string{PKCEChallengeMethodSHA256, PKCEChallengeMethodPlain}, expectSubjectTypesSupported: []string{SubjectTypePublic, SubjectTypePairwise}, }, { desc: "ShouldHaveTokenAuthMethodsNone", pkcePlainChallenge: true, - clients: map[string]*Client{"a": {SectorIdentifier: "yes"}}, + clients: map[string]Client{"a": &BaseClient{SectorIdentifier: "yes"}}, expectCodeChallengeMethodsSupported: []string{PKCEChallengeMethodSHA256, PKCEChallengeMethodPlain}, expectSubjectTypesSupported: []string{SubjectTypePublic, SubjectTypePairwise}, }, { desc: "ShouldHaveTokenAuthMethodsNone", pkcePlainChallenge: true, - clients: map[string]*Client{ - "a": {SectorIdentifier: "yes"}, - "b": {SectorIdentifier: "yes"}, + clients: map[string]Client{ + "a": &BaseClient{SectorIdentifier: "yes"}, + "b": &BaseClient{SectorIdentifier: "yes"}, }, expectCodeChallengeMethodsSupported: []string{PKCEChallengeMethodSHA256, PKCEChallengeMethodPlain}, expectSubjectTypesSupported: []string{SubjectTypePublic, SubjectTypePairwise}, @@ -73,7 +73,7 @@ func TestNewOpenIDConnectWellKnownConfiguration(t *testing.T) { }, } - actual := NewOpenIDConnectWellKnownConfiguration(&c, tc.clients) + actual := NewOpenIDConnectWellKnownConfiguration(&c) for _, codeChallengeMethod := range tc.expectCodeChallengeMethodsSupported { assert.Contains(t, actual.CodeChallengeMethodsSupported, codeChallengeMethod) } diff --git a/internal/oidc/provider.go b/internal/oidc/provider.go index 54bd1595d..b8333351b 100644 --- a/internal/oidc/provider.go +++ b/internal/oidc/provider.go @@ -37,17 +37,14 @@ func NewOpenIDConnectProvider(config *schema.OpenIDConnectConfiguration, store s provider.Config.LoadHandlers(provider.Store, provider.KeyManager.Strategy()) - provider.discovery = NewOpenIDConnectWellKnownConfiguration(config, provider.Store.clients) + provider.discovery = NewOpenIDConnectWellKnownConfiguration(config) return provider, nil } // GetOAuth2WellKnownConfiguration returns the discovery document for the OAuth Configuration. func (p *OpenIDConnectProvider) GetOAuth2WellKnownConfiguration(issuer string) OAuth2WellKnownConfiguration { - options := OAuth2WellKnownConfiguration{ - CommonDiscoveryOptions: p.discovery.CommonDiscoveryOptions, - OAuth2DiscoveryOptions: p.discovery.OAuth2DiscoveryOptions, - } + options := p.discovery.OAuth2WellKnownConfiguration.Copy() options.Issuer = issuer @@ -63,13 +60,7 @@ func (p *OpenIDConnectProvider) GetOAuth2WellKnownConfiguration(issuer string) O // GetOpenIDConnectWellKnownConfiguration returns the discovery document for the OpenID Configuration. func (p *OpenIDConnectProvider) GetOpenIDConnectWellKnownConfiguration(issuer string) OpenIDConnectWellKnownConfiguration { - options := OpenIDConnectWellKnownConfiguration{ - CommonDiscoveryOptions: p.discovery.CommonDiscoveryOptions, - OAuth2DiscoveryOptions: p.discovery.OAuth2DiscoveryOptions, - OpenIDConnectDiscoveryOptions: p.discovery.OpenIDConnectDiscoveryOptions, - OpenIDConnectFrontChannelLogoutDiscoveryOptions: p.discovery.OpenIDConnectFrontChannelLogoutDiscoveryOptions, - OpenIDConnectBackChannelLogoutDiscoveryOptions: p.discovery.OpenIDConnectBackChannelLogoutDiscoveryOptions, - } + options := p.discovery.Copy() options.Issuer = issuer diff --git a/internal/oidc/provider_test.go b/internal/oidc/provider_test.go index 3045c6fc3..4a1a0b115 100644 --- a/internal/oidc/provider_test.go +++ b/internal/oidc/provider_test.go @@ -27,15 +27,15 @@ func TestNewOpenIDConnectProvider_ShouldEnableOptionalDiscoveryValues(t *testing IssuerCertificateChain: schema.X509CertificateChain{}, IssuerPrivateKey: mustParseRSAPrivateKey(exampleIssuerPrivateKey), EnablePKCEPlainChallenge: true, - HMACSecret: "asbdhaaskmdlkamdklasmdlkams", + HMACSecret: badhmac, Clients: []schema.OpenIDConnectClientConfiguration{ { - ID: "a-client", - Secret: MustDecodeSecret("$plaintext$a-client-secret"), - SectorIdentifier: url.URL{Host: "google.com"}, - Policy: "one_factor", + ID: myclient, + Secret: MustDecodeSecret(badsecret), + SectorIdentifier: url.URL{Host: examplecomsid}, + Policy: onefactor, RedirectURIs: []string{ - "https://google.com", + examplecom, }, }, }, @@ -43,7 +43,7 @@ func TestNewOpenIDConnectProvider_ShouldEnableOptionalDiscoveryValues(t *testing assert.NoError(t, err) - disco := provider.GetOpenIDConnectWellKnownConfiguration("https://example.com") + disco := provider.GetOpenIDConnectWellKnownConfiguration(examplecom) assert.Len(t, disco.SubjectTypesSupported, 2) assert.Contains(t, disco.SubjectTypesSupported, SubjectTypePublic) @@ -58,12 +58,12 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GoodConfiguration(t *tes provider, err := NewOpenIDConnectProvider(&schema.OpenIDConnectConfiguration{ IssuerCertificateChain: schema.X509CertificateChain{}, IssuerPrivateKey: mustParseRSAPrivateKey(exampleIssuerPrivateKey), - HMACSecret: "asbdhaaskmdlkamdklasmdlkams", + HMACSecret: badhmac, Clients: []schema.OpenIDConnectClientConfiguration{ { ID: "a-client", Secret: MustDecodeSecret("$plaintext$a-client-secret"), - Policy: "one_factor", + Policy: onefactor, RedirectURIs: []string{ "https://google.com", }, @@ -72,7 +72,7 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GoodConfiguration(t *tes ID: "b-client", Description: "Normal Description", Secret: MustDecodeSecret("$plaintext$b-client-secret"), - Policy: "two_factor", + Policy: twofactor, RedirectURIs: []string{ "https://google.com", }, @@ -103,7 +103,7 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GetOpenIDConnectWellKnow { ID: "a-client", Secret: MustDecodeSecret("$plaintext$a-client-secret"), - Policy: "one_factor", + Policy: onefactor, RedirectURIs: []string{ "https://google.com", }, @@ -113,9 +113,9 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GetOpenIDConnectWellKnow assert.NoError(t, err) - disco := provider.GetOpenIDConnectWellKnownConfiguration("https://example.com") + disco := provider.GetOpenIDConnectWellKnownConfiguration(examplecom) - assert.Equal(t, "https://example.com", disco.Issuer) + assert.Equal(t, examplecom, disco.Issuer) assert.Equal(t, "https://example.com/jwks.json", disco.JWKSURI) assert.Equal(t, "https://example.com/api/oidc/authorization", disco.AuthorizationEndpoint) assert.Equal(t, "https://example.com/api/oidc/token", disco.TokenEndpoint) @@ -139,8 +139,9 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GetOpenIDConnectWellKnow assert.Contains(t, disco.ResponseModesSupported, ResponseModeQuery) assert.Contains(t, disco.ResponseModesSupported, ResponseModeFragment) - assert.Len(t, disco.SubjectTypesSupported, 1) + assert.Len(t, disco.SubjectTypesSupported, 2) assert.Contains(t, disco.SubjectTypesSupported, SubjectTypePublic) + assert.Contains(t, disco.SubjectTypesSupported, SubjectTypePairwise) assert.Len(t, disco.ResponseTypesSupported, 7) assert.Contains(t, disco.ResponseTypesSupported, ResponseTypeAuthorizationCodeFlow) @@ -151,10 +152,9 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GetOpenIDConnectWellKnow assert.Contains(t, disco.ResponseTypesSupported, ResponseTypeHybridFlowToken) assert.Contains(t, disco.ResponseTypesSupported, ResponseTypeHybridFlowBoth) - assert.Len(t, disco.TokenEndpointAuthMethodsSupported, 4) + assert.Len(t, disco.TokenEndpointAuthMethodsSupported, 3) assert.Contains(t, disco.TokenEndpointAuthMethodsSupported, ClientAuthMethodClientSecretBasic) assert.Contains(t, disco.TokenEndpointAuthMethodsSupported, ClientAuthMethodClientSecretPost) - assert.Contains(t, disco.TokenEndpointAuthMethodsSupported, ClientAuthMethodClientSecretJWT) assert.Contains(t, disco.TokenEndpointAuthMethodsSupported, ClientAuthMethodNone) assert.Len(t, disco.GrantTypesSupported, 3) @@ -169,9 +169,7 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GetOpenIDConnectWellKnow assert.Contains(t, disco.UserinfoSigningAlgValuesSupported, SigningAlgorithmRSAWithSHA256) assert.Contains(t, disco.UserinfoSigningAlgValuesSupported, SigningAlgorithmNone) - assert.Len(t, disco.RequestObjectSigningAlgValuesSupported, 2) - assert.Contains(t, disco.RequestObjectSigningAlgValuesSupported, SigningAlgorithmRSAWithSHA256) - assert.Contains(t, disco.RequestObjectSigningAlgValuesSupported, SigningAlgorithmNone) + assert.Len(t, disco.RequestObjectSigningAlgValuesSupported, 0) assert.Len(t, disco.ClaimsSupported, 18) assert.Contains(t, disco.ClaimsSupported, ClaimAuthenticationMethodsReference) @@ -203,7 +201,7 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GetOAuth2WellKnownConfig { ID: "a-client", Secret: MustDecodeSecret("$plaintext$a-client-secret"), - Policy: "one_factor", + Policy: onefactor, RedirectURIs: []string{ "https://google.com", }, @@ -213,9 +211,9 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GetOAuth2WellKnownConfig assert.NoError(t, err) - disco := provider.GetOAuth2WellKnownConfiguration("https://example.com") + disco := provider.GetOAuth2WellKnownConfiguration(examplecom) - assert.Equal(t, "https://example.com", disco.Issuer) + assert.Equal(t, examplecom, disco.Issuer) assert.Equal(t, "https://example.com/jwks.json", disco.JWKSURI) assert.Equal(t, "https://example.com/api/oidc/authorization", disco.AuthorizationEndpoint) assert.Equal(t, "https://example.com/api/oidc/token", disco.TokenEndpoint) @@ -238,8 +236,9 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GetOAuth2WellKnownConfig assert.Contains(t, disco.ResponseModesSupported, ResponseModeQuery) assert.Contains(t, disco.ResponseModesSupported, ResponseModeFragment) - assert.Len(t, disco.SubjectTypesSupported, 1) + assert.Len(t, disco.SubjectTypesSupported, 2) assert.Contains(t, disco.SubjectTypesSupported, SubjectTypePublic) + assert.Contains(t, disco.SubjectTypesSupported, SubjectTypePairwise) assert.Len(t, disco.ResponseTypesSupported, 7) assert.Contains(t, disco.ResponseTypesSupported, ResponseTypeAuthorizationCodeFlow) @@ -250,10 +249,9 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GetOAuth2WellKnownConfig assert.Contains(t, disco.ResponseTypesSupported, ResponseTypeHybridFlowToken) assert.Contains(t, disco.ResponseTypesSupported, ResponseTypeHybridFlowBoth) - assert.Len(t, disco.TokenEndpointAuthMethodsSupported, 4) + assert.Len(t, disco.TokenEndpointAuthMethodsSupported, 3) assert.Contains(t, disco.TokenEndpointAuthMethodsSupported, ClientAuthMethodClientSecretBasic) assert.Contains(t, disco.TokenEndpointAuthMethodsSupported, ClientAuthMethodClientSecretPost) - assert.Contains(t, disco.TokenEndpointAuthMethodsSupported, ClientAuthMethodClientSecretJWT) assert.Contains(t, disco.TokenEndpointAuthMethodsSupported, ClientAuthMethodNone) assert.Len(t, disco.GrantTypesSupported, 3) @@ -292,7 +290,7 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GetOpenIDConnectWellKnow { ID: "a-client", Secret: MustDecodeSecret("$plaintext$a-client-secret"), - Policy: "one_factor", + Policy: onefactor, RedirectURIs: []string{ "https://google.com", }, @@ -302,7 +300,7 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GetOpenIDConnectWellKnow assert.NoError(t, err) - disco := provider.GetOpenIDConnectWellKnownConfiguration("https://example.com") + disco := provider.GetOpenIDConnectWellKnownConfiguration(examplecom) require.Len(t, disco.CodeChallengeMethodsSupported, 2) assert.Equal(t, PKCEChallengeMethodSHA256, disco.CodeChallengeMethodsSupported[0]) diff --git a/internal/oidc/store.go b/internal/oidc/store.go index a5a181c2b..2f21b368c 100644 --- a/internal/oidc/store.go +++ b/internal/oidc/store.go @@ -24,7 +24,7 @@ func NewStore(config *schema.OpenIDConnectConfiguration, provider storage.Provid store = &Store{ provider: provider, - clients: map[string]*Client{}, + clients: map[string]Client{}, } for _, client := range config.Clients { @@ -72,11 +72,11 @@ func (s *Store) GetClientPolicy(id string) (level authorization.Level) { return authorization.TwoFactor } - return client.Policy + return client.GetAuthorizationPolicy() } // GetFullClient returns a fosite.Client asserted as an Client matching the provided id. -func (s *Store) GetFullClient(id string) (client *Client, err error) { +func (s *Store) GetFullClient(id string) (client Client, err error) { client, ok := s.clients[id] if !ok { return nil, fosite.ErrInvalidClient diff --git a/internal/oidc/store_test.go b/internal/oidc/store_test.go index 580e864e4..def1d4e8e 100644 --- a/internal/oidc/store_test.go +++ b/internal/oidc/store_test.go @@ -4,6 +4,7 @@ import ( "context" "testing" + "github.com/ory/fosite" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -17,23 +18,23 @@ func TestOpenIDConnectStore_GetClientPolicy(t *testing.T) { IssuerPrivateKey: mustParseRSAPrivateKey(exampleIssuerPrivateKey), Clients: []schema.OpenIDConnectClientConfiguration{ { - ID: "myclient", - Description: "myclient desc", - Policy: "one_factor", + ID: myclient, + Description: myclientdesc, + Policy: onefactor, Scopes: []string{ScopeOpenID, ScopeProfile}, Secret: MustDecodeSecret("$plaintext$mysecret"), }, { ID: "myotherclient", - Description: "myclient desc", - Policy: "two_factor", + Description: myclientdesc, + Policy: twofactor, Scopes: []string{ScopeOpenID, ScopeProfile}, Secret: MustDecodeSecret("$plaintext$mysecret"), }, }, }, nil) - policyOne := s.GetClientPolicy("myclient") + policyOne := s.GetClientPolicy(myclient) assert.Equal(t, authorization.OneFactor, policyOne) policyTwo := s.GetClientPolicy("myotherclient") @@ -49,9 +50,9 @@ func TestOpenIDConnectStore_GetInternalClient(t *testing.T) { IssuerPrivateKey: mustParseRSAPrivateKey(exampleIssuerPrivateKey), Clients: []schema.OpenIDConnectClientConfiguration{ { - ID: "myclient", - Description: "myclient desc", - Policy: "one_factor", + ID: myclient, + Description: myclientdesc, + Policy: onefactor, Scopes: []string{ScopeOpenID, ScopeProfile}, Secret: MustDecodeSecret("$plaintext$mysecret"), }, @@ -62,17 +63,19 @@ func TestOpenIDConnectStore_GetInternalClient(t *testing.T) { assert.EqualError(t, err, "invalid_client") assert.Nil(t, client) - client, err = s.GetClient(context.Background(), "myclient") + client, err = s.GetClient(context.Background(), myclient) require.NoError(t, err) require.NotNil(t, client) - assert.Equal(t, "myclient", client.GetID()) + assert.Equal(t, myclient, client.GetID()) } func TestOpenIDConnectStore_GetInternalClient_ValidClient(t *testing.T) { + id := myclient + c1 := schema.OpenIDConnectClientConfiguration{ - ID: "myclient", - Description: "myclient desc", - Policy: "one_factor", + ID: id, + Description: myclientdesc, + Policy: onefactor, Scopes: []string{ScopeOpenID, ScopeProfile}, Secret: MustDecodeSecret("$plaintext$mysecret"), } @@ -83,24 +86,24 @@ func TestOpenIDConnectStore_GetInternalClient_ValidClient(t *testing.T) { Clients: []schema.OpenIDConnectClientConfiguration{c1}, }, nil) - client, err := s.GetFullClient(c1.ID) + client, err := s.GetFullClient(id) require.NoError(t, err) require.NotNil(t, client) - assert.Equal(t, client.ID, c1.ID) - assert.Equal(t, client.Description, c1.Description) - assert.Equal(t, client.Scopes, c1.Scopes) - assert.Equal(t, client.GrantTypes, c1.GrantTypes) - assert.Equal(t, client.ResponseTypes, c1.ResponseTypes) - assert.Equal(t, client.RedirectURIs, c1.RedirectURIs) - assert.Equal(t, client.Policy, authorization.OneFactor) - assert.Equal(t, client.Secret.Encode(), "$plaintext$mysecret") + assert.Equal(t, id, client.GetID()) + assert.Equal(t, myclientdesc, client.GetDescription()) + assert.Equal(t, fosite.Arguments(c1.Scopes), client.GetScopes()) + assert.Equal(t, fosite.Arguments([]string{GrantTypeAuthorizationCode}), client.GetGrantTypes()) + assert.Equal(t, fosite.Arguments([]string{ResponseTypeAuthorizationCodeFlow}), client.GetResponseTypes()) + assert.Equal(t, []string(nil), client.GetRedirectURIs()) + assert.Equal(t, authorization.OneFactor, client.GetAuthorizationPolicy()) + assert.Equal(t, "$plaintext$mysecret", client.GetSecret().Encode()) } func TestOpenIDConnectStore_GetInternalClient_InvalidClient(t *testing.T) { c1 := schema.OpenIDConnectClientConfiguration{ - ID: "myclient", - Description: "myclient desc", - Policy: "one_factor", + ID: myclient, + Description: myclientdesc, + Policy: onefactor, Scopes: []string{ScopeOpenID, ScopeProfile}, Secret: MustDecodeSecret("$plaintext$mysecret"), } @@ -122,16 +125,16 @@ func TestOpenIDConnectStore_IsValidClientID(t *testing.T) { IssuerPrivateKey: mustParseRSAPrivateKey(exampleIssuerPrivateKey), Clients: []schema.OpenIDConnectClientConfiguration{ { - ID: "myclient", - Description: "myclient desc", - Policy: "one_factor", + ID: myclient, + Description: myclientdesc, + Policy: onefactor, Scopes: []string{ScopeOpenID, ScopeProfile}, Secret: MustDecodeSecret("$plaintext$mysecret"), }, }, }, nil) - validClient := s.IsValidClientID("myclient") + validClient := s.IsValidClientID(myclient) invalidClient := s.IsValidClientID("myinvalidclient") assert.True(t, validClient) diff --git a/internal/oidc/types.go b/internal/oidc/types.go index 7403f2fed..8606fe3a3 100644 --- a/internal/oidc/types.go +++ b/internal/oidc/types.go @@ -12,6 +12,7 @@ import ( "github.com/ory/herodot" "gopkg.in/square/go-jose.v2" + "github.com/authelia/authelia/v4/internal/authentication" "github.com/authelia/authelia/v4/internal/authorization" "github.com/authelia/authelia/v4/internal/model" "github.com/authelia/authelia/v4/internal/storage" @@ -97,17 +98,19 @@ type OpenIDConnectProvider struct { // openid.OpenIDConnectRequestStorage, and partially implements rfc7523.RFC7523KeyStorage. type Store struct { provider storage.Provider - clients map[string]*Client + clients map[string]Client } -// Client represents the client internally. -type Client struct { +// BaseClient is the base for all clients. +type BaseClient struct { ID string Description string Secret algorithm.Digest SectorIdentifier string Public bool + EnforcePAR bool + EnforcePKCE bool EnforcePKCEChallengeMethod bool PKCEChallengeMethod string @@ -119,8 +122,6 @@ type Client struct { ResponseTypes []string ResponseModes []fosite.ResponseModeType - EnforcePAR bool - UserinfoSigningAlgorithm string Policy authorization.Level @@ -128,6 +129,43 @@ type Client struct { Consent ClientConsent } +// FullClient is the client with comprehensive supported features. +type FullClient struct { + *BaseClient + + RequestURIs []string + JSONWebKeys *jose.JSONWebKeySet + JSONWebKeysURI string + RequestObjectSigningAlgorithm string + TokenEndpointAuthMethod string + TokenEndpointAuthSigningAlgorithm string +} + +// Client represents the internal client definitions. +type Client interface { + fosite.Client + fosite.ResponseModeClient + + GetDescription() string + GetSecret() algorithm.Digest + GetSectorIdentifier() string + GetConsentResponseBody(consent *model.OAuth2ConsentSession) ConsentGetResponseBody + GetUserinfoSigningAlgorithm() string + + GetPAREnforcement() bool + GetPKCEEnforcement() bool + GetPKCEChallengeMethodEnforcement() bool + GetPKCEChallengeMethod() string + GetAuthorizationPolicy() authorization.Level + GetConsentPolicy() ClientConsent + + IsAuthenticationLevelSufficient(level authentication.Level) bool + + ValidatePKCEPolicy(r fosite.Requester) (err error) + ValidatePARPolicy(r fosite.Requester, prefix string) (err error) + ValidateResponseModePolicy(r fosite.AuthorizeRequester) (err error) +} + // NewClientConsent converts the schema.OpenIDConnectClientConsentConfig into a oidc.ClientConsent. func NewClientConsent(mode string, duration *time.Duration) ClientConsent { switch mode { @@ -344,6 +382,12 @@ type CommonDiscoveryOptions struct { Client if it is given. */ OPTOSURI string `json:"op_tos_uri,omitempty"` + + /* + A JWT containing metadata values about the authorization server as claims. This is a string value consisting of + the entire signed JWT. A "signed_metadata" metadata value SHOULD NOT appear as a claim in the JWT. + */ + SignedMetadata string `json:"signed_metadata,omitempty"` } // OAuth2DiscoveryOptions represents the discovery options specific to OAuth 2.0. @@ -427,6 +471,98 @@ type OAuth2DiscoveryOptions struct { CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported,omitempty"` } +type OAuth2JWTIntrospectionResponseDiscoveryOptions struct { + /* + OPTIONAL. JSON array containing a list of the JWS [RFC7515] signing algorithms ("alg" values) as defined in JWA + [RFC7518] supported by the introspection endpoint to sign the response. + */ + IntrospectionSigningAlgValuesSupported []string `json:"introspection_signing_alg_values_supported,omitempty"` + + /* + OPTIONAL. JSON array containing a list of the JWE [RFC7516] encryption algorithms ("alg" values) as defined in + JWA [RFC7518] supported by the introspection endpoint to encrypt the content encryption key for introspection + responses (content key encryption). + */ + IntrospectionEncryptionAlgValuesSupported []string `json:"introspection_encryption_alg_values_supported"` + + /* + OPTIONAL. JSON array containing a list of the JWE [RFC7516] encryption algorithms ("enc" values) as defined in + JWA [RFC7518] supported by the introspection endpoint to encrypt the response (content encryption). + */ + IntrospectionEncryptionEncValuesSupported []string `json:"introspection_encryption_enc_values_supported"` +} + +type OAuth2DeviceAuthorizationGrantDiscoveryOptions struct { + /* + OPTIONAL. URL of the authorization server's device authorization endpoint, as defined in Section 3.1. + */ + DeviceAuthorizationEndpoint string `json:"device_authorization_endpoint"` +} + +type OAuth2MutualTLSClientAuthenticationDiscoveryOptions struct { + /* + OPTIONAL. Boolean value indicating server support for mutual-TLS client certificate-bound access tokens. If + omitted, the default value is false. + */ + TLSClientCertificateBoundAccessTokens bool `json:"tls_client_certificate_bound_access_tokens"` + + /* + OPTIONAL. A JSON object containing alternative authorization server endpoints that, when present, an OAuth + client intending to do mutual TLS uses in preference to the conventional endpoints. The parameter value itself + consists of one or more endpoint parameters, such as token_endpoint, revocation_endpoint, + introspection_endpoint, etc., conventionally defined for the top level of authorization server metadata. An + OAuth client intending to do mutual TLS (for OAuth client authentication and/or to acquire or use + certificate-bound tokens) when making a request directly to the authorization server MUST use the alias URL of + the endpoint within the mtls_endpoint_aliases, when present, in preference to the endpoint URL of the same name + at the top level of metadata. When an endpoint is not present in mtls_endpoint_aliases, then the client uses the + conventional endpoint URL defined at the top level of the authorization server metadata. Metadata parameters + within mtls_endpoint_aliases that do not define endpoints to which an OAuth client makes a direct request have + no meaning and SHOULD be ignored. + */ + MutualTLSEndpointAliases struct { + AuthorizationEndpoint string `json:"authorization_endpoint,omitempty"` + TokenEndpoint string `json:"token_endpoint,omitempty"` + IntrospectionEndpoint string `json:"introspection_endpoint,omitempty"` + RevocationEndpoint string `json:"revocation_endpoint,omitempty"` + EndSessionEndpoint string `json:"end_session_endpoint,omitempty"` + UserinfoEndpoint string `json:"userinfo_endpoint,omitempty"` + BackChannelAuthenticationEndpoint string `json:"backchannel_authentication_endpoint,omitempty"` + FederationRegistrationEndpoint string `json:"federation_registration_endpoint,omitempty"` + PushedAuthorizationRequestEndpoint string `json:"pushed_authorization_request_endpoint,omitempty"` + RegistrationEndpoint string `json:"registration_endpoint,omitempty"` + } `json:"mtls_endpoint_aliases"` +} + +type OAuth2JWTSecuredAuthorizationRequestDiscoveryOptions struct { + /* + Indicates where authorization request needs to be protected as Request Object and provided through either + request or request_uri parameter. + */ + RequireSignedRequestObject bool `json:"require_signed_request_object"` +} + +type OAuth2IssuerIdentificationDiscoveryOptions struct { + AuthorizationResponseIssuerParameterSupported bool `json:"authorization_response_iss_parameter_supported"` +} + +// OAuth2PushedAuthorizationDiscoveryOptions represents the well known discovery document specific to the +// OAuth 2.0 Pushed Authorization Requests (RFC9126) implementation. +// +// OAuth 2.0 Pushed Authorization Requests: https://datatracker.ietf.org/doc/html/rfc9126#section-5 +type OAuth2PushedAuthorizationDiscoveryOptions struct { + /* + The URL of the pushed authorization request endpoint at which a client can post an authorization request to + exchange for a "request_uri" value usable at the authorization server. + */ + PushedAuthorizationRequestEndpoint string `json:"pushed_authorization_request_endpoint"` + + /* + Boolean parameter indicating whether the authorization server accepts authorization request data only via PAR. + If omitted, the default value is "false". + */ + RequirePushedAuthorizationRequests bool `json:"require_pushed_authorization_requests"` +} + // OpenIDConnectDiscoveryOptions represents the discovery options specific to OpenID Connect. type OpenIDConnectDiscoveryOptions struct { /* @@ -553,6 +689,12 @@ type OpenIDConnectDiscoveryOptions struct { ClaimLocalesSupported []string `json:"claims_locales_supported,omitempty"` /* + OPTIONAL. Boolean value specifying whether the OP supports use of the request parameter, with true indicating + support. If omitted, the default value is false. + */ + RequestParameterSupported bool `json:"request_parameter_supported"` + + /* OPTIONAL. Boolean value specifying whether the OP supports use of the request_uri parameter, with true indicating support. If omitted, the default value is true. */ @@ -612,39 +754,202 @@ type OpenIDConnectBackChannelLogoutDiscoveryOptions struct { BackChannelLogoutSessionSupported bool `json:"backchannel_logout_session_supported"` } -// PushedAuthorizationDiscoveryOptions represents the well known discovery document specific to the -// OAuth 2.0 Pushed Authorization Requests (RFC9126) implementation. +// OpenIDConnectSessionManagementDiscoveryOptions represents the discovery options specific to OpenID Connect 1.0 +// Session Management. // -// OAuth 2.0 Pushed Authorization Requests: https://datatracker.ietf.org/doc/html/rfc9126#section-5 -type PushedAuthorizationDiscoveryOptions struct { +// To support OpenID Connect Session Management, the RP needs to obtain the Session Management related OP metadata. This +// OP metadata is normally obtained via the OP's Discovery response, as described in OpenID Connect Discovery 1.0, or +// MAY be learned via other mechanisms. This OpenID Provider Metadata parameter MUST be included in the Server's +// discovery responses when Session Management and Discovery are supported. +// +// See Also: +// +// OpenID Connect 1.0 Session Management: https://openid.net/specs/openid-connect-session-1_0.html +type OpenIDConnectSessionManagementDiscoveryOptions struct { /* - The URL of the pushed authorization request endpoint at which a client can post an authorization request to - exchange for a "request_uri" value usable at the authorization server. + REQUIRED. URL of an OP iframe that supports cross-origin communications for session state information with the + RP Client, using the HTML5 postMessage API. This URL MUST use the https scheme and MAY contain port, path, and + query parameter components. The page is loaded from an invisible iframe embedded in an RP page so that it can + run in the OP's security context. It accepts postMessage requests from the relevant RP iframe and uses + postMessage to post back the login status of the End-User at the OP. */ - PushedAuthorizationRequestEndpoint string `json:"pushed_authorization_request_endpoint,omitempty"` + CheckSessionIFrame string `json:"check_session_iframe"` +} +// OpenIDConnectRPInitiatedLogoutDiscoveryOptions represents the discovery options specific to +// OpenID Connect RP-Initiated Logout 1.0. +// +// To support OpenID Connect RP-Initiated Logout, the RP needs to obtain the RP-Initiated Logout related OP metadata. +// This OP metadata is normally obtained via the OP's Discovery response, as described in OpenID Connect Discovery 1.0, +// or MAY be learned via other mechanisms. This OpenID Provider Metadata parameter MUST be included in the Server's +// discovery responses when RP-Initiated Logout and Discovery are supported. +// +// See Also: +// +// OpenID Connect RP-Initiated Logout 1.0: https://openid.net/specs/openid-connect-rpinitiated-1_0.html +type OpenIDConnectRPInitiatedLogoutDiscoveryOptions struct { /* - Boolean parameter indicating whether the authorization server accepts authorization request data only via PAR. - If omitted, the default value is "false". + REQUIRED. URL at the OP to which an RP can perform a redirect to request that the End-User be logged out at the + OP. This URL MUST use the https scheme and MAY contain port, path, and query parameter components. */ - RequirePushedAuthorizationRequests bool `json:"require_pushed_authorization_requests"` + EndSessionEndpoint string `json:"end_session_endpoint"` +} + +// OpenIDConnectPromptCreateDiscoveryOptions represents the discovery options specific to Initiating User Registration +// via OpenID Connect 1.0 functionality. +// +// This specification extends the OpenID Connect Discovery Metadata Section 3. +// +// See Also: +// +// Initiating User Registration via OpenID Connect 1.0: https://openid.net/specs/openid-connect-prompt-create-1_0.html +type OpenIDConnectPromptCreateDiscoveryOptions struct { + /* + OPTIONAL. JSON array containing the list of prompt values that this OP supports. + + This metadata element is OPTIONAL in the context of the OpenID Provider not supporting the create value. If + omitted, the Relying Party should assume that this specification is not supported. The OpenID Provider MAY + provide this metadata element even if it doesn't support the create value. + Specific to this specification, a value of create in the array indicates to the Relying party that this OpenID + Provider supports this specification. If an OpenID Provider supports this specification it MUST define this metadata + element in the openid-configuration file. Additionally, if this metadata element is defined by the OpenID + Provider, the OP must also specify all other prompt values which it supports. + See Also: + OpenID.PromptCreate: https://openid.net/specs/openid-connect-prompt-create-1_0.html + */ + PromptValuesSupported []string `json:"prompt_values_supported,omitempty"` +} + +// OpenIDConnectClientInitiatedBackChannelAuthFlowDiscoveryOptions represents the discovery options specific to +// OpenID Connect Client-Initiated Backchannel Authentication Flow - Core 1.0 +// +// The following authorization server metadata parameters are introduced by this specification for OPs publishing their +// support of the CIBA flow and details thereof. +// +// See Also: +// +// OpenID Connect Client-Initiated Backchannel Authentication Flow - Core 1.0: +// https://openid.net/specs/openid-client-initiated-backchannel-authentication-core-1_0.html#rfc.section.4 +type OpenIDConnectClientInitiatedBackChannelAuthFlowDiscoveryOptions struct { + /* + REQUIRED. URL of the OP's Backchannel Authentication Endpoint as defined in Section 7. + */ + BackChannelAuthenticationEndpoint string `json:"backchannel_authentication_endpoint"` + + /* + REQUIRED. JSON array containing one or more of the following values: poll, ping, and push. + */ + BackChannelTokenDeliveryModesSupported []string `json:"backchannel_token_delivery_modes_supported"` + + /* + OPTIONAL. JSON array containing a list of the JWS signing algorithms (alg values) supported by the OP for signed + authentication requests, which are described in Section 7.1.1. If omitted, signed authentication requests are + not supported by the OP. + */ + BackChannelAuthRequestSigningAlgValuesSupported []string `json:"backchannel_authentication_request_signing_alg_values_supported,omitempty"` + + /* + OPTIONAL. Boolean value specifying whether the OP supports the use of the user_code parameter, with true + indicating support. If omitted, the default value is false. + */ + BackChannelUserCodeParameterSupported bool `json:"backchannel_user_code_parameter_supported"` +} + +// OpenIDConnectJWTSecuredAuthorizationResponseModeDiscoveryOptions represents the discovery options specific to +// JWT Secured Authorization Response Mode for OAuth 2.0 (JARM). +// +// Authorization servers SHOULD publish the supported algorithms for signing and encrypting the JWT of an authorization +// response by utilizing OAuth 2.0 Authorization Server Metadata [RFC8414] parameters. The following parameters are +// introduced by this specification. +// +// See Also: +// +// JWT Secured Authorization Response Mode for OAuth 2.0 (JARM): +// https://openid.net/specs/oauth-v2-jarm.html#name-authorization-server-metada +type OpenIDConnectJWTSecuredAuthorizationResponseModeDiscoveryOptions struct { + /* + OPTIONAL. A JSON array containing a list of the JWS [RFC7515] signing algorithms (alg values) supported by the + authorization endpoint to sign the response. + */ + AuthorizationSigningAlgValuesSupported []string `json:"authorization_signing_alg_values_supported,omitempty"` + + /* + OPTIONAL. A JSON array containing a list of the JWE [RFC7516] encryption algorithms (alg values) supported by + the authorization endpoint to encrypt the response. + */ + AuthorizationEncryptionAlgValuesSupported []string `json:"authorization_encryption_alg_values_supported,omitempty"` + + /* + OPTIONAL. A JSON array containing a list of the JWE [RFC7516] encryption algorithms (enc values) supported by + the authorization endpoint to encrypt the response. + */ + AuthorizationEncryptionEncValuesSupported []string `json:"authorization_encryption_enc_values_supported,omitempty"` +} + +type OpenIDFederationDiscoveryOptions struct { + /* + OPTIONAL. URL of the OP's federation-specific Dynamic Client Registration Endpoint. If the OP supports explicit + client registration as described in Section 10.2, then this claim is REQUIRED. + */ + FederationRegistrationEndpoint string `json:"federation_registration_endpoint,omitempty"` + + /* + REQUIRED. Array specifying the federation types supported. Federation-type values defined by this specification + are automatic and explicit. + */ + ClientRegistrationTypesSupported []string `json:"client_registration_types_supported"` + + /* + OPTIONAL. A JSON Object defining the client authentications supported for each endpoint. The endpoint names are + defined in the IANA "OAuth Authorization Server Metadata" registry [IANA.OAuth.Parameters]. Other endpoints and + authentication methods are possible if made recognizable according to established standards and not in conflict + with the operating principles of this specification. In OpenID Connect Core, no client authentication is + performed at the authentication endpoint. Instead, the request itself is authenticated. The OP maps information + in the request (like the redirect_uri) to information it has gained on the client through static or dynamic + registration. If the mapping is successful, the request can be processed. If the RP uses Automatic Registration, + as defined in Section 10.1, the OP has no prior knowledge of the RP. Therefore, the OP must start by gathering + information about the RP using the process outlined in Section 6. Once it has the RP's metadata, the OP can + verify the request in the same way as if it had known the RP's metadata beforehand. To make the request + verification more secure, we demand the use of a client authentication or verification method that proves that + the RP is in possession of a key that appears in the RP's metadata. + */ + RequestAuthenticationMethodsSupported []string `json:"request_authentication_methods_supported,omitempty"` + + /* + OPTIONAL. JSON array containing a list of the JWS signing algorithms (alg values) supported for the signature on + the JWT [RFC7519] used in the request_object contained in the request parameter of an authorization request or + in the private_key_jwt of a pushed authorization request. This entry MUST be present if either of these + authentication methods are specified in the request_authentication_methods_supported entry. No default + algorithms are implied if this entry is omitted. Servers SHOULD support RS256. The value none MUST NOT be used. + */ + RequestAuthenticationSigningAlgValuesSupproted []string `json:"request_authentication_signing_alg_values_supported,omitempty"` } // OAuth2WellKnownConfiguration represents the well known discovery document specific to OAuth 2.0. type OAuth2WellKnownConfiguration struct { CommonDiscoveryOptions OAuth2DiscoveryOptions - PushedAuthorizationDiscoveryOptions + *OAuth2DeviceAuthorizationGrantDiscoveryOptions + *OAuth2MutualTLSClientAuthenticationDiscoveryOptions + *OAuth2IssuerIdentificationDiscoveryOptions + *OAuth2JWTIntrospectionResponseDiscoveryOptions + *OAuth2JWTSecuredAuthorizationRequestDiscoveryOptions + *OAuth2PushedAuthorizationDiscoveryOptions } // OpenIDConnectWellKnownConfiguration represents the well known discovery document specific to OpenID Connect. type OpenIDConnectWellKnownConfiguration struct { - CommonDiscoveryOptions - OAuth2DiscoveryOptions - PushedAuthorizationDiscoveryOptions + OAuth2WellKnownConfiguration + OpenIDConnectDiscoveryOptions - OpenIDConnectFrontChannelLogoutDiscoveryOptions - OpenIDConnectBackChannelLogoutDiscoveryOptions + *OpenIDConnectFrontChannelLogoutDiscoveryOptions + *OpenIDConnectBackChannelLogoutDiscoveryOptions + *OpenIDConnectSessionManagementDiscoveryOptions + *OpenIDConnectRPInitiatedLogoutDiscoveryOptions + *OpenIDConnectPromptCreateDiscoveryOptions + *OpenIDConnectClientInitiatedBackChannelAuthFlowDiscoveryOptions + *OpenIDConnectJWTSecuredAuthorizationResponseModeDiscoveryOptions + *OpenIDFederationDiscoveryOptions } // OpenIDConnectContext represents the context implementation that is used by some OpenID Connect 1.0 implementations. diff --git a/internal/oidc/types_test.go b/internal/oidc/types_test.go index b84461a07..d604ad009 100644 --- a/internal/oidc/types_test.go +++ b/internal/oidc/types_test.go @@ -40,7 +40,7 @@ func TestNewSessionWithAuthorizeRequest(t *testing.T) { Request: fosite.Request{ ID: requestID.String(), Form: formValues, - Client: &Client{ID: "example"}, + Client: &BaseClient{ID: "example"}, }, } @@ -50,7 +50,7 @@ func TestNewSessionWithAuthorizeRequest(t *testing.T) { requested := time.Unix(1647332518, 0) authAt := time.Unix(1647332500, 0) - issuer := "https://example.com" + issuer := examplecom amr := []string{AMRPasswordBasedAuthentication} consent := &model.OAuth2ConsentSession{ |
