diff options
| author | James Elliott <james-d-elliott@users.noreply.github.com> | 2022-10-20 13:16:36 +1100 | 
|---|---|---|
| committer | GitHub <noreply@github.com> | 2022-10-20 13:16:36 +1100 | 
| commit | 3aaca0604f2ed479d7f00fb5087ffed059f87a71 (patch) | |
| tree | dfa3a2622b3368fac30d5eee2956b4d5fcc5fa17 | |
| parent | b1a6dae99ac5b34f065391e266ac1cd87bac5b14 (diff) | |
feat(oidc): implicit consent (#4080)
This adds multiple consent modes to OpenID Connect clients. Specifically it allows configuration of a new consent mode called implicit which never asks for user consent.
97 files changed, 3273 insertions, 1062 deletions
diff --git a/api/openapi.yml b/api/openapi.yml index 10ec36b9d..15443a24e 100644 --- a/api/openapi.yml +++ b/api/openapi.yml @@ -176,7 +176,7 @@ paths:          content:            application/json:              schema: -              $ref: '#/components/schemas/handlers.firstFactorRequestBody' +              $ref: '#/components/schemas/handlers.bodyFirstFactorRequest'        responses:          "200":            description: Successful Operation @@ -446,7 +446,7 @@ paths:          content:            application/json:              schema: -              $ref: '#/components/schemas/handlers.signTOTPRequestBody' +              $ref: '#/components/schemas/handlers.bodySignTOTPRequest'        responses:          "200":            description: Successful Operation @@ -579,7 +579,7 @@ paths:          content:            application/json:              schema: -              $ref: '#/components/schemas/handlers.signDuoRequestBody' +              $ref: '#/components/schemas/handlers.bodySignDuoRequest'        responses:          "200":            description: Successful Operation @@ -785,7 +785,7 @@ components:                      items:                        type: string                        example: push -    handlers.firstFactorRequestBody: +    handlers.bodyFirstFactorRequest:        required:          - username          - password @@ -800,6 +800,12 @@ components:          targetURL:            type: string            example: https://home.example.com +        workflow: +          type: string +          example: openid_connect +        workflowID: +          type: string +          example: 3ebcfbc5-b0fd-4ee0-9d3c-080ae1e7298c          requestMethod:            type: string            example: GET @@ -852,13 +858,21 @@ components:          password:            type: string            example: password -    handlers.signDuoRequestBody: +    handlers.bodySignDuoRequest:        type: object        properties:          targetURL:            type: string            example: https://secure.example.com -    handlers.signTOTPRequestBody: +        passcode: +          type: string +        workflow: +          type: string +          example: openid_connect +        workflowID: +          type: string +          example: 3ebcfbc5-b0fd-4ee0-9d3c-080ae1e7298c +    handlers.bodySignTOTPRequest:        type: object        properties:          token: @@ -867,6 +881,12 @@ components:          targetURL:            type: string            example: https://secure.example.com +        workflow: +          type: string +          example: openid_connect +        workflowID: +          type: string +          example: 3ebcfbc5-b0fd-4ee0-9d3c-080ae1e7298c      handlers.StateResponse:        type: object        properties: @@ -1047,6 +1067,12 @@ components:                      userHandle:                        type: string                        format: byte +                    workflow: +                      type: string +                      example: openid_connect +                    workflowID: +                      type: string +                      example: 3ebcfbc5-b0fd-4ee0-9d3c-080ae1e7298c      webauthn.PublicKeyCredentialCreationOptions:        type: object        properties: diff --git a/config.template.yml b/config.template.yml index 26742f953..16ea13b17 100644 --- a/config.template.yml +++ b/config.template.yml @@ -972,10 +972,12 @@ notifier:          ## The policy to require for this client; one_factor or two_factor.          # authorization_policy: two_factor -        ## By default users cannot remember pre-configured consents. Setting this value to a period of time using a -        ## duration notation will enable users to remember consent for this client. The time configured is the amount -        ## of time the pre-configured consent is valid for granting new authorizations to the user. -        # pre_configured_consent_duration: +        ## The consent mode controls how consent is obtained. +        # consent_mode: auto + +        ## This value controls the duration a consent on this client remains remembered when the consent mode is +        ## configured as 'auto' or 'pre-configured'. +        # pre_configured_consent_duration: 1w          ## Audience this client is allowed to request.          # audience: [] diff --git a/docs/content/en/configuration/identity-providers/open-id-connect.md b/docs/content/en/configuration/identity-providers/open-id-connect.md index a8d6997a4..57e31742d 100644 --- a/docs/content/en/configuration/identity-providers/open-id-connect.md +++ b/docs/content/en/configuration/identity-providers/open-id-connect.md @@ -122,7 +122,8 @@ identity_providers:          sector_identifier: ''          public: false          authorization_policy: two_factor -        pre_configured_consent_duration: '' +        consent_mode: explicit +        pre_configured_consent_duration: 1w          audience: []          scopes:            - openid @@ -183,9 +184,13 @@ certificate immediately following it if present.  *__Important Note:__ This can also be defined using a [secret](../methods/secrets.md) which is __strongly recommended__  especially for containerized deployments.* -The private key in DER base64 ([RFC4648]) encoded PEM format used to sign/encrypt the [OpenID Connect] issued [JWT]'s. -The key must be generated by the administrator and can be done by following the -[Generating an RSA Keypair](../miscellaneous/guides.md#generating-an-rsa-keypair) guide. +The private key used to sign/encrypt the [OpenID Connect] issued [JWT]'s. The key must be generated by the administrator +and can be done by following the [Generating an RSA Keypair](../miscellaneous/guides.md#generating-an-rsa-keypair) guide. + +The private key *__MUST__*: +* Be a PEM block encoded in the DER base64 format ([RFC4648]). +* Be an RSA Key. +* Have a key size of at least 2048 bits.  If the [issuer_certificate_chain](#issuer_certificate_chain) is provided the private key must include matching public  key data for the first certificate in the chain. @@ -404,19 +409,38 @@ URI.  The authorization policy for this client: either `one_factor` or `two_factor`. +#### consent_mode + +{{< confkey type="string" default="auto" required="no" >}} + +Configures the consent mode. The following table describes the different modes: + +|     Value      |                                                                                   Description                                                                                    | +|:--------------:|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:| +|      auto      |                  Automatically determined (default). Uses `explicit` unless [pre_configured_consent_duration] is specified in which case uses `pre-configured`.                  | +|    explicit    |                                                    Requires the user provide unique explicit consent for every authorization.                                                    | +|    implicit    | Automatically assumes consent for every authorization, never asking the user if they wish to give consent. *__Note:__* this option is not technically part of the specification. | +| pre-configured |                                             Allows the end-user to remember their consent for the [pre_configured_consent_duration].                                             | + +[pre_configured_consent_duration]: #pre_configured_consent_duration +  #### pre_configured_consent_duration -{{< confkey type="duration" required="no" >}} +{{< confkey type="duration" default="1w" required="no" >}}  *__Note:__ This setting uses the [duration notation format](../prologue/common.md#duration-notation-format). Please see  the [common options](../prologue/common.md#duration-notation-format) documentation for information on this format.* -Configuring this enables users of this client to remember their consent as a pre-configured consent. The period of time -dictates how long a users choice to remember the pre-configured consent lasts. +Specifying this in the configuration without a consent [consent_mode] enables the `pre-configured` mode. If this is +specified as well as the [consent_mode] then it only has an effect if the [consent_mode] is `pre-configured` or `auto`. + +The period of time dictates how long a users choice to remember the pre-configured consent lasts.  Pre-configured consents are only valid if the subject, client id are exactly the same and the requested scopes/audience  match exactly with the granted scopes/audience. +[consent_mode]: #consent_mode +  #### audience  {{< confkey type="list(string)" required="no" >}} diff --git a/docs/content/en/configuration/storage/migrations.md b/docs/content/en/configuration/storage/migrations.md index b3cc68287..fbc84df83 100644 --- a/docs/content/en/configuration/storage/migrations.md +++ b/docs/content/en/configuration/storage/migrations.md @@ -34,3 +34,4 @@ this instance if you wanted to downgrade to pre1 you would need to use an Authel  |       3        |      4.34.2      |     WebAuthn - fix V2 migration kid column length and provide migration path for anyone on V2      |  |       4        |      4.35.0      |               Added OpenID Connect storage tables and opaque user identifier tables                |  |       5        |      4.35.1      | Fixed the oauth2_consent_session table to accept NULL subjects for users who are not yet signed in | +|       6        |      4.37.0      |          Adjusted the OpenID Connect tables to allow pre-configured consent improvements           | diff --git a/docs/content/en/roadmap/active/openid-connect.md b/docs/content/en/roadmap/active/openid-connect.md index 17a7e8f09..180a5a24a 100644 --- a/docs/content/en/roadmap/active/openid-connect.md +++ b/docs/content/en/roadmap/active/openid-connect.md @@ -93,16 +93,23 @@ Feature List:  ### Beta 5 -{{< roadmap-status stage="in-progress" version="v4.37.0" >}} +{{< roadmap-status stage="complete" version="v4.37.0" >}}  Feature List:  * [JWK's backed by X509 Certificate Chains](https://www.rfc-editor.org/rfc/rfc7517#section-4.7) -* Per-Client [Consent](https://openid.net/specs/openid-connect-core-1_0.html#Consent) Mode -  * Explicit -  * Implicit -  * Pre-Configured  * Hashed Client Secrets +* Per-Client [Consent](https://openid.net/specs/openid-connect-core-1_0.html#Consent) Mode: +  * Explicit: +    * The default +    * Always asks for end-user consent +  * Implicit: +    * Not expressly standards compliant +    * Never asks for end-user consent +    * Not compatible with the consent prompt type +  * Pre-Configured: +    * Allows users to save consent sessions for a duration configured by the administrator +    * Operates nearly identically to the explicit consent mode  ### Beta 6 diff --git a/internal/configuration/config.template.yml b/internal/configuration/config.template.yml index 26742f953..16ea13b17 100644 --- a/internal/configuration/config.template.yml +++ b/internal/configuration/config.template.yml @@ -972,10 +972,12 @@ notifier:          ## The policy to require for this client; one_factor or two_factor.          # authorization_policy: two_factor -        ## By default users cannot remember pre-configured consents. Setting this value to a period of time using a -        ## duration notation will enable users to remember consent for this client. The time configured is the amount -        ## of time the pre-configured consent is valid for granting new authorizations to the user. -        # pre_configured_consent_duration: +        ## The consent mode controls how consent is obtained. +        # consent_mode: auto + +        ## This value controls the duration a consent on this client remains remembered when the consent mode is +        ## configured as 'auto' or 'pre-configured'. +        # pre_configured_consent_duration: 1w          ## Audience this client is allowed to request.          # audience: [] diff --git a/internal/configuration/decode_hooks.go b/internal/configuration/decode_hooks.go index d3c5d5c44..3dca0409f 100644 --- a/internal/configuration/decode_hooks.go +++ b/internal/configuration/decode_hooks.go @@ -396,6 +396,10 @@ func StringToPrivateKeyHookFunc() mapstructure.DecodeHookFuncType {  				return nil, fmt.Errorf(errFmtDecodeHookCouldNotParseBasic, "*", expectedType, fmt.Errorf("the data is for a %T not a *%s", r, expectedType))  			} +			if err = r.Validate(); err != nil { +				return nil, fmt.Errorf(errFmtDecodeHookCouldNotParseBasic, "*", expectedType, err) +			} +  			return r, nil  		case *ecdsa.PrivateKey:  			if expectedType != expectedTypeECDSA { diff --git a/internal/configuration/provider_test.go b/internal/configuration/provider_test.go index 1f4b6db95..b5eb50ecc 100644 --- a/internal/configuration/provider_test.go +++ b/internal/configuration/provider_test.go @@ -219,6 +219,23 @@ func TestShouldLoadURLList(t *testing.T) {  	assert.Equal(t, "https://example.com", config.IdentityProviders.OIDC.CORS.AllowedOrigins[1].String())  } +func TestShouldConfigureConsent(t *testing.T) { +	testReset() + +	val := schema.NewStructValidator() +	keys, config, err := Load(val, NewDefaultSources([]string{"./test_resources/config_oidc.yml"}, DefaultEnvPrefix, DefaultEnvDelimiter)...) + +	assert.NoError(t, err) + +	validator.ValidateKeys(keys, DefaultEnvPrefix, val) + +	assert.Len(t, val.Errors(), 0) +	assert.Len(t, val.Warnings(), 0) + +	require.Len(t, config.IdentityProviders.OIDC.Clients, 1) +	assert.Equal(t, config.IdentityProviders.OIDC.Clients[0].ConsentMode, "explicit") +} +  func TestShouldValidateAndRaiseErrorsOnBadConfiguration(t *testing.T) {  	testReset() diff --git a/internal/configuration/schema/identity_providers.go b/internal/configuration/schema/identity_providers.go index 6c5f461f2..916dbd6b7 100644 --- a/internal/configuration/schema/identity_providers.go +++ b/internal/configuration/schema/identity_providers.go @@ -61,7 +61,8 @@ type OpenIDConnectClientConfiguration struct {  	Policy string `koanf:"authorization_policy"` -	PreConfiguredConsentDuration *time.Duration `koanf:"pre_configured_consent_duration"` +	ConsentMode                  string         `koanf:"consent_mode"` +	ConsentPreConfiguredDuration *time.Duration `koanf:"pre_configured_consent_duration"`  }  // DefaultOpenIDConnectConfiguration contains defaults for OIDC. @@ -73,6 +74,8 @@ var DefaultOpenIDConnectConfiguration = OpenIDConnectConfiguration{  	EnforcePKCE:           "public_clients_only",  } +var defaultOIDCClientConsentPreConfiguredDuration = time.Hour * 24 * 7 +  // DefaultOpenIDConnectClientConfiguration contains defaults for OIDC Clients.  var DefaultOpenIDConnectClientConfiguration = OpenIDConnectClientConfiguration{  	Policy:        "two_factor", @@ -81,5 +84,7 @@ var DefaultOpenIDConnectClientConfiguration = OpenIDConnectClientConfiguration{  	ResponseTypes: []string{"code"},  	ResponseModes: []string{"form_post", "query", "fragment"}, -	UserinfoSigningAlgorithm: "none", +	UserinfoSigningAlgorithm:     "none", +	ConsentMode:                  "auto", +	ConsentPreConfiguredDuration: &defaultOIDCClientConsentPreConfiguredDuration,  } diff --git a/internal/configuration/schema/keys.go b/internal/configuration/schema/keys.go index ffcf2840f..618a74403 100644 --- a/internal/configuration/schema/keys.go +++ b/internal/configuration/schema/keys.go @@ -45,6 +45,7 @@ var Keys = []string{  	"identity_providers.oidc.clients[].response_modes",  	"identity_providers.oidc.clients[].userinfo_signing_algorithm",  	"identity_providers.oidc.clients[].authorization_policy", +	"identity_providers.oidc.clients[].consent_mode",  	"identity_providers.oidc.clients[].pre_configured_consent_duration",  	"authentication_backend.password_reset.disable",  	"authentication_backend.password_reset.custom_url", diff --git a/internal/configuration/test_resources/config_oidc.yml b/internal/configuration/test_resources/config_oidc.yml index 974df781e..840894586 100644 --- a/internal/configuration/test_resources/config_oidc.yml +++ b/internal/configuration/test_resources/config_oidc.yml @@ -130,4 +130,8 @@ identity_providers:        allowed_origins:          - https://google.com          - https://example.com +    clients: +      - id: abc +        secret: 123 +        consent_mode: explicit  ... diff --git a/internal/configuration/validator/const.go b/internal/configuration/validator/const.go index adc19f36f..30a7ab357 100644 --- a/internal/configuration/validator/const.go +++ b/internal/configuration/validator/const.go @@ -135,10 +135,11 @@ const (  const (  	errFmtOIDCNoClientsConfigured = "identity_providers: oidc: option 'clients' must have one or " +  		"more clients configured" -	errFmtOIDCNoPrivateKey            = "identity_providers: oidc: option 'issuer_private_key' is required" -	errFmtOIDCCertificateMismatch     = "identity_providers: oidc: option 'issuer_private_key' does not appear to be the private key the certificate provided by option 'issuer_certificate_chain'" -	errFmtOIDCCertificateChain        = "identity_providers: oidc: option 'issuer_certificate_chain' produced an error during validation of the chain: %w" -	errFmtOIDCEnforcePKCEInvalidValue = "identity_providers: oidc: option 'enforce_pkce' must be 'never', " + +	errFmtOIDCNoPrivateKey             = "identity_providers: oidc: option 'issuer_private_key' is required" +	errFmtOIDCInvalidPrivateKeyBitSize = "identity_providers: oidc: option 'issuer_private_key' must be an RSA private key with %d bits or more but it only has %d bits" +	errFmtOIDCCertificateMismatch      = "identity_providers: oidc: option 'issuer_private_key' does not appear to be the private key the certificate provided by option 'issuer_certificate_chain'" +	errFmtOIDCCertificateChain         = "identity_providers: oidc: option 'issuer_certificate_chain' produced an error during validation of the chain: %w" +	errFmtOIDCEnforcePKCEInvalidValue  = "identity_providers: oidc: option 'enforce_pkce' must be 'never', " +  		"'public_clients_only' or 'always', but it is configured as '%s'"  	errFmtOIDCCORSInvalidOrigin                    = "identity_providers: oidc: cors: option 'allowed_origins' contains an invalid value '%s' as it has a %s: origins must only be scheme, hostname, and an optional port" @@ -165,6 +166,8 @@ const (  		"invalid value: redirect uri '%s' must have the scheme 'http' or 'https' but it has no scheme"  	errFmtOIDCClientInvalidPolicy = "identity_providers: oidc: client '%s': option 'policy' must be 'one_factor' " +  		"or 'two_factor' but it is configured as '%s'" +	errFmtOIDCClientInvalidConsentMode = "identity_providers: oidc: client '%s': consent: option 'mode' must be one of " + +		"'%s' but it is configured as '%s'"  	errFmtOIDCClientInvalidEntry = "identity_providers: oidc: client '%s': option '%s' must only have the values " +  		"'%s' but one option is configured as '%s'"  	errFmtOIDCClientInvalidUserinfoAlgorithm = "identity_providers: oidc: client '%s': option " + @@ -346,15 +349,14 @@ var (  var validDefault2FAMethods = []string{"totp", "webauthn", "mobile_push"} -var validOIDCScopes = []string{oidc.ScopeOpenID, oidc.ScopeEmail, oidc.ScopeProfile, oidc.ScopeGroups, oidc.ScopeOfflineAccess} - -var validOIDCGrantTypes = []string{"implicit", "refresh_token", "authorization_code", "password", "client_credentials"} - -var validOIDCResponseModes = []string{"form_post", "query", "fragment"} - -var validOIDCUserinfoAlgorithms = []string{"none", "RS256"} - -var validOIDCCORSEndpoints = []string{oidc.AuthorizationEndpoint, oidc.TokenEndpoint, oidc.IntrospectionEndpoint, oidc.RevocationEndpoint, oidc.UserinfoEndpoint} +var ( +	validOIDCScopes             = []string{oidc.ScopeOpenID, oidc.ScopeEmail, oidc.ScopeProfile, oidc.ScopeGroups, oidc.ScopeOfflineAccess} +	validOIDCGrantTypes         = []string{oidc.GrantTypeImplicit, oidc.GrantTypeRefreshToken, oidc.GrantTypeAuthorizationCode, oidc.GrantTypePassword, oidc.GrantTypeClientCredentials} +	validOIDCResponseModes      = []string{oidc.ResponseModeFormPost, oidc.ResponseModeQuery, oidc.ResponseModeFragment} +	validOIDCUserinfoAlgorithms = []string{oidc.SigningAlgorithmNone, oidc.SigningAlgorithmRSAWithSHA256} +	validOIDCCORSEndpoints      = []string{oidc.EndpointAuthorization, oidc.EndpointToken, oidc.EndpointIntrospection, oidc.EndpointRevocation, oidc.EndpointUserinfo} +	validOIDCClientConsentModes = []string{"auto", oidc.ClientConsentModeImplicit.String(), oidc.ClientConsentModeExplicit.String(), oidc.ClientConsentModePreConfigured.String()} +)  var reKeyReplacer = regexp.MustCompile(`\[\d+]`) diff --git a/internal/configuration/validator/identity_providers.go b/internal/configuration/validator/identity_providers.go index ae0f4808c..bd1fcacaa 100644 --- a/internal/configuration/validator/identity_providers.go +++ b/internal/configuration/validator/identity_providers.go @@ -7,6 +7,7 @@ import (  	"time"  	"github.com/authelia/authelia/v4/internal/configuration/schema" +	"github.com/authelia/authelia/v4/internal/oidc"  	"github.com/authelia/authelia/v4/internal/utils"  ) @@ -22,15 +23,22 @@ func validateOIDC(config *schema.OpenIDConnectConfiguration, validator *schema.S  	setOIDCDefaults(config) -	if config.IssuerPrivateKey == nil { +	switch { +	case config.IssuerPrivateKey == nil:  		validator.Push(fmt.Errorf(errFmtOIDCNoPrivateKey)) -	} else if config.IssuerCertificateChain.HasCertificates() { -		if !config.IssuerCertificateChain.EqualKey(config.IssuerPrivateKey) { -			validator.Push(fmt.Errorf(errFmtOIDCCertificateMismatch)) +	default: +		if config.IssuerCertificateChain.HasCertificates() { +			if !config.IssuerCertificateChain.EqualKey(config.IssuerPrivateKey) { +				validator.Push(fmt.Errorf(errFmtOIDCCertificateMismatch)) +			} + +			if err := config.IssuerCertificateChain.Validate(); err != nil { +				validator.Push(fmt.Errorf(errFmtOIDCCertificateChain, err)) +			}  		} -		if err := config.IssuerCertificateChain.Validate(); err != nil { -			validator.Push(fmt.Errorf(errFmtOIDCCertificateChain, err)) +		if config.IssuerPrivateKey.Size()*8 < 2048 { +			validator.Push(fmt.Errorf(errFmtOIDCInvalidPrivateKeyBitSize, 2048, config.IssuerPrivateKey.Size()*8))  		}  	} @@ -132,6 +140,7 @@ func validateOIDCOptionsCORSEndpoints(config *schema.OpenIDConnectConfiguration,  	}  } +//nolint:gocyclo // TODO: Refactor.  func validateOIDCClients(config *schema.OpenIDConnectConfiguration, validator *schema.StructValidator) {  	invalidID, duplicateIDs := false, false @@ -167,6 +176,23 @@ func validateOIDCClients(config *schema.OpenIDConnectConfiguration, validator *s  			validator.Push(fmt.Errorf(errFmtOIDCClientInvalidPolicy, client.ID, client.Policy))  		} +		switch { +		case utils.IsStringInSlice(client.ConsentMode, []string{"", "auto"}): +			if client.ConsentPreConfiguredDuration != nil { +				config.Clients[c].ConsentMode = oidc.ClientConsentModePreConfigured.String() +			} else { +				config.Clients[c].ConsentMode = oidc.ClientConsentModeExplicit.String() +			} +		case utils.IsStringInSlice(client.ConsentMode, validOIDCClientConsentModes): +			break +		default: +			validator.Push(fmt.Errorf(errFmtOIDCClientInvalidConsentMode, client.ID, strings.Join(append(validOIDCClientConsentModes, "auto"), "', '"), client.ConsentMode)) +		} + +		if client.ConsentPreConfiguredDuration == nil { +			config.Clients[c].ConsentPreConfiguredDuration = schema.DefaultOpenIDConnectClientConfiguration.ConsentPreConfiguredDuration +		} +  		validateOIDCClientSectorIdentifier(client, validator)  		validateOIDCClientScopes(c, config, validator)  		validateOIDCClientGrantTypes(c, config, validator) @@ -227,8 +253,8 @@ func validateOIDCClientScopes(c int, configuration *schema.OpenIDConnectConfigur  		return  	} -	if !utils.IsStringInSlice("openid", configuration.Clients[c].Scopes) { -		configuration.Clients[c].Scopes = append(configuration.Clients[c].Scopes, "openid") +	if !utils.IsStringInSlice(oidc.ScopeOpenID, configuration.Clients[c].Scopes) { +		configuration.Clients[c].Scopes = append(configuration.Clients[c].Scopes, oidc.ScopeOpenID)  	}  	for _, scope := range configuration.Clients[c].Scopes { diff --git a/internal/configuration/validator/identity_providers_test.go b/internal/configuration/validator/identity_providers_test.go index e8f7d0404..eb913a279 100644 --- a/internal/configuration/validator/identity_providers_test.go +++ b/internal/configuration/validator/identity_providers_test.go @@ -39,9 +39,9 @@ func TestShouldNotRaiseErrorWhenCORSEndpointsValid(t *testing.T) {  	config := &schema.IdentityProvidersConfiguration{  		OIDC: &schema.OpenIDConnectConfiguration{  			HMACSecret:       "rLABDrx87et5KvRHVUgTm3pezWWd8LMN", -			IssuerPrivateKey: &rsa.PrivateKey{}, +			IssuerPrivateKey: MustParseRSAPrivateKey(testKey1),  			CORS: schema.OpenIDConnectCORSConfiguration{ -				Endpoints: []string{oidc.AuthorizationEndpoint, oidc.TokenEndpoint, oidc.IntrospectionEndpoint, oidc.RevocationEndpoint, oidc.UserinfoEndpoint}, +				Endpoints: []string{oidc.EndpointAuthorization, oidc.EndpointToken, oidc.EndpointIntrospection, oidc.EndpointRevocation, oidc.EndpointUserinfo},  			},  			Clients: []schema.OpenIDConnectClientConfiguration{  				{ @@ -62,9 +62,9 @@ func TestShouldRaiseErrorWhenCORSEndpointsNotValid(t *testing.T) {  	config := &schema.IdentityProvidersConfiguration{  		OIDC: &schema.OpenIDConnectConfiguration{  			HMACSecret:       "rLABDrx87et5KvRHVUgTm3pezWWd8LMN", -			IssuerPrivateKey: &rsa.PrivateKey{}, +			IssuerPrivateKey: MustParseRSAPrivateKey(testKey1),  			CORS: schema.OpenIDConnectCORSConfiguration{ -				Endpoints: []string{oidc.AuthorizationEndpoint, oidc.TokenEndpoint, oidc.IntrospectionEndpoint, oidc.RevocationEndpoint, oidc.UserinfoEndpoint, "invalid_endpoint"}, +				Endpoints: []string{oidc.EndpointAuthorization, oidc.EndpointToken, oidc.EndpointIntrospection, oidc.EndpointRevocation, oidc.EndpointUserinfo, "invalid_endpoint"},  			},  			Clients: []schema.OpenIDConnectClientConfiguration{  				{ @@ -87,7 +87,7 @@ func TestShouldRaiseErrorWhenOIDCPKCEEnforceValueInvalid(t *testing.T) {  	config := &schema.IdentityProvidersConfiguration{  		OIDC: &schema.OpenIDConnectConfiguration{  			HMACSecret:       "rLABDrx87et5KvRHVUgTm3pezWWd8LMN", -			IssuerPrivateKey: &rsa.PrivateKey{}, +			IssuerPrivateKey: MustParseRSAPrivateKey(testKey1),  			EnforcePKCE:      testInvalid,  		},  	} @@ -106,7 +106,7 @@ func TestShouldRaiseErrorWhenOIDCCORSOriginsHasInvalidValues(t *testing.T) {  	config := &schema.IdentityProvidersConfiguration{  		OIDC: &schema.OpenIDConnectConfiguration{  			HMACSecret:       "rLABDrx87et5KvRHVUgTm3pezWWd8LMN", -			IssuerPrivateKey: &rsa.PrivateKey{}, +			IssuerPrivateKey: MustParseRSAPrivateKey(testKey1),  			CORS: schema.OpenIDConnectCORSConfiguration{  				AllowedOrigins:                       utils.URLsFromStringSlice([]string{"https://example.com/", "https://site.example.com/subpath", "https://site.example.com?example=true", "*"}),  				AllowedOriginsFromClientRedirectURIs: true, @@ -142,7 +142,7 @@ func TestShouldRaiseErrorWhenOIDCServerNoClients(t *testing.T) {  	config := &schema.IdentityProvidersConfiguration{  		OIDC: &schema.OpenIDConnectConfiguration{  			HMACSecret:       "rLABDrx87et5KvRHVUgTm3pezWWd8LMN", -			IssuerPrivateKey: &rsa.PrivateKey{}, +			IssuerPrivateKey: MustParseRSAPrivateKey(testKey1),  		},  	} @@ -322,7 +322,7 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) {  			config := &schema.IdentityProvidersConfiguration{  				OIDC: &schema.OpenIDConnectConfiguration{  					HMACSecret:       "rLABDrx87et5KvRHVUgTm3pezWWd8LMN", -					IssuerPrivateKey: &rsa.PrivateKey{}, +					IssuerPrivateKey: MustParseRSAPrivateKey(testKey1),  					Clients:          tc.Clients,  				},  			} @@ -346,7 +346,7 @@ func TestShouldRaiseErrorWhenOIDCClientConfiguredWithBadScopes(t *testing.T) {  	config := &schema.IdentityProvidersConfiguration{  		OIDC: &schema.OpenIDConnectConfiguration{  			HMACSecret:       "rLABDrx87et5KvRHVUgTm3pezWWd8LMN", -			IssuerPrivateKey: &rsa.PrivateKey{}, +			IssuerPrivateKey: MustParseRSAPrivateKey(testKey1),  			Clients: []schema.OpenIDConnectClientConfiguration{  				{  					ID:     "good_id", @@ -372,7 +372,7 @@ func TestShouldRaiseErrorWhenOIDCClientConfiguredWithBadGrantTypes(t *testing.T)  	config := &schema.IdentityProvidersConfiguration{  		OIDC: &schema.OpenIDConnectConfiguration{  			HMACSecret:       "rLABDrx87et5KvRHVUgTm3pezWWd8LMN", -			IssuerPrivateKey: &rsa.PrivateKey{}, +			IssuerPrivateKey: MustParseRSAPrivateKey(testKey1),  			Clients: []schema.OpenIDConnectClientConfiguration{  				{  					ID:         "good_id", @@ -398,8 +398,8 @@ func TestShouldNotErrorOnCertificateValid(t *testing.T) {  	config := &schema.IdentityProvidersConfiguration{  		OIDC: &schema.OpenIDConnectConfiguration{  			HMACSecret:             "rLABDrx87et5KvRHVUgTm3pezWWd8LMN", -			IssuerCertificateChain: mustParseX509CertificateChain(testCert1), -			IssuerPrivateKey:       mustParseRSAPrivateKey(testKey1), +			IssuerCertificateChain: MustParseX509CertificateChain(testCert1), +			IssuerPrivateKey:       MustParseRSAPrivateKey(testKey1),  			Clients: []schema.OpenIDConnectClientConfiguration{  				{  					ID:     "good_id", @@ -424,8 +424,8 @@ func TestShouldRaiseErrorOnCertificateNotValid(t *testing.T) {  	config := &schema.IdentityProvidersConfiguration{  		OIDC: &schema.OpenIDConnectConfiguration{  			HMACSecret:             "rLABDrx87et5KvRHVUgTm3pezWWd8LMN", -			IssuerCertificateChain: mustParseX509CertificateChain(testCert1), -			IssuerPrivateKey:       mustParseRSAPrivateKey(testKey2), +			IssuerCertificateChain: MustParseX509CertificateChain(testCert1), +			IssuerPrivateKey:       MustParseRSAPrivateKey(testKey2),  			Clients: []schema.OpenIDConnectClientConfiguration{  				{  					ID:     "good_id", @@ -447,12 +447,39 @@ func TestShouldRaiseErrorOnCertificateNotValid(t *testing.T) {  	assert.EqualError(t, validator.Errors()[0], "identity_providers: oidc: option 'issuer_private_key' does not appear to be the private key the certificate provided by option 'issuer_certificate_chain'")  } +func TestShouldRaiseErrorOnKeySizeTooSmall(t *testing.T) { +	validator := schema.NewStructValidator() +	config := &schema.IdentityProvidersConfiguration{ +		OIDC: &schema.OpenIDConnectConfiguration{ +			HMACSecret:       "rLABDrx87et5KvRHVUgTm3pezWWd8LMN", +			IssuerPrivateKey: MustParseRSAPrivateKey(testKey3), +			Clients: []schema.OpenIDConnectClientConfiguration{ +				{ +					ID:     "good_id", +					Secret: "good_secret", +					Policy: "two_factor", +					RedirectURIs: []string{ +						"https://google.com/callback", +					}, +				}, +			}, +		}, +	} + +	ValidateIdentityProviders(config, validator) + +	assert.Len(t, validator.Warnings(), 0) +	require.Len(t, validator.Errors(), 1) + +	assert.EqualError(t, validator.Errors()[0], "identity_providers: oidc: option 'issuer_private_key' must be an RSA private key with 2048 bits or more but it only has 1024 bits") +} +  func TestShouldRaiseErrorWhenOIDCClientConfiguredWithBadResponseModes(t *testing.T) {  	validator := schema.NewStructValidator()  	config := &schema.IdentityProvidersConfiguration{  		OIDC: &schema.OpenIDConnectConfiguration{  			HMACSecret:       "rLABDrx87et5KvRHVUgTm3pezWWd8LMN", -			IssuerPrivateKey: &rsa.PrivateKey{}, +			IssuerPrivateKey: MustParseRSAPrivateKey(testKey1),  			Clients: []schema.OpenIDConnectClientConfiguration{  				{  					ID:            "good_id", @@ -478,7 +505,7 @@ func TestShouldRaiseErrorWhenOIDCClientConfiguredWithBadUserinfoAlg(t *testing.T  	config := &schema.IdentityProvidersConfiguration{  		OIDC: &schema.OpenIDConnectConfiguration{  			HMACSecret:       "rLABDrx87et5KvRHVUgTm3pezWWd8LMN", -			IssuerPrivateKey: &rsa.PrivateKey{}, +			IssuerPrivateKey: MustParseRSAPrivateKey(testKey1),  			Clients: []schema.OpenIDConnectClientConfiguration{  				{  					ID:                       "good_id", @@ -504,7 +531,7 @@ func TestValidateIdentityProvidersShouldRaiseWarningOnSecurityIssue(t *testing.T  	config := &schema.IdentityProvidersConfiguration{  		OIDC: &schema.OpenIDConnectConfiguration{  			HMACSecret:              "abc", -			IssuerPrivateKey:        &rsa.PrivateKey{}, +			IssuerPrivateKey:        MustParseRSAPrivateKey(testKey1),  			MinimumParameterEntropy: 1,  			Clients: []schema.OpenIDConnectClientConfiguration{  				{ @@ -532,7 +559,7 @@ func TestValidateIdentityProvidersShouldRaiseErrorsOnInvalidClientTypes(t *testi  	config := &schema.IdentityProvidersConfiguration{  		OIDC: &schema.OpenIDConnectConfiguration{  			HMACSecret:       "hmac1", -			IssuerPrivateKey: &rsa.PrivateKey{}, +			IssuerPrivateKey: MustParseRSAPrivateKey(testKey1),  			Clients: []schema.OpenIDConnectClientConfiguration{  				{  					ID:     "client-with-invalid-secret", @@ -570,7 +597,7 @@ func TestValidateIdentityProvidersShouldNotRaiseErrorsOnValidPublicClients(t *te  	config := &schema.IdentityProvidersConfiguration{  		OIDC: &schema.OpenIDConnectConfiguration{  			HMACSecret:       "hmac1", -			IssuerPrivateKey: &rsa.PrivateKey{}, +			IssuerPrivateKey: MustParseRSAPrivateKey(testKey1),  			Clients: []schema.OpenIDConnectClientConfiguration{  				{  					ID:     "installed-app-client", @@ -611,7 +638,7 @@ func TestValidateIdentityProvidersShouldSetDefaultValues(t *testing.T) {  	config := &schema.IdentityProvidersConfiguration{  		OIDC: &schema.OpenIDConnectConfiguration{  			HMACSecret:       "rLABDrx87et5KvRHVUgTm3pezWWd8LMN", -			IssuerPrivateKey: &rsa.PrivateKey{}, +			IssuerPrivateKey: MustParseRSAPrivateKey(testKey1),  			Clients: []schema.OpenIDConnectClientConfiguration{  				{  					ID:     "a-client", @@ -748,6 +775,34 @@ func TestValidateOIDCClientRedirectURIsSupportingPrivateUseURISchemes(t *testing  	})  } +func MustParseRSAPrivateKey(data string) *rsa.PrivateKey { +	block, _ := pem.Decode([]byte(data)) +	if block == nil || block.Bytes == nil || len(block.Bytes) == 0 { +		panic("not pem encoded") +	} + +	if block.Type != "RSA PRIVATE KEY" { +		panic("not private key") +	} + +	key, err := x509.ParsePKCS1PrivateKey(block.Bytes) +	if err != nil { +		panic(err) +	} + +	return key +} + +func MustParseX509CertificateChain(data string) schema.X509CertificateChain { +	chain, err := schema.NewX509CertificateChain(data) + +	if err != nil { +		panic(err) +	} + +	return *chain +} +  var (  	testCert1 = `  -----BEGIN CERTIFICATE----- @@ -826,32 +881,20 @@ tQnOWwKBgQC40yZY0PcjuILhy+sIc0Wvh7LUA7taSdTye149kRvbvsCDN7Jh75lM  USjhLXY0Nld2zBm9r8wMb81mXH29uvD+tDqqsICvyuKlA/tyzXR+QTr7dCVKVwu0  1YjCJ36UpTsLre2f8nOSLtNmRfDPtbOE2mkOoO9dD9UU0XZwnvn9xw==  -----END RSA PRIVATE KEY-----` -) - -func mustParseRSAPrivateKey(data string) *rsa.PrivateKey { -	block, _ := pem.Decode([]byte(data)) -	if block == nil || block.Bytes == nil || len(block.Bytes) == 0 { -		panic("not pem encoded") -	} -	if block.Type != "RSA PRIVATE KEY" { -		panic("not private key") -	} - -	key, err := x509.ParsePKCS1PrivateKey(block.Bytes) -	if err != nil { -		panic(err) -	} - -	return key -} - -func mustParseX509CertificateChain(data string) schema.X509CertificateChain { -	chain, err := schema.NewX509CertificateChain(data) - -	if err != nil { -		panic(err) -	} - -	return *chain -} +	testKey3 = `-----BEGIN RSA PRIVATE KEY----- +MIICXgIBAAKBgQDBi7fdmUmlpWklpgAvNUdhDrpsDVqAHuEzVApK6f6ohYAi0/q2 ++YmOwyPKDSrOc6Sy1myJtV3FbZGvYaQhnokc4bnkS9DH0lY+6Hk2vKps5PrhRY/q +1EjnfwXvzhAzb25rGFwKcSvfvndMTVvxgqXVob+3pRt9maD6HFHAh2/NCQIDAQAB +AoGACT2bfLgJ3R/FomeHkLlxe//RBMGqdX2D8QhtKWB8qR0engsS6FOHrspAVjBE +v/Cjh2pXake/f2KY1w/JX1WLZEFXja2RFPeeDiiC/4S7pKCySUVeHO9rQ4SY5Frg +/s/QWWtmq7+1iu2DXhdGJA6fIurzSoDgUXo3NGFCYqIFaAECQQDUi9AAgEljmc2q +dAUQD0KNTcJFkpTafhfPiYc2GT1vS/bArtXRmvJmbIiRfVuGbM8z5ES7JGd5FyYL +i2WCCzUBAkEA6R14GVhN8NIPWEUrzjgOvjKlc2ZHskT3dYb3djpm69TK7GjLtHyq +qO7l4VJowsXI+o/6YucagF6+rH0O0VrwCQJBAM8twYDbi63knA8MrGqtFUg7haTf +bu1Tf84y1nOrQrEcMNg9E/sOuD2SicSXlwF/SrHgTgbFQ39LSzBxnm6WkgECQQCh +AQmB98tdGLggbyXiODV2h+Rd37aFGb0QHzerIIsVNtMwlPCcp733D4kWJqTUYWZ+ +KBL3XEahgs6Os5EYZ4aBAkEAjKE+2/nBYUdHVusjMXeNsE5rqwJND5zvYzmToG7+ +xhv4RUAe4dHL4IDQoQRjhr3Nw+JYvtzBx0Iq/178xMnGKg== +-----END RSA PRIVATE KEY-----` +) diff --git a/internal/handlers/const.go b/internal/handlers/const.go index f2514a080..ae9e4d603 100644 --- a/internal/handlers/const.go +++ b/internal/handlers/const.go @@ -29,6 +29,19 @@ var (  )  const ( +	queryArgRD         = "rd" +	queryArgID         = "id" +	queryArgConsentID  = "consent_id" +	queryArgWorkflow   = "workflow" +	queryArgWorkflowID = "workflow_id" +) + +var ( +	qryArgID        = []byte(queryArgID) +	qryArgConsentID = []byte(queryArgConsentID) +) + +const (  	// Forbidden means the user is forbidden the access to a resource.  	Forbidden authorizationMatching = iota  	// NotAuthorized means the user can access the resource with more permissions. @@ -63,6 +76,39 @@ const (  )  const ( +	logFmtAuthorizationPrefix = "Authorization Request with id '%s' on client with id '%s' " + +	logFmtErrConsentCantDetermineConsentMode = logFmtAuthorizationPrefix + "could not be processed: error occurred generating consent: client consent mode could not be reliably determined" + +	logFmtConsentPrefix = logFmtAuthorizationPrefix + "using consent mode '%s' " + +	logFmtErrConsentParseChallengeID = logFmtConsentPrefix + "could not be processed: error occurred parsing the consent id (challenge) '%s': %+v" +	logFmtErrConsentPreConfLookup    = logFmtConsentPrefix + "had error looking up pre-configured consent sessions: %+v" +	logFmtErrConsentPreConfRowsClose = logFmtConsentPrefix + "had error closing rows while looking up pre-configured consent sessions: %+v" +	logFmtErrConsentZeroID           = logFmtConsentPrefix + "could not be processed: the consent id had a zero value" +	logFmtErrConsentCantGetSubject   = logFmtConsentPrefix + "could not be processed: error occurred retrieving subject identifier for user '%s' and sector identifier '%s': %+v" +	logFmtErrConsentGenerateError    = logFmtConsentPrefix + "could not be processed: error occurred %s consent: %+v" + +	logFmtDbgConsentGenerate                  = logFmtConsentPrefix + "proceeding to generate a new consent session" +	logFmtDbgConsentAuthenticationSufficiency = logFmtConsentPrefix + "authentication level '%s' is %s for client level '%s'" +	logFmtDbgConsentRedirect                  = logFmtConsentPrefix + "is being redirected to '%s'" +	logFmtDbgConsentPreConfSuccessfulLookup   = logFmtConsentPrefix + "successfully looked up pre-configured consent with signature of client id '%s' and subject '%s' and scopes '%s' with id '%d'" +	logFmtDbgConsentPreConfUnsuccessfulLookup = logFmtConsentPrefix + "unsuccessfully looked up pre-configured consent with signature of client id '%s' and subject '%s' and scopes '%s'" +	logFmtDbgConsentPreConfTryingLookup       = logFmtConsentPrefix + "attempting to discover pre-configurations with signature of client id '%s' and subject '%s' and scopes '%s'" + +	logFmtErrConsentWithIDCouldNotBeProcessed = logFmtConsentPrefix + "could not be processed: error occurred performing consent for consent session with id '%s': " + +	logFmtErrConsentLookupLoadingSession        = logFmtErrConsentWithIDCouldNotBeProcessed + "error occurred while loading session: %+v" +	logFmtErrConsentSessionSubjectNotAuthorized = logFmtErrConsentWithIDCouldNotBeProcessed + "user '%s' with subject '%s' is not authorized to consent for subject '%s'" +	logFmtErrConsentCantGrant                   = logFmtErrConsentWithIDCouldNotBeProcessed + "the session does not appear to be valid for %s consent: either the subject is null, the consent has already been granted, or the consent session is a pre-configured session" +	logFmtErrConsentCantGrantPreConf            = logFmtErrConsentWithIDCouldNotBeProcessed + "the session does not appear to be valid for pre-configured consent: either the subject is null, the consent has been granted and is either not pre-configured, or the pre-configuration is expired" +	logFmtErrConsentCantGrantRejected           = logFmtErrConsentWithIDCouldNotBeProcessed + "the user explicitly rejected this consent session" +	logFmtErrConsentSaveSessionResponse         = logFmtErrConsentWithIDCouldNotBeProcessed + "error occurred saving consent session response: %+v" +	logFmtErrConsentSaveSession                 = logFmtErrConsentWithIDCouldNotBeProcessed + "error occurred saving consent session: %+v" +	logFmtErrConsentGenerate                    = logFmtConsentPrefix + "could not be processed: error occurred generating consent: %+v" +) + +const (  	testInactivity     = time.Second * 10  	testRedirectionURL = "http://redirection.local"  	testUsername       = "john" diff --git a/internal/handlers/handler_checks_safe_redirection.go b/internal/handlers/handler_checks_safe_redirection.go index d53055965..96b66114e 100644 --- a/internal/handlers/handler_checks_safe_redirection.go +++ b/internal/handlers/handler_checks_safe_redirection.go @@ -3,7 +3,6 @@ package handlers  import (  	"fmt" -	"github.com/authelia/authelia/v4/internal/authentication"  	"github.com/authelia/authelia/v4/internal/middlewares"  	"github.com/authelia/authelia/v4/internal/utils"  ) @@ -12,7 +11,7 @@ import (  func CheckSafeRedirectionPOST(ctx *middlewares.AutheliaCtx) {  	userSession := ctx.GetSession() -	if userSession.AuthenticationLevel == authentication.NotAuthenticated { +	if userSession.IsAnonymous() {  		ctx.ReplyUnauthorized()  		return  	} diff --git a/internal/handlers/handler_firstfactor.go b/internal/handlers/handler_firstfactor.go index 967a9953f..f52119576 100644 --- a/internal/handlers/handler_firstfactor.go +++ b/internal/handlers/handler_firstfactor.go @@ -23,7 +23,7 @@ func FirstFactorPOST(delayFunc middlewares.TimingAttackDelayFunc) middlewares.Re  			defer delayFunc(ctx, requestTime, &successful)  		} -		bodyJSON := firstFactorRequestBody{} +		bodyJSON := bodyFirstFactorRequest{}  		if err := ctx.ParseBody(&bodyJSON); err != nil {  			ctx.Logger.Errorf(logFmtErrParseRequestBody, regulation.AuthType1FA, err) @@ -136,7 +136,7 @@ func FirstFactorPOST(delayFunc middlewares.TimingAttackDelayFunc) middlewares.Re  		successful = true  		if bodyJSON.Workflow == workflowOpenIDConnect { -			handleOIDCWorkflowResponse(ctx, bodyJSON.TargetURL) +			handleOIDCWorkflowResponse(ctx, bodyJSON.TargetURL, bodyJSON.WorkflowID)  		} else {  			Handle1FAResponse(ctx, bodyJSON.TargetURL, bodyJSON.RequestMethod, userSession.Username, userSession.Groups)  		} diff --git a/internal/handlers/handler_oauth_introspection.go b/internal/handlers/handler_oauth_introspection.go index f53e5a195..a131f7398 100644 --- a/internal/handlers/handler_oauth_introspection.go +++ b/internal/handlers/handler_oauth_introspection.go @@ -20,12 +20,12 @@ func OAuthIntrospectionPOST(ctx *middlewares.AutheliaCtx, rw http.ResponseWriter  	oidcSession := oidc.NewSession() -	if responder, err = ctx.Providers.OpenIDConnect.Fosite.NewIntrospectionRequest(ctx, req, oidcSession); err != nil { +	if responder, err = ctx.Providers.OpenIDConnect.NewIntrospectionRequest(ctx, req, oidcSession); err != nil {  		rfc := fosite.ErrorToRFC6749Error(err)  		ctx.Logger.Errorf("Introspection Request failed with error: %s", rfc.WithExposeDebug(true).GetDescription()) -		ctx.Providers.OpenIDConnect.Fosite.WriteIntrospectionError(rw, err) +		ctx.Providers.OpenIDConnect.WriteIntrospectionError(rw, err)  		return  	} @@ -34,5 +34,5 @@ func OAuthIntrospectionPOST(ctx *middlewares.AutheliaCtx, rw http.ResponseWriter  	ctx.Logger.Tracef("Introspection Request yeilded a %s (active: %t) requested at %s created with request id '%s' on client with id '%s'", responder.GetTokenUse(), responder.IsActive(), requester.GetRequestedAt().String(), requester.GetID(), requester.GetClient().GetID()) -	ctx.Providers.OpenIDConnect.Fosite.WriteIntrospectionResponse(rw, responder) +	ctx.Providers.OpenIDConnect.WriteIntrospectionResponse(rw, responder)  } diff --git a/internal/handlers/handler_oauth_revocation.go b/internal/handlers/handler_oauth_revocation.go index 95af94b9e..89d423311 100644 --- a/internal/handlers/handler_oauth_revocation.go +++ b/internal/handlers/handler_oauth_revocation.go @@ -14,11 +14,11 @@ import (  func OAuthRevocationPOST(ctx *middlewares.AutheliaCtx, rw http.ResponseWriter, req *http.Request) {  	var err error -	if err = ctx.Providers.OpenIDConnect.Fosite.NewRevocationRequest(ctx, req); err != nil { +	if err = ctx.Providers.OpenIDConnect.NewRevocationRequest(ctx, req); err != nil {  		rfc := fosite.ErrorToRFC6749Error(err)  		ctx.Logger.Errorf("Revocation Request failed with error: %s", rfc.WithExposeDebug(true).GetDescription())  	} -	ctx.Providers.OpenIDConnect.Fosite.WriteRevocationResponse(rw, err) +	ctx.Providers.OpenIDConnect.WriteRevocationResponse(rw, err)  } diff --git a/internal/handlers/handler_oidc_authorization.go b/internal/handlers/handler_oidc_authorization.go index 8d16146a5..7c24c7c8f 100644 --- a/internal/handlers/handler_oidc_authorization.go +++ b/internal/handlers/handler_oidc_authorization.go @@ -3,6 +3,7 @@ package handlers  import (  	"errors"  	"net/http" +	"net/url"  	"time"  	"github.com/ory/fosite" @@ -21,16 +22,16 @@ func OpenIDConnectAuthorizationGET(ctx *middlewares.AutheliaCtx, rw http.Respons  		responder fosite.AuthorizeResponder  		client    *oidc.Client  		authTime  time.Time -		issuer    string +		issuer    *url.URL  		err       error  	) -	if requester, err = ctx.Providers.OpenIDConnect.Fosite.NewAuthorizeRequest(ctx, r); err != nil { +	if requester, err = ctx.Providers.OpenIDConnect.NewAuthorizeRequest(ctx, r); err != nil {  		rfc := fosite.ErrorToRFC6749Error(err)  		ctx.Logger.Errorf("Authorization Request failed with error: %s", rfc.WithExposeDebug(true).GetDescription()) -		ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, err) +		ctx.Providers.OpenIDConnect.WriteAuthorizeError(rw, requester, err)  		return  	} @@ -39,22 +40,22 @@ func OpenIDConnectAuthorizationGET(ctx *middlewares.AutheliaCtx, rw http.Respons  	ctx.Logger.Debugf("Authorization Request with id '%s' on client with id '%s' is being processed", requester.GetID(), clientID) -	if client, err = ctx.Providers.OpenIDConnect.Store.GetFullClient(clientID); err != nil { +	if client, err = ctx.Providers.OpenIDConnect.GetFullClient(clientID); err != nil {  		if errors.Is(err, fosite.ErrNotFound) {  			ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' could not be processed: client was not found", requester.GetID(), clientID)  		} else {  			ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' could not be processed: failed to find client: %+v", requester.GetID(), clientID, err)  		} -		ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, err) +		ctx.Providers.OpenIDConnect.WriteAuthorizeError(rw, requester, err)  		return  	} -	if issuer, err = ctx.ExternalRootURL(); err != nil { +	if issuer, err = ctx.IssuerURL(); err != nil {  		ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' could not be processed: error occurred determining issuer: %+v", requester.GetID(), clientID, err) -		ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrServerError.WithHint("Could not determine issuer.")) +		ctx.Providers.OpenIDConnect.WriteAuthorizeError(rw, requester, oidc.ErrIssuerCouldNotDerive)  		return  	} @@ -75,7 +76,7 @@ func OpenIDConnectAuthorizationGET(ctx *middlewares.AutheliaCtx, rw http.Respons  	if authTime, err = userSession.AuthenticatedTime(client.Policy); err != nil {  		ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' could not be processed: error occurred checking authentication time: %+v", requester.GetID(), client.GetID(), err) -		ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrServerError.WithHint("Could not obtain the authentication time.")) +		ctx.Providers.OpenIDConnect.WriteAuthorizeError(rw, requester, fosite.ErrServerError.WithHint("Could not obtain the authentication time."))  		return  	} @@ -88,12 +89,12 @@ func OpenIDConnectAuthorizationGET(ctx *middlewares.AutheliaCtx, rw http.Respons  	ctx.Logger.Tracef("Authorization Request with id '%s' on client with id '%s' creating session for Authorization Response for subject '%s' with username '%s' with claims: %+v",  		requester.GetID(), oidcSession.ClientID, oidcSession.Subject, oidcSession.Username, oidcSession.Claims) -	if responder, err = ctx.Providers.OpenIDConnect.Fosite.NewAuthorizeResponse(ctx, requester, oidcSession); err != nil { +	if responder, err = ctx.Providers.OpenIDConnect.NewAuthorizeResponse(ctx, requester, oidcSession); err != nil {  		rfc := fosite.ErrorToRFC6749Error(err)  		ctx.Logger.Errorf("Authorization Response for Request with id '%s' on client with id '%s' could not be created: %s", requester.GetID(), clientID, rfc.WithExposeDebug(true).GetDescription()) -		ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, err) +		ctx.Providers.OpenIDConnect.WriteAuthorizeError(rw, requester, err)  		return  	} @@ -101,10 +102,10 @@ func OpenIDConnectAuthorizationGET(ctx *middlewares.AutheliaCtx, rw http.Respons  	if err = ctx.Providers.StorageProvider.SaveOAuth2ConsentSessionGranted(ctx, consent.ID); err != nil {  		ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' could not be processed: error occurred saving consent session: %+v", requester.GetID(), client.GetID(), err) -		ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrServerError.WithHint("Could not save the session.")) +		ctx.Providers.OpenIDConnect.WriteAuthorizeError(rw, requester, oidc.ErrConsentCouldNotSave)  		return  	} -	ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeResponse(rw, requester, responder) +	ctx.Providers.OpenIDConnect.WriteAuthorizeResponse(rw, requester, responder)  } diff --git a/internal/handlers/handler_oidc_authorization_consent.go b/internal/handlers/handler_oidc_authorization_consent.go index 3fdfb25d1..04dfacbb1 100644 --- a/internal/handlers/handler_oidc_authorization_consent.go +++ b/internal/handlers/handler_oidc_authorization_consent.go @@ -1,6 +1,7 @@  package handlers  import ( +	"errors"  	"fmt"  	"net/http"  	"net/url" @@ -16,115 +17,68 @@ import (  	"github.com/authelia/authelia/v4/internal/model"  	"github.com/authelia/authelia/v4/internal/oidc"  	"github.com/authelia/authelia/v4/internal/session" -	"github.com/authelia/authelia/v4/internal/storage"  	"github.com/authelia/authelia/v4/internal/utils"  ) -func handleOIDCAuthorizationConsent(ctx *middlewares.AutheliaCtx, rootURI string, client *oidc.Client, +func handleOIDCAuthorizationConsent(ctx *middlewares.AutheliaCtx, issuer *url.URL, client *oidc.Client,  	userSession session.UserSession,  	rw http.ResponseWriter, r *http.Request, requester fosite.AuthorizeRequester) (consent *model.OAuth2ConsentSession, handled bool) {  	var ( -		issuer  *url.URL  		subject uuid.UUID  		err     error  	) -	if issuer, err = url.ParseRequestURI(rootURI); err != nil { -		ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrServerError.WithHint("Could not safely determine the issuer.")) +	var handler handlerAuthorizationConsent -		return nil, true -	} - -	if !strings.HasSuffix(issuer.Path, "/") { -		issuer.Path += "/" -	} +	switch { +	case userSession.IsAnonymous(): +		handler = handleOIDCAuthorizationConsentNotAuthenticated +	case client.IsAuthenticationLevelSufficient(userSession.AuthenticationLevel): +		if subject, err = ctx.Providers.OpenIDConnect.GetSubject(ctx, client.GetSectorIdentifier(), userSession.Username); err != nil { +			ctx.Logger.Errorf(logFmtErrConsentCantGetSubject, requester.GetID(), client.GetID(), client.Consent, userSession.Username, client.GetSectorIdentifier(), err) -	// This prevents the consent request from being generated until the authentication level is sufficient. -	if !client.IsAuthenticationLevelSufficient(userSession.AuthenticationLevel) || userSession.Username == "" { -		redirectURL := getOIDCAuthorizationRedirectURL(issuer, requester) - -		ctx.Logger.Debugf("Authorization Request with id '%s' on client with id '%s' is being redirected due to insufficient authentication", requester.GetID(), client.GetID()) - -		http.Redirect(rw, r, redirectURL.String(), http.StatusFound) - -		return nil, true -	} - -	if subject, err = ctx.Providers.OpenIDConnect.Store.GetSubject(ctx, client.GetSectorIdentifier(), userSession.Username); err != nil { -		ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' could not be processed: error occurred retrieving subject identifier for user '%s' and sector identifier '%s': %+v", requester.GetID(), client.GetID(), userSession.Username, client.GetSectorIdentifier(), err) - -		ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrServerError.WithHint("Could not retrieve the subject.")) - -		return nil, true -	} - -	var consentIDBytes []byte - -	if consentIDBytes = ctx.QueryArgs().Peek("consent_id"); len(consentIDBytes) != 0 { -		var consentID uuid.UUID - -		if consentID, err = uuid.Parse(string(consentIDBytes)); err != nil { -			ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrServerError.WithHint("Consent Session ID was Malformed.")) +			ctx.Providers.OpenIDConnect.WriteAuthorizeError(rw, requester, oidc.ErrSubjectCouldNotLookup)  			return nil, true  		} -		ctx.Logger.Debugf("Authorization Request with id '%s' on client with id '%s' proceeding to lookup consent by challenge id '%s'", requester.GetID(), client.GetID(), consentID) - -		return handleOIDCAuthorizationConsentWithChallengeID(ctx, issuer, client, userSession, subject, consentID, rw, r, requester) -	} - -	return handleOIDCAuthorizationConsentGenerate(ctx, issuer, client, userSession, subject, rw, r, requester) -} - -func handleOIDCAuthorizationConsentWithChallengeID(ctx *middlewares.AutheliaCtx, issuer *url.URL, client *oidc.Client, -	userSession session.UserSession, subject, challengeID uuid.UUID, -	rw http.ResponseWriter, r *http.Request, requester fosite.AuthorizeRequester) (consent *model.OAuth2ConsentSession, handled bool) { -	var ( -		err error -	) - -	if consent, err = ctx.Providers.StorageProvider.LoadOAuth2ConsentSessionByChallengeID(ctx, challengeID); err != nil { -		ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' could not be processed: error occurred during consent session lookup: %+v", requester.GetID(), requester.GetClient().GetID(), err) - -		ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrServerError.WithHint("Failed to lookup consent session.")) - -		return nil, true -	} - -	if err = verifyOIDCUserAuthorizedForConsent(ctx, client, userSession, consent, subject); err != nil { -		ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' could not process consent session with challenge id '%s': could not authorize the user user '%s' for this consent session: %v", requester.GetID(), client.GetID(), consent.ChallengeID, userSession.Username, err) +		switch client.Consent.Mode { +		case oidc.ClientConsentModeExplicit: +			handler = handleOIDCAuthorizationConsentModeExplicit +		case oidc.ClientConsentModeImplicit: +			handler = handleOIDCAuthorizationConsentModeImplicit +		case oidc.ClientConsentModePreConfigured: +			handler = handleOIDCAuthorizationConsentModePreConfigured +		default: +			ctx.Logger.Errorf(logFmtErrConsentCantDetermineConsentMode, requester.GetID(), client.GetID()) -		ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrServerError.WithHint("The user is not authorized to perform consent.")) - -		return nil, true -	} - -	if consent.Responded() { -		if consent.Granted { -			ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' could not be processed: this consent session with challenge id '%s' was already granted", requester.GetID(), client.GetID(), consent.ChallengeID) - -			ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrServerError.WithHint("Authorization already granted.")) +			ctx.Providers.OpenIDConnect.WriteAuthorizeError(rw, requester, fosite.ErrServerError.WithHint("Could not determine the client consent mode."))  			return nil, true  		} +	default: +		if subject, err = ctx.Providers.OpenIDConnect.GetSubject(ctx, client.GetSectorIdentifier(), userSession.Username); err != nil { +			ctx.Logger.Errorf(logFmtErrConsentCantGetSubject, requester.GetID(), client.GetID(), client.Consent, userSession.Username, client.GetSectorIdentifier(), err) -		ctx.Logger.Debugf("Authorization Request with id '%s' loaded consent session with id '%d' and challenge id '%s' for client id '%s' and subject '%s' and scopes '%s'", requester.GetID(), consent.ID, consent.ChallengeID, client.GetID(), consent.Subject.UUID, strings.Join(requester.GetRequestedScopes(), " ")) - -		if consent.IsDenied() { -			ctx.Logger.Warnf("Authorization Request with id '%s' and challenge id '%s' for client id '%s' and subject '%s' and scopes '%s' was not denied by the user durng the consent session", requester.GetID(), consent.ChallengeID, client.GetID(), consent.Subject.UUID, strings.Join(requester.GetRequestedScopes(), " ")) - -			ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrAccessDenied) +			ctx.Providers.OpenIDConnect.WriteAuthorizeError(rw, requester, oidc.ErrSubjectCouldNotLookup)  			return nil, true  		} -		return consent, false +		handler = handleOIDCAuthorizationConsentGenerate  	} -	handleOIDCAuthorizationConsentRedirect(ctx, issuer, consent, client, userSession, rw, r, requester) +	return handler(ctx, issuer, client, userSession, subject, rw, r, requester) +} -	return consent, true +func handleOIDCAuthorizationConsentNotAuthenticated(_ *middlewares.AutheliaCtx, issuer *url.URL, _ *oidc.Client, +	_ session.UserSession, _ uuid.UUID, +	rw http.ResponseWriter, r *http.Request, requester fosite.AuthorizeRequester) (consent *model.OAuth2ConsentSession, handled bool) { +	redirectionURL := handleOIDCAuthorizationConsentGetRedirectionURL(issuer, nil, requester) + +	http.Redirect(rw, r, redirectionURL.String(), http.StatusFound) + +	return nil, true  }  func handleOIDCAuthorizationConsentGenerate(ctx *middlewares.AutheliaCtx, issuer *url.URL, client *oidc.Client, @@ -134,36 +88,28 @@ func handleOIDCAuthorizationConsentGenerate(ctx *middlewares.AutheliaCtx, issuer  		err error  	) -	scopes, audience := getOIDCExpectedScopesAndAudienceFromRequest(requester) +	ctx.Logger.Debugf(logFmtDbgConsentGenerate, requester.GetID(), client.GetID(), client.Consent) -	if consent, err = getOIDCPreConfiguredConsent(ctx, client.GetID(), subject, scopes, audience); err != nil { -		ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' had error looking up pre-configured consent sessions: %+v", requester.GetID(), requester.GetClient().GetID(), err) +	if len(ctx.QueryArgs().PeekBytes(qryArgConsentID)) != 0 { +		ctx.Logger.Errorf(logFmtErrConsentGenerateError, requester.GetID(), client.GetID(), client.Consent, "generating", errors.New("consent id value was present when it should be absent")) -		ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrServerError.WithHint("Could not lookup the consent session.")) +		ctx.Providers.OpenIDConnect.WriteAuthorizeError(rw, requester, oidc.ErrConsentCouldNotGenerate)  		return nil, true  	} -	if consent != nil { -		ctx.Logger.Debugf("Authorization Request with id '%s' on client with id '%s' successfully looked up pre-configured consent with challenge id '%s'", requester.GetID(), client.GetID(), consent.ChallengeID) - -		return consent, false -	} - -	ctx.Logger.Debugf("Authorization Request with id '%s' on client with id '%s' proceeding to generate a new consent due to unsuccessful lookup of pre-configured consent", requester.GetID(), client.GetID()) -  	if consent, err = model.NewOAuth2ConsentSession(subject, requester); err != nil { -		ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' could not be processed: error occurred generating consent: %+v", requester.GetID(), requester.GetClient().GetID(), err) +		ctx.Logger.Errorf(logFmtErrConsentGenerateError, requester.GetID(), client.GetID(), client.Consent, "generating", err) -		ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrServerError.WithHint("Could not generate the consent session.")) +		ctx.Providers.OpenIDConnect.WriteAuthorizeError(rw, requester, oidc.ErrConsentCouldNotGenerate)  		return nil, true  	}  	if err = ctx.Providers.StorageProvider.SaveOAuth2ConsentSession(ctx, *consent); err != nil { -		ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' could not be processed: error occurred saving consent session: %+v", requester.GetID(), client.GetID(), err) +		ctx.Logger.Errorf(logFmtErrConsentGenerateError, requester.GetID(), client.GetID(), client.Consent, "saving", err) -		ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrServerError.WithHint("Could not save the consent session.")) +		ctx.Providers.OpenIDConnect.WriteAuthorizeError(rw, requester, oidc.ErrConsentCouldNotSave)  		return nil, true  	} @@ -179,70 +125,87 @@ func handleOIDCAuthorizationConsentRedirect(ctx *middlewares.AutheliaCtx, issuer  	if client.IsAuthenticationLevelSufficient(userSession.AuthenticationLevel) {  		location, _ = url.ParseRequestURI(issuer.String()) -		location.Path = path.Join(location.Path, "/consent") +		location.Path = path.Join(location.Path, oidc.EndpointPathConsent)  		query := location.Query() -		query.Set("consent_id", consent.ChallengeID.String()) +		query.Set(queryArgID, consent.ChallengeID.String())  		location.RawQuery = query.Encode() -		ctx.Logger.Debugf("Authorization Request with id '%s' on client with id '%s' authentication level '%s' is sufficient for client level '%s'", requester.GetID(), client.GetID(), authentication.LevelToString(userSession.AuthenticationLevel), authorization.LevelToString(client.Policy)) +		ctx.Logger.Debugf(logFmtDbgConsentAuthenticationSufficiency, requester.GetID(), client.GetID(), client.Consent, authentication.LevelToString(userSession.AuthenticationLevel), "sufficient", authorization.LevelToString(client.Policy))  	} else { -		location = getOIDCAuthorizationRedirectURL(issuer, requester) +		location = handleOIDCAuthorizationConsentGetRedirectionURL(issuer, consent, requester) -		ctx.Logger.Debugf("Authorization Request with id '%s' on client with id '%s' authentication level '%s' is insufficient for client level '%s'", requester.GetID(), client.GetID(), authentication.LevelToString(userSession.AuthenticationLevel), authorization.LevelToString(client.Policy)) +		ctx.Logger.Debugf(logFmtDbgConsentAuthenticationSufficiency, requester.GetID(), client.GetID(), client.Consent, authentication.LevelToString(userSession.AuthenticationLevel), "insufficient", authorization.LevelToString(client.Policy))  	} -	ctx.Logger.Debugf("Authorization Request with id '%s' on client with id '%s' is being redirected to '%s'", requester.GetID(), client.GetID(), location) +	ctx.Logger.Debugf(logFmtDbgConsentRedirect, requester.GetID(), client.GetID(), client.Consent, location)  	http.Redirect(rw, r, location.String(), http.StatusFound)  } -func verifyOIDCUserAuthorizedForConsent(ctx *middlewares.AutheliaCtx, client *oidc.Client, userSession session.UserSession, consent *model.OAuth2ConsentSession, subject uuid.UUID) (err error) { -	var sid, csid uint32 +func handleOIDCAuthorizationConsentGetRedirectionURL(issuer *url.URL, consent *model.OAuth2ConsentSession, requester fosite.AuthorizeRequester) (redirectURL *url.URL) { +	iss := issuer.String() -	csid = consent.Subject.UUID.ID() +	if !strings.HasSuffix(iss, "/") { +		iss += "/" +	} + +	redirectURL, _ = url.ParseRequestURI(iss) + +	query := redirectURL.Query() +	query.Set(queryArgWorkflow, workflowOpenIDConnect) -	if !consent.Subject.Valid || csid == 0 { -		return fmt.Errorf("the consent subject is null for consent session with id '%d'", consent.ID) +	switch { +	case consent != nil: +		query.Set(queryArgWorkflowID, consent.ChallengeID.String()) +	case requester != nil: +		rd, _ := url.ParseRequestURI(iss) +		rd.Path = path.Join(rd.Path, oidc.EndpointPathAuthorization) +		rd.RawQuery = requester.GetRequestForm().Encode() + +		query.Set(queryArgRD, rd.String())  	} +	redirectURL.RawQuery = query.Encode() + +	return redirectURL +} + +func verifyOIDCUserAuthorizedForConsent(ctx *middlewares.AutheliaCtx, client *oidc.Client, userSession session.UserSession, consent *model.OAuth2ConsentSession, subject uuid.UUID) (err error) { +	var sid uint32 +  	if client == nil { -		if client, err = ctx.Providers.OpenIDConnect.Store.GetFullClient(consent.ClientID); err != nil { +		if client, err = ctx.Providers.OpenIDConnect.GetFullClient(consent.ClientID); err != nil {  			return fmt.Errorf("failed to retrieve client: %w", err)  		}  	}  	if sid = subject.ID(); sid == 0 { -		if subject, err = ctx.Providers.OpenIDConnect.Store.GetSubject(ctx, client.GetSectorIdentifier(), userSession.Username); err != nil { +		if subject, err = ctx.Providers.OpenIDConnect.GetSubject(ctx, client.GetSectorIdentifier(), userSession.Username); err != nil {  			return fmt.Errorf("failed to lookup subject: %w", err)  		}  		sid = subject.ID()  	} -	if csid != sid { -		return fmt.Errorf("the consent subject identifier '%s' isn't owned by user '%s' who has a subject identifier of '%s' with sector identifier '%s'", consent.Subject.UUID, userSession.Username, subject, client.GetSectorIdentifier()) -	} - -	return nil -} - -func getOIDCAuthorizationRedirectURL(issuer *url.URL, requester fosite.AuthorizeRequester) (redirectURL *url.URL) { -	redirectURL, _ = url.ParseRequestURI(issuer.String()) - -	authorizationURL, _ := url.ParseRequestURI(issuer.String()) +	if !consent.Subject.Valid { +		if sid == 0 { +			return fmt.Errorf("the consent subject is null for consent session with id '%d' for anonymous user", consent.ID) +		} -	authorizationURL.Path = path.Join(authorizationURL.Path, oidc.AuthorizationPath) -	authorizationURL.RawQuery = requester.GetRequestForm().Encode() +		consent.Subject = uuid.NullUUID{UUID: subject, Valid: true} -	query := redirectURL.Query() -	query.Set("rd", authorizationURL.String()) -	query.Set("workflow", workflowOpenIDConnect) +		if err = ctx.Providers.StorageProvider.SaveOAuth2ConsentSessionSubject(ctx, *consent); err != nil { +			return fmt.Errorf("failed to update the consent subject: %w", err) +		} +	} -	redirectURL.RawQuery = query.Encode() +	if consent.Subject.UUID.ID() != sid { +		return fmt.Errorf("the consent subject identifier '%s' isn't owned by user '%s' who has a subject identifier of '%s' with sector identifier '%s'", consent.Subject.UUID, userSession.Username, subject, client.GetSectorIdentifier()) +	} -	return redirectURL +	return nil  }  func getOIDCExpectedScopesAndAudienceFromRequest(requester fosite.Requester) (scopes, audience []string) { @@ -256,45 +219,3 @@ func getOIDCExpectedScopesAndAudience(clientID string, scopes, audience []string  	return scopes, audience  } - -func getOIDCPreConfiguredConsent(ctx *middlewares.AutheliaCtx, clientID string, subject uuid.UUID, scopes, audience []string) (consent *model.OAuth2ConsentSession, err error) { -	var ( -		rows *storage.ConsentSessionRows -	) - -	ctx.Logger.Debugf("Consent Session is being checked for pre-configuration with signature of client id '%s' and subject '%s'", clientID, subject) - -	if rows, err = ctx.Providers.StorageProvider.LoadOAuth2ConsentSessionsPreConfigured(ctx, clientID, subject); err != nil { -		ctx.Logger.Debugf("Consent Session checked for pre-configuration with signature of client id '%s' and subject '%s' failed with error during load: %+v", clientID, subject, err) - -		return nil, err -	} - -	defer func() { -		if err := rows.Close(); err != nil { -			ctx.Logger.Errorf("Consent Session checked for pre-configuration with signature of client id '%s' and subject '%s' failed to close rows with error: %+v", clientID, subject, err) -		} -	}() - -	for rows.Next() { -		if consent, err = rows.Get(); err != nil { -			ctx.Logger.Debugf("Consent Session checked for pre-configuration with signature of client id '%s' and subject '%s' failed with error during iteration: %+v", clientID, subject, err) - -			return nil, err -		} - -		if consent.HasExactGrants(scopes, audience) && consent.CanGrant() { -			break -		} -	} - -	if consent != nil && consent.HasExactGrants(scopes, audience) && consent.CanGrant() { -		ctx.Logger.Debugf("Consent Session checked for pre-configuration with signature of client id '%s' and subject '%s' found a result with challenge id '%s'", clientID, subject, consent.ChallengeID) - -		return consent, nil -	} - -	ctx.Logger.Debugf("Consent Session checked for pre-configuration with signature of client id '%s' and subject '%s' did not find any results", clientID, subject) - -	return nil, nil -} diff --git a/internal/handlers/handler_oidc_authorization_consent_explicit.go b/internal/handlers/handler_oidc_authorization_consent_explicit.go new file mode 100644 index 000000000..3d2009964 --- /dev/null +++ b/internal/handlers/handler_oidc_authorization_consent_explicit.go @@ -0,0 +1,96 @@ +package handlers + +import ( +	"net/http" +	"net/url" + +	"github.com/google/uuid" +	"github.com/ory/fosite" + +	"github.com/authelia/authelia/v4/internal/middlewares" +	"github.com/authelia/authelia/v4/internal/model" +	"github.com/authelia/authelia/v4/internal/oidc" +	"github.com/authelia/authelia/v4/internal/session" +) + +func handleOIDCAuthorizationConsentModeExplicit(ctx *middlewares.AutheliaCtx, issuer *url.URL, client *oidc.Client, +	userSession session.UserSession, subject uuid.UUID, +	rw http.ResponseWriter, r *http.Request, requester fosite.AuthorizeRequester) (consent *model.OAuth2ConsentSession, handled bool) { +	var ( +		consentID uuid.UUID +		err       error +	) + +	bytesConsentID := ctx.QueryArgs().PeekBytes(qryArgConsentID) + +	switch len(bytesConsentID) { +	case 0: +		return handleOIDCAuthorizationConsentGenerate(ctx, issuer, client, userSession, subject, rw, r, requester) +	default: +		if consentID, err = uuid.Parse(string(bytesConsentID)); err != nil { +			ctx.Logger.Errorf(logFmtErrConsentParseChallengeID, requester.GetID(), client.GetID(), client.Consent, bytesConsentID, err) + +			ctx.Providers.OpenIDConnect.WriteAuthorizeError(rw, requester, oidc.ErrConsentMalformedChallengeID) + +			return nil, true +		} + +		return handleOIDCAuthorizationConsentModeExplicitWithID(ctx, issuer, client, userSession, subject, consentID, rw, r, requester) +	} +} + +func handleOIDCAuthorizationConsentModeExplicitWithID(ctx *middlewares.AutheliaCtx, issuer *url.URL, client *oidc.Client, +	userSession session.UserSession, subject uuid.UUID, consentID uuid.UUID, +	rw http.ResponseWriter, r *http.Request, requester fosite.AuthorizeRequester) (consent *model.OAuth2ConsentSession, handled bool) { +	var ( +		err error +	) + +	if consentID.ID() == 0 { +		ctx.Logger.Errorf(logFmtErrConsentZeroID, requester.GetID(), client.GetID(), client.Consent) + +		ctx.Providers.OpenIDConnect.WriteAuthorizeError(rw, requester, oidc.ErrConsentCouldNotLookup) + +		return nil, true +	} + +	if consent, err = ctx.Providers.StorageProvider.LoadOAuth2ConsentSessionByChallengeID(ctx, consentID); err != nil { +		ctx.Logger.Errorf(logFmtErrConsentLookupLoadingSession, requester.GetID(), client.GetID(), client.Consent, consentID, err) + +		ctx.Providers.OpenIDConnect.WriteAuthorizeError(rw, requester, oidc.ErrConsentCouldNotLookup) + +		return nil, true +	} + +	if subject.ID() != consent.Subject.UUID.ID() { +		ctx.Logger.Errorf(logFmtErrConsentSessionSubjectNotAuthorized, requester.GetID(), client.GetID(), client.Consent, consent.ChallengeID, userSession.Username, subject, consent.Subject.UUID) + +		ctx.Providers.OpenIDConnect.WriteAuthorizeError(rw, requester, oidc.ErrConsentCouldNotLookup) + +		return nil, true +	} + +	if !consent.CanGrant() { +		ctx.Logger.Errorf(logFmtErrConsentCantGrant, requester.GetID(), client.GetID(), client.Consent, consent.ChallengeID, "explicit") + +		ctx.Providers.OpenIDConnect.WriteAuthorizeError(rw, requester, oidc.ErrConsentCouldNotPerform) + +		return nil, true +	} + +	if !consent.IsAuthorized() { +		if consent.Responded() { +			ctx.Logger.Errorf(logFmtErrConsentCantGrantRejected, requester.GetID(), client.GetID(), client.Consent, consent.ChallengeID) + +			ctx.Providers.OpenIDConnect.WriteAuthorizeError(rw, requester, fosite.ErrAccessDenied) + +			return nil, true +		} + +		handleOIDCAuthorizationConsentRedirect(ctx, issuer, consent, client, userSession, rw, r, requester) + +		return nil, true +	} + +	return consent, false +} diff --git a/internal/handlers/handler_oidc_authorization_consent_implicit.go b/internal/handlers/handler_oidc_authorization_consent_implicit.go new file mode 100644 index 000000000..851da7a32 --- /dev/null +++ b/internal/handlers/handler_oidc_authorization_consent_implicit.go @@ -0,0 +1,136 @@ +package handlers + +import ( +	"net/http" +	"net/url" + +	"github.com/google/uuid" +	"github.com/ory/fosite" + +	"github.com/authelia/authelia/v4/internal/middlewares" +	"github.com/authelia/authelia/v4/internal/model" +	"github.com/authelia/authelia/v4/internal/oidc" +	"github.com/authelia/authelia/v4/internal/session" +) + +func handleOIDCAuthorizationConsentModeImplicit(ctx *middlewares.AutheliaCtx, issuer *url.URL, client *oidc.Client, +	userSession session.UserSession, subject uuid.UUID, +	rw http.ResponseWriter, r *http.Request, requester fosite.AuthorizeRequester) (consent *model.OAuth2ConsentSession, handled bool) { +	var ( +		consentID uuid.UUID +		err       error +	) + +	bytesConsentID := ctx.QueryArgs().PeekBytes(qryArgConsentID) + +	switch len(bytesConsentID) { +	case 0: +		return handleOIDCAuthorizationConsentModeImplicitWithoutID(ctx, issuer, client, userSession, subject, rw, r, requester) +	default: +		if consentID, err = uuid.Parse(string(bytesConsentID)); err != nil { +			ctx.Logger.Errorf(logFmtErrConsentParseChallengeID, requester.GetID(), client.GetID(), client.Consent, bytesConsentID, err) + +			ctx.Providers.OpenIDConnect.WriteAuthorizeError(rw, requester, oidc.ErrConsentMalformedChallengeID) + +			return nil, true +		} + +		return handleOIDCAuthorizationConsentModeImplicitWithID(ctx, issuer, client, userSession, subject, consentID, rw, r, requester) +	} +} + +func handleOIDCAuthorizationConsentModeImplicitWithID(ctx *middlewares.AutheliaCtx, _ *url.URL, client *oidc.Client, +	userSession session.UserSession, subject uuid.UUID, consentID uuid.UUID, +	rw http.ResponseWriter, _ *http.Request, requester fosite.AuthorizeRequester) (consent *model.OAuth2ConsentSession, handled bool) { +	var ( +		err error +	) + +	if consentID.ID() == 0 { +		ctx.Logger.Errorf(logFmtErrConsentZeroID, requester.GetID(), client.GetID(), client.Consent) + +		ctx.Providers.OpenIDConnect.WriteAuthorizeError(rw, requester, oidc.ErrConsentCouldNotLookup) + +		return nil, true +	} + +	if consent, err = ctx.Providers.StorageProvider.LoadOAuth2ConsentSessionByChallengeID(ctx, consentID); err != nil { +		ctx.Logger.Errorf(logFmtErrConsentLookupLoadingSession, requester.GetID(), client.GetID(), client.Consent, consentID, err) + +		ctx.Providers.OpenIDConnect.WriteAuthorizeError(rw, requester, oidc.ErrConsentCouldNotLookup) + +		return nil, true +	} + +	if subject.ID() != consent.Subject.UUID.ID() { +		ctx.Logger.Errorf(logFmtErrConsentSessionSubjectNotAuthorized, requester.GetID(), client.GetID(), client.Consent, consent.ChallengeID, userSession.Username, subject, consent.Subject.UUID) + +		ctx.Providers.OpenIDConnect.WriteAuthorizeError(rw, requester, oidc.ErrConsentCouldNotLookup) + +		return nil, true +	} + +	if !consent.CanGrant() { +		ctx.Logger.Errorf(logFmtErrConsentCantGrant, requester.GetID(), client.GetID(), client.Consent, consent.ChallengeID, "implicit") + +		ctx.Providers.OpenIDConnect.WriteAuthorizeError(rw, requester, oidc.ErrConsentCouldNotPerform) + +		return nil, true +	} + +	consent.Grant() + +	if err = ctx.Providers.StorageProvider.SaveOAuth2ConsentSessionResponse(ctx, *consent, false); err != nil { +		ctx.Logger.Errorf(logFmtErrConsentSaveSessionResponse, requester.GetID(), client.GetID(), client.Consent, consent.ChallengeID, err) + +		ctx.Providers.OpenIDConnect.WriteAuthorizeError(rw, requester, oidc.ErrConsentCouldNotSave) + +		return nil, true +	} + +	return consent, false +} + +func handleOIDCAuthorizationConsentModeImplicitWithoutID(ctx *middlewares.AutheliaCtx, _ *url.URL, client *oidc.Client, +	_ session.UserSession, subject uuid.UUID, +	rw http.ResponseWriter, _ *http.Request, requester fosite.AuthorizeRequester) (consent *model.OAuth2ConsentSession, handled bool) { +	var ( +		err error +	) + +	if consent, err = model.NewOAuth2ConsentSession(subject, requester); err != nil { +		ctx.Logger.Errorf(logFmtErrConsentGenerate, requester.GetID(), client.GetID(), client.Consent, err) + +		ctx.Providers.OpenIDConnect.WriteAuthorizeError(rw, requester, oidc.ErrConsentCouldNotGenerate) + +		return nil, true +	} + +	if err = ctx.Providers.StorageProvider.SaveOAuth2ConsentSession(ctx, *consent); err != nil { +		ctx.Logger.Errorf(logFmtErrConsentSaveSession, requester.GetID(), client.GetID(), client.Consent, consent.ChallengeID, err) + +		ctx.Providers.OpenIDConnect.WriteAuthorizeError(rw, requester, oidc.ErrConsentCouldNotSave) + +		return nil, true +	} + +	if consent, err = ctx.Providers.StorageProvider.LoadOAuth2ConsentSessionByChallengeID(ctx, consent.ChallengeID); err != nil { +		ctx.Logger.Errorf(logFmtErrConsentSaveSession, requester.GetID(), client.GetID(), client.Consent, consent.ChallengeID, err) + +		ctx.Providers.OpenIDConnect.WriteAuthorizeError(rw, requester, oidc.ErrConsentCouldNotSave) + +		return nil, true +	} + +	consent.Grant() + +	if err = ctx.Providers.StorageProvider.SaveOAuth2ConsentSessionResponse(ctx, *consent, false); err != nil { +		ctx.Logger.Errorf(logFmtErrConsentSaveSessionResponse, requester.GetID(), client.GetID(), client.Consent, consent.ChallengeID, err) + +		ctx.Providers.OpenIDConnect.WriteAuthorizeError(rw, requester, oidc.ErrConsentCouldNotSave) + +		return nil, true +	} + +	return consent, false +} diff --git a/internal/handlers/handler_oidc_authorization_consent_pre_configured.go b/internal/handlers/handler_oidc_authorization_consent_pre_configured.go new file mode 100644 index 000000000..07e2d9150 --- /dev/null +++ b/internal/handlers/handler_oidc_authorization_consent_pre_configured.go @@ -0,0 +1,220 @@ +package handlers + +import ( +	"database/sql" +	"fmt" +	"net/http" +	"net/url" +	"strings" + +	"github.com/google/uuid" +	"github.com/ory/fosite" + +	"github.com/authelia/authelia/v4/internal/middlewares" +	"github.com/authelia/authelia/v4/internal/model" +	"github.com/authelia/authelia/v4/internal/oidc" +	"github.com/authelia/authelia/v4/internal/session" +	"github.com/authelia/authelia/v4/internal/storage" +) + +func handleOIDCAuthorizationConsentModePreConfigured(ctx *middlewares.AutheliaCtx, issuer *url.URL, client *oidc.Client, +	userSession session.UserSession, subject uuid.UUID, +	rw http.ResponseWriter, r *http.Request, requester fosite.AuthorizeRequester) (consent *model.OAuth2ConsentSession, handled bool) { +	var ( +		consentID uuid.UUID +		err       error +	) + +	bytesConsentID := ctx.QueryArgs().PeekBytes(qryArgConsentID) + +	switch len(bytesConsentID) { +	case 0: +		return handleOIDCAuthorizationConsentModePreConfiguredWithoutID(ctx, issuer, client, userSession, subject, rw, r, requester) +	default: +		if consentID, err = uuid.Parse(string(bytesConsentID)); err != nil { +			ctx.Logger.Errorf(logFmtErrConsentParseChallengeID, requester.GetID(), client.GetID(), client.Consent, bytesConsentID, err) + +			ctx.Providers.OpenIDConnect.WriteAuthorizeError(rw, requester, oidc.ErrConsentMalformedChallengeID) + +			return nil, true +		} + +		return handleOIDCAuthorizationConsentModePreConfiguredWithID(ctx, issuer, client, userSession, subject, consentID, rw, r, requester) +	} +} + +func handleOIDCAuthorizationConsentModePreConfiguredWithID(ctx *middlewares.AutheliaCtx, issuer *url.URL, client *oidc.Client, +	userSession session.UserSession, subject uuid.UUID, consentID uuid.UUID, +	rw http.ResponseWriter, r *http.Request, requester fosite.AuthorizeRequester) (consent *model.OAuth2ConsentSession, handled bool) { +	var ( +		config *model.OAuth2ConsentPreConfig +		err    error +	) + +	if consentID.ID() == 0 { +		ctx.Logger.Errorf(logFmtErrConsentZeroID, requester.GetID(), client.GetID(), client.Consent) + +		ctx.Providers.OpenIDConnect.WriteAuthorizeError(rw, requester, oidc.ErrConsentCouldNotLookup) + +		return nil, true +	} + +	if consent, err = ctx.Providers.StorageProvider.LoadOAuth2ConsentSessionByChallengeID(ctx, consentID); err != nil { +		ctx.Logger.Errorf(logFmtErrConsentLookupLoadingSession, requester.GetID(), client.GetID(), client.Consent, consentID, err) + +		ctx.Providers.OpenIDConnect.WriteAuthorizeError(rw, requester, oidc.ErrConsentCouldNotLookup) + +		return nil, true +	} + +	if subject.ID() != consent.Subject.UUID.ID() { +		ctx.Logger.Errorf(logFmtErrConsentSessionSubjectNotAuthorized, requester.GetID(), client.GetID(), client.Consent, consent.ChallengeID, userSession.Username, subject, consent.Subject.UUID) + +		ctx.Providers.OpenIDConnect.WriteAuthorizeError(rw, requester, oidc.ErrConsentCouldNotLookup) + +		return nil, true +	} + +	if !consent.CanGrant() { +		ctx.Logger.Errorf(logFmtErrConsentCantGrantPreConf, requester.GetID(), client.GetID(), client.Consent, consent.ChallengeID) + +		ctx.Providers.OpenIDConnect.WriteAuthorizeError(rw, requester, oidc.ErrConsentCouldNotPerform) + +		return nil, true +	} + +	if config, err = handleOIDCAuthorizationConsentModePreConfiguredGetPreConfig(ctx, client, subject, requester); err != nil { +		ctx.Logger.Errorf(logFmtErrConsentPreConfLookup, requester.GetID(), client.GetID(), client.Consent, err) + +		ctx.Providers.OpenIDConnect.WriteAuthorizeError(rw, requester, oidc.ErrConsentCouldNotLookup) + +		return nil, true +	} + +	if config != nil { +		consent.Grant() + +		consent.PreConfiguration = sql.NullInt64{Int64: config.ID, Valid: true} + +		if err = ctx.Providers.StorageProvider.SaveOAuth2ConsentSessionResponse(ctx, *consent, false); err != nil { +			ctx.Logger.Errorf(logFmtErrConsentSaveSessionResponse, requester.GetID(), client.GetID(), client.Consent, consent.ChallengeID, err) + +			ctx.Providers.OpenIDConnect.WriteAuthorizeError(rw, requester, oidc.ErrConsentCouldNotSave) + +			return nil, true +		} + +		return consent, false +	} + +	if !consent.IsAuthorized() { +		if consent.Responded() { +			ctx.Logger.Errorf(logFmtErrConsentCantGrantRejected, requester.GetID(), client.GetID(), client.Consent, consent.ChallengeID) + +			ctx.Providers.OpenIDConnect.WriteAuthorizeError(rw, requester, fosite.ErrAccessDenied) + +			return nil, true +		} + +		handleOIDCAuthorizationConsentRedirect(ctx, issuer, consent, client, userSession, rw, r, requester) + +		return nil, true +	} + +	return consent, false +} + +func handleOIDCAuthorizationConsentModePreConfiguredWithoutID(ctx *middlewares.AutheliaCtx, issuer *url.URL, client *oidc.Client, +	userSession session.UserSession, subject uuid.UUID, +	rw http.ResponseWriter, r *http.Request, requester fosite.AuthorizeRequester) (consent *model.OAuth2ConsentSession, handled bool) { +	var ( +		config *model.OAuth2ConsentPreConfig +		err    error +	) + +	if config, err = handleOIDCAuthorizationConsentModePreConfiguredGetPreConfig(ctx, client, subject, requester); err != nil { +		ctx.Logger.Errorf(logFmtErrConsentPreConfLookup, requester.GetID(), client.GetID(), client.Consent, err) + +		ctx.Providers.OpenIDConnect.WriteAuthorizeError(rw, requester, oidc.ErrConsentCouldNotLookup) + +		return nil, true +	} + +	if config == nil { +		return handleOIDCAuthorizationConsentGenerate(ctx, issuer, client, userSession, subject, rw, r, requester) +	} + +	if consent, err = model.NewOAuth2ConsentSession(subject, requester); err != nil { +		ctx.Logger.Errorf(logFmtErrConsentGenerate, requester.GetID(), client.GetID(), client.Consent, err) + +		ctx.Providers.OpenIDConnect.WriteAuthorizeError(rw, requester, oidc.ErrConsentCouldNotGenerate) + +		return nil, true +	} + +	if err = ctx.Providers.StorageProvider.SaveOAuth2ConsentSession(ctx, *consent); err != nil { +		ctx.Logger.Errorf(logFmtErrConsentSaveSession, requester.GetID(), client.GetID(), client.Consent, consent.ChallengeID, err) + +		ctx.Providers.OpenIDConnect.WriteAuthorizeError(rw, requester, oidc.ErrConsentCouldNotSave) + +		return nil, true +	} + +	if consent, err = ctx.Providers.StorageProvider.LoadOAuth2ConsentSessionByChallengeID(ctx, consent.ChallengeID); err != nil { +		ctx.Logger.Errorf(logFmtErrConsentSaveSession, requester.GetID(), client.GetID(), client.Consent, consent.ChallengeID, err) + +		ctx.Providers.OpenIDConnect.WriteAuthorizeError(rw, requester, oidc.ErrConsentCouldNotSave) + +		return nil, true +	} + +	consent.Grant() + +	consent.PreConfiguration = sql.NullInt64{Int64: config.ID, Valid: true} + +	if err = ctx.Providers.StorageProvider.SaveOAuth2ConsentSessionResponse(ctx, *consent, false); err != nil { +		ctx.Logger.Errorf(logFmtErrConsentSaveSessionResponse, requester.GetID(), client.GetID(), client.Consent, consent.ChallengeID, err) + +		ctx.Providers.OpenIDConnect.WriteAuthorizeError(rw, requester, oidc.ErrConsentCouldNotSave) + +		return nil, true +	} + +	return consent, false +} + +func handleOIDCAuthorizationConsentModePreConfiguredGetPreConfig(ctx *middlewares.AutheliaCtx, client *oidc.Client, subject uuid.UUID, requester fosite.Requester) (config *model.OAuth2ConsentPreConfig, err error) { +	var ( +		rows *storage.ConsentPreConfigRows +	) + +	ctx.Logger.Debugf(logFmtDbgConsentPreConfTryingLookup, requester.GetID(), client.GetID(), client.Consent, client.GetID(), subject, strings.Join(requester.GetRequestedScopes(), " ")) + +	if rows, err = ctx.Providers.StorageProvider.LoadOAuth2ConsentPreConfigurations(ctx, client.GetID(), subject); err != nil { +		return nil, fmt.Errorf("error loading rows: %w", err) +	} + +	defer func() { +		if err := rows.Close(); err != nil { +			ctx.Logger.Errorf(logFmtErrConsentPreConfRowsClose, requester.GetID(), client.GetID(), client.Consent, err) +		} +	}() + +	scopes, audience := getOIDCExpectedScopesAndAudienceFromRequest(requester) + +	for rows.Next() { +		if config, err = rows.Get(); err != nil { +			return nil, fmt.Errorf("error iterating rows: %w", err) +		} + +		if config.HasExactGrants(scopes, audience) && config.CanConsent() { +			ctx.Logger.Debugf(logFmtDbgConsentPreConfSuccessfulLookup, requester.GetID(), client.GetID(), client.Consent, client.GetID(), subject, strings.Join(requester.GetRequestedScopes(), " "), config.ID) + +			return config, nil +		} +	} + +	ctx.Logger.Debugf(logFmtDbgConsentPreConfUnsuccessfulLookup, requester.GetID(), client.GetID(), client.Consent, client.GetID(), subject, strings.Join(requester.GetRequestedScopes(), " ")) + +	return nil, nil +} diff --git a/internal/handlers/handler_oidc_consent.go b/internal/handlers/handler_oidc_consent.go index 5b34ffea8..1418ea37c 100644 --- a/internal/handlers/handler_oidc_consent.go +++ b/internal/handlers/handler_oidc_consent.go @@ -1,11 +1,11 @@  package handlers  import ( +	"database/sql"  	"encoding/json"  	"fmt"  	"net/url"  	"path" -	"strings"  	"time"  	"github.com/google/uuid" @@ -14,7 +14,6 @@ import (  	"github.com/authelia/authelia/v4/internal/model"  	"github.com/authelia/authelia/v4/internal/oidc"  	"github.com/authelia/authelia/v4/internal/session" -	"github.com/authelia/authelia/v4/internal/utils"  )  // OpenIDConnectConsentGET handles requests to provide consent for OpenID Connect. @@ -24,22 +23,20 @@ func OpenIDConnectConsentGET(ctx *middlewares.AutheliaCtx) {  		err       error  	) -	if consentID, err = uuid.Parse(string(ctx.RequestCtx.QueryArgs().Peek("consent_id"))); err != nil { -		ctx.Logger.Errorf("Unable to convert '%s' into a UUID: %+v", ctx.RequestCtx.QueryArgs().Peek("consent_id"), err) +	if consentID, err = uuid.Parse(string(ctx.RequestCtx.QueryArgs().PeekBytes(qryArgID))); err != nil { +		ctx.Logger.Errorf("Unable to convert '%s' into a UUID: %+v", ctx.RequestCtx.QueryArgs().PeekBytes(qryArgID), err)  		ctx.ReplyForbidden()  		return  	} -	userSession, consent, client, handled := oidcConsentGetSessionsAndClient(ctx, consentID) -	if handled { -		return -	} - -	if !client.IsAuthenticationLevelSufficient(userSession.AuthenticationLevel) { -		ctx.Logger.Errorf("Unable to perform consent without sufficient authentication for user '%s' and client id '%s'", userSession.Username, consent.ClientID) -		ctx.ReplyForbidden() +	var ( +		consent *model.OAuth2ConsentSession +		client  *oidc.Client +		handled bool +	) +	if _, consent, client, handled = oidcConsentGetSessionsAndClient(ctx, consentID); handled {  		return  	} @@ -49,8 +46,6 @@ func OpenIDConnectConsentGET(ctx *middlewares.AutheliaCtx) {  }  // OpenIDConnectConsentPOST handles consent responses for OpenID Connect. -// -//nolint:gocyclo // TODO: Consider refactoring time permitting.  func OpenIDConnectConsentPOST(ctx *middlewares.AutheliaCtx) {  	var (  		consentID uuid.UUID @@ -66,21 +61,20 @@ func OpenIDConnectConsentPOST(ctx *middlewares.AutheliaCtx) {  	}  	if consentID, err = uuid.Parse(bodyJSON.ConsentID); err != nil { -		ctx.Logger.Errorf("Unable to convert '%s' into a UUID: %+v", ctx.RequestCtx.QueryArgs().Peek("consent_id"), err) +		ctx.Logger.Errorf("Unable to convert '%s' into a UUID: %+v", bodyJSON.ConsentID, err)  		ctx.ReplyForbidden()  		return  	} -	userSession, consent, client, handled := oidcConsentGetSessionsAndClient(ctx, consentID) -	if handled { -		return -	} - -	if !client.IsAuthenticationLevelSufficient(userSession.AuthenticationLevel) { -		ctx.Logger.Debugf("Insufficient permissions to give consent during POST current level: %d, require 2FA: %d", userSession.AuthenticationLevel, client.Policy) -		ctx.ReplyForbidden() +	var ( +		userSession session.UserSession +		consent     *model.OAuth2ConsentSession +		client      *oidc.Client +		handled     bool +	) +	if userSession, consent, client, handled = oidcConsentGetSessionsAndClient(ctx, consentID); handled {  		return  	} @@ -93,32 +87,35 @@ func OpenIDConnectConsentPOST(ctx *middlewares.AutheliaCtx) {  	}  	if bodyJSON.Consent { -		if bodyJSON.PreConfigure { -			if client.PreConfiguredConsentDuration == nil { -				ctx.Logger.Warnf("Consent session with id '%s' for user '%s': consent pre-configuration was requested and was ignored because it is not permitted on this client", consent.ChallengeID, userSession.Username) -			} else { -				expiresAt := time.Now().Add(*client.PreConfiguredConsentDuration) -				consent.ExpiresAt = &expiresAt +		consent.Grant() -				ctx.Logger.Debugf("Consent session with id '%s' for user '%s': pre-configured and set to expire at %v", consent.ChallengeID, userSession.Username, consent.ExpiresAt) -			} -		} +		if bodyJSON.PreConfigure { +			if client.Consent.Mode == oidc.ClientConsentModePreConfigured { +				config := model.OAuth2ConsentPreConfig{ +					ClientID:  consent.ClientID, +					Subject:   consent.Subject.UUID, +					CreatedAt: time.Now(), +					ExpiresAt: sql.NullTime{Time: time.Now().Add(client.Consent.Duration), Valid: true}, +					Scopes:    consent.GrantedScopes, +					Audience:  consent.GrantedAudience, +				} -		consent.GrantedScopes = consent.RequestedScopes -		consent.GrantedAudience = consent.RequestedAudience +				var id int64 -		if !utils.IsStringInSlice(consent.ClientID, consent.GrantedAudience) { -			consent.GrantedAudience = append(consent.GrantedAudience, consent.ClientID) -		} -	} +				if id, err = ctx.Providers.StorageProvider.SaveOAuth2ConsentPreConfiguration(ctx, config); err != nil { +					ctx.Logger.Errorf("Failed to save the consent pre-configuration to the database: %+v", err) +					ctx.SetJSONError(messageOperationFailed) -	var externalRootURL string +					return +				} -	if externalRootURL, err = ctx.ExternalRootURL(); err != nil { -		ctx.Logger.Errorf("Could not determine the external URL during consent session processing with id '%s' for user '%s': %v", consent.ChallengeID, userSession.Username, err) -		ctx.SetJSONError(messageOperationFailed) +				consent.PreConfiguration = sql.NullInt64{Int64: id, Valid: true} -		return +				ctx.Logger.Debugf("Consent session with id '%s' for user '%s': pre-configured and set to expire at %v", consent.ChallengeID, userSession.Username, config.ExpiresAt.Time) +			} else { +				ctx.Logger.Warnf("Consent session with id '%s' for user '%s': consent pre-configuration was requested and was ignored because it is not permitted on this client", consent.ChallengeID, userSession.Username) +			} +		}  	}  	if err = ctx.Providers.StorageProvider.SaveOAuth2ConsentSessionResponse(ctx, *consent, bodyJSON.Consent); err != nil { @@ -133,17 +130,13 @@ func OpenIDConnectConsentPOST(ctx *middlewares.AutheliaCtx) {  		query       url.Values  	) -	if redirectURI, err = url.ParseRequestURI(externalRootURL); err != nil { +	if redirectURI, err = ctx.IssuerURL(); err != nil {  		ctx.Logger.Errorf("Failed to parse the consent redirect URL: %+v", err)  		ctx.SetJSONError(messageOperationFailed)  		return  	} -	if !strings.HasSuffix(redirectURI.Path, "/") { -		redirectURI.Path += "/" -	} -  	if query, err = url.ParseQuery(consent.Form); err != nil {  		ctx.Logger.Errorf("Failed to parse the consent form values: %+v", err)  		ctx.SetJSONError(messageOperationFailed) @@ -151,9 +144,9 @@ func OpenIDConnectConsentPOST(ctx *middlewares.AutheliaCtx) {  		return  	} -	query.Set("consent_id", consent.ChallengeID.String()) +	query.Set(queryArgConsentID, consent.ChallengeID.String()) -	redirectURI.Path = path.Join(redirectURI.Path, oidc.AuthorizationPath) +	redirectURI.Path = path.Join(redirectURI.Path, oidc.EndpointPathAuthorization)  	redirectURI.RawQuery = query.Encode()  	response := oidc.ConsentPostResponseBody{RedirectURI: redirectURI.String()} @@ -177,7 +170,7 @@ func oidcConsentGetSessionsAndClient(ctx *middlewares.AutheliaCtx, consentID uui  		return userSession, nil, nil, true  	} -	if client, err = ctx.Providers.OpenIDConnect.Store.GetFullClient(consent.ClientID); err != nil { +	if client, err = ctx.Providers.OpenIDConnect.GetFullClient(consent.ClientID); err != nil {  		ctx.Logger.Errorf("Unable to find related client configuration with name '%s': %v", consent.ClientID, err)  		ctx.ReplyForbidden() @@ -192,5 +185,33 @@ func oidcConsentGetSessionsAndClient(ctx *middlewares.AutheliaCtx, consentID uui  		return userSession, nil, nil, true  	} +	switch client.Consent.Mode { +	case oidc.ClientConsentModeImplicit: +		ctx.Logger.Errorf("Unable to perform OpenID Connect Consent for user '%s' and client id '%s': the client is using the implicit consent mode", userSession.Username, consent.ClientID) +		ctx.ReplyForbidden() + +		return +	default: +		switch { +		case consent.Responded(): +			ctx.Logger.Errorf("Unable to perform OpenID Connect Consent for user '%s' and client id '%s': the client is using the explicit consent mode and this consent session has already been responded to", userSession.Username, consent.ClientID) +			ctx.ReplyForbidden() + +			return userSession, nil, nil, true +		case !consent.CanGrant(): +			ctx.Logger.Errorf("Unable to perform OpenID Connect Consent for user '%s' and client id '%s': the specified consent session cannot be granted", userSession.Username, consent.ClientID) +			ctx.ReplyForbidden() + +			return userSession, nil, nil, true +		} +	} + +	if !client.IsAuthenticationLevelSufficient(userSession.AuthenticationLevel) { +		ctx.Logger.Errorf("Unable to perform OpenID Connect Consent for user '%s' and client id '%s': the user is not sufficiently authenticated", userSession.Username, consent.ClientID) +		ctx.ReplyForbidden() + +		return userSession, nil, nil, true +	} +  	return userSession, consent, client, false  } diff --git a/internal/handlers/handler_oidc_token.go b/internal/handlers/handler_oidc_token.go index d8efdae23..b7fe94efe 100644 --- a/internal/handlers/handler_oidc_token.go +++ b/internal/handlers/handler_oidc_token.go @@ -21,12 +21,12 @@ func OpenIDConnectTokenPOST(ctx *middlewares.AutheliaCtx, rw http.ResponseWriter  	oidcSession := oidc.NewSession() -	if requester, err = ctx.Providers.OpenIDConnect.Fosite.NewAccessRequest(ctx, req, oidcSession); err != nil { +	if requester, err = ctx.Providers.OpenIDConnect.NewAccessRequest(ctx, req, oidcSession); err != nil {  		rfc := fosite.ErrorToRFC6749Error(err)  		ctx.Logger.Errorf("Access Request failed with error: %s", rfc.WithExposeDebug(true).GetDescription()) -		ctx.Providers.OpenIDConnect.Fosite.WriteAccessError(rw, requester, err) +		ctx.Providers.OpenIDConnect.WriteAccessError(rw, requester, err)  		return  	} @@ -46,12 +46,12 @@ func OpenIDConnectTokenPOST(ctx *middlewares.AutheliaCtx, rw http.ResponseWriter  	ctx.Logger.Tracef("Access Request with id '%s' on client with id '%s' response is being generated for session with type '%T'", requester.GetID(), client.GetID(), requester.GetSession()) -	if responder, err = ctx.Providers.OpenIDConnect.Fosite.NewAccessResponse(ctx, requester); err != nil { +	if responder, err = ctx.Providers.OpenIDConnect.NewAccessResponse(ctx, requester); err != nil {  		rfc := fosite.ErrorToRFC6749Error(err)  		ctx.Logger.Errorf("Access Response for Request with id '%s' failed to be created with error: %s", requester.GetID(), rfc.WithExposeDebug(true).GetDescription()) -		ctx.Providers.OpenIDConnect.Fosite.WriteAccessError(rw, requester, err) +		ctx.Providers.OpenIDConnect.WriteAccessError(rw, requester, err)  		return  	} @@ -60,5 +60,5 @@ func OpenIDConnectTokenPOST(ctx *middlewares.AutheliaCtx, rw http.ResponseWriter  	ctx.Logger.Tracef("Access Request with id '%s' on client with id '%s' produced the following claims: %+v", requester.GetID(), client.GetID(), responder.ToMap()) -	ctx.Providers.OpenIDConnect.Fosite.WriteAccessResponse(rw, requester, responder) +	ctx.Providers.OpenIDConnect.WriteAccessResponse(rw, requester, responder)  } diff --git a/internal/handlers/handler_oidc_userinfo.go b/internal/handlers/handler_oidc_userinfo.go index 99d3a6012..ddb61cb35 100644 --- a/internal/handlers/handler_oidc_userinfo.go +++ b/internal/handlers/handler_oidc_userinfo.go @@ -29,7 +29,7 @@ func OpenIDConnectUserinfo(ctx *middlewares.AutheliaCtx, rw http.ResponseWriter,  	oidcSession := oidc.NewSession() -	if tokenType, requester, err = ctx.Providers.OpenIDConnect.Fosite.IntrospectToken( +	if tokenType, requester, err = ctx.Providers.OpenIDConnect.IntrospectToken(  		req.Context(), fosite.AccessTokenFromRequest(req), fosite.AccessToken, oidcSession); err != nil {  		rfc := fosite.ErrorToRFC6749Error(err) @@ -50,27 +50,27 @@ func OpenIDConnectUserinfo(ctx *middlewares.AutheliaCtx, rw http.ResponseWriter,  		ctx.Logger.Errorf("UserInfo Request with id '%s' on client with id '%s' failed with error: bearer authorization failed as the token is not an access_token", requester.GetID(), client.GetID())  		errStr := "Only access tokens are allowed in the authorization header." -		rw.Header().Set("WWW-Authenticate", fmt.Sprintf(`Bearer error="invalid_token",error_description="%s"`, errStr)) +		rw.Header().Set(fasthttp.HeaderWWWAuthenticate, fmt.Sprintf(`Bearer error="invalid_token",error_description="%s"`, errStr))  		ctx.Providers.OpenIDConnect.WriteErrorCode(rw, req, http.StatusUnauthorized, errors.New(errStr))  		return  	} -	if client, err = ctx.Providers.OpenIDConnect.Store.GetFullClient(clientID); err != nil { +	if client, err = ctx.Providers.OpenIDConnect.GetFullClient(clientID); err != nil {  		ctx.Providers.OpenIDConnect.WriteError(rw, req, errors.WithStack(fosite.ErrServerError.WithHint("Unable to assert type of client")))  		return  	}  	claims := requester.GetSession().(*model.OpenIDSession).IDTokenClaims().ToMap() -	delete(claims, "jti") -	delete(claims, "sid") -	delete(claims, "at_hash") -	delete(claims, "c_hash") -	delete(claims, "exp") -	delete(claims, "nonce") +	delete(claims, oidc.ClaimJWTID) +	delete(claims, oidc.ClaimSessionID) +	delete(claims, oidc.ClaimAccessTokenHash) +	delete(claims, oidc.ClaimCodeHash) +	delete(claims, oidc.ClaimExpirationTime) +	delete(claims, oidc.ClaimNonce) -	audience, ok := claims["aud"].([]string) +	audience, ok := claims[oidc.ClaimAudience].([]string)  	if !ok || len(audience) == 0 {  		audience = []string{client.GetID()} @@ -89,14 +89,14 @@ func OpenIDConnectUserinfo(ctx *middlewares.AutheliaCtx, rw http.ResponseWriter,  		}  	} -	claims["aud"] = audience +	claims[oidc.ClaimAudience] = audience  	var token string  	ctx.Logger.Tracef("UserInfo Response with id '%s' on client with id '%s' is being sent with the following claims: %+v", requester.GetID(), clientID, claims)  	switch client.UserinfoSigningAlgorithm { -	case "RS256": +	case oidc.SigningAlgorithmRSAWithSHA256:  		var jti uuid.UUID  		if jti, err = uuid.NewRandom(); err != nil { @@ -105,11 +105,13 @@ func OpenIDConnectUserinfo(ctx *middlewares.AutheliaCtx, rw http.ResponseWriter,  			return  		} -		claims["jti"] = jti.String() -		claims["iat"] = time.Now().Unix() +		claims[oidc.ClaimJWTID] = jti.String() +		claims[oidc.ClaimIssuedAt] = time.Now().Unix()  		headers := &jwt.Headers{ -			Extra: map[string]any{"kid": ctx.Providers.OpenIDConnect.KeyManager.GetActiveKeyID()}, +			Extra: map[string]any{ +				oidc.JWTHeaderKeyIdentifier: ctx.Providers.OpenIDConnect.KeyManager.GetActiveKeyID(), +			},  		}  		if token, _, err = ctx.Providers.OpenIDConnect.KeyManager.Strategy().Generate(req.Context(), claims, headers); err != nil { diff --git a/internal/handlers/handler_oidc_wellknown.go b/internal/handlers/handler_oidc_wellknown.go index 89d346e02..1ac819817 100644 --- a/internal/handlers/handler_oidc_wellknown.go +++ b/internal/handlers/handler_oidc_wellknown.go @@ -1,6 +1,8 @@  package handlers  import ( +	"net/url" +  	"github.com/valyala/fasthttp"  	"github.com/authelia/authelia/v4/internal/middlewares" @@ -13,8 +15,12 @@ import (  //  // https://openid.net/specs/openid-connect-discovery-1_0.html  func OpenIDConnectConfigurationWellKnownGET(ctx *middlewares.AutheliaCtx) { -	issuer, err := ctx.ExternalRootURL() -	if err != nil { +	var ( +		issuer *url.URL +		err    error +	) + +	if issuer, err = ctx.IssuerURL(); err != nil {  		ctx.Logger.Errorf("Error occurred determining OpenID Connect issuer details: %+v", err)  		ctx.ReplyStatusCode(fasthttp.StatusBadRequest) @@ -22,7 +28,7 @@ func OpenIDConnectConfigurationWellKnownGET(ctx *middlewares.AutheliaCtx) {  		return  	} -	wellKnown := ctx.Providers.OpenIDConnect.GetOpenIDConnectWellKnownConfiguration(issuer) +	wellKnown := ctx.Providers.OpenIDConnect.GetOpenIDConnectWellKnownConfiguration(issuer.String())  	if err = ctx.ReplyJSON(wellKnown, fasthttp.StatusOK); err != nil {  		ctx.Logger.Errorf("Error occurred in JSON encode: %+v", err) @@ -41,8 +47,12 @@ func OpenIDConnectConfigurationWellKnownGET(ctx *middlewares.AutheliaCtx) {  //  // https://datatracker.ietf.org/doc/html/rfc8414  func OAuthAuthorizationServerWellKnownGET(ctx *middlewares.AutheliaCtx) { -	issuer, err := ctx.ExternalRootURL() -	if err != nil { +	var ( +		issuer *url.URL +		err    error +	) + +	if issuer, err = ctx.IssuerURL(); err != nil {  		ctx.Logger.Errorf("Error occurred determining OpenID Connect issuer details: %+v", err)  		ctx.ReplyStatusCode(fasthttp.StatusBadRequest) @@ -50,7 +60,7 @@ func OAuthAuthorizationServerWellKnownGET(ctx *middlewares.AutheliaCtx) {  		return  	} -	wellKnown := ctx.Providers.OpenIDConnect.GetOAuth2WellKnownConfiguration(issuer) +	wellKnown := ctx.Providers.OpenIDConnect.GetOAuth2WellKnownConfiguration(issuer.String())  	if err = ctx.ReplyJSON(wellKnown, fasthttp.StatusOK); err != nil {  		ctx.Logger.Errorf("Error occurred in JSON encode: %+v", err) diff --git a/internal/handlers/handler_sign_duo.go b/internal/handlers/handler_sign_duo.go index 704ce4320..0ebe285b0 100644 --- a/internal/handlers/handler_sign_duo.go +++ b/internal/handlers/handler_sign_duo.go @@ -16,7 +16,7 @@ import (  func DuoPOST(duoAPI duo.API) middlewares.RequestHandler {  	return func(ctx *middlewares.AutheliaCtx) {  		var ( -			bodyJSON       = &signDuoRequestBody{} +			bodyJSON       = &bodySignDuoRequest{}  			device, method string  		) @@ -90,7 +90,7 @@ func DuoPOST(duoAPI duo.API) middlewares.RequestHandler {  }  // HandleInitialDeviceSelection handler for retrieving all available devices. -func HandleInitialDeviceSelection(ctx *middlewares.AutheliaCtx, userSession *session.UserSession, duoAPI duo.API, bodyJSON *signDuoRequestBody) (device string, method string, err error) { +func HandleInitialDeviceSelection(ctx *middlewares.AutheliaCtx, userSession *session.UserSession, duoAPI duo.API, bodyJSON *bodySignDuoRequest) (device string, method string, err error) {  	result, message, devices, enrollURL, err := DuoPreAuth(ctx, duoAPI)  	if err != nil {  		ctx.Logger.Errorf("Failed to perform Duo PreAuth for user '%s': %+v", userSession.Username, err) @@ -135,7 +135,7 @@ func HandleInitialDeviceSelection(ctx *middlewares.AutheliaCtx, userSession *ses  }  // HandlePreferredDeviceCheck handler to check if the saved device and method is still valid. -func HandlePreferredDeviceCheck(ctx *middlewares.AutheliaCtx, userSession *session.UserSession, duoAPI duo.API, device string, method string, bodyJSON *signDuoRequestBody) (string, string, error) { +func HandlePreferredDeviceCheck(ctx *middlewares.AutheliaCtx, userSession *session.UserSession, duoAPI duo.API, device string, method string, bodyJSON *bodySignDuoRequest) (string, string, error) {  	result, message, devices, enrollURL, err := DuoPreAuth(ctx, duoAPI)  	if err != nil {  		ctx.Logger.Errorf("Failed to perform Duo PreAuth for user '%s': %+v", userSession.Username, err) @@ -243,7 +243,7 @@ func HandleAutoSelection(ctx *middlewares.AutheliaCtx, devices []DuoDevice, user  }  // HandleAllow handler for successful logins. -func HandleAllow(ctx *middlewares.AutheliaCtx, bodyJSON *signDuoRequestBody) { +func HandleAllow(ctx *middlewares.AutheliaCtx, bodyJSON *bodySignDuoRequest) {  	userSession := ctx.GetSession()  	err := ctx.Providers.SessionProvider.RegenerateSession(ctx.RequestCtx) @@ -267,7 +267,7 @@ func HandleAllow(ctx *middlewares.AutheliaCtx, bodyJSON *signDuoRequestBody) {  	}  	if bodyJSON.Workflow == workflowOpenIDConnect { -		handleOIDCWorkflowResponse(ctx, bodyJSON.TargetURL) +		handleOIDCWorkflowResponse(ctx, bodyJSON.TargetURL, bodyJSON.WorkflowID)  	} else {  		Handle2FAResponse(ctx, bodyJSON.TargetURL)  	} diff --git a/internal/handlers/handler_sign_duo_test.go b/internal/handlers/handler_sign_duo_test.go index 771fc49bf..c08c8f1ee 100644 --- a/internal/handlers/handler_sign_duo_test.go +++ b/internal/handlers/handler_sign_duo_test.go @@ -54,7 +54,7 @@ func (s *SecondFactorDuoPostSuite) TestShouldEnroll() {  	duoMock.EXPECT().PreAuthCall(s.mock.Ctx, gomock.Eq(values)).Return(&preAuthResponse, nil) -	bodyBytes, err := json.Marshal(signDuoRequestBody{}) +	bodyBytes, err := json.Marshal(bodySignDuoRequest{})  	s.Require().NoError(err)  	s.mock.Ctx.Request.SetBody(bodyBytes) @@ -113,7 +113,7 @@ func (s *SecondFactorDuoPostSuite) TestShouldAutoSelect() {  	duoMock.EXPECT().AuthCall(s.mock.Ctx, gomock.Eq(values)).Return(&authResponse, nil) -	bodyBytes, err := json.Marshal(signDuoRequestBody{TargetURL: "https://target.example.com"}) +	bodyBytes, err := json.Marshal(bodySignDuoRequest{TargetURL: "https://target.example.com"})  	s.Require().NoError(err)  	s.mock.Ctx.Request.SetBody(bodyBytes) @@ -142,7 +142,7 @@ func (s *SecondFactorDuoPostSuite) TestShouldDenyAutoSelect() {  	values.Set("factor", "push")  	values.Set("device", "12345ABCDEFGHIJ67890") -	bodyBytes, err := json.Marshal(signDuoRequestBody{}) +	bodyBytes, err := json.Marshal(bodySignDuoRequest{})  	s.Require().NoError(err)  	s.mock.Ctx.Request.SetBody(bodyBytes) @@ -162,7 +162,7 @@ func (s *SecondFactorDuoPostSuite) TestShouldFailAutoSelect() {  	duoMock.EXPECT().PreAuthCall(s.mock.Ctx, gomock.Any()).Return(nil, fmt.Errorf("Connnection error")) -	bodyBytes, err := json.Marshal(signDuoRequestBody{TargetURL: "https://target.example.com"}) +	bodyBytes, err := json.Marshal(bodySignDuoRequest{TargetURL: "https://target.example.com"})  	s.Require().NoError(err)  	s.mock.Ctx.Request.SetBody(bodyBytes) @@ -191,7 +191,7 @@ func (s *SecondFactorDuoPostSuite) TestShouldDeleteOldDeviceAndEnroll() {  	s.mock.StorageMock.EXPECT().DeletePreferredDuoDevice(s.mock.Ctx, "john").Return(nil) -	bodyBytes, err := json.Marshal(signDuoRequestBody{}) +	bodyBytes, err := json.Marshal(bodySignDuoRequest{})  	s.Require().NoError(err)  	s.mock.Ctx.Request.SetBody(bodyBytes) @@ -225,7 +225,7 @@ func (s *SecondFactorDuoPostSuite) TestShouldDeleteOldDeviceAndCallPreauthAPIWit  	s.mock.StorageMock.EXPECT().DeletePreferredDuoDevice(s.mock.Ctx, "john").Return(nil) -	bodyBytes, err := json.Marshal(signDuoRequestBody{}) +	bodyBytes, err := json.Marshal(bodySignDuoRequest{})  	s.Require().NoError(err)  	s.mock.Ctx.Request.SetBody(bodyBytes) @@ -263,7 +263,7 @@ func (s *SecondFactorDuoPostSuite) TestShouldUseOldDeviceAndSelect() {  	duoMock.EXPECT().PreAuthCall(s.mock.Ctx, gomock.Eq(values)).Return(&preAuthResponse, nil) -	bodyBytes, err := json.Marshal(signDuoRequestBody{}) +	bodyBytes, err := json.Marshal(bodySignDuoRequest{})  	s.Require().NoError(err)  	s.mock.Ctx.Request.SetBody(bodyBytes) @@ -319,7 +319,7 @@ func (s *SecondFactorDuoPostSuite) TestShouldUseInvalidMethodAndAutoSelect() {  	duoMock.EXPECT().AuthCall(s.mock.Ctx, gomock.Eq(values)).Return(&authResponse, nil) -	bodyBytes, err := json.Marshal(signDuoRequestBody{TargetURL: "https://target.example.com"}) +	bodyBytes, err := json.Marshal(bodySignDuoRequest{TargetURL: "https://target.example.com"})  	s.Require().NoError(err)  	s.mock.Ctx.Request.SetBody(bodyBytes) @@ -342,7 +342,7 @@ func (s *SecondFactorDuoPostSuite) TestShouldCallDuoPreauthAPIAndAllowAccess() {  	duoMock.EXPECT().PreAuthCall(s.mock.Ctx, gomock.Eq(values)).Return(&preAuthResponse, nil) -	bodyBytes, err := json.Marshal(signDuoRequestBody{TargetURL: "https://target.example.com"}) +	bodyBytes, err := json.Marshal(bodySignDuoRequest{TargetURL: "https://target.example.com"})  	s.Require().NoError(err)  	s.mock.Ctx.Request.SetBody(bodyBytes) @@ -372,7 +372,7 @@ func (s *SecondFactorDuoPostSuite) TestShouldCallDuoPreauthAPIAndDenyAccess() {  	values.Set("factor", "push")  	values.Set("device", "12345ABCDEFGHIJ67890") -	bodyBytes, err := json.Marshal(signDuoRequestBody{}) +	bodyBytes, err := json.Marshal(bodySignDuoRequest{})  	s.Require().NoError(err)  	s.mock.Ctx.Request.SetBody(bodyBytes) @@ -390,7 +390,7 @@ func (s *SecondFactorDuoPostSuite) TestShouldCallDuoPreauthAPIAndFail() {  	duoMock.EXPECT().PreAuthCall(s.mock.Ctx, gomock.Any()).Return(nil, fmt.Errorf("Connnection error")) -	bodyBytes, err := json.Marshal(signDuoRequestBody{}) +	bodyBytes, err := json.Marshal(bodySignDuoRequest{})  	s.Require().NoError(err)  	s.mock.Ctx.Request.SetBody(bodyBytes) @@ -442,7 +442,7 @@ func (s *SecondFactorDuoPostSuite) TestShouldCallDuoAPIAndDenyAccess() {  	duoMock.EXPECT().AuthCall(s.mock.Ctx, gomock.Eq(values)).Return(&response, nil) -	bodyBytes, err := json.Marshal(signDuoRequestBody{}) +	bodyBytes, err := json.Marshal(bodySignDuoRequest{})  	s.Require().NoError(err)  	s.mock.Ctx.Request.SetBody(bodyBytes) @@ -473,7 +473,7 @@ func (s *SecondFactorDuoPostSuite) TestShouldCallDuoAPIAndFail() {  	duoMock.EXPECT().AuthCall(s.mock.Ctx, gomock.Any()).Return(nil, fmt.Errorf("Connnection error")) -	bodyBytes, err := json.Marshal(signDuoRequestBody{}) +	bodyBytes, err := json.Marshal(bodySignDuoRequest{})  	s.Require().NoError(err)  	s.mock.Ctx.Request.SetBody(bodyBytes) @@ -521,7 +521,7 @@ func (s *SecondFactorDuoPostSuite) TestShouldRedirectUserToDefaultURL() {  	s.mock.Ctx.Configuration.DefaultRedirectionURL = testRedirectionURL -	bodyBytes, err := json.Marshal(signDuoRequestBody{}) +	bodyBytes, err := json.Marshal(bodySignDuoRequest{})  	s.Require().NoError(err)  	s.mock.Ctx.Request.SetBody(bodyBytes) @@ -568,7 +568,7 @@ func (s *SecondFactorDuoPostSuite) TestShouldNotReturnRedirectURL() {  	duoMock.EXPECT().AuthCall(s.mock.Ctx, gomock.Any()).Return(&response, nil) -	bodyBytes, err := json.Marshal(signDuoRequestBody{}) +	bodyBytes, err := json.Marshal(bodySignDuoRequest{})  	s.Require().NoError(err)  	s.mock.Ctx.Request.SetBody(bodyBytes) @@ -613,15 +613,15 @@ func (s *SecondFactorDuoPostSuite) TestShouldRedirectUserToSafeTargetURL() {  	duoMock.EXPECT().AuthCall(s.mock.Ctx, gomock.Any()).Return(&response, nil) -	bodyBytes, err := json.Marshal(signDuoRequestBody{ -		TargetURL: "https://mydomain.example.com", +	bodyBytes, err := json.Marshal(bodySignDuoRequest{ +		TargetURL: "https://example.com",  	})  	s.Require().NoError(err)  	s.mock.Ctx.Request.SetBody(bodyBytes)  	DuoPOST(duoMock)(s.mock.Ctx)  	s.mock.Assert200OK(s.T(), redirectResponse{ -		Redirect: "https://mydomain.example.com", +		Redirect: "https://example.com",  	})  } @@ -662,8 +662,8 @@ func (s *SecondFactorDuoPostSuite) TestShouldNotRedirectToUnsafeURL() {  	duoMock.EXPECT().AuthCall(s.mock.Ctx, gomock.Any()).Return(&response, nil) -	bodyBytes, err := json.Marshal(signDuoRequestBody{ -		TargetURL: "http://mydomain.example.com", +	bodyBytes, err := json.Marshal(bodySignDuoRequest{ +		TargetURL: "http://example.com",  	})  	s.Require().NoError(err)  	s.mock.Ctx.Request.SetBody(bodyBytes) @@ -709,8 +709,8 @@ func (s *SecondFactorDuoPostSuite) TestShouldRegenerateSessionForPreventingSessi  	duoMock.EXPECT().AuthCall(s.mock.Ctx, gomock.Any()).Return(&response, nil) -	bodyBytes, err := json.Marshal(signDuoRequestBody{ -		TargetURL: "http://mydomain.example.com", +	bodyBytes, err := json.Marshal(bodySignDuoRequest{ +		TargetURL: "http://example.com",  	})  	s.Require().NoError(err)  	s.mock.Ctx.Request.SetBody(bodyBytes) diff --git a/internal/handlers/handler_sign_totp.go b/internal/handlers/handler_sign_totp.go index 98e04561e..3d584fddd 100644 --- a/internal/handlers/handler_sign_totp.go +++ b/internal/handlers/handler_sign_totp.go @@ -7,7 +7,7 @@ import (  // TimeBasedOneTimePasswordPOST validate the TOTP passcode provided by the user.  func TimeBasedOneTimePasswordPOST(ctx *middlewares.AutheliaCtx) { -	bodyJSON := signTOTPRequestBody{} +	bodyJSON := bodySignTOTPRequest{}  	if err := ctx.ParseBody(&bodyJSON); err != nil {  		ctx.Logger.Errorf(logFmtErrParseRequestBody, regulation.AuthTypeTOTP, err) @@ -79,7 +79,7 @@ func TimeBasedOneTimePasswordPOST(ctx *middlewares.AutheliaCtx) {  	}  	if bodyJSON.Workflow == workflowOpenIDConnect { -		handleOIDCWorkflowResponse(ctx, bodyJSON.TargetURL) +		handleOIDCWorkflowResponse(ctx, bodyJSON.TargetURL, bodyJSON.WorkflowID)  	} else {  		Handle2FAResponse(ctx, bodyJSON.TargetURL)  	} diff --git a/internal/handlers/handler_sign_totp_test.go b/internal/handlers/handler_sign_totp_test.go index 13b84c351..6ade2e0dc 100644 --- a/internal/handlers/handler_sign_totp_test.go +++ b/internal/handlers/handler_sign_totp_test.go @@ -59,7 +59,7 @@ func (s *HandlerSignTOTPSuite) TestShouldRedirectUserToDefaultURL() {  	s.mock.Ctx.Configuration.DefaultRedirectionURL = testRedirectionURL -	bodyBytes, err := json.Marshal(signTOTPRequestBody{ +	bodyBytes, err := json.Marshal(bodySignTOTPRequest{  		Token: "abc",  	})  	s.Require().NoError(err) @@ -97,7 +97,7 @@ func (s *HandlerSignTOTPSuite) TestShouldFailWhenTOTPSignInInfoFailsToUpdate() {  	s.mock.Ctx.Configuration.DefaultRedirectionURL = testRedirectionURL -	bodyBytes, err := json.Marshal(signTOTPRequestBody{ +	bodyBytes, err := json.Marshal(bodySignTOTPRequest{  		Token: "abc",  	})  	s.Require().NoError(err) @@ -131,7 +131,7 @@ func (s *HandlerSignTOTPSuite) TestShouldNotReturnRedirectURL() {  		EXPECT().  		UpdateTOTPConfigurationSignIn(s.mock.Ctx, gomock.Any(), gomock.Any()) -	bodyBytes, err := json.Marshal(signTOTPRequestBody{ +	bodyBytes, err := json.Marshal(bodySignTOTPRequest{  		Token: "abc",  	})  	s.Require().NoError(err) @@ -165,7 +165,7 @@ func (s *HandlerSignTOTPSuite) TestShouldRedirectUserToSafeTargetURL() {  		EXPECT().  		UpdateTOTPConfigurationSignIn(s.mock.Ctx, gomock.Any(), gomock.Any()) -	bodyBytes, err := json.Marshal(signTOTPRequestBody{ +	bodyBytes, err := json.Marshal(bodySignTOTPRequest{  		Token:     "abc",  		TargetURL: "https://mydomain.example.com",  	}) @@ -203,7 +203,7 @@ func (s *HandlerSignTOTPSuite) TestShouldNotRedirectToUnsafeURL() {  		Validate(gomock.Eq("abc"), gomock.Eq(&model.TOTPConfiguration{Secret: []byte("secret")})).  		Return(true, nil) -	bodyBytes, err := json.Marshal(signTOTPRequestBody{ +	bodyBytes, err := json.Marshal(bodySignTOTPRequest{  		Token:     "abc",  		TargetURL: "http://mydomain.example.com",  	}) @@ -241,7 +241,7 @@ func (s *HandlerSignTOTPSuite) TestShouldRegenerateSessionForPreventingSessionFi  		EXPECT().  		UpdateTOTPConfigurationSignIn(s.mock.Ctx, gomock.Any(), gomock.Any()) -	bodyBytes, err := json.Marshal(signTOTPRequestBody{ +	bodyBytes, err := json.Marshal(bodySignTOTPRequest{  		Token: "abc",  	})  	s.Require().NoError(err) diff --git a/internal/handlers/handler_sign_webauthn.go b/internal/handlers/handler_sign_webauthn.go index 6dec89009..89a980626 100644 --- a/internal/handlers/handler_sign_webauthn.go +++ b/internal/handlers/handler_sign_webauthn.go @@ -84,7 +84,7 @@ func WebauthnAssertionPOST(ctx *middlewares.AutheliaCtx) {  		err error  		w   *webauthn.WebAuthn -		bodyJSON signWebauthnRequestBody +		bodyJSON bodySignWebauthnRequest  	)  	if err = ctx.ParseBody(&bodyJSON); err != nil { @@ -198,7 +198,7 @@ func WebauthnAssertionPOST(ctx *middlewares.AutheliaCtx) {  	}  	if bodyJSON.Workflow == workflowOpenIDConnect { -		handleOIDCWorkflowResponse(ctx, bodyJSON.TargetURL) +		handleOIDCWorkflowResponse(ctx, bodyJSON.TargetURL, bodyJSON.WorkflowID)  	} else {  		Handle2FAResponse(ctx, bodyJSON.TargetURL)  	} diff --git a/internal/handlers/handler_user_info.go b/internal/handlers/handler_user_info.go index 28489b6e7..6e4f5dc0d 100644 --- a/internal/handlers/handler_user_info.go +++ b/internal/handlers/handler_user_info.go @@ -74,7 +74,7 @@ func UserInfoGET(ctx *middlewares.AutheliaCtx) {  // MethodPreferencePOST update the user preferences regarding 2FA method.  func MethodPreferencePOST(ctx *middlewares.AutheliaCtx) { -	bodyJSON := preferred2FAMethodBody{} +	bodyJSON := bodyPreferred2FAMethod{}  	err := ctx.ParseBody(&bodyJSON)  	if err != nil { diff --git a/internal/handlers/handler_verify.go b/internal/handlers/handler_verify.go index 883852da7..258aec61d 100644 --- a/internal/handlers/handler_verify.go +++ b/internal/handlers/handler_verify.go @@ -143,7 +143,7 @@ func isSessionInactiveTooLong(ctx *middlewares.AutheliaCtx, userSession *session  func verifySessionCookie(ctx *middlewares.AutheliaCtx, targetURL *url.URL, userSession *session.UserSession, refreshProfile bool,  	refreshProfileInterval time.Duration) (username, name string, groups, emails []string, authLevel authentication.Level, err error) {  	// No username in the session means the user is anonymous. -	isUserAnonymous := userSession.Username == "" +	isUserAnonymous := userSession.IsAnonymous()  	if isUserAnonymous && userSession.AuthenticationLevel != authentication.NotAuthenticated {  		return "", "", nil, nil, authentication.NotAuthenticated, fmt.Errorf("an anonymous user cannot be authenticated (this might be the sign of a security compromise)") @@ -221,7 +221,7 @@ func handleUnauthorized(ctx *middlewares.AutheliaCtx, targetURL fmt.Stringer, is  		qry := redirectionURL.Query() -		qry.Set("rd", targetURL.String()) +		qry.Set(queryArgRD, targetURL.String())  		if rm != "" {  			qry.Set("rm", rm) @@ -322,7 +322,7 @@ func verifySessionHasUpToDateProfile(ctx *middlewares.AutheliaCtx, targetURL *ur  	// See https://www.authelia.com/o/threatmodel#potential-future-guarantees  	ctx.Logger.Tracef("Checking if we need check the authentication backend for an updated profile for %s.", userSession.Username) -	if !refreshProfile || userSession.Username == "" || targetURL == nil { +	if !refreshProfile || userSession.IsAnonymous() || targetURL == nil {  		return nil  	} diff --git a/internal/handlers/handler_verify_test.go b/internal/handlers/handler_verify_test.go index 304339ed6..a0ef56858 100644 --- a/internal/handlers/handler_verify_test.go +++ b/internal/handlers/handler_verify_test.go @@ -851,7 +851,7 @@ func TestShouldRedirectWhenSessionInactiveForTooLongAndRDParamProvided(t *testin  	err := mock.Ctx.SaveSession(userSession)  	require.NoError(t, err) -	mock.Ctx.QueryArgs().Add("rd", "https://login.example.com") +	mock.Ctx.QueryArgs().Add(queryArgRD, "https://login.example.com")  	mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com")  	mock.Ctx.Request.Header.Set("X-Forwarded-Method", "GET")  	mock.Ctx.Request.Header.Set("Accept", "text/html; charset=utf-8") @@ -870,7 +870,7 @@ func TestShouldRedirectWithCorrectStatusCodeBasedOnRequestMethod(t *testing.T) {  	mock := mocks.NewMockAutheliaCtx(t)  	defer mock.Close() -	mock.Ctx.QueryArgs().Add("rd", "https://login.example.com") +	mock.Ctx.QueryArgs().Add(queryArgRD, "https://login.example.com")  	mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com")  	mock.Ctx.Request.Header.Set("X-Forwarded-Method", "GET")  	mock.Ctx.Request.Header.Set("Accept", "text/html; charset=utf-8") @@ -881,7 +881,7 @@ func TestShouldRedirectWithCorrectStatusCodeBasedOnRequestMethod(t *testing.T) {  		string(mock.Ctx.Response.Body()))  	assert.Equal(t, 302, mock.Ctx.Response.StatusCode()) -	mock.Ctx.QueryArgs().Add("rd", "https://login.example.com") +	mock.Ctx.QueryArgs().Add(queryArgRD, "https://login.example.com")  	mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com")  	mock.Ctx.Request.Header.Set("X-Forwarded-Method", "POST")  	mock.Ctx.Request.Header.Set("Accept", "text/html; charset=utf-8") @@ -1449,7 +1449,7 @@ func TestShouldNotRedirectRequestsForBypassACLWhenInactiveForTooLong(t *testing.  	require.NoError(t, err)  	// Should respond 200 OK. -	mock.Ctx.QueryArgs().Add("rd", "https://login.example.com") +	mock.Ctx.QueryArgs().Add(queryArgRD, "https://login.example.com")  	mock.Ctx.Request.Header.Set("X-Forwarded-Method", "GET")  	mock.Ctx.Request.Header.Set("Accept", "text/html; charset=utf-8")  	mock.Ctx.Request.Header.Set("X-Original-URL", "https://bypass.example.com") @@ -1458,7 +1458,7 @@ func TestShouldNotRedirectRequestsForBypassACLWhenInactiveForTooLong(t *testing.  	assert.Nil(t, mock.Ctx.Response.Header.Peek("Location"))  	// Should respond 302 Found. -	mock.Ctx.QueryArgs().Add("rd", "https://login.example.com") +	mock.Ctx.QueryArgs().Add(queryArgRD, "https://login.example.com")  	mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com")  	mock.Ctx.Request.Header.Set("X-Forwarded-Method", "GET")  	mock.Ctx.Request.Header.Set("Accept", "text/html; charset=utf-8") @@ -1467,7 +1467,7 @@ func TestShouldNotRedirectRequestsForBypassACLWhenInactiveForTooLong(t *testing.  	assert.Equal(t, "https://login.example.com/?rd=https%3A%2F%2Ftwo-factor.example.com&rm=GET", string(mock.Ctx.Response.Header.Peek("Location")))  	// Should respond 401 Unauthorized. -	mock.Ctx.QueryArgs().Del("rd") +	mock.Ctx.QueryArgs().Del(queryArgRD)  	mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com")  	mock.Ctx.Request.Header.Set("X-Forwarded-Method", "GET")  	mock.Ctx.Request.Header.Set("Accept", "text/html; charset=utf-8") diff --git a/internal/handlers/oidc.go b/internal/handlers/oidc.go index 2b2051f1b..9febd5fe5 100644 --- a/internal/handlers/oidc.go +++ b/internal/handlers/oidc.go @@ -21,13 +21,14 @@ func oidcGrantRequests(ar fosite.AuthorizeRequester, consent *model.OAuth2Consen  			extraClaims[oidc.ClaimGroups] = userSession.Groups  		case oidc.ScopeProfile:  			extraClaims[oidc.ClaimPreferredUsername] = userSession.Username -			extraClaims[oidc.ClaimDisplayName] = userSession.DisplayName +			extraClaims[oidc.ClaimFullName] = userSession.DisplayName  		case oidc.ScopeEmail:  			if len(userSession.Emails) != 0 { -				extraClaims[oidc.ClaimEmail] = userSession.Emails[0] +				extraClaims[oidc.ClaimPreferredEmail] = userSession.Emails[0]  				if len(userSession.Emails) > 1 {  					extraClaims[oidc.ClaimEmailAlts] = userSession.Emails[1:]  				} +  				// TODO (james-d-elliott): actually verify emails and record that information.  				extraClaims[oidc.ClaimEmailVerified] = true  			} diff --git a/internal/handlers/oidc_test.go b/internal/handlers/oidc_test.go index 95ff6e34c..648fdefa2 100644 --- a/internal/handlers/oidc_test.go +++ b/internal/handlers/oidc_test.go @@ -23,8 +23,8 @@ func TestShouldGrantAppropriateClaimsForScopeProfile(t *testing.T) {  	require.Contains(t, extraClaims, oidc.ClaimPreferredUsername)  	assert.Equal(t, "john", extraClaims[oidc.ClaimPreferredUsername]) -	require.Contains(t, extraClaims, oidc.ClaimDisplayName) -	assert.Equal(t, "John Smith", extraClaims[oidc.ClaimDisplayName]) +	require.Contains(t, extraClaims, oidc.ClaimFullName) +	assert.Equal(t, "John Smith", extraClaims[oidc.ClaimFullName])  }  func TestShouldGrantAppropriateClaimsForScopeGroups(t *testing.T) { @@ -59,8 +59,8 @@ func TestShouldGrantAppropriateClaimsForScopeEmail(t *testing.T) {  	assert.Len(t, extraClaims, 3) -	require.Contains(t, extraClaims, oidc.ClaimEmail) -	assert.Equal(t, "j.smith@authelia.com", extraClaims[oidc.ClaimEmail]) +	require.Contains(t, extraClaims, oidc.ClaimPreferredEmail) +	assert.Equal(t, "j.smith@authelia.com", extraClaims[oidc.ClaimPreferredEmail])  	require.Contains(t, extraClaims, oidc.ClaimEmailAlts)  	assert.Len(t, extraClaims[oidc.ClaimEmailAlts], 1) @@ -73,8 +73,8 @@ func TestShouldGrantAppropriateClaimsForScopeEmail(t *testing.T) {  	assert.Len(t, extraClaims, 2) -	require.Contains(t, extraClaims, oidc.ClaimEmail) -	assert.Equal(t, "f.smith@authelia.com", extraClaims[oidc.ClaimEmail]) +	require.Contains(t, extraClaims, oidc.ClaimPreferredEmail) +	assert.Equal(t, "f.smith@authelia.com", extraClaims[oidc.ClaimPreferredEmail])  	require.Contains(t, extraClaims, oidc.ClaimEmailVerified)  	assert.Equal(t, true, extraClaims[oidc.ClaimEmailVerified]) @@ -92,8 +92,8 @@ func TestShouldGrantAppropriateClaimsForScopeOpenIDAndProfile(t *testing.T) {  	require.Contains(t, extraClaims, oidc.ClaimPreferredUsername)  	assert.Equal(t, "john", extraClaims[oidc.ClaimPreferredUsername]) -	require.Contains(t, extraClaims, oidc.ClaimDisplayName) -	assert.Equal(t, "John Smith", extraClaims[oidc.ClaimDisplayName]) +	require.Contains(t, extraClaims, oidc.ClaimFullName) +	assert.Equal(t, "John Smith", extraClaims[oidc.ClaimFullName])  	extraClaims = oidcGrantRequests(nil, consent, &oidcUserSessionFred) @@ -102,8 +102,8 @@ func TestShouldGrantAppropriateClaimsForScopeOpenIDAndProfile(t *testing.T) {  	require.Contains(t, extraClaims, oidc.ClaimPreferredUsername)  	assert.Equal(t, "fred", extraClaims[oidc.ClaimPreferredUsername]) -	require.Contains(t, extraClaims, oidc.ClaimDisplayName) -	assert.Equal(t, extraClaims[oidc.ClaimDisplayName], "Fred Smith") +	require.Contains(t, extraClaims, oidc.ClaimFullName) +	assert.Equal(t, extraClaims[oidc.ClaimFullName], "Fred Smith")  }  var ( diff --git a/internal/handlers/response.go b/internal/handlers/response.go index 83f5de59d..33599ea8e 100644 --- a/internal/handlers/response.go +++ b/internal/handlers/response.go @@ -3,66 +3,19 @@ package handlers  import (  	"fmt"  	"net/url" +	"path"  	"time" +	"github.com/google/uuid"  	"github.com/valyala/fasthttp"  	"github.com/authelia/authelia/v4/internal/authorization"  	"github.com/authelia/authelia/v4/internal/middlewares" +	"github.com/authelia/authelia/v4/internal/model"  	"github.com/authelia/authelia/v4/internal/oidc"  	"github.com/authelia/authelia/v4/internal/utils"  ) -// handleOIDCWorkflowResponse handle the redirection upon authentication in the OIDC workflow. -func handleOIDCWorkflowResponse(ctx *middlewares.AutheliaCtx, targetURI string) { -	if len(targetURI) == 0 { -		ctx.Error(fmt.Errorf("unable to parse target URL %s: empty value", targetURI), messageAuthenticationFailed) - -		return -	} - -	var ( -		targetURL *url.URL -		err       error -	) - -	if targetURL, err = url.ParseRequestURI(targetURI); err != nil { -		ctx.Error(fmt.Errorf("unable to parse target URL %s: %w", targetURI, err), messageAuthenticationFailed) - -		return -	} - -	var ( -		id     string -		client *oidc.Client -	) - -	if id = targetURL.Query().Get("client_id"); len(id) == 0 { -		ctx.Error(fmt.Errorf("unable to get client id from from URL '%s'", targetURL), messageAuthenticationFailed) - -		return -	} - -	if client, err = ctx.Providers.OpenIDConnect.Store.GetFullClient(id); err != nil { -		ctx.Error(fmt.Errorf("unable to get client for client with id '%s' from URL '%s': %w", id, targetURL, err), messageAuthenticationFailed) - -		return -	} - -	userSession := ctx.GetSession() - -	if !client.IsAuthenticationLevelSufficient(userSession.AuthenticationLevel) { -		ctx.Logger.Warnf("OpenID Connect client '%s' requires 2FA, cannot be redirected yet", client.ID) -		ctx.ReplyOK() - -		return -	} - -	if err = ctx.SetJSONBody(redirectResponse{Redirect: targetURL.String()}); err != nil { -		ctx.Logger.Errorf("Unable to set default redirection URL in body: %s", err) -	} -} -  // Handle1FAResponse handle the redirection upon 1FA authentication.  func Handle1FAResponse(ctx *middlewares.AutheliaCtx, targetURI, requestMethod string, username string, groups []string) {  	var err error @@ -166,6 +119,130 @@ func Handle2FAResponse(ctx *middlewares.AutheliaCtx, targetURI string) {  	ctx.ReplyOK()  } +// handleOIDCWorkflowResponse handle the redirection upon authentication in the OIDC workflow. +func handleOIDCWorkflowResponse(ctx *middlewares.AutheliaCtx, targetURI, workflowID string) { +	switch { +	case len(workflowID) != 0: +		handleOIDCWorkflowResponseWithID(ctx, workflowID) +	case len(targetURI) != 0: +		handleOIDCWorkflowResponseWithTargetURL(ctx, targetURI) +	default: +		ctx.Error(fmt.Errorf("invalid post data: must contain either a target url or a workflow id"), messageAuthenticationFailed) +	} +} + +func handleOIDCWorkflowResponseWithTargetURL(ctx *middlewares.AutheliaCtx, targetURI string) { +	var ( +		issuerURL *url.URL +		targetURL *url.URL +		err       error +	) + +	if targetURL, err = url.ParseRequestURI(targetURI); err != nil { +		ctx.Error(fmt.Errorf("unable to parse target URL '%s': %w", targetURI, err), messageAuthenticationFailed) + +		return +	} + +	if issuerURL, err = ctx.IssuerURL(); err != nil { +		ctx.Error(fmt.Errorf("unable to get issuer for redirection: %w", err), messageAuthenticationFailed) + +		return +	} + +	if targetURL.Host != issuerURL.Host { +		ctx.Error(fmt.Errorf("unable to redirect to '%s': target host '%s' does not match expected issuer host '%s'", targetURL, targetURL.Host, issuerURL.Host), messageAuthenticationFailed) + +		return +	} + +	userSession := ctx.GetSession() + +	if userSession.IsAnonymous() { +		ctx.Error(fmt.Errorf("unable to redirect to '%s': user is anonymous", targetURL), messageAuthenticationFailed) + +		return +	} + +	if err = ctx.SetJSONBody(redirectResponse{Redirect: targetURL.String()}); err != nil { +		ctx.Logger.Errorf("Unable to set default redirection URL in body: %s", err) +	} +} + +func handleOIDCWorkflowResponseWithID(ctx *middlewares.AutheliaCtx, id string) { +	var ( +		workflowID uuid.UUID +		client     *oidc.Client +		consent    *model.OAuth2ConsentSession +		err        error +	) + +	if workflowID, err = uuid.Parse(id); err != nil { +		ctx.Error(fmt.Errorf("unable to parse consent session challenge id '%s': %w", id, err), messageAuthenticationFailed) + +		return +	} + +	if consent, err = ctx.Providers.StorageProvider.LoadOAuth2ConsentSessionByChallengeID(ctx, workflowID); err != nil { +		ctx.Error(fmt.Errorf("unable to load consent session by challenge id '%s': %w", id, err), messageAuthenticationFailed) + +		return +	} + +	if consent.Responded() { +		ctx.Error(fmt.Errorf("consent has already been responded to '%s': %w", id, err), messageAuthenticationFailed) + +		return +	} + +	if client, err = ctx.Providers.OpenIDConnect.GetFullClient(consent.ClientID); err != nil { +		ctx.Error(fmt.Errorf("unable to get client for client with id '%s' with consent challenge id '%s': %w", id, consent.ChallengeID, err), messageAuthenticationFailed) + +		return +	} + +	userSession := ctx.GetSession() + +	if userSession.IsAnonymous() { +		ctx.Error(fmt.Errorf("unable to redirect for authorization/consent for client with id '%s' with consent challenge id '%s': user is anonymous", client.ID, consent.ChallengeID), messageAuthenticationFailed) + +		return +	} + +	if !client.IsAuthenticationLevelSufficient(userSession.AuthenticationLevel) { +		ctx.Logger.Warnf("OpenID Connect client '%s' requires 2FA, cannot be redirected yet", client.ID) +		ctx.ReplyOK() + +		return +	} + +	var ( +		targetURL *url.URL +		form      url.Values +	) + +	if targetURL, err = ctx.IssuerURL(); err != nil { +		ctx.Error(fmt.Errorf("unable to get issuer for redirection: %w", err), messageAuthenticationFailed) + +		return +	} + +	if form, err = consent.GetForm(); err != nil { +		ctx.Error(fmt.Errorf("unable to get authorization form values from consent session with challenge id '%s': %w", consent.ChallengeID, err), messageAuthenticationFailed) + +		return +	} + +	form.Set(queryArgConsentID, workflowID.String()) + +	targetURL.Path = path.Join(targetURL.Path, oidc.EndpointPathAuthorization) +	targetURL.RawQuery = form.Encode() + +	if err = ctx.SetJSONBody(redirectResponse{Redirect: targetURL.String()}); err != nil { +		ctx.Logger.Errorf("Unable to set default redirection URL in body: %s", err) +	} +} +  func markAuthenticationAttempt(ctx *middlewares.AutheliaCtx, successful bool, bannedUntil *time.Time, username string, authType string, errAuth error) (err error) {  	// We only Mark if there was no underlying error.  	ctx.Logger.Debugf("Mark %s authentication attempt made by user '%s'", authType, username) @@ -178,7 +255,7 @@ func markAuthenticationAttempt(ctx *middlewares.AutheliaCtx, successful bool, ba  	if referer != nil {  		refererURL, err := url.ParseRequestURI(string(referer))  		if err == nil { -			requestURI = refererURL.Query().Get("rd") +			requestURI = refererURL.Query().Get(queryArgRD)  			requestMethod = refererURL.Query().Get("rm")  		}  	} diff --git a/internal/handlers/types.go b/internal/handlers/types.go index b2be6e512..517d297a6 100644 --- a/internal/handlers/types.go +++ b/internal/handlers/types.go @@ -1,7 +1,17 @@  package handlers  import ( +	"net/http" +	"net/url" + +	"github.com/google/uuid" +	"github.com/ory/fosite" +  	"github.com/authelia/authelia/v4/internal/authentication" +	"github.com/authelia/authelia/v4/internal/middlewares" +	"github.com/authelia/authelia/v4/internal/model" +	"github.com/authelia/authelia/v4/internal/oidc" +	"github.com/authelia/authelia/v4/internal/session"  )  // MethodList is the list of available methods. @@ -14,36 +24,41 @@ type configurationBody struct {  	AvailableMethods MethodList `json:"available_methods"`  } -// signTOTPRequestBody model of the request body received by TOTP authentication endpoint. -type signTOTPRequestBody struct { -	Token     string `json:"token" valid:"required"` -	TargetURL string `json:"targetURL"` -	Workflow  string `json:"workflow"` +// bodySignTOTPRequest is the  model of the request body of TOTP 2FA authentication endpoint. +type bodySignTOTPRequest struct { +	Token      string `json:"token" valid:"required"` +	TargetURL  string `json:"targetURL"` +	Workflow   string `json:"workflow"` +	WorkflowID string `json:"workflowID"`  } -// signWebauthnRequestBody model of the request body of Webauthn authentication endpoint. -type signWebauthnRequestBody struct { -	TargetURL string `json:"targetURL"` -	Workflow  string `json:"workflow"` +// bodySignWebauthnRequest is the  model of the request body of WebAuthn 2FA authentication endpoint. +type bodySignWebauthnRequest struct { +	TargetURL  string `json:"targetURL"` +	Workflow   string `json:"workflow"` +	WorkflowID string `json:"workflowID"`  } -type signDuoRequestBody struct { -	TargetURL string `json:"targetURL"` -	Passcode  string `json:"passcode"` -	Workflow  string `json:"workflow"` +// bodySignDuoRequest is the  model of the request body of Duo 2FA authentication endpoint. +type bodySignDuoRequest struct { +	TargetURL  string `json:"targetURL"` +	Passcode   string `json:"passcode"` +	Workflow   string `json:"workflow"` +	WorkflowID string `json:"workflowID"`  } -// preferred2FAMethodBody the selected 2FA method. -type preferred2FAMethodBody struct { +// bodyPreferred2FAMethod the selected 2FA method. +type bodyPreferred2FAMethod struct {  	Method string `json:"method" valid:"required"`  } -// firstFactorRequestBody represents the JSON body received by the endpoint. -type firstFactorRequestBody struct { +// bodyFirstFactorRequest represents the JSON body received by the endpoint. +type bodyFirstFactorRequest struct {  	Username       string `json:"username" valid:"required"`  	Password       string `json:"password" valid:"required"`  	TargetURL      string `json:"targetURL"`  	Workflow       string `json:"workflow"` +	WorkflowID     string `json:"workflowID"`  	RequestMethod  string `json:"requestMethod"`  	KeepMeLoggedIn *bool  `json:"keepMeLoggedIn"`  	// KeepMeLoggedIn: Cannot require this field because of https://github.com/asaskevich/govalidator/pull/329 @@ -128,3 +143,9 @@ type PasswordPolicyBody struct {  	RequireNumber    bool   `json:"require_number"`  	RequireSpecial   bool   `json:"require_special"`  } + +type handlerAuthorizationConsent func( +	ctx *middlewares.AutheliaCtx, issuer *url.URL, client *oidc.Client, +	userSession session.UserSession, subject uuid.UUID, +	rw http.ResponseWriter, r *http.Request, +	requester fosite.AuthorizeRequester) (consent *model.OAuth2ConsentSession, handled bool) diff --git a/internal/middlewares/authelia_context.go b/internal/middlewares/authelia_context.go index af5bcbd66..862f16d4d 100644 --- a/internal/middlewares/authelia_context.go +++ b/internal/middlewares/authelia_context.go @@ -218,6 +218,29 @@ func (ctx *AutheliaCtx) ExternalRootURL() (string, error) {  	return externalRootURL, nil  } +// IssuerURL returns the expected Issuer. +func (ctx *AutheliaCtx) IssuerURL() (issuerURL *url.URL, err error) { +	issuerURL = &url.URL{ +		Scheme: "https", +	} + +	if scheme := ctx.XForwardedProto(); scheme != nil { +		issuerURL.Scheme = string(scheme) +	} + +	if host := ctx.XForwardedHost(); len(host) != 0 { +		issuerURL.Host = string(host) +	} else { +		return nil, errMissingXForwardedHost +	} + +	if base := ctx.BasePath(); base != "" { +		issuerURL.Path = path.Join(issuerURL.Path, base) +	} + +	return issuerURL, nil +} +  // XOriginalURL return the content of the X-Original-URL header.  func (ctx *AutheliaCtx) XOriginalURL() []byte {  	return ctx.RequestCtx.Request.Header.PeekBytes(headerXOriginalURL) diff --git a/internal/middlewares/authelia_context_test.go b/internal/middlewares/authelia_context_test.go index b15b5eb77..ab8882f03 100644 --- a/internal/middlewares/authelia_context_test.go +++ b/internal/middlewares/authelia_context_test.go @@ -6,6 +6,7 @@ import (  	"github.com/golang/mock/gomock"  	"github.com/stretchr/testify/assert" +	"github.com/stretchr/testify/require"  	"github.com/valyala/fasthttp"  	"github.com/authelia/authelia/v4/internal/configuration/schema" @@ -15,6 +16,61 @@ import (  	"github.com/authelia/authelia/v4/internal/session"  ) +func TestIssuerURL(t *testing.T) { +	testCases := []struct { +		name              string +		proto, host, base string +		expected          string +		err               string +	}{ +		{ +			name:  "Standard", +			proto: "https", host: "auth.example.com", base: "", +			expected: "https://auth.example.com", +		}, +		{ +			name:  "Base", +			proto: "https", host: "example.com", base: "auth", +			expected: "https://example.com/auth", +		}, +		{ +			name:  "NoHost", +			proto: "https", host: "", base: "", +			err: "Missing header X-Forwarded-Host", +		}, +	} + +	for _, tc := range testCases { +		t.Run(tc.name, func(t *testing.T) { +			mock := mocks.NewMockAutheliaCtx(t) +			defer mock.Close() + +			mock.Ctx.Request.Header.Set("X-Forwarded-Proto", tc.proto) +			mock.Ctx.Request.Header.Set("X-Forwarded-Host", tc.host) + +			if tc.base != "" { +				mock.Ctx.SetUserValue("base_url", tc.base) +			} + +			actual, err := mock.Ctx.IssuerURL() + +			switch tc.err { +			case "": +				assert.NoError(t, err) +				require.NotNil(t, actual) + +				assert.Equal(t, tc.expected, actual.String()) +				assert.Equal(t, tc.proto, actual.Scheme) +				assert.Equal(t, tc.host, actual.Host) +				assert.Equal(t, tc.base, actual.Path) +			default: +				assert.EqualError(t, err, tc.err) +				assert.Nil(t, actual) +			} +		}) +	} +} +  func TestShouldCallNextWithAutheliaCtx(t *testing.T) {  	ctrl := gomock.NewController(t)  	ctx := &fasthttp.RequestCtx{} diff --git a/internal/middlewares/types.go b/internal/middlewares/types.go index 58991a049..f26e57fc7 100644 --- a/internal/middlewares/types.go +++ b/internal/middlewares/types.go @@ -35,7 +35,7 @@ type Providers struct {  	Authorizer      *authorization.Authorizer  	SessionProvider *session.Provider  	Regulator       *regulation.Regulator -	OpenIDConnect   oidc.OpenIDConnectProvider +	OpenIDConnect   *oidc.OpenIDConnectProvider  	Metrics         metrics.Provider  	NTP             *ntp.Provider  	UserProvider    authentication.UserProvider diff --git a/internal/mocks/storage.go b/internal/mocks/storage.go index 980469a35..2a20f8acc 100644 --- a/internal/mocks/storage.go +++ b/internal/mocks/storage.go @@ -6,6 +6,7 @@ package mocks  import (  	context "context" +	sql "database/sql"  	reflect "reflect"  	time "time" @@ -239,34 +240,34 @@ func (mr *MockStorageMockRecorder) LoadOAuth2BlacklistedJTI(arg0, arg1 interface  	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadOAuth2BlacklistedJTI", reflect.TypeOf((*MockStorage)(nil).LoadOAuth2BlacklistedJTI), arg0, arg1)  } -// LoadOAuth2ConsentSessionByChallengeID mocks base method. -func (m *MockStorage) LoadOAuth2ConsentSessionByChallengeID(arg0 context.Context, arg1 uuid.UUID) (*model.OAuth2ConsentSession, error) { +// LoadOAuth2ConsentPreConfigurations mocks base method. +func (m *MockStorage) LoadOAuth2ConsentPreConfigurations(arg0 context.Context, arg1 string, arg2 uuid.UUID) (*storage.ConsentPreConfigRows, error) {  	m.ctrl.T.Helper() -	ret := m.ctrl.Call(m, "LoadOAuth2ConsentSessionByChallengeID", arg0, arg1) -	ret0, _ := ret[0].(*model.OAuth2ConsentSession) +	ret := m.ctrl.Call(m, "LoadOAuth2ConsentPreConfigurations", arg0, arg1, arg2) +	ret0, _ := ret[0].(*storage.ConsentPreConfigRows)  	ret1, _ := ret[1].(error)  	return ret0, ret1  } -// LoadOAuth2ConsentSessionByChallengeID indicates an expected call of LoadOAuth2ConsentSessionByChallengeID. -func (mr *MockStorageMockRecorder) LoadOAuth2ConsentSessionByChallengeID(arg0, arg1 interface{}) *gomock.Call { +// LoadOAuth2ConsentPreConfigurations indicates an expected call of LoadOAuth2ConsentPreConfigurations. +func (mr *MockStorageMockRecorder) LoadOAuth2ConsentPreConfigurations(arg0, arg1, arg2 interface{}) *gomock.Call {  	mr.mock.ctrl.T.Helper() -	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadOAuth2ConsentSessionByChallengeID", reflect.TypeOf((*MockStorage)(nil).LoadOAuth2ConsentSessionByChallengeID), arg0, arg1) +	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadOAuth2ConsentPreConfigurations", reflect.TypeOf((*MockStorage)(nil).LoadOAuth2ConsentPreConfigurations), arg0, arg1, arg2)  } -// LoadOAuth2ConsentSessionsPreConfigured mocks base method. -func (m *MockStorage) LoadOAuth2ConsentSessionsPreConfigured(arg0 context.Context, arg1 string, arg2 uuid.UUID) (*storage.ConsentSessionRows, error) { +// LoadOAuth2ConsentSessionByChallengeID mocks base method. +func (m *MockStorage) LoadOAuth2ConsentSessionByChallengeID(arg0 context.Context, arg1 uuid.UUID) (*model.OAuth2ConsentSession, error) {  	m.ctrl.T.Helper() -	ret := m.ctrl.Call(m, "LoadOAuth2ConsentSessionsPreConfigured", arg0, arg1, arg2) -	ret0, _ := ret[0].(*storage.ConsentSessionRows) +	ret := m.ctrl.Call(m, "LoadOAuth2ConsentSessionByChallengeID", arg0, arg1) +	ret0, _ := ret[0].(*model.OAuth2ConsentSession)  	ret1, _ := ret[1].(error)  	return ret0, ret1  } -// LoadOAuth2ConsentSessionsPreConfigured indicates an expected call of LoadOAuth2ConsentSessionsPreConfigured. -func (mr *MockStorageMockRecorder) LoadOAuth2ConsentSessionsPreConfigured(arg0, arg1, arg2 interface{}) *gomock.Call { +// LoadOAuth2ConsentSessionByChallengeID indicates an expected call of LoadOAuth2ConsentSessionByChallengeID. +func (mr *MockStorageMockRecorder) LoadOAuth2ConsentSessionByChallengeID(arg0, arg1 interface{}) *gomock.Call {  	mr.mock.ctrl.T.Helper() -	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadOAuth2ConsentSessionsPreConfigured", reflect.TypeOf((*MockStorage)(nil).LoadOAuth2ConsentSessionsPreConfigured), arg0, arg1, arg2) +	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadOAuth2ConsentSessionByChallengeID", reflect.TypeOf((*MockStorage)(nil).LoadOAuth2ConsentSessionByChallengeID), arg0, arg1)  }  // LoadOAuth2Session mocks base method. @@ -504,6 +505,21 @@ func (mr *MockStorageMockRecorder) SaveOAuth2BlacklistedJTI(arg0, arg1 interface  	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveOAuth2BlacklistedJTI", reflect.TypeOf((*MockStorage)(nil).SaveOAuth2BlacklistedJTI), arg0, arg1)  } +// SaveOAuth2ConsentPreConfiguration mocks base method. +func (m *MockStorage) SaveOAuth2ConsentPreConfiguration(arg0 context.Context, arg1 model.OAuth2ConsentPreConfig) (int64, error) { +	m.ctrl.T.Helper() +	ret := m.ctrl.Call(m, "SaveOAuth2ConsentPreConfiguration", arg0, arg1) +	ret0, _ := ret[0].(int64) +	ret1, _ := ret[1].(error) +	return ret0, ret1 +} + +// SaveOAuth2ConsentPreConfiguration indicates an expected call of SaveOAuth2ConsentPreConfiguration. +func (mr *MockStorageMockRecorder) SaveOAuth2ConsentPreConfiguration(arg0, arg1 interface{}) *gomock.Call { +	mr.mock.ctrl.T.Helper() +	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveOAuth2ConsentPreConfiguration", reflect.TypeOf((*MockStorage)(nil).SaveOAuth2ConsentPreConfiguration), arg0, arg1) +} +  // SaveOAuth2ConsentSession mocks base method.  func (m *MockStorage) SaveOAuth2ConsentSession(arg0 context.Context, arg1 model.OAuth2ConsentSession) error {  	m.ctrl.T.Helper() @@ -791,7 +807,7 @@ func (mr *MockStorageMockRecorder) StartupCheck() *gomock.Call {  }  // UpdateTOTPConfigurationSignIn mocks base method. -func (m *MockStorage) UpdateTOTPConfigurationSignIn(arg0 context.Context, arg1 int, arg2 *time.Time) error { +func (m *MockStorage) UpdateTOTPConfigurationSignIn(arg0 context.Context, arg1 int, arg2 sql.NullTime) error {  	m.ctrl.T.Helper()  	ret := m.ctrl.Call(m, "UpdateTOTPConfigurationSignIn", arg0, arg1, arg2)  	ret0, _ := ret[0].(error) @@ -805,7 +821,7 @@ func (mr *MockStorageMockRecorder) UpdateTOTPConfigurationSignIn(arg0, arg1, arg  }  // UpdateWebauthnDeviceSignIn mocks base method. -func (m *MockStorage) UpdateWebauthnDeviceSignIn(arg0 context.Context, arg1 int, arg2 string, arg3 *time.Time, arg4 uint32, arg5 bool) error { +func (m *MockStorage) UpdateWebauthnDeviceSignIn(arg0 context.Context, arg1 int, arg2 string, arg3 sql.NullTime, arg4 uint32, arg5 bool) error {  	m.ctrl.T.Helper()  	ret := m.ctrl.Call(m, "UpdateWebauthnDeviceSignIn", arg0, arg1, arg2, arg3, arg4, arg5)  	ret0, _ := ret[0].(error) diff --git a/internal/model/identity_verification.go b/internal/model/identity_verification.go index f0f58bc45..f6d4491ad 100644 --- a/internal/model/identity_verification.go +++ b/internal/model/identity_verification.go @@ -1,6 +1,7 @@  package model  import ( +	"database/sql"  	"net"  	"time" @@ -22,15 +23,15 @@ func NewIdentityVerification(jti uuid.UUID, username, action string, ip net.IP)  // IdentityVerification represents an identity verification row in the database.  type IdentityVerification struct { -	ID         int        `db:"id"` -	JTI        uuid.UUID  `db:"jti"` -	IssuedAt   time.Time  `db:"iat"` -	IssuedIP   IP         `db:"issued_ip"` -	ExpiresAt  time.Time  `db:"exp"` -	Action     string     `db:"action"` -	Username   string     `db:"username"` -	Consumed   *time.Time `db:"consumed"` -	ConsumedIP NullIP     `db:"consumed_ip"` +	ID         int          `db:"id"` +	JTI        uuid.UUID    `db:"jti"` +	IssuedAt   time.Time    `db:"iat"` +	IssuedIP   IP           `db:"issued_ip"` +	ExpiresAt  time.Time    `db:"exp"` +	Action     string       `db:"action"` +	Username   string       `db:"username"` +	Consumed   sql.NullTime `db:"consumed"` +	ConsumedIP NullIP       `db:"consumed_ip"`  }  // ToIdentityVerificationClaim converts the IdentityVerification into a IdentityVerificationClaim. diff --git a/internal/model/oidc.go b/internal/model/oidc.go index 99c778ac2..1d742543d 100644 --- a/internal/model/oidc.go +++ b/internal/model/oidc.go @@ -3,6 +3,7 @@ package model  import (  	"context"  	"crypto/sha256" +	"database/sql"  	"encoding/json"  	"fmt"  	"net/url" @@ -84,6 +85,42 @@ func NewOAuth2BlacklistedJTI(jti string, exp time.Time) (jtiBlacklist OAuth2Blac  	}  } +// OAuth2ConsentPreConfig stores information about an OAuth2.0 Pre-Configured Consent. +type OAuth2ConsentPreConfig struct { +	ID       int64     `db:"id"` +	ClientID string    `db:"client_id"` +	Subject  uuid.UUID `db:"subject"` + +	CreatedAt time.Time    `db:"created_at"` +	ExpiresAt sql.NullTime `db:"expires_at"` + +	Revoked bool `db:"revoked"` + +	Scopes   StringSlicePipeDelimited `db:"scopes"` +	Audience StringSlicePipeDelimited `db:"audience"` +} + +// HasExactGrants returns true if the granted audience and scopes of this consent pre-configuration matches exactly with +// another audience and set of scopes. +func (s *OAuth2ConsentPreConfig) HasExactGrants(scopes, audience []string) (has bool) { +	return s.HasExactGrantedScopes(scopes) && s.HasExactGrantedAudience(audience) +} + +// HasExactGrantedAudience returns true if the granted audience of this consent matches exactly with another audience. +func (s *OAuth2ConsentPreConfig) HasExactGrantedAudience(audience []string) (has bool) { +	return !utils.IsStringSlicesDifferent(s.Audience, audience) +} + +// HasExactGrantedScopes returns true if the granted scopes of this consent matches exactly with another set of scopes. +func (s *OAuth2ConsentPreConfig) HasExactGrantedScopes(scopes []string) (has bool) { +	return !utils.IsStringSlicesDifferent(s.Scopes, scopes) +} + +// CanConsent returns true if this pre-configuration can still provide consent. +func (s *OAuth2ConsentPreConfig) CanConsent() bool { +	return !s.Revoked && (!s.ExpiresAt.Valid || s.ExpiresAt.Time.After(time.Now())) +} +  // OAuth2ConsentSession stores information about an OAuth2.0 Consent.  type OAuth2ConsentSession struct {  	ID          int           `db:"id"` @@ -94,9 +131,8 @@ type OAuth2ConsentSession struct {  	Authorized bool `db:"authorized"`  	Granted    bool `db:"granted"` -	RequestedAt time.Time  `db:"requested_at"` -	RespondedAt *time.Time `db:"responded_at"` -	ExpiresAt   *time.Time `db:"expires_at"` +	RequestedAt time.Time    `db:"requested_at"` +	RespondedAt sql.NullTime `db:"responded_at"`  	Form string `db:"form_data"` @@ -104,55 +140,63 @@ type OAuth2ConsentSession struct {  	GrantedScopes     StringSlicePipeDelimited `db:"granted_scopes"`  	RequestedAudience StringSlicePipeDelimited `db:"requested_audience"`  	GrantedAudience   StringSlicePipeDelimited `db:"granted_audience"` + +	PreConfiguration sql.NullInt64 +} + +// Grant grants the requested scopes and audience. +func (s *OAuth2ConsentSession) Grant() { +	s.GrantedScopes = s.RequestedScopes +	s.GrantedAudience = s.RequestedAudience + +	if !utils.IsStringInSlice(s.ClientID, s.GrantedAudience) { +		s.GrantedAudience = append(s.GrantedAudience, s.ClientID) +	}  }  // HasExactGrants returns true if the granted audience and scopes of this consent matches exactly with another  // audience and set of scopes. -func (s OAuth2ConsentSession) HasExactGrants(scopes, audience []string) (has bool) { +func (s *OAuth2ConsentSession) HasExactGrants(scopes, audience []string) (has bool) {  	return s.HasExactGrantedScopes(scopes) && s.HasExactGrantedAudience(audience)  }  // HasExactGrantedAudience returns true if the granted audience of this consent matches exactly with another audience. -func (s OAuth2ConsentSession) HasExactGrantedAudience(audience []string) (has bool) { +func (s *OAuth2ConsentSession) HasExactGrantedAudience(audience []string) (has bool) {  	return !utils.IsStringSlicesDifferent(s.GrantedAudience, audience)  }  // HasExactGrantedScopes returns true if the granted scopes of this consent matches exactly with another set of scopes. -func (s OAuth2ConsentSession) HasExactGrantedScopes(scopes []string) (has bool) { +func (s *OAuth2ConsentSession) HasExactGrantedScopes(scopes []string) (has bool) {  	return !utils.IsStringSlicesDifferent(s.GrantedScopes, scopes)  } +// Responded returns true if the user has responded to the consent session. +func (s *OAuth2ConsentSession) Responded() bool { +	return s.RespondedAt.Valid +} +  // IsAuthorized returns true if the user has responded to the consent session and it was authorized. -func (s OAuth2ConsentSession) IsAuthorized() bool { +func (s *OAuth2ConsentSession) IsAuthorized() bool {  	return s.Responded() && s.Authorized  } -// CanGrant returns true if the user has responded to the consent session, it was authorized, and it either hast not -// previously been granted or the ability to grant has not expired. -func (s OAuth2ConsentSession) CanGrant() bool { -	if !s.Responded() { -		return false -	} +// IsDenied returns true if the user has responded to the consent session and it was not authorized. +func (s *OAuth2ConsentSession) IsDenied() bool { +	return s.Responded() && !s.Authorized +} -	if s.Granted && (s.ExpiresAt == nil || s.ExpiresAt.Before(time.Now())) { +// CanGrant returns true if the session can still grant a token. This is NOT indicative of if there is a user response +// to this consent request or if the user rejected the consent request. +func (s *OAuth2ConsentSession) CanGrant() bool { +	if !s.Subject.Valid || s.Granted {  		return false  	}  	return true  } -// IsDenied returns true if the user has responded to the consent session and it was not authorized. -func (s OAuth2ConsentSession) IsDenied() bool { -	return s.Responded() && !s.Authorized -} - -// Responded returns true if the user has responded to the consent session. -func (s OAuth2ConsentSession) Responded() bool { -	return s.RespondedAt != nil -} -  // GetForm returns the form. -func (s OAuth2ConsentSession) GetForm() (form url.Values, err error) { +func (s *OAuth2ConsentSession) GetForm() (form url.Values, err error) {  	return url.ParseQuery(s.Form)  } diff --git a/internal/model/totp_configuration.go b/internal/model/totp_configuration.go index 74777e4a2..f8f23d41b 100644 --- a/internal/model/totp_configuration.go +++ b/internal/model/totp_configuration.go @@ -1,6 +1,7 @@  package model  import ( +	"database/sql"  	"image"  	"net/url"  	"strconv" @@ -11,15 +12,23 @@ import (  // TOTPConfiguration represents a users TOTP configuration row in the database.  type TOTPConfiguration struct { -	ID         int        `db:"id" json:"-"` -	CreatedAt  time.Time  `db:"created_at" json:"-"` -	LastUsedAt *time.Time `db:"last_used_at" json:"-"` -	Username   string     `db:"username" json:"-"` -	Issuer     string     `db:"issuer" json:"-"` -	Algorithm  string     `db:"algorithm" json:"-"` -	Digits     uint       `db:"digits" json:"digits"` -	Period     uint       `db:"period" json:"period"` -	Secret     []byte     `db:"secret" json:"-"` +	ID         int          `db:"id" json:"-"` +	CreatedAt  time.Time    `db:"created_at" json:"-"` +	LastUsedAt sql.NullTime `db:"last_used_at" json:"-"` +	Username   string       `db:"username" json:"-"` +	Issuer     string       `db:"issuer" json:"-"` +	Algorithm  string       `db:"algorithm" json:"-"` +	Digits     uint         `db:"digits" json:"digits"` +	Period     uint         `db:"period" json:"period"` +	Secret     []byte       `db:"secret" json:"-"` +} + +func (c *TOTPConfiguration) LastUsed() *time.Time { +	if c.LastUsedAt.Valid { +		return &c.LastUsedAt.Time +	} + +	return nil  }  // URI shows the configuration in the URI representation. @@ -43,7 +52,7 @@ func (c *TOTPConfiguration) URI() (uri string) {  // UpdateSignInInfo adjusts the values of the TOTPConfiguration after a sign in.  func (c *TOTPConfiguration) UpdateSignInInfo(now time.Time) { -	c.LastUsedAt = &now +	c.LastUsedAt = sql.NullTime{Time: now, Valid: true}  }  // Key returns the *otp.Key using TOTPConfiguration.URI with otp.NewKeyFromURL. diff --git a/internal/model/webauthn.go b/internal/model/webauthn.go index a6fcc4084..bd9400f89 100644 --- a/internal/model/webauthn.go +++ b/internal/model/webauthn.go @@ -1,6 +1,7 @@  package model  import ( +	"database/sql"  	"encoding/hex"  	"strings"  	"time" @@ -133,24 +134,24 @@ func NewWebauthnDeviceFromCredential(rpid, username, description string, credent  // WebauthnDevice represents a Webauthn Device in the database storage.  type WebauthnDevice struct { -	ID              int        `db:"id"` -	CreatedAt       time.Time  `db:"created_at"` -	LastUsedAt      *time.Time `db:"last_used_at"` -	RPID            string     `db:"rpid"` -	Username        string     `db:"username"` -	Description     string     `db:"description"` -	KID             Base64     `db:"kid"` -	PublicKey       []byte     `db:"public_key"` -	AttestationType string     `db:"attestation_type"` -	Transport       string     `db:"transport"` -	AAGUID          uuid.UUID  `db:"aaguid"` -	SignCount       uint32     `db:"sign_count"` -	CloneWarning    bool       `db:"clone_warning"` +	ID              int          `db:"id"` +	CreatedAt       time.Time    `db:"created_at"` +	LastUsedAt      sql.NullTime `db:"last_used_at"` +	RPID            string       `db:"rpid"` +	Username        string       `db:"username"` +	Description     string       `db:"description"` +	KID             Base64       `db:"kid"` +	PublicKey       []byte       `db:"public_key"` +	AttestationType string       `db:"attestation_type"` +	Transport       string       `db:"transport"` +	AAGUID          uuid.UUID    `db:"aaguid"` +	SignCount       uint32       `db:"sign_count"` +	CloneWarning    bool         `db:"clone_warning"`  }  // UpdateSignInInfo adjusts the values of the WebauthnDevice after a sign in.  func (w *WebauthnDevice) UpdateSignInInfo(config *webauthn.Config, now time.Time, signCount uint32) { -	w.LastUsedAt = &now +	w.LastUsedAt = sql.NullTime{Time: now, Valid: true}  	w.SignCount = signCount diff --git a/internal/oidc/client.go b/internal/oidc/client.go index c5b7cb975..7bdbc8a36 100644 --- a/internal/oidc/client.go +++ b/internal/oidc/client.go @@ -29,7 +29,7 @@ func NewClient(config schema.OpenIDConnectClientConfiguration) (client *Client)  		Policy: authorization.StringToLevel(config.Policy), -		PreConfiguredConsentDuration: config.PreConfiguredConsentDuration, +		Consent: NewClientConsent(config.ConsentMode, config.ConsentPreConfiguredDuration),  	}  	for _, mode := range config.ResponseModes { @@ -40,26 +40,30 @@ func NewClient(config schema.OpenIDConnectClientConfiguration) (client *Client)  }  // IsAuthenticationLevelSufficient returns if the provided authentication.Level is sufficient for the client of the AutheliaClient. -func (c Client) IsAuthenticationLevelSufficient(level authentication.Level) bool { +func (c *Client) IsAuthenticationLevelSufficient(level authentication.Level) bool { +	if level == authentication.NotAuthenticated { +		return false +	} +  	return authorization.IsAuthLevelSufficient(level, c.Policy)  }  // GetID returns the ID. -func (c Client) GetID() string { +func (c *Client) GetID() string {  	return c.ID  }  // GetSectorIdentifier returns the SectorIdentifier for this client. -func (c Client) GetSectorIdentifier() string { +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 { +func (c *Client) GetConsentResponseBody(consent *model.OAuth2ConsentSession) ConsentGetResponseBody {  	body := ConsentGetResponseBody{  		ClientID:          c.ID,  		ClientDescription: c.Description, -		PreConfiguration:  c.PreConfiguredConsentDuration != nil, +		PreConfiguration:  c.Consent.Mode == ClientConsentModePreConfigured,  	}  	if consent != nil { @@ -71,17 +75,17 @@ func (c Client) GetConsentResponseBody(consent *model.OAuth2ConsentSession) Cons  }  // GetHashedSecret returns the Secret. -func (c Client) GetHashedSecret() []byte { +func (c *Client) GetHashedSecret() []byte {  	return c.Secret  }  // GetRedirectURIs returns the RedirectURIs. -func (c Client) GetRedirectURIs() []string { +func (c *Client) GetRedirectURIs() []string {  	return c.RedirectURIs  }  // GetGrantTypes returns the GrantTypes. -func (c Client) GetGrantTypes() fosite.Arguments { +func (c *Client) GetGrantTypes() fosite.Arguments {  	if len(c.GrantTypes) == 0 {  		return fosite.Arguments{"authorization_code"}  	} @@ -90,7 +94,7 @@ func (c Client) GetGrantTypes() fosite.Arguments {  }  // GetResponseTypes returns the ResponseTypes. -func (c Client) GetResponseTypes() fosite.Arguments { +func (c *Client) GetResponseTypes() fosite.Arguments {  	if len(c.ResponseTypes) == 0 {  		return fosite.Arguments{"code"}  	} @@ -99,23 +103,23 @@ func (c Client) GetResponseTypes() fosite.Arguments {  }  // GetScopes returns the Scopes. -func (c Client) GetScopes() fosite.Arguments { +func (c *Client) GetScopes() fosite.Arguments {  	return c.Scopes  }  // IsPublic returns the value of the Public property. -func (c Client) IsPublic() bool { +func (c *Client) IsPublic() bool {  	return c.Public  }  // GetAudience returns the Audience. -func (c Client) GetAudience() fosite.Arguments { +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 { +func (c *Client) GetResponseModes() []fosite.ResponseModeType {  	return c.ResponseModes  } diff --git a/internal/oidc/client_test.go b/internal/oidc/client_test.go index aaacd5cdf..8483efa5c 100644 --- a/internal/oidc/client_test.go +++ b/internal/oidc/client_test.go @@ -48,7 +48,7 @@ func TestIsAuthenticationLevelSufficient(t *testing.T) {  	c := Client{}  	c.Policy = authorization.Bypass -	assert.True(t, c.IsAuthenticationLevelSufficient(authentication.NotAuthenticated)) +	assert.False(t, c.IsAuthenticationLevelSufficient(authentication.NotAuthenticated))  	assert.True(t, c.IsAuthenticationLevelSufficient(authentication.OneFactor))  	assert.True(t, c.IsAuthenticationLevelSufficient(authentication.TwoFactor)) diff --git a/internal/oidc/const.go b/internal/oidc/const.go index 72d395dd1..c61396d4a 100644 --- a/internal/oidc/const.go +++ b/internal/oidc/const.go @@ -9,38 +9,101 @@ const (  	ScopeGroups        = "groups"  ) -// Claim strings. +// Registered Claim strings. See https://www.iana.org/assignments/jwt/jwt.xhtml.  const ( -	ClaimGroups            = "groups" -	ClaimDisplayName       = "name" -	ClaimPreferredUsername = "preferred_username" -	ClaimEmail             = "email" -	ClaimEmailVerified     = "email_verified" -	ClaimEmailAlts         = "alt_emails" +	ClaimJWTID                               = "jti" +	ClaimSessionID                           = "sid" +	ClaimAccessTokenHash                     = "at_hash" +	ClaimCodeHash                            = "c_hash" +	ClaimIssuedAt                            = "iat" +	ClaimNotBefore                           = "nbf" +	ClaimRequestedAt                         = "rat" +	ClaimExpirationTime                      = "exp" +	ClaimAuthenticationTime                  = "auth_time" +	ClaimIssuer                              = "iss" +	ClaimSubject                             = "sub" +	ClaimNonce                               = "nonce" +	ClaimAudience                            = "aud" +	ClaimGroups                              = "groups" +	ClaimFullName                            = "name" +	ClaimPreferredUsername                   = "preferred_username" +	ClaimPreferredEmail                      = "email" +	ClaimEmailVerified                       = "email_verified" +	ClaimAuthorizedParty                     = "azp" +	ClaimAuthenticationContextClassReference = "acr" +	ClaimAuthenticationMethodsReference      = "amr" +	ClaimClientIdentifier                    = "client_id" +) + +const ( +	// ClaimEmailAlts is an unregistered/custom claim. +	// It represents the emails which are not considered primary. +	ClaimEmailAlts = "alt_emails" +) + +// Response Mode strings. +const ( +	ResponseModeQuery    = "query" +	ResponseModeFormPost = "form_post" +	ResponseModeFragment = "fragment" +) + +// Grant Type strings. +const ( +	GrantTypeImplicit          = implicit +	GrantTypeRefreshToken      = "refresh_token" +	GrantTypeAuthorizationCode = "authorization_code" +	GrantTypePassword          = "password" +	GrantTypeClientCredentials = "client_credentials" +) + +// Signing Algorithm strings. +const ( +	SigningAlgorithmNone          = none +	SigningAlgorithmRSAWithSHA256 = "RS256" +) + +// Subject Type strings. +const ( +	SubjectTypePublic   = "public" +	SubjectTypePairwise = "pairwise" +) + +// Proof Key Code Exchange Challenge Method strings. +const ( +	PKCEChallengeMethodPlain  = "plain" +	PKCEChallengeMethodSHA256 = "S256"  )  // Endpoints.  const ( -	AuthorizationEndpoint = "authorization" -	TokenEndpoint         = "token" -	UserinfoEndpoint      = "userinfo" -	IntrospectionEndpoint = "introspection" -	RevocationEndpoint    = "revocation" +	EndpointAuthorization = "authorization" +	EndpointToken         = "token" +	EndpointUserinfo      = "userinfo" +	EndpointIntrospection = "introspection" +	EndpointRevocation    = "revocation" +) + +// JWT Headers. +const ( +	// JWTHeaderKeyIdentifier is the JWT Header referencing the JWS Key Identifier used to sign a token. +	JWTHeaderKeyIdentifier = "kid"  )  // Paths.  const ( -	WellKnownOpenIDConfigurationPath      = "/.well-known/openid-configuration" -	WellKnownOAuthAuthorizationServerPath = "/.well-known/oauth-authorization-server" -	JWKsPath                              = "/jwks.json" +	EndpointPathConsent                           = "/consent" +	EndpointPathWellKnownOpenIDConfiguration      = "/.well-known/openid-configuration" +	EndpointPathWellKnownOAuthAuthorizationServer = "/.well-known/oauth-authorization-server" +	EndpointPathJWKs                              = "/jwks.json" -	RootPath = "/api/oidc" +	EndpointPathRoot = "/api/oidc" -	AuthorizationPath = RootPath + "/" + AuthorizationEndpoint -	TokenPath         = RootPath + "/" + TokenEndpoint -	UserinfoPath      = RootPath + "/" + UserinfoEndpoint -	IntrospectionPath = RootPath + "/" + IntrospectionEndpoint -	RevocationPath    = RootPath + "/" + RevocationEndpoint +	EndpointPathAuthorization = EndpointPathRoot + "/" + EndpointAuthorization +	EndpointPathToken         = EndpointPathRoot + "/" + EndpointToken +	EndpointPathUserinfo      = EndpointPathRoot + "/" + EndpointUserinfo +	EndpointPathIntrospection = EndpointPathRoot + "/" + EndpointIntrospection +	EndpointPathRevocation    = EndpointPathRoot + "/" + EndpointRevocation  )  // Authentication Method Reference Values https://datatracker.ietf.org/doc/html/rfc8176 @@ -136,3 +199,10 @@ const (  	// RFC8176: https://datatracker.ietf.org/doc/html/rfc8176  	AMRShortMessageService = "sms"  ) + +const ( +	implicit      = "implicit" +	explicit      = "explicit" +	preconfigured = "pre-configured" +	none          = "none" +) diff --git a/internal/oidc/discovery.go b/internal/oidc/discovery.go index 145c0da8b..5f311c030 100644 --- a/internal/oidc/discovery.go +++ b/internal/oidc/discovery.go @@ -1,11 +1,11 @@  package oidc  // NewOpenIDConnectWellKnownConfiguration generates a new OpenIDConnectWellKnownConfiguration. -func NewOpenIDConnectWellKnownConfiguration(enablePKCEPlainChallenge, pairwise bool) (config OpenIDConnectWellKnownConfiguration) { +func NewOpenIDConnectWellKnownConfiguration(enablePKCEPlainChallenge bool, clients map[string]*Client) (config OpenIDConnectWellKnownConfiguration) {  	config = OpenIDConnectWellKnownConfiguration{  		CommonDiscoveryOptions: CommonDiscoveryOptions{  			SubjectTypesSupported: []string{ -				"public", +				SubjectTypePublic,  			},  			ResponseTypesSupported: []string{  				"code", @@ -18,9 +18,9 @@ func NewOpenIDConnectWellKnownConfiguration(enablePKCEPlainChallenge, pairwise b  				"none",  			},  			ResponseModesSupported: []string{ -				"form_post", -				"query", -				"fragment", +				ResponseModeFormPost, +				ResponseModeQuery, +				ResponseModeFragment,  			},  			ScopesSupported: []string{  				ScopeOfflineAccess, @@ -30,52 +30,64 @@ func NewOpenIDConnectWellKnownConfiguration(enablePKCEPlainChallenge, pairwise b  				ScopeEmail,  			},  			ClaimsSupported: []string{ -				"amr", -				"aud", -				"azp", -				"client_id", -				"exp", -				"iat", -				"iss", -				"jti", -				"rat", -				"sub", -				"auth_time", -				"nonce", -				ClaimEmail, +				ClaimAuthenticationMethodsReference, +				ClaimAudience, +				ClaimAuthorizedParty, +				ClaimClientIdentifier, +				ClaimExpirationTime, +				ClaimIssuedAt, +				ClaimIssuer, +				ClaimJWTID, +				ClaimRequestedAt, +				ClaimSubject, +				ClaimAuthenticationTime, +				ClaimNonce, +				ClaimPreferredEmail,  				ClaimEmailVerified,  				ClaimEmailAlts,  				ClaimGroups,  				ClaimPreferredUsername, -				ClaimDisplayName, +				ClaimFullName,  			},  		},  		OAuth2DiscoveryOptions: OAuth2DiscoveryOptions{  			CodeChallengeMethodsSupported: []string{ -				"S256", +				PKCEChallengeMethodSHA256,  			},  		},  		OpenIDConnectDiscoveryOptions: OpenIDConnectDiscoveryOptions{  			IDTokenSigningAlgValuesSupported: []string{ -				"RS256", +				SigningAlgorithmRSAWithSHA256,  			},  			UserinfoSigningAlgValuesSupported: []string{ -				"none", -				"RS256", +				SigningAlgorithmNone, +				SigningAlgorithmRSAWithSHA256,  			},  			RequestObjectSigningAlgValuesSupported: []string{ -				"none", -				"RS256", +				SigningAlgorithmNone, +				SigningAlgorithmRSAWithSHA256,  			},  		},  	} +	var pairwise, public bool + +	for _, client := range clients { +		if pairwise && public { +			break +		} + +		if client.SectorIdentifier != "" { +			pairwise = true +		} +	} +  	if pairwise { -		config.SubjectTypesSupported = append(config.SubjectTypesSupported, "pairwise") +		config.SubjectTypesSupported = append(config.SubjectTypesSupported, SubjectTypePairwise)  	}  	if enablePKCEPlainChallenge { -		config.CodeChallengeMethodsSupported = append(config.CodeChallengeMethodsSupported, "plain") +		config.CodeChallengeMethodsSupported = append(config.CodeChallengeMethodsSupported, PKCEChallengeMethodPlain)  	}  	return config diff --git a/internal/oidc/discovery_test.go b/internal/oidc/discovery_test.go index 256f5f70b..7f99df15c 100644 --- a/internal/oidc/discovery_test.go +++ b/internal/oidc/discovery_test.go @@ -8,43 +8,62 @@ import (  func TestNewOpenIDConnectWellKnownConfiguration(t *testing.T) {  	testCases := []struct { -		desc                                                             string -		pkcePlainChallenge, pairwise                                     bool +		desc               string +		pkcePlainChallenge bool +		clients            map[string]*Client +  		expectCodeChallengeMethodsSupported, expectSubjectTypesSupported []string  	}{  		{  			desc:                                "ShouldHaveChallengeMethodsS256ANDSubjectTypesSupportedPublic",  			pkcePlainChallenge:                  false, -			pairwise:                            false, -			expectCodeChallengeMethodsSupported: []string{"S256"}, -			expectSubjectTypesSupported:         []string{"public"}, +			clients:                             map[string]*Client{"a": {}}, +			expectCodeChallengeMethodsSupported: []string{PKCEChallengeMethodSHA256}, +			expectSubjectTypesSupported:         []string{SubjectTypePublic},  		},  		{  			desc:                                "ShouldHaveChallengeMethodsS256PlainANDSubjectTypesSupportedPublic",  			pkcePlainChallenge:                  true, -			pairwise:                            false, -			expectCodeChallengeMethodsSupported: []string{"S256", "plain"}, -			expectSubjectTypesSupported:         []string{"public"}, +			clients:                             map[string]*Client{"a": {}}, +			expectCodeChallengeMethodsSupported: []string{PKCEChallengeMethodSHA256, PKCEChallengeMethodPlain}, +			expectSubjectTypesSupported:         []string{SubjectTypePublic},  		},  		{  			desc:                                "ShouldHaveChallengeMethodsS256ANDSubjectTypesSupportedPublicPairwise",  			pkcePlainChallenge:                  false, -			pairwise:                            true, -			expectCodeChallengeMethodsSupported: []string{"S256"}, -			expectSubjectTypesSupported:         []string{"public", "pairwise"}, +			clients:                             map[string]*Client{"a": {SectorIdentifier: "yes"}}, +			expectCodeChallengeMethodsSupported: []string{PKCEChallengeMethodSHA256}, +			expectSubjectTypesSupported:         []string{SubjectTypePublic, SubjectTypePairwise},  		},  		{  			desc:                                "ShouldHaveChallengeMethodsS256PlainANDSubjectTypesSupportedPublicPairwise",  			pkcePlainChallenge:                  true, -			pairwise:                            true, -			expectCodeChallengeMethodsSupported: []string{"S256", "plain"}, -			expectSubjectTypesSupported:         []string{"public", "pairwise"}, +			clients:                             map[string]*Client{"a": {SectorIdentifier: "yes"}}, +			expectCodeChallengeMethodsSupported: []string{PKCEChallengeMethodSHA256, PKCEChallengeMethodPlain}, +			expectSubjectTypesSupported:         []string{SubjectTypePublic, SubjectTypePairwise}, +		}, +		{ +			desc:                                "ShouldHaveTokenAuthMethodsNone", +			pkcePlainChallenge:                  true, +			clients:                             map[string]*Client{"a": {SectorIdentifier: "yes"}}, +			expectCodeChallengeMethodsSupported: []string{PKCEChallengeMethodSHA256, PKCEChallengeMethodPlain}, +			expectSubjectTypesSupported:         []string{SubjectTypePublic, SubjectTypePairwise}, +		}, +		{ +			desc:               "ShouldHaveTokenAuthMethodsNone", +			pkcePlainChallenge: true, +			clients: map[string]*Client{ +				"a": {SectorIdentifier: "yes"}, +				"b": {SectorIdentifier: "yes"}, +			}, +			expectCodeChallengeMethodsSupported: []string{PKCEChallengeMethodSHA256, PKCEChallengeMethodPlain}, +			expectSubjectTypesSupported:         []string{SubjectTypePublic, SubjectTypePairwise},  		},  	}  	for _, tc := range testCases {  		t.Run(tc.desc, func(t *testing.T) { -			actual := NewOpenIDConnectWellKnownConfiguration(tc.pkcePlainChallenge, tc.pairwise) +			actual := NewOpenIDConnectWellKnownConfiguration(tc.pkcePlainChallenge, tc.clients)  			for _, codeChallengeMethod := range tc.expectCodeChallengeMethodsSupported {  				assert.Contains(t, actual.CodeChallengeMethodsSupported, codeChallengeMethod)  			} diff --git a/internal/oidc/errors.go b/internal/oidc/errors.go index 239b3e549..465689257 100644 --- a/internal/oidc/errors.go +++ b/internal/oidc/errors.go @@ -1,5 +1,19 @@  package oidc -import "errors" +import ( +	"errors" + +	"github.com/ory/fosite" +)  var errPasswordsDoNotMatch = errors.New("the passwords don't match") + +var ( +	ErrIssuerCouldNotDerive        = fosite.ErrServerError.WithHint("Could not safely derive the issuer.") +	ErrSubjectCouldNotLookup       = fosite.ErrServerError.WithHint("Could not lookup user subject.") +	ErrConsentCouldNotPerform      = fosite.ErrServerError.WithHint("Could not perform consent.") +	ErrConsentCouldNotGenerate     = fosite.ErrServerError.WithHint("Could not generate the consent session.") +	ErrConsentCouldNotSave         = fosite.ErrServerError.WithHint("Could not save the consent session.") +	ErrConsentCouldNotLookup       = fosite.ErrServerError.WithHint("Failed to lookup the consent session.") +	ErrConsentMalformedChallengeID = fosite.ErrServerError.WithHint("Malformed consent session challenge ID.") +) diff --git a/internal/oidc/keys.go b/internal/oidc/keys.go index acbfc17d4..7f5e3d86a 100644 --- a/internal/oidc/keys.go +++ b/internal/oidc/keys.go @@ -125,7 +125,7 @@ func NewJWK(chain schema.X509CertificateChain, key *rsa.PrivateKey) (j *JWK, err  	}  	jwk := &jose.JSONWebKey{ -		Algorithm: "RS256", +		Algorithm: SigningAlgorithmRSAWithSHA256,  		Use:       "sig",  		Key:       &key.PublicKey,  	} diff --git a/internal/oidc/provider.go b/internal/oidc/provider.go index 5ca1993f4..8843a6807 100644 --- a/internal/oidc/provider.go +++ b/internal/oidc/provider.go @@ -1,10 +1,13 @@  package oidc  import ( +	"crypto/sha512"  	"fmt" -	"net/http"  	"github.com/ory/fosite/compose" +	"github.com/ory/fosite/handler/oauth2" +	"github.com/ory/fosite/handler/openid" +	"github.com/ory/fosite/token/hmac"  	"github.com/ory/herodot"  	"github.com/authelia/authelia/v4/internal/configuration/schema" @@ -13,14 +16,14 @@ import (  )  // NewOpenIDConnectProvider new-ups a OpenIDConnectProvider. -func NewOpenIDConnectProvider(config *schema.OpenIDConnectConfiguration, storageProvider storage.Provider) (provider OpenIDConnectProvider, err error) { +func NewOpenIDConnectProvider(config *schema.OpenIDConnectConfiguration, store storage.Provider) (provider *OpenIDConnectProvider, err error) {  	if config == nil { -		return provider, nil +		return nil, nil  	} -	provider = OpenIDConnectProvider{ -		Fosite: nil, -		Store:  NewOpenIDConnectStore(config, storageProvider), +	provider = &OpenIDConnectProvider{ +		JSONWriter: herodot.NewJSONWriter(nil), +		Store:      NewOpenIDConnectStore(config, store),  	}  	cconfig := &compose.Config{ @@ -35,32 +38,34 @@ func NewOpenIDConnectProvider(config *schema.OpenIDConnectConfiguration, storage  		EnablePKCEPlainChallengeMethod: config.EnablePKCEPlainChallenge,  	} -	keyManager, err := NewKeyManagerWithConfiguration(config) -	if err != nil { -		return provider, err +	if provider.KeyManager, err = NewKeyManagerWithConfiguration(config); err != nil { +		return nil, err  	} -	provider.KeyManager = keyManager - -	key, err := provider.KeyManager.GetActivePrivateKey() -	if err != nil { -		return provider, err -	} +	jwtStrategy := provider.KeyManager.Strategy()  	strategy := &compose.CommonStrategy{ -		CoreStrategy: compose.NewOAuth2HMACStrategy( -			cconfig, -			[]byte(utils.HashSHA256FromString(config.HMACSecret)), -			nil, -		), -		OpenIDConnectTokenStrategy: compose.NewOpenIDConnectStrategy( -			cconfig, -			key, -		), -		JWTStrategy: provider.KeyManager.Strategy(), +		CoreStrategy: &oauth2.HMACSHAStrategy{ +			Enigma: &hmac.HMACStrategy{ +				GlobalSecret:         []byte(utils.HashSHA256FromString(config.HMACSecret)), +				RotatedGlobalSecrets: nil, +				TokenEntropy:         cconfig.GetTokenEntropy(), +				Hash:                 sha512.New512_256, +			}, +			AccessTokenLifespan:   cconfig.GetAccessTokenLifespan(), +			AuthorizeCodeLifespan: cconfig.GetAuthorizeCodeLifespan(), +			RefreshTokenLifespan:  cconfig.GetRefreshTokenLifespan(), +		}, +		OpenIDConnectTokenStrategy: &openid.DefaultStrategy{ +			JWTStrategy:         jwtStrategy, +			Expiry:              cconfig.GetIDTokenLifespan(), +			Issuer:              cconfig.IDTokenIssuer, +			MinParameterEntropy: cconfig.GetMinParameterEntropy(), +		}, +		JWTStrategy: jwtStrategy,  	} -	provider.Fosite = compose.Compose( +	provider.OAuth2Provider = compose.Compose(  		cconfig,  		provider.Store,  		strategy, @@ -89,60 +94,32 @@ func NewOpenIDConnectProvider(config *schema.OpenIDConnectConfiguration, storage  		compose.OAuth2PKCEFactory,  	) -	provider.discovery = NewOpenIDConnectWellKnownConfiguration(config.EnablePKCEPlainChallenge, provider.Pairwise()) - -	provider.herodot = herodot.NewJSONWriter(nil) +	provider.discovery = NewOpenIDConnectWellKnownConfiguration(config.EnablePKCEPlainChallenge, provider.Store.clients)  	return provider, nil  } -// Pairwise returns true if this provider is configured with clients that require pairwise. -func (p OpenIDConnectProvider) Pairwise() bool { -	for _, c := range p.Store.clients { -		if c.SectorIdentifier != "" { -			return true -		} -	} - -	return false -} - -// Write writes data with herodot.JSONWriter. -func (p OpenIDConnectProvider) Write(w http.ResponseWriter, r *http.Request, e any, opts ...herodot.EncoderOptions) { -	p.herodot.Write(w, r, e, opts...) -} - -// WriteError writes an error with herodot.JSONWriter. -func (p OpenIDConnectProvider) WriteError(w http.ResponseWriter, r *http.Request, err error, opts ...herodot.Option) { -	p.herodot.WriteError(w, r, err, opts...) -} - -// WriteErrorCode writes an error with an error code with herodot.JSONWriter. -func (p OpenIDConnectProvider) WriteErrorCode(w http.ResponseWriter, r *http.Request, code int, err error, opts ...herodot.Option) { -	p.herodot.WriteErrorCode(w, r, code, err, opts...) -} -  // GetOAuth2WellKnownConfiguration returns the discovery document for the OAuth Configuration. -func (p OpenIDConnectProvider) GetOAuth2WellKnownConfiguration(issuer string) OAuth2WellKnownConfiguration { +func (p *OpenIDConnectProvider) GetOAuth2WellKnownConfiguration(issuer string) OAuth2WellKnownConfiguration {  	options := OAuth2WellKnownConfiguration{  		CommonDiscoveryOptions: p.discovery.CommonDiscoveryOptions,  		OAuth2DiscoveryOptions: p.discovery.OAuth2DiscoveryOptions,  	}  	options.Issuer = issuer -	options.JWKSURI = fmt.Sprintf("%s%s", issuer, JWKsPath) +	options.JWKSURI = fmt.Sprintf("%s%s", issuer, EndpointPathJWKs) -	options.IntrospectionEndpoint = fmt.Sprintf("%s%s", issuer, IntrospectionPath) -	options.TokenEndpoint = fmt.Sprintf("%s%s", issuer, TokenPath) +	options.IntrospectionEndpoint = fmt.Sprintf("%s%s", issuer, EndpointPathIntrospection) +	options.TokenEndpoint = fmt.Sprintf("%s%s", issuer, EndpointPathToken) -	options.AuthorizationEndpoint = fmt.Sprintf("%s%s", issuer, AuthorizationPath) -	options.RevocationEndpoint = fmt.Sprintf("%s%s", issuer, RevocationPath) +	options.AuthorizationEndpoint = fmt.Sprintf("%s%s", issuer, EndpointPathAuthorization) +	options.RevocationEndpoint = fmt.Sprintf("%s%s", issuer, EndpointPathRevocation)  	return options  }  // GetOpenIDConnectWellKnownConfiguration returns the discovery document for the OpenID Configuration. -func (p OpenIDConnectProvider) GetOpenIDConnectWellKnownConfiguration(issuer string) OpenIDConnectWellKnownConfiguration { +func (p *OpenIDConnectProvider) GetOpenIDConnectWellKnownConfiguration(issuer string) OpenIDConnectWellKnownConfiguration {  	options := OpenIDConnectWellKnownConfiguration{  		CommonDiscoveryOptions:                          p.discovery.CommonDiscoveryOptions,  		OAuth2DiscoveryOptions:                          p.discovery.OAuth2DiscoveryOptions, @@ -152,14 +129,14 @@ func (p OpenIDConnectProvider) GetOpenIDConnectWellKnownConfiguration(issuer str  	}  	options.Issuer = issuer -	options.JWKSURI = fmt.Sprintf("%s%s", issuer, JWKsPath) +	options.JWKSURI = fmt.Sprintf("%s%s", issuer, EndpointPathJWKs) -	options.IntrospectionEndpoint = fmt.Sprintf("%s%s", issuer, IntrospectionPath) -	options.TokenEndpoint = fmt.Sprintf("%s%s", issuer, TokenPath) +	options.IntrospectionEndpoint = fmt.Sprintf("%s%s", issuer, EndpointPathIntrospection) +	options.TokenEndpoint = fmt.Sprintf("%s%s", issuer, EndpointPathToken) -	options.AuthorizationEndpoint = fmt.Sprintf("%s%s", issuer, AuthorizationPath) -	options.RevocationEndpoint = fmt.Sprintf("%s%s", issuer, RevocationPath) -	options.UserinfoEndpoint = fmt.Sprintf("%s%s", issuer, UserinfoPath) +	options.AuthorizationEndpoint = fmt.Sprintf("%s%s", issuer, EndpointPathAuthorization) +	options.RevocationEndpoint = fmt.Sprintf("%s%s", issuer, EndpointPathRevocation) +	options.UserinfoEndpoint = fmt.Sprintf("%s%s", issuer, EndpointPathUserinfo)  	return options  } diff --git a/internal/oidc/provider_test.go b/internal/oidc/provider_test.go index b55c6f92a..6d0c3ecef 100644 --- a/internal/oidc/provider_test.go +++ b/internal/oidc/provider_test.go @@ -19,8 +19,7 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_NotConfigured(t *testing  	provider, err := NewOpenIDConnectProvider(nil, nil)  	assert.NoError(t, err) -	assert.Nil(t, provider.Fosite) -	assert.Nil(t, provider.Store) +	assert.Nil(t, provider)  }  func TestNewOpenIDConnectProvider_ShouldEnableOptionalDiscoveryValues(t *testing.T) { @@ -44,17 +43,15 @@ func TestNewOpenIDConnectProvider_ShouldEnableOptionalDiscoveryValues(t *testing  	assert.NoError(t, err) -	assert.True(t, provider.Pairwise()) -  	disco := provider.GetOpenIDConnectWellKnownConfiguration("https://example.com")  	assert.Len(t, disco.SubjectTypesSupported, 2) -	assert.Contains(t, disco.SubjectTypesSupported, "public") -	assert.Contains(t, disco.SubjectTypesSupported, "pairwise") +	assert.Contains(t, disco.SubjectTypesSupported, SubjectTypePublic) +	assert.Contains(t, disco.SubjectTypesSupported, SubjectTypePairwise)  	assert.Len(t, disco.CodeChallengeMethodsSupported, 2) -	assert.Contains(t, disco.CodeChallengeMethodsSupported, "S256") -	assert.Contains(t, disco.CodeChallengeMethodsSupported, "S256") +	assert.Contains(t, disco.CodeChallengeMethodsSupported, PKCEChallengeMethodSHA256) +	assert.Contains(t, disco.CodeChallengeMethodsSupported, PKCEChallengeMethodSHA256)  }  func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GoodConfiguration(t *testing.T) { @@ -80,10 +77,10 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GoodConfiguration(t *tes  					"https://google.com",  				},  				Scopes: []string{ -					"groups", +					ScopeGroups,  				},  				GrantTypes: []string{ -					"refresh_token", +					GrantTypeRefreshToken,  				},  				ResponseTypes: []string{  					"token", @@ -116,8 +113,6 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GetOpenIDConnectWellKnow  	assert.NoError(t, err) -	assert.False(t, provider.Pairwise()) -  	disco := provider.GetOpenIDConnectWellKnownConfiguration("https://example.com")  	assert.Equal(t, "https://example.com", disco.Issuer) @@ -130,7 +125,7 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GetOpenIDConnectWellKnow  	assert.Equal(t, "", disco.RegistrationEndpoint)  	assert.Len(t, disco.CodeChallengeMethodsSupported, 1) -	assert.Contains(t, disco.CodeChallengeMethodsSupported, "S256") +	assert.Contains(t, disco.CodeChallengeMethodsSupported, PKCEChallengeMethodSHA256)  	assert.Len(t, disco.ScopesSupported, 5)  	assert.Contains(t, disco.ScopesSupported, ScopeOpenID) @@ -140,12 +135,12 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GetOpenIDConnectWellKnow  	assert.Contains(t, disco.ScopesSupported, ScopeEmail)  	assert.Len(t, disco.ResponseModesSupported, 3) -	assert.Contains(t, disco.ResponseModesSupported, "form_post") -	assert.Contains(t, disco.ResponseModesSupported, "query") -	assert.Contains(t, disco.ResponseModesSupported, "fragment") +	assert.Contains(t, disco.ResponseModesSupported, ResponseModeFormPost) +	assert.Contains(t, disco.ResponseModesSupported, ResponseModeQuery) +	assert.Contains(t, disco.ResponseModesSupported, ResponseModeFragment)  	assert.Len(t, disco.SubjectTypesSupported, 1) -	assert.Contains(t, disco.SubjectTypesSupported, "public") +	assert.Contains(t, disco.SubjectTypesSupported, SubjectTypePublic)  	assert.Len(t, disco.ResponseTypesSupported, 8)  	assert.Contains(t, disco.ResponseTypesSupported, "code") @@ -158,35 +153,35 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GetOpenIDConnectWellKnow  	assert.Contains(t, disco.ResponseTypesSupported, "none")  	assert.Len(t, disco.IDTokenSigningAlgValuesSupported, 1) -	assert.Contains(t, disco.IDTokenSigningAlgValuesSupported, "RS256") +	assert.Contains(t, disco.IDTokenSigningAlgValuesSupported, SigningAlgorithmRSAWithSHA256)  	assert.Len(t, disco.UserinfoSigningAlgValuesSupported, 2) -	assert.Contains(t, disco.UserinfoSigningAlgValuesSupported, "RS256") -	assert.Contains(t, disco.UserinfoSigningAlgValuesSupported, "none") +	assert.Contains(t, disco.UserinfoSigningAlgValuesSupported, SigningAlgorithmRSAWithSHA256) +	assert.Contains(t, disco.UserinfoSigningAlgValuesSupported, SigningAlgorithmNone)  	assert.Len(t, disco.RequestObjectSigningAlgValuesSupported, 2) -	assert.Contains(t, disco.RequestObjectSigningAlgValuesSupported, "RS256") -	assert.Contains(t, disco.RequestObjectSigningAlgValuesSupported, "none") +	assert.Contains(t, disco.RequestObjectSigningAlgValuesSupported, SigningAlgorithmRSAWithSHA256) +	assert.Contains(t, disco.RequestObjectSigningAlgValuesSupported, SigningAlgorithmNone)  	assert.Len(t, disco.ClaimsSupported, 18) -	assert.Contains(t, disco.ClaimsSupported, "amr") -	assert.Contains(t, disco.ClaimsSupported, "aud") -	assert.Contains(t, disco.ClaimsSupported, "azp") -	assert.Contains(t, disco.ClaimsSupported, "client_id") -	assert.Contains(t, disco.ClaimsSupported, "exp") -	assert.Contains(t, disco.ClaimsSupported, "iat") -	assert.Contains(t, disco.ClaimsSupported, "iss") -	assert.Contains(t, disco.ClaimsSupported, "jti") -	assert.Contains(t, disco.ClaimsSupported, "rat") -	assert.Contains(t, disco.ClaimsSupported, "sub") -	assert.Contains(t, disco.ClaimsSupported, "auth_time") -	assert.Contains(t, disco.ClaimsSupported, "nonce") -	assert.Contains(t, disco.ClaimsSupported, ClaimEmail) +	assert.Contains(t, disco.ClaimsSupported, ClaimAuthenticationMethodsReference) +	assert.Contains(t, disco.ClaimsSupported, ClaimAudience) +	assert.Contains(t, disco.ClaimsSupported, ClaimAuthorizedParty) +	assert.Contains(t, disco.ClaimsSupported, ClaimClientIdentifier) +	assert.Contains(t, disco.ClaimsSupported, ClaimExpirationTime) +	assert.Contains(t, disco.ClaimsSupported, ClaimIssuedAt) +	assert.Contains(t, disco.ClaimsSupported, ClaimIssuer) +	assert.Contains(t, disco.ClaimsSupported, ClaimJWTID) +	assert.Contains(t, disco.ClaimsSupported, ClaimRequestedAt) +	assert.Contains(t, disco.ClaimsSupported, ClaimSubject) +	assert.Contains(t, disco.ClaimsSupported, ClaimAuthenticationTime) +	assert.Contains(t, disco.ClaimsSupported, ClaimNonce) +	assert.Contains(t, disco.ClaimsSupported, ClaimPreferredEmail)  	assert.Contains(t, disco.ClaimsSupported, ClaimEmailVerified)  	assert.Contains(t, disco.ClaimsSupported, ClaimEmailAlts)  	assert.Contains(t, disco.ClaimsSupported, ClaimGroups)  	assert.Contains(t, disco.ClaimsSupported, ClaimPreferredUsername) -	assert.Contains(t, disco.ClaimsSupported, ClaimDisplayName) +	assert.Contains(t, disco.ClaimsSupported, ClaimFullName)  }  func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GetOAuth2WellKnownConfiguration(t *testing.T) { @@ -229,12 +224,12 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GetOAuth2WellKnownConfig  	assert.Contains(t, disco.ScopesSupported, ScopeEmail)  	assert.Len(t, disco.ResponseModesSupported, 3) -	assert.Contains(t, disco.ResponseModesSupported, "form_post") -	assert.Contains(t, disco.ResponseModesSupported, "query") -	assert.Contains(t, disco.ResponseModesSupported, "fragment") +	assert.Contains(t, disco.ResponseModesSupported, ResponseModeFormPost) +	assert.Contains(t, disco.ResponseModesSupported, ResponseModeQuery) +	assert.Contains(t, disco.ResponseModesSupported, ResponseModeFragment)  	assert.Len(t, disco.SubjectTypesSupported, 1) -	assert.Contains(t, disco.SubjectTypesSupported, "public") +	assert.Contains(t, disco.SubjectTypesSupported, SubjectTypePublic)  	assert.Len(t, disco.ResponseTypesSupported, 8)  	assert.Contains(t, disco.ResponseTypesSupported, "code") @@ -247,24 +242,24 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GetOAuth2WellKnownConfig  	assert.Contains(t, disco.ResponseTypesSupported, "none")  	assert.Len(t, disco.ClaimsSupported, 18) -	assert.Contains(t, disco.ClaimsSupported, "amr") -	assert.Contains(t, disco.ClaimsSupported, "aud") -	assert.Contains(t, disco.ClaimsSupported, "azp") -	assert.Contains(t, disco.ClaimsSupported, "client_id") -	assert.Contains(t, disco.ClaimsSupported, "exp") -	assert.Contains(t, disco.ClaimsSupported, "iat") -	assert.Contains(t, disco.ClaimsSupported, "iss") -	assert.Contains(t, disco.ClaimsSupported, "jti") -	assert.Contains(t, disco.ClaimsSupported, "rat") -	assert.Contains(t, disco.ClaimsSupported, "sub") -	assert.Contains(t, disco.ClaimsSupported, "auth_time") -	assert.Contains(t, disco.ClaimsSupported, "nonce") -	assert.Contains(t, disco.ClaimsSupported, ClaimEmail) +	assert.Contains(t, disco.ClaimsSupported, ClaimAuthenticationMethodsReference) +	assert.Contains(t, disco.ClaimsSupported, ClaimAudience) +	assert.Contains(t, disco.ClaimsSupported, ClaimAuthorizedParty) +	assert.Contains(t, disco.ClaimsSupported, ClaimClientIdentifier) +	assert.Contains(t, disco.ClaimsSupported, ClaimExpirationTime) +	assert.Contains(t, disco.ClaimsSupported, ClaimIssuedAt) +	assert.Contains(t, disco.ClaimsSupported, ClaimIssuer) +	assert.Contains(t, disco.ClaimsSupported, ClaimJWTID) +	assert.Contains(t, disco.ClaimsSupported, ClaimRequestedAt) +	assert.Contains(t, disco.ClaimsSupported, ClaimSubject) +	assert.Contains(t, disco.ClaimsSupported, ClaimAuthenticationTime) +	assert.Contains(t, disco.ClaimsSupported, ClaimNonce) +	assert.Contains(t, disco.ClaimsSupported, ClaimPreferredEmail)  	assert.Contains(t, disco.ClaimsSupported, ClaimEmailVerified)  	assert.Contains(t, disco.ClaimsSupported, ClaimEmailAlts)  	assert.Contains(t, disco.ClaimsSupported, ClaimGroups)  	assert.Contains(t, disco.ClaimsSupported, ClaimPreferredUsername) -	assert.Contains(t, disco.ClaimsSupported, ClaimDisplayName) +	assert.Contains(t, disco.ClaimsSupported, ClaimFullName)  }  func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GetOpenIDConnectWellKnownConfigurationWithPlainPKCE(t *testing.T) { @@ -290,8 +285,8 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GetOpenIDConnectWellKnow  	disco := provider.GetOpenIDConnectWellKnownConfiguration("https://example.com")  	require.Len(t, disco.CodeChallengeMethodsSupported, 2) -	assert.Equal(t, "S256", disco.CodeChallengeMethodsSupported[0]) -	assert.Equal(t, "plain", disco.CodeChallengeMethodsSupported[1]) +	assert.Equal(t, PKCEChallengeMethodSHA256, disco.CodeChallengeMethodsSupported[0]) +	assert.Equal(t, PKCEChallengeMethodPlain, disco.CodeChallengeMethodsSupported[1])  }  func mustParseRSAPrivateKey(data string) *rsa.PrivateKey { diff --git a/internal/oidc/store.go b/internal/oidc/store.go index af3fff7d0..1974e057f 100644 --- a/internal/oidc/store.go +++ b/internal/oidc/store.go @@ -18,11 +18,11 @@ import (  	"github.com/authelia/authelia/v4/internal/storage"  ) -// NewOpenIDConnectStore returns a OpenIDConnectStore when provided with a schema.OpenIDConnectConfiguration and storage.Provider. -func NewOpenIDConnectStore(config *schema.OpenIDConnectConfiguration, provider storage.Provider) (store *OpenIDConnectStore) { +// NewOpenIDConnectStore returns a Store when provided with a schema.OpenIDConnectConfiguration and storage.Provider. +func NewOpenIDConnectStore(config *schema.OpenIDConnectConfiguration, provider storage.Provider) (store *Store) {  	logger := logging.Logger() -	store = &OpenIDConnectStore{ +	store = &Store{  		provider: provider,  		clients:  map[string]*Client{},  	} @@ -38,7 +38,7 @@ func NewOpenIDConnectStore(config *schema.OpenIDConnectConfiguration, provider s  }  // GenerateOpaqueUserID either retrieves or creates an opaque user id from a sectorID and username. -func (s *OpenIDConnectStore) GenerateOpaqueUserID(ctx context.Context, sectorID, username string) (opaqueID *model.UserOpaqueIdentifier, err error) { +func (s *Store) GenerateOpaqueUserID(ctx context.Context, sectorID, username string) (opaqueID *model.UserOpaqueIdentifier, err error) {  	if opaqueID, err = s.provider.LoadUserOpaqueIdentifierBySignature(ctx, "openid", sectorID, username); err != nil {  		return nil, err  	} else if opaqueID == nil { @@ -55,7 +55,7 @@ func (s *OpenIDConnectStore) GenerateOpaqueUserID(ctx context.Context, sectorID,  }  // GetSubject returns a subject UUID for a username. If it exists, it returns the existing one, otherwise it creates and saves it. -func (s *OpenIDConnectStore) GetSubject(ctx context.Context, sectorID, username string) (subject uuid.UUID, err error) { +func (s *Store) GetSubject(ctx context.Context, sectorID, username string) (subject uuid.UUID, err error) {  	var opaqueID *model.UserOpaqueIdentifier  	if opaqueID, err = s.GenerateOpaqueUserID(ctx, sectorID, username); err != nil { @@ -66,7 +66,7 @@ func (s *OpenIDConnectStore) GetSubject(ctx context.Context, sectorID, username  }  // GetClientPolicy retrieves the policy from the client with the matching provided id. -func (s *OpenIDConnectStore) GetClientPolicy(id string) (level authorization.Level) { +func (s *Store) GetClientPolicy(id string) (level authorization.Level) {  	client, err := s.GetFullClient(id)  	if err != nil {  		return authorization.TwoFactor @@ -76,7 +76,7 @@ func (s *OpenIDConnectStore) GetClientPolicy(id string) (level authorization.Lev  }  // GetFullClient returns a fosite.Client asserted as an Client matching the provided id. -func (s *OpenIDConnectStore) 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.ErrNotFound @@ -86,7 +86,7 @@ func (s *OpenIDConnectStore) GetFullClient(id string) (client *Client, err error  }  // IsValidClientID returns true if the provided id exists in the OpenIDConnectProvider.Clients map. -func (s *OpenIDConnectStore) IsValidClientID(id string) (valid bool) { +func (s *Store) IsValidClientID(id string) (valid bool) {  	_, err := s.GetFullClient(id)  	return err == nil @@ -94,31 +94,31 @@ func (s *OpenIDConnectStore) IsValidClientID(id string) (valid bool) {  // BeginTX starts a transaction.  // This implements a portion of fosite storage.Transactional interface. -func (s *OpenIDConnectStore) BeginTX(ctx context.Context) (c context.Context, err error) { +func (s *Store) BeginTX(ctx context.Context) (c context.Context, err error) {  	return s.provider.BeginTX(ctx)  }  // Commit completes a transaction.  // This implements a portion of fosite storage.Transactional interface. -func (s *OpenIDConnectStore) Commit(ctx context.Context) (err error) { +func (s *Store) Commit(ctx context.Context) (err error) {  	return s.provider.Commit(ctx)  }  // Rollback rolls a transaction back.  // This implements a portion of fosite storage.Transactional interface. -func (s *OpenIDConnectStore) Rollback(ctx context.Context) (err error) { +func (s *Store) Rollback(ctx context.Context) (err error) {  	return s.provider.Rollback(ctx)  }  // GetClient loads the client by its ID or returns an error if the client does not exist or another error occurred.  // This implements a portion of fosite.ClientManager. -func (s *OpenIDConnectStore) GetClient(_ context.Context, id string) (client fosite.Client, err error) { +func (s *Store) GetClient(_ context.Context, id string) (client fosite.Client, err error) {  	return s.GetFullClient(id)  }  // ClientAssertionJWTValid returns an error if the JTI is known or the DB check failed and nil if the JTI is not known.  // This implements a portion of fosite.ClientManager. -func (s *OpenIDConnectStore) ClientAssertionJWTValid(ctx context.Context, jti string) (err error) { +func (s *Store) ClientAssertionJWTValid(ctx context.Context, jti string) (err error) {  	signature := fmt.Sprintf("%x", sha256.Sum256([]byte(jti)))  	blacklistedJTI, err := s.provider.LoadOAuth2BlacklistedJTI(ctx, signature) @@ -138,7 +138,7 @@ func (s *OpenIDConnectStore) ClientAssertionJWTValid(ctx context.Context, jti st  // SetClientAssertionJWT marks a JTI as known for the given expiry time. Before inserting the new JTI, it will clean  // up any existing JTIs that have expired as those tokens can not be replayed due to the expiry.  // This implements a portion of fosite.ClientManager. -func (s *OpenIDConnectStore) SetClientAssertionJWT(ctx context.Context, jti string, exp time.Time) (err error) { +func (s *Store) SetClientAssertionJWT(ctx context.Context, jti string, exp time.Time) (err error) {  	blacklistedJTI := model.NewOAuth2BlacklistedJTI(jti, exp)  	return s.provider.SaveOAuth2BlacklistedJTI(ctx, blacklistedJTI) @@ -146,7 +146,7 @@ func (s *OpenIDConnectStore) SetClientAssertionJWT(ctx context.Context, jti stri  // CreateAuthorizeCodeSession stores the authorization request for a given authorization code.  // This implements a portion of oauth2.AuthorizeCodeStorage. -func (s *OpenIDConnectStore) CreateAuthorizeCodeSession(ctx context.Context, code string, request fosite.Requester) (err error) { +func (s *Store) CreateAuthorizeCodeSession(ctx context.Context, code string, request fosite.Requester) (err error) {  	return s.saveSession(ctx, storage.OAuth2SessionTypeAuthorizeCode, code, request)  } @@ -154,7 +154,7 @@ func (s *OpenIDConnectStore) CreateAuthorizeCodeSession(ctx context.Context, cod  // code should be set to invalid and consecutive requests to GetAuthorizeCodeSession should return the  // ErrInvalidatedAuthorizeCode error.  // This implements a portion of oauth2.AuthorizeCodeStorage. -func (s *OpenIDConnectStore) InvalidateAuthorizeCodeSession(ctx context.Context, code string) (err error) { +func (s *Store) InvalidateAuthorizeCodeSession(ctx context.Context, code string) (err error) {  	return s.provider.DeactivateOAuth2Session(ctx, storage.OAuth2SessionTypeAuthorizeCode, code)  } @@ -163,45 +163,45 @@ func (s *OpenIDConnectStore) InvalidateAuthorizeCodeSession(ctx context.Context,  // method should return the ErrInvalidatedAuthorizeCode error.  // Make sure to also return the fosite.Requester value when returning the fosite.ErrInvalidatedAuthorizeCode error!  // This implements a portion of oauth2.AuthorizeCodeStorage. -func (s *OpenIDConnectStore) GetAuthorizeCodeSession(ctx context.Context, code string, session fosite.Session) (request fosite.Requester, err error) { +func (s *Store) GetAuthorizeCodeSession(ctx context.Context, code string, session fosite.Session) (request fosite.Requester, err error) {  	// TODO: Implement the fosite.ErrInvalidatedAuthorizeCode error above. This requires splitting the invalidated sessions and deleted sessions.  	return s.loadSessionBySignature(ctx, storage.OAuth2SessionTypeAuthorizeCode, code, session)  }  // CreateAccessTokenSession stores the authorization request for a given access token.  // This implements a portion of oauth2.AccessTokenStorage. -func (s *OpenIDConnectStore) CreateAccessTokenSession(ctx context.Context, signature string, request fosite.Requester) (err error) { +func (s *Store) CreateAccessTokenSession(ctx context.Context, signature string, request fosite.Requester) (err error) {  	return s.saveSession(ctx, storage.OAuth2SessionTypeAccessToken, signature, request)  }  // DeleteAccessTokenSession marks an access token session as deleted.  // This implements a portion of oauth2.AccessTokenStorage. -func (s *OpenIDConnectStore) DeleteAccessTokenSession(ctx context.Context, signature string) (err error) { +func (s *Store) DeleteAccessTokenSession(ctx context.Context, signature string) (err error) {  	return s.revokeSessionBySignature(ctx, storage.OAuth2SessionTypeAccessToken, signature)  }  // RevokeAccessToken revokes an access token as specified in: https://tools.ietf.org/html/rfc7009#section-2.1  // If the token passed to the request is an access token, the server MAY revoke the respective refresh token as well.  // This implements a portion of oauth2.TokenRevocationStorage. -func (s *OpenIDConnectStore) RevokeAccessToken(ctx context.Context, requestID string) (err error) { +func (s *Store) RevokeAccessToken(ctx context.Context, requestID string) (err error) {  	return s.revokeSessionByRequestID(ctx, storage.OAuth2SessionTypeAccessToken, requestID)  }  // GetAccessTokenSession gets the authorization request for a given access token.  // This implements a portion of oauth2.AccessTokenStorage. -func (s *OpenIDConnectStore) GetAccessTokenSession(ctx context.Context, signature string, session fosite.Session) (request fosite.Requester, err error) { +func (s *Store) GetAccessTokenSession(ctx context.Context, signature string, session fosite.Session) (request fosite.Requester, err error) {  	return s.loadSessionBySignature(ctx, storage.OAuth2SessionTypeAccessToken, signature, session)  }  // CreateRefreshTokenSession stores the authorization request for a given refresh token.  // This implements a portion of oauth2.RefreshTokenStorage. -func (s *OpenIDConnectStore) CreateRefreshTokenSession(ctx context.Context, signature string, request fosite.Requester) (err error) { +func (s *Store) CreateRefreshTokenSession(ctx context.Context, signature string, request fosite.Requester) (err error) {  	return s.saveSession(ctx, storage.OAuth2SessionTypeRefreshToken, signature, request)  }  // DeleteRefreshTokenSession marks the authorization request for a given refresh token as deleted.  // This implements a portion of oauth2.RefreshTokenStorage. -func (s *OpenIDConnectStore) DeleteRefreshTokenSession(ctx context.Context, signature string) (err error) { +func (s *Store) DeleteRefreshTokenSession(ctx context.Context, signature string) (err error) {  	return s.revokeSessionBySignature(ctx, storage.OAuth2SessionTypeRefreshToken, signature)  } @@ -209,51 +209,51 @@ func (s *OpenIDConnectStore) DeleteRefreshTokenSession(ctx context.Context, sign  // If the particular token is a refresh token and the authorization server supports the revocation of access tokens,  // then the authorization server SHOULD also invalidate all access tokens based on the same authorization grant (see Implementation Note).  // This implements a portion of oauth2.TokenRevocationStorage. -func (s *OpenIDConnectStore) RevokeRefreshToken(ctx context.Context, requestID string) (err error) { +func (s *Store) RevokeRefreshToken(ctx context.Context, requestID string) (err error) {  	return s.provider.DeactivateOAuth2SessionByRequestID(ctx, storage.OAuth2SessionTypeRefreshToken, requestID)  }  // RevokeRefreshTokenMaybeGracePeriod revokes an access token as specified in: https://tools.ietf.org/html/rfc7009#section-2.1  // If the token passed to the request is an access token, the server MAY revoke the respective refresh token as well.  // This implements a portion of oauth2.TokenRevocationStorage. -func (s *OpenIDConnectStore) RevokeRefreshTokenMaybeGracePeriod(ctx context.Context, requestID string, signature string) (err error) { +func (s *Store) RevokeRefreshTokenMaybeGracePeriod(ctx context.Context, requestID string, signature string) (err error) {  	return s.RevokeRefreshToken(ctx, requestID)  }  // GetRefreshTokenSession gets the authorization request for a given refresh token.  // This implements a portion of oauth2.RefreshTokenStorage. -func (s *OpenIDConnectStore) GetRefreshTokenSession(ctx context.Context, signature string, session fosite.Session) (request fosite.Requester, err error) { +func (s *Store) GetRefreshTokenSession(ctx context.Context, signature string, session fosite.Session) (request fosite.Requester, err error) {  	return s.loadSessionBySignature(ctx, storage.OAuth2SessionTypeRefreshToken, signature, session)  }  // CreatePKCERequestSession stores the authorization request for a given PKCE request.  // This implements a portion of pkce.PKCERequestStorage. -func (s *OpenIDConnectStore) CreatePKCERequestSession(ctx context.Context, signature string, request fosite.Requester) (err error) { +func (s *Store) CreatePKCERequestSession(ctx context.Context, signature string, request fosite.Requester) (err error) {  	return s.saveSession(ctx, storage.OAuth2SessionTypePKCEChallenge, signature, request)  }  // DeletePKCERequestSession marks the authorization request for a given PKCE request as deleted.  // This implements a portion of pkce.PKCERequestStorage. -func (s *OpenIDConnectStore) DeletePKCERequestSession(ctx context.Context, signature string) (err error) { +func (s *Store) DeletePKCERequestSession(ctx context.Context, signature string) (err error) {  	return s.revokeSessionBySignature(ctx, storage.OAuth2SessionTypeAccessToken, signature)  }  // GetPKCERequestSession gets the authorization request for a given PKCE request.  // This implements a portion of pkce.PKCERequestStorage. -func (s *OpenIDConnectStore) GetPKCERequestSession(ctx context.Context, signature string, session fosite.Session) (requester fosite.Requester, err error) { +func (s *Store) GetPKCERequestSession(ctx context.Context, signature string, session fosite.Session) (requester fosite.Requester, err error) {  	return s.loadSessionBySignature(ctx, storage.OAuth2SessionTypePKCEChallenge, signature, session)  }  // CreateOpenIDConnectSession creates an open id connect session for a given authorize code.  // This is relevant for explicit open id connect flow.  // This implements a portion of openid.OpenIDConnectRequestStorage. -func (s *OpenIDConnectStore) CreateOpenIDConnectSession(ctx context.Context, authorizeCode string, request fosite.Requester) (err error) { +func (s *Store) CreateOpenIDConnectSession(ctx context.Context, authorizeCode string, request fosite.Requester) (err error) {  	return s.saveSession(ctx, storage.OAuth2SessionTypeOpenIDConnect, authorizeCode, request)  }  // DeleteOpenIDConnectSession just implements the method required by fosite even though it's unused.  // This implements a portion of openid.OpenIDConnectRequestStorage. -func (s *OpenIDConnectStore) DeleteOpenIDConnectSession(ctx context.Context, authorizeCode string) (err error) { +func (s *Store) DeleteOpenIDConnectSession(ctx context.Context, authorizeCode string) (err error) {  	return s.revokeSessionBySignature(ctx, storage.OAuth2SessionTypeAccessToken, authorizeCode)  } @@ -262,12 +262,12 @@ func (s *OpenIDConnectStore) DeleteOpenIDConnectSession(ctx context.Context, aut  // - ErrNoSessionFound if no session was found  // - or an arbitrary error if an error occurred.  // This implements a portion of openid.OpenIDConnectRequestStorage. -func (s *OpenIDConnectStore) GetOpenIDConnectSession(ctx context.Context, authorizeCode string, request fosite.Requester) (r fosite.Requester, err error) { +func (s *Store) GetOpenIDConnectSession(ctx context.Context, authorizeCode string, request fosite.Requester) (r fosite.Requester, err error) {  	return s.loadSessionBySignature(ctx, storage.OAuth2SessionTypeOpenIDConnect, authorizeCode, request.GetSession())  }  // IsJWTUsed implements an interface required for RFC7523. -func (s *OpenIDConnectStore) IsJWTUsed(ctx context.Context, jti string) (used bool, err error) { +func (s *Store) IsJWTUsed(ctx context.Context, jti string) (used bool, err error) {  	if err = s.ClientAssertionJWTValid(ctx, jti); err != nil {  		return true, err  	} @@ -276,11 +276,11 @@ func (s *OpenIDConnectStore) IsJWTUsed(ctx context.Context, jti string) (used bo  }  // MarkJWTUsedForTime implements an interface required for rfc7523.RFC7523KeyStorage. -func (s *OpenIDConnectStore) MarkJWTUsedForTime(ctx context.Context, jti string, exp time.Time) (err error) { +func (s *Store) MarkJWTUsedForTime(ctx context.Context, jti string, exp time.Time) (err error) {  	return s.SetClientAssertionJWT(ctx, jti, exp)  } -func (s *OpenIDConnectStore) loadSessionBySignature(ctx context.Context, sessionType storage.OAuth2SessionType, signature string, session fosite.Session) (r fosite.Requester, err error) { +func (s *Store) loadSessionBySignature(ctx context.Context, sessionType storage.OAuth2SessionType, signature string, session fosite.Session) (r fosite.Requester, err error) {  	var (  		sessionModel *model.OAuth2Session  	) @@ -306,7 +306,7 @@ func (s *OpenIDConnectStore) loadSessionBySignature(ctx context.Context, session  	return r, nil  } -func (s *OpenIDConnectStore) saveSession(ctx context.Context, sessionType storage.OAuth2SessionType, signature string, r fosite.Requester) (err error) { +func (s *Store) saveSession(ctx context.Context, sessionType storage.OAuth2SessionType, signature string, r fosite.Requester) (err error) {  	var session *model.OAuth2Session  	if session, err = model.NewOAuth2SessionFromRequest(signature, r); err != nil { @@ -316,11 +316,11 @@ func (s *OpenIDConnectStore) saveSession(ctx context.Context, sessionType storag  	return s.provider.SaveOAuth2Session(ctx, sessionType, *session)  } -func (s *OpenIDConnectStore) revokeSessionBySignature(ctx context.Context, sessionType storage.OAuth2SessionType, signature string) (err error) { +func (s *Store) revokeSessionBySignature(ctx context.Context, sessionType storage.OAuth2SessionType, signature string) (err error) {  	return s.provider.RevokeOAuth2Session(ctx, sessionType, signature)  } -func (s *OpenIDConnectStore) revokeSessionByRequestID(ctx context.Context, sessionType storage.OAuth2SessionType, requestID string) (err error) { +func (s *Store) revokeSessionByRequestID(ctx context.Context, sessionType storage.OAuth2SessionType, requestID string) (err error) {  	if err = s.provider.RevokeOAuth2SessionByRequestID(ctx, sessionType, requestID); err != nil {  		switch {  		case errors.Is(err, sql.ErrNoRows): diff --git a/internal/oidc/store_test.go b/internal/oidc/store_test.go index 504af5e07..08cd7ae25 100644 --- a/internal/oidc/store_test.go +++ b/internal/oidc/store_test.go @@ -20,14 +20,14 @@ func TestOpenIDConnectStore_GetClientPolicy(t *testing.T) {  				ID:          "myclient",  				Description: "myclient desc",  				Policy:      "one_factor", -				Scopes:      []string{"openid", "profile"}, +				Scopes:      []string{ScopeOpenID, ScopeProfile},  				Secret:      "mysecret",  			},  			{  				ID:          "myotherclient",  				Description: "myclient desc",  				Policy:      "two_factor", -				Scopes:      []string{"openid", "profile"}, +				Scopes:      []string{ScopeOpenID, ScopeProfile},  				Secret:      "mysecret",  			},  		}, @@ -52,7 +52,7 @@ func TestOpenIDConnectStore_GetInternalClient(t *testing.T) {  				ID:          "myclient",  				Description: "myclient desc",  				Policy:      "one_factor", -				Scopes:      []string{"openid", "profile"}, +				Scopes:      []string{ScopeOpenID, ScopeProfile},  				Secret:      "mysecret",  			},  		}, @@ -73,7 +73,7 @@ func TestOpenIDConnectStore_GetInternalClient_ValidClient(t *testing.T) {  		ID:          "myclient",  		Description: "myclient desc",  		Policy:      "one_factor", -		Scopes:      []string{"openid", "profile"}, +		Scopes:      []string{ScopeOpenID, ScopeProfile},  		Secret:      "mysecret",  	} @@ -101,7 +101,7 @@ func TestOpenIDConnectStore_GetInternalClient_InvalidClient(t *testing.T) {  		ID:          "myclient",  		Description: "myclient desc",  		Policy:      "one_factor", -		Scopes:      []string{"openid", "profile"}, +		Scopes:      []string{ScopeOpenID, ScopeProfile},  		Secret:      "mysecret",  	} @@ -125,7 +125,7 @@ func TestOpenIDConnectStore_IsValidClientID(t *testing.T) {  				ID:          "myclient",  				Description: "myclient desc",  				Policy:      "one_factor", -				Scopes:      []string{"openid", "profile"}, +				Scopes:      []string{ScopeOpenID, ScopeProfile},  				Secret:      "mysecret",  			},  		}, diff --git a/internal/oidc/types.go b/internal/oidc/types.go index 411a6fd94..d48b023bf 100644 --- a/internal/oidc/types.go +++ b/internal/oidc/types.go @@ -1,13 +1,14 @@  package oidc  import ( +	"net/url"  	"time"  	"github.com/ory/fosite"  	"github.com/ory/fosite/handler/openid"  	"github.com/ory/fosite/token/jwt"  	"github.com/ory/herodot" -	"gopkg.in/square/go-jose.v2" +	jose "gopkg.in/square/go-jose.v2"  	"github.com/authelia/authelia/v4/internal/authorization"  	"github.com/authelia/authelia/v4/internal/model" @@ -31,7 +32,7 @@ func NewSession() (session *model.OpenIDSession) {  }  // NewSessionWithAuthorizeRequest uses details from an AuthorizeRequester to generate an OpenIDSession. -func NewSessionWithAuthorizeRequest(issuer, kid, username string, amr []string, extra map[string]any, +func NewSessionWithAuthorizeRequest(issuer *url.URL, kid, username string, amr []string, extra map[string]any,  	authTime time.Time, consent *model.OAuth2ConsentSession, requester fosite.AuthorizeRequester) (session *model.OpenIDSession) {  	if extra == nil {  		extra = map[string]any{} @@ -41,11 +42,11 @@ func NewSessionWithAuthorizeRequest(issuer, kid, username string, amr []string,  		DefaultSession: &openid.DefaultSession{  			Claims: &jwt.IDTokenClaims{  				Subject:     consent.Subject.UUID.String(), -				Issuer:      issuer, +				Issuer:      issuer.String(),  				AuthTime:    authTime,  				RequestedAt: consent.RequestedAt,  				IssuedAt:    time.Now(), -				Nonce:       requester.GetRequestForm().Get("nonce"), +				Nonce:       requester.GetRequestForm().Get(ClaimNonce),  				Audience:    requester.GetGrantedAudience(),  				Extra:       extra, @@ -53,7 +54,7 @@ func NewSessionWithAuthorizeRequest(issuer, kid, username string, amr []string,  			},  			Headers: &jwt.Headers{  				Extra: map[string]any{ -					"kid": kid, +					JWTHeaderKeyIdentifier: kid,  				},  			},  			Subject:  consent.Subject.UUID.String(), @@ -69,29 +70,29 @@ func NewSessionWithAuthorizeRequest(issuer, kid, username string, amr []string,  		session.Claims.Audience = append(session.Claims.Audience, requester.GetClient().GetID())  	} -	session.Claims.Add("azp", session.ClientID) -	session.Claims.Add("client_id", session.ClientID) +	session.Claims.Add(ClaimAuthorizedParty, session.ClientID) +	session.Claims.Add(ClaimClientIdentifier, session.ClientID)  	return session  }  // OpenIDConnectProvider for OpenID Connect.  type OpenIDConnectProvider struct { -	Fosite     fosite.OAuth2Provider -	Store      *OpenIDConnectStore -	KeyManager *KeyManager +	fosite.OAuth2Provider +	*herodot.JSONWriter +	*Store -	herodot *herodot.JSONWriter +	KeyManager *KeyManager  	discovery OpenIDConnectWellKnownConfiguration  } -// OpenIDConnectStore is Authelia's internal representation of the fosite.Storage interface. It maps the following +// Store is Authelia's internal representation of the fosite.Storage interface. It maps the following  // interfaces to the storage.Provider interface:  // fosite.Storage, fosite.ClientManager, storage.Transactional, oauth2.AuthorizeCodeStorage, oauth2.AccessTokenStorage,  // oauth2.RefreshTokenStorage, oauth2.TokenRevocationStorage, pkce.PKCERequestStorage,  // openid.OpenIDConnectRequestStorage, and partially implements rfc7523.RFC7523KeyStorage. -type OpenIDConnectStore struct { +type Store struct {  	provider storage.Provider  	clients  map[string]*Client  } @@ -115,7 +116,63 @@ type Client struct {  	Policy authorization.Level -	PreConfiguredConsentDuration *time.Duration +	Consent ClientConsent +} + +// NewClientConsent converts the schema.OpenIDConnectClientConsentConfig into a oidc.ClientConsent. +func NewClientConsent(mode string, duration *time.Duration) ClientConsent { +	switch mode { +	case ClientConsentModeImplicit.String(): +		return ClientConsent{Mode: ClientConsentModeImplicit} +	case ClientConsentModePreConfigured.String(): +		return ClientConsent{Mode: ClientConsentModePreConfigured, Duration: *duration} +	case ClientConsentModeExplicit.String(): +		return ClientConsent{Mode: ClientConsentModeExplicit} +	default: +		return ClientConsent{Mode: ClientConsentModeExplicit} +	} +} + +// ClientConsent is the consent configuration for a client. +type ClientConsent struct { +	Mode     ClientConsentMode +	Duration time.Duration +} + +// String returns the string representation of the ClientConsentMode. +func (c ClientConsent) String() string { +	return c.Mode.String() +} + +// ClientConsentMode represents the consent mode for a client. +type ClientConsentMode int + +const ( +	// ClientConsentModeExplicit means the client does not implicitly assume consent, and does not allow pre-configured +	// consent sessions. +	ClientConsentModeExplicit ClientConsentMode = iota + +	// ClientConsentModePreConfigured means the client does not implicitly assume consent, but does allow pre-configured +	// consent sessions. +	ClientConsentModePreConfigured + +	// ClientConsentModeImplicit means the client does implicitly assume consent, and does not allow pre-configured +	// consent sessions. +	ClientConsentModeImplicit +) + +// String returns the string representation of the ClientConsentMode. +func (c ClientConsentMode) String() string { +	switch c { +	case ClientConsentModeExplicit: +		return explicit +	case ClientConsentModeImplicit: +		return implicit +	case ClientConsentModePreConfigured: +		return preconfigured +	default: +		return "" +	}  }  // KeyManager keeps track of all of the active/inactive rsa keys and provides them to services requiring them. @@ -139,8 +196,8 @@ type ConsentGetResponseBody struct {  // ConsentPostRequestBody schema of the request body of the consent POST endpoint.  type ConsentPostRequestBody struct { +	ConsentID    string `json:"id"`  	ClientID     string `json:"client_id"` -	ConsentID    string `json:"consent_id"`  	Consent      bool   `json:"consent"`  	PreConfigure bool   `json:"pre_configure"`  } diff --git a/internal/oidc/types_test.go b/internal/oidc/types_test.go index 04e4aec8f..b84461a07 100644 --- a/internal/oidc/types_test.go +++ b/internal/oidc/types_test.go @@ -34,7 +34,7 @@ func TestNewSessionWithAuthorizeRequest(t *testing.T) {  	formValues := url.Values{} -	formValues.Set("nonce", "abc123xyzauthelia") +	formValues.Set(ClaimNonce, "abc123xyzauthelia")  	request := &fosite.AuthorizeRequest{  		Request: fosite.Request{ @@ -45,7 +45,7 @@ func TestNewSessionWithAuthorizeRequest(t *testing.T) {  	}  	extra := map[string]any{ -		"preferred_username": "john", +		ClaimPreferredUsername: "john",  	}  	requested := time.Unix(1647332518, 0) @@ -59,7 +59,7 @@ func TestNewSessionWithAuthorizeRequest(t *testing.T) {  		Subject:     uuid.NullUUID{UUID: subject, Valid: true},  	} -	session := NewSessionWithAuthorizeRequest(issuer, "primary", "john", amr, extra, authAt, consent, request) +	session := NewSessionWithAuthorizeRequest(MustParseRequestURI(issuer), "primary", "john", amr, extra, authAt, consent, request)  	require.NotNil(t, session)  	require.NotNil(t, session.Extra) @@ -80,21 +80,27 @@ func TestNewSessionWithAuthorizeRequest(t *testing.T) {  	assert.Equal(t, authAt, session.Claims.AuthTime)  	assert.Equal(t, requested, session.Claims.RequestedAt)  	assert.Equal(t, issuer, session.Claims.Issuer) -	assert.Equal(t, "john", session.Claims.Extra["preferred_username"]) +	assert.Equal(t, "john", session.Claims.Extra[ClaimPreferredUsername]) -	assert.Equal(t, "primary", session.Headers.Get("kid")) - -	require.Contains(t, session.Claims.Extra, "preferred_username") +	assert.Equal(t, "primary", session.Headers.Get(JWTHeaderKeyIdentifier))  	consent = &model.OAuth2ConsentSession{  		ChallengeID: uuid.New(),  		RequestedAt: requested,  	} -	session = NewSessionWithAuthorizeRequest(issuer, "primary", "john", nil, nil, authAt, consent, request) +	session = NewSessionWithAuthorizeRequest(MustParseRequestURI(issuer), "primary", "john", nil, nil, authAt, consent, request)  	require.NotNil(t, session)  	require.NotNil(t, session.Claims)  	assert.NotNil(t, session.Claims.Extra)  	assert.Nil(t, session.Claims.AuthenticationMethodsReferences)  } + +func MustParseRequestURI(input string) *url.URL { +	if requestURI, err := url.ParseRequestURI(input); err != nil { +		panic(err) +	} else { +		return requestURI +	} +} diff --git a/internal/server/handlers.go b/internal/server/handlers.go index b9c9c95da..b899ebe06 100644 --- a/internal/server/handlers.go +++ b/internal/server/handlers.go @@ -237,7 +237,7 @@ func handleRouter(config schema.Configuration, providers middlewares.Providers)  		r.GET("/debug/vars", expvarhandler.ExpvarHandler)  	} -	if providers.OpenIDConnect.Fosite != nil { +	if providers.OpenIDConnect != nil {  		middlewareOIDC := middlewares.NewBridgeBuilder(config, providers).WithPreMiddlewares(  			middlewares.SecurityHeaders, middlewares.SecurityHeadersCSPNone, middlewares.SecurityHeadersNoStore,  		).Build() @@ -247,14 +247,14 @@ func handleRouter(config schema.Configuration, providers middlewares.Providers)  		allowedOrigins := utils.StringSliceFromURLs(config.IdentityProviders.OIDC.CORS.AllowedOrigins) -		r.OPTIONS(oidc.WellKnownOpenIDConfigurationPath, policyCORSPublicGET.HandleOPTIONS) -		r.GET(oidc.WellKnownOpenIDConfigurationPath, policyCORSPublicGET.Middleware(middlewareOIDC(handlers.OpenIDConnectConfigurationWellKnownGET))) +		r.OPTIONS(oidc.EndpointPathWellKnownOpenIDConfiguration, policyCORSPublicGET.HandleOPTIONS) +		r.GET(oidc.EndpointPathWellKnownOpenIDConfiguration, policyCORSPublicGET.Middleware(middlewareOIDC(handlers.OpenIDConnectConfigurationWellKnownGET))) -		r.OPTIONS(oidc.WellKnownOAuthAuthorizationServerPath, policyCORSPublicGET.HandleOPTIONS) -		r.GET(oidc.WellKnownOAuthAuthorizationServerPath, policyCORSPublicGET.Middleware(middlewareOIDC(handlers.OAuthAuthorizationServerWellKnownGET))) +		r.OPTIONS(oidc.EndpointPathWellKnownOAuthAuthorizationServer, policyCORSPublicGET.HandleOPTIONS) +		r.GET(oidc.EndpointPathWellKnownOAuthAuthorizationServer, policyCORSPublicGET.Middleware(middlewareOIDC(handlers.OAuthAuthorizationServerWellKnownGET))) -		r.OPTIONS(oidc.JWKsPath, policyCORSPublicGET.HandleOPTIONS) -		r.GET(oidc.JWKsPath, policyCORSPublicGET.Middleware(middlewareAPI(handlers.JSONWebKeySetGET))) +		r.OPTIONS(oidc.EndpointPathJWKs, policyCORSPublicGET.HandleOPTIONS) +		r.GET(oidc.EndpointPathJWKs, policyCORSPublicGET.Middleware(middlewareAPI(handlers.JSONWebKeySetGET)))  		// TODO (james-d-elliott): Remove in GA. This is a legacy implementation of the above endpoint.  		r.OPTIONS("/api/oidc/jwks", policyCORSPublicGET.HandleOPTIONS) @@ -263,11 +263,11 @@ func handleRouter(config schema.Configuration, providers middlewares.Providers)  		policyCORSAuthorization := middlewares.NewCORSPolicyBuilder().  			WithAllowedMethods("OPTIONS", "GET").  			WithAllowedOrigins(allowedOrigins...). -			WithEnabled(utils.IsStringInSlice(oidc.AuthorizationEndpoint, config.IdentityProviders.OIDC.CORS.Endpoints)). +			WithEnabled(utils.IsStringInSlice(oidc.EndpointAuthorization, config.IdentityProviders.OIDC.CORS.Endpoints)).  			Build() -		r.OPTIONS(oidc.AuthorizationPath, policyCORSAuthorization.HandleOnlyOPTIONS) -		r.GET(oidc.AuthorizationPath, middlewareOIDC(middlewares.NewHTTPToAutheliaHandlerAdaptor(handlers.OpenIDConnectAuthorizationGET))) +		r.OPTIONS(oidc.EndpointPathAuthorization, policyCORSAuthorization.HandleOnlyOPTIONS) +		r.GET(oidc.EndpointPathAuthorization, middlewareOIDC(middlewares.NewHTTPToAutheliaHandlerAdaptor(handlers.OpenIDConnectAuthorizationGET)))  		// TODO (james-d-elliott): Remove in GA. This is a legacy endpoint.  		r.OPTIONS("/api/oidc/authorize", policyCORSAuthorization.HandleOnlyOPTIONS) @@ -277,32 +277,32 @@ func handleRouter(config schema.Configuration, providers middlewares.Providers)  			WithAllowCredentials(true).  			WithAllowedMethods("OPTIONS", "POST").  			WithAllowedOrigins(allowedOrigins...). -			WithEnabled(utils.IsStringInSlice(oidc.TokenEndpoint, config.IdentityProviders.OIDC.CORS.Endpoints)). +			WithEnabled(utils.IsStringInSlice(oidc.EndpointToken, config.IdentityProviders.OIDC.CORS.Endpoints)).  			Build() -		r.OPTIONS(oidc.TokenPath, policyCORSToken.HandleOPTIONS) -		r.POST(oidc.TokenPath, policyCORSToken.Middleware(middlewareOIDC(middlewares.NewHTTPToAutheliaHandlerAdaptor(handlers.OpenIDConnectTokenPOST)))) +		r.OPTIONS(oidc.EndpointPathToken, policyCORSToken.HandleOPTIONS) +		r.POST(oidc.EndpointPathToken, policyCORSToken.Middleware(middlewareOIDC(middlewares.NewHTTPToAutheliaHandlerAdaptor(handlers.OpenIDConnectTokenPOST))))  		policyCORSUserinfo := middlewares.NewCORSPolicyBuilder().  			WithAllowCredentials(true).  			WithAllowedMethods("OPTIONS", "GET", "POST").  			WithAllowedOrigins(allowedOrigins...). -			WithEnabled(utils.IsStringInSlice(oidc.UserinfoEndpoint, config.IdentityProviders.OIDC.CORS.Endpoints)). +			WithEnabled(utils.IsStringInSlice(oidc.EndpointUserinfo, config.IdentityProviders.OIDC.CORS.Endpoints)).  			Build() -		r.OPTIONS(oidc.UserinfoPath, policyCORSUserinfo.HandleOPTIONS) -		r.GET(oidc.UserinfoPath, policyCORSUserinfo.Middleware(middlewareOIDC(middlewares.NewHTTPToAutheliaHandlerAdaptor(handlers.OpenIDConnectUserinfo)))) -		r.POST(oidc.UserinfoPath, policyCORSUserinfo.Middleware(middlewareOIDC(middlewares.NewHTTPToAutheliaHandlerAdaptor(handlers.OpenIDConnectUserinfo)))) +		r.OPTIONS(oidc.EndpointPathUserinfo, policyCORSUserinfo.HandleOPTIONS) +		r.GET(oidc.EndpointPathUserinfo, policyCORSUserinfo.Middleware(middlewareOIDC(middlewares.NewHTTPToAutheliaHandlerAdaptor(handlers.OpenIDConnectUserinfo)))) +		r.POST(oidc.EndpointPathUserinfo, policyCORSUserinfo.Middleware(middlewareOIDC(middlewares.NewHTTPToAutheliaHandlerAdaptor(handlers.OpenIDConnectUserinfo))))  		policyCORSIntrospection := middlewares.NewCORSPolicyBuilder().  			WithAllowCredentials(true).  			WithAllowedMethods("OPTIONS", "POST").  			WithAllowedOrigins(allowedOrigins...). -			WithEnabled(utils.IsStringInSlice(oidc.IntrospectionEndpoint, config.IdentityProviders.OIDC.CORS.Endpoints)). +			WithEnabled(utils.IsStringInSlice(oidc.EndpointIntrospection, config.IdentityProviders.OIDC.CORS.Endpoints)).  			Build() -		r.OPTIONS(oidc.IntrospectionPath, policyCORSIntrospection.HandleOPTIONS) -		r.POST(oidc.IntrospectionPath, policyCORSIntrospection.Middleware(middlewareOIDC(middlewares.NewHTTPToAutheliaHandlerAdaptor(handlers.OAuthIntrospectionPOST)))) +		r.OPTIONS(oidc.EndpointPathIntrospection, policyCORSIntrospection.HandleOPTIONS) +		r.POST(oidc.EndpointPathIntrospection, policyCORSIntrospection.Middleware(middlewareOIDC(middlewares.NewHTTPToAutheliaHandlerAdaptor(handlers.OAuthIntrospectionPOST))))  		// TODO (james-d-elliott): Remove in GA. This is a legacy implementation of the above endpoint.  		r.OPTIONS("/api/oidc/introspect", policyCORSIntrospection.HandleOPTIONS) @@ -312,11 +312,11 @@ func handleRouter(config schema.Configuration, providers middlewares.Providers)  			WithAllowCredentials(true).  			WithAllowedMethods("OPTIONS", "POST").  			WithAllowedOrigins(allowedOrigins...). -			WithEnabled(utils.IsStringInSlice(oidc.RevocationEndpoint, config.IdentityProviders.OIDC.CORS.Endpoints)). +			WithEnabled(utils.IsStringInSlice(oidc.EndpointRevocation, config.IdentityProviders.OIDC.CORS.Endpoints)).  			Build() -		r.OPTIONS(oidc.RevocationPath, policyCORSRevocation.HandleOPTIONS) -		r.POST(oidc.RevocationPath, policyCORSRevocation.Middleware(middlewareOIDC(middlewares.NewHTTPToAutheliaHandlerAdaptor(handlers.OAuthRevocationPOST)))) +		r.OPTIONS(oidc.EndpointPathRevocation, policyCORSRevocation.HandleOPTIONS) +		r.POST(oidc.EndpointPathRevocation, policyCORSRevocation.Middleware(middlewareOIDC(middlewares.NewHTTPToAutheliaHandlerAdaptor(handlers.OAuthRevocationPOST))))  		// TODO (james-d-elliott): Remove in GA. This is a legacy implementation of the above endpoint.  		r.OPTIONS("/api/oidc/revoke", policyCORSRevocation.HandleOPTIONS) diff --git a/internal/session/user_session.go b/internal/session/user_session.go index d691ec695..88511eb5d 100644 --- a/internal/session/user_session.go +++ b/internal/session/user_session.go @@ -17,6 +17,11 @@ func NewDefaultUserSession() UserSession {  	}  } +// IsAnonymous returns true if the username is empty or the AuthenticationLevel is authentication.NotAuthenticated. +func (s *UserSession) IsAnonymous() bool { +	return s.Username == "" || s.AuthenticationLevel == authentication.NotAuthenticated +} +  // SetOneFactor sets the 1FA AMR's and expected property values for one factor authentication.  func (s *UserSession) SetOneFactor(now time.Time, details *authentication.UserDetails, keepMeLoggedIn bool) {  	s.FirstFactorAuthnTimestamp = now.Unix() diff --git a/internal/storage/const.go b/internal/storage/const.go index 4f451d8e3..dc249115f 100644 --- a/internal/storage/const.go +++ b/internal/storage/const.go @@ -13,13 +13,14 @@ const (  	tableUserPreferences      = "user_preferences"  	tableWebauthnDevices      = "webauthn_devices" -	tableOAuth2ConsentSession       = "oauth2_consent_session" -	tableOAuth2AuthorizeCodeSession = "oauth2_authorization_code_session" -	tableOAuth2AccessTokenSession   = "oauth2_access_token_session"  //nolint:gosec // This is not a hardcoded credential. -	tableOAuth2RefreshTokenSession  = "oauth2_refresh_token_session" //nolint:gosec // This is not a hardcoded credential. -	tableOAuth2PKCERequestSession   = "oauth2_pkce_request_session" -	tableOAuth2OpenIDConnectSession = "oauth2_openid_connect_session" -	tableOAuth2BlacklistedJTI       = "oauth2_blacklisted_jti" +	tableOAuth2ConsentSession          = "oauth2_consent_session" +	tableOAuth2ConsentPreConfiguration = "oauth2_consent_preconfiguration" +	tableOAuth2AuthorizeCodeSession    = "oauth2_authorization_code_session" +	tableOAuth2AccessTokenSession      = "oauth2_access_token_session"  //nolint:gosec // This is not a hardcoded credential. +	tableOAuth2RefreshTokenSession     = "oauth2_refresh_token_session" //nolint:gosec // This is not a hardcoded credential. +	tableOAuth2PKCERequestSession      = "oauth2_pkce_request_session" +	tableOAuth2OpenIDConnectSession    = "oauth2_openid_connect_session" +	tableOAuth2BlacklistedJTI          = "oauth2_blacklisted_jti"  	tableMigrations = "migrations"  	tableEncryption = "encryption" @@ -77,11 +78,6 @@ const (  )  const ( -	// This is the latest schema version for the purpose of tests. -	testLatestVersion = 5 -) - -const (  	// SchemaLatest represents the value expected for a "migrate to latest" migration. It's the maximum 32bit signed integer.  	SchemaLatest = 2147483647  ) diff --git a/internal/storage/migrations/V0004.OpenIDConenct.mysql.up.sql b/internal/storage/migrations/V0004.OpenIDConnect.mysql.up.sql index 4fc3adc70..4fc3adc70 100644 --- a/internal/storage/migrations/V0004.OpenIDConenct.mysql.up.sql +++ b/internal/storage/migrations/V0004.OpenIDConnect.mysql.up.sql diff --git a/internal/storage/migrations/V0004.OpenIDConenct.postgres.up.sql b/internal/storage/migrations/V0004.OpenIDConnect.postgres.up.sql index c5685dd21..c5685dd21 100644 --- a/internal/storage/migrations/V0004.OpenIDConenct.postgres.up.sql +++ b/internal/storage/migrations/V0004.OpenIDConnect.postgres.up.sql diff --git a/internal/storage/migrations/V0004.OpenIDConenct.sqlite.up.sql b/internal/storage/migrations/V0004.OpenIDConnect.sqlite.up.sql index 372fbeb58..372fbeb58 100644 --- a/internal/storage/migrations/V0004.OpenIDConenct.sqlite.up.sql +++ b/internal/storage/migrations/V0004.OpenIDConnect.sqlite.up.sql diff --git a/internal/storage/migrations/V0006.ConsentPreConfiguration.mysql.down.sql b/internal/storage/migrations/V0006.ConsentPreConfiguration.mysql.down.sql new file mode 100644 index 000000000..e06f19d81 --- /dev/null +++ b/internal/storage/migrations/V0006.ConsentPreConfiguration.mysql.down.sql @@ -0,0 +1,175 @@ +DROP TABLE oauth2_authorization_code_session; +DROP TABLE oauth2_access_token_session; +DROP TABLE oauth2_refresh_token_session; +DROP TABLE oauth2_pkce_request_session; +DROP TABLE oauth2_openid_connect_session; +DROP TABLE oauth2_consent_session; +DROP TABLE oauth2_consent_preconfiguration; + +CREATE TABLE oauth2_consent_session ( +    id INTEGER AUTO_INCREMENT, +    challenge_id CHAR(36) NOT NULL, +    client_id VARCHAR(255) NOT NULL, +    subject CHAR(36) NULL DEFAULT NULL, +    authorized BOOLEAN NOT NULL DEFAULT FALSE, +    granted BOOLEAN NOT NULL DEFAULT FALSE, +    requested_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, +    responded_at TIMESTAMP NULL DEFAULT NULL, +    expires_at TIMESTAMP NULL DEFAULT NULL, +    form_data TEXT NOT NULL, +    requested_scopes TEXT NOT NULL, +    granted_scopes TEXT NOT NULL, +    requested_audience TEXT NULL, +    granted_audience TEXT NULL, +    PRIMARY KEY (id), +    CONSTRAINT oauth2_consent_session_subject_fkey +        FOREIGN KEY (subject) +            REFERENCES user_opaque_identifier(identifier) ON UPDATE RESTRICT ON DELETE RESTRICT +); + +CREATE UNIQUE INDEX oauth2_consent_session_challenge_id_key ON oauth2_consent_session (challenge_id); + +CREATE TABLE oauth2_authorization_code_session ( +    id INTEGER AUTO_INCREMENT, +    challenge_id CHAR(36) NOT NULL, +    request_id VARCHAR(40) NOT NULL, +    client_id VARCHAR(255) NOT NULL, +    signature VARCHAR(255) NOT NULL, +    subject CHAR(36) NOT NULL, +    requested_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, +    requested_scopes TEXT NOT NULL, +    granted_scopes TEXT NOT NULL, +    requested_audience TEXT NULL, +    granted_audience TEXT NULL, +    active BOOLEAN NOT NULL DEFAULT FALSE, +    revoked BOOLEAN NOT NULL DEFAULT FALSE, +    form_data TEXT NOT NULL, +    session_data BLOB NOT NULL, +    PRIMARY KEY (id), +    CONSTRAINT oauth2_authorization_code_session_challenge_id_fkey +        FOREIGN KEY (challenge_id) +            REFERENCES oauth2_consent_session(challenge_id) ON UPDATE CASCADE ON DELETE CASCADE, +    CONSTRAINT oauth2_authorization_code_session_subject_fkey +        FOREIGN KEY (subject) +            REFERENCES user_opaque_identifier(identifier) ON UPDATE RESTRICT ON DELETE RESTRICT +); + +CREATE INDEX oauth2_authorization_code_session_request_id_idx ON oauth2_authorization_code_session (request_id); +CREATE INDEX oauth2_authorization_code_session_client_id_idx ON oauth2_authorization_code_session (client_id); +CREATE INDEX oauth2_authorization_code_session_client_id_subject_idx ON oauth2_authorization_code_session (client_id, subject); + +CREATE TABLE oauth2_access_token_session ( +    id INTEGER AUTO_INCREMENT, +    challenge_id CHAR(36) NOT NULL, +    request_id VARCHAR(40) NOT NULL, +    client_id VARCHAR(255) NOT NULL, +    signature VARCHAR(255) NOT NULL, +    subject CHAR(36) NOT NULL, +    requested_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, +    requested_scopes TEXT NOT NULL, +    granted_scopes TEXT NOT NULL, +    requested_audience TEXT NULL, +    granted_audience TEXT NULL, +    active BOOLEAN NOT NULL DEFAULT FALSE, +    revoked BOOLEAN NOT NULL DEFAULT FALSE, +    form_data TEXT NOT NULL, +    session_data BLOB NOT NULL, +    PRIMARY KEY (id), +    CONSTRAINT oauth2_access_token_session_challenge_id_fkey +        FOREIGN KEY(challenge_id) +            REFERENCES oauth2_consent_session(challenge_id) ON UPDATE CASCADE ON DELETE CASCADE, +    CONSTRAINT oauth2_access_token_session_subject_fkey +        FOREIGN KEY(subject) +            REFERENCES user_opaque_identifier(identifier) ON UPDATE RESTRICT ON DELETE RESTRICT +); + +CREATE INDEX oauth2_access_token_session_request_id_idx ON oauth2_access_token_session (request_id); +CREATE INDEX oauth2_access_token_session_client_id_idx ON oauth2_access_token_session (client_id); +CREATE INDEX oauth2_access_token_session_client_id_subject_idx ON oauth2_access_token_session (client_id, subject); + +CREATE TABLE oauth2_refresh_token_session ( +    id INTEGER AUTO_INCREMENT, +    challenge_id CHAR(36) NOT NULL, +    request_id VARCHAR(40) NOT NULL, +    client_id VARCHAR(255) NOT NULL, +    signature VARCHAR(255) NOT NULL, +    subject CHAR(36) NOT NULL, +    requested_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, +    requested_scopes TEXT NOT NULL, +    granted_scopes TEXT NOT NULL, +    requested_audience TEXT NULL, +    granted_audience TEXT NULL, +    active BOOLEAN NOT NULL DEFAULT FALSE, +    revoked BOOLEAN NOT NULL DEFAULT FALSE, +    form_data TEXT NOT NULL, +    session_data BLOB NOT NULL, +    PRIMARY KEY (id), +    CONSTRAINT oauth2_refresh_token_session_challenge_id_fkey +        FOREIGN KEY(challenge_id) +            REFERENCES oauth2_consent_session(challenge_id) ON UPDATE CASCADE ON DELETE CASCADE, +    CONSTRAINT oauth2_refresh_token_session_subject_fkey +        FOREIGN KEY(subject) +            REFERENCES user_opaque_identifier(identifier) ON UPDATE RESTRICT ON DELETE RESTRICT +); + +CREATE INDEX oauth2_refresh_token_session_request_id_idx ON oauth2_refresh_token_session (request_id); +CREATE INDEX oauth2_refresh_token_session_client_id_idx ON oauth2_refresh_token_session (client_id); +CREATE INDEX oauth2_refresh_token_session_client_id_subject_idx ON oauth2_refresh_token_session (client_id, subject); + +CREATE TABLE oauth2_pkce_request_session ( +    id INTEGER AUTO_INCREMENT, +    challenge_id CHAR(36) NOT NULL, +    request_id VARCHAR(40) NOT NULL, +    client_id VARCHAR(255) NOT NULL, +    signature VARCHAR(255) NOT NULL, +    subject CHAR(36) NOT NULL, +    requested_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, +    requested_scopes TEXT NOT NULL, +    granted_scopes TEXT NOT NULL, +    requested_audience TEXT NULL, +    granted_audience TEXT NULL, +    active BOOLEAN NOT NULL DEFAULT FALSE, +    revoked BOOLEAN NOT NULL DEFAULT FALSE, +    form_data TEXT NOT NULL, +    session_data BLOB NOT NULL, +    PRIMARY KEY (id), +    CONSTRAINT oauth2_pkce_request_session_challenge_id_fkey +        FOREIGN KEY(challenge_id) +            REFERENCES oauth2_consent_session(challenge_id) ON UPDATE CASCADE ON DELETE CASCADE, +    CONSTRAINT oauth2_pkce_request_session_subject_fkey +        FOREIGN KEY(subject) +            REFERENCES user_opaque_identifier(identifier) ON UPDATE RESTRICT ON DELETE RESTRICT +); + +CREATE INDEX oauth2_pkce_request_session_request_id_idx ON oauth2_pkce_request_session (request_id); +CREATE INDEX oauth2_pkce_request_session_client_id_idx ON oauth2_pkce_request_session (client_id); +CREATE INDEX oauth2_pkce_request_session_client_id_subject_idx ON oauth2_pkce_request_session (client_id, subject); + +CREATE TABLE oauth2_openid_connect_session ( +    id INTEGER AUTO_INCREMENT, +    challenge_id CHAR(36) NOT NULL, +    request_id VARCHAR(40) NOT NULL, +    client_id VARCHAR(255) NOT NULL, +    signature VARCHAR(255) NOT NULL, +    subject CHAR(36) NOT NULL, +    requested_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, +    requested_scopes TEXT NOT NULL, +    granted_scopes TEXT NOT NULL, +    requested_audience TEXT NULL, +    granted_audience TEXT NULL, +    active BOOLEAN NOT NULL DEFAULT FALSE, +    revoked BOOLEAN NOT NULL DEFAULT FALSE, +    form_data TEXT NOT NULL, +    session_data BLOB NOT NULL, +    PRIMARY KEY (id), +    CONSTRAINT oauth2_openid_connect_session_challenge_id_fkey +        FOREIGN KEY(challenge_id) +            REFERENCES oauth2_consent_session(challenge_id) ON UPDATE CASCADE ON DELETE CASCADE, +    CONSTRAINT oauth2_openid_connect_session_subject_fkey +        FOREIGN KEY(subject) +            REFERENCES user_opaque_identifier(identifier) ON UPDATE RESTRICT ON DELETE RESTRICT +); + +CREATE INDEX oauth2_openid_connect_session_request_id_idx ON oauth2_openid_connect_session (request_id); +CREATE INDEX oauth2_openid_connect_session_client_id_idx ON oauth2_openid_connect_session (client_id); +CREATE INDEX oauth2_openid_connect_session_client_id_subject_idx ON oauth2_openid_connect_session (client_id, subject); diff --git a/internal/storage/migrations/V0006.ConsentPreConfiguration.mysql.up.sql b/internal/storage/migrations/V0006.ConsentPreConfiguration.mysql.up.sql new file mode 100644 index 000000000..693b0ed91 --- /dev/null +++ b/internal/storage/migrations/V0006.ConsentPreConfiguration.mysql.up.sql @@ -0,0 +1,198 @@ +CREATE TABLE oauth2_consent_preconfiguration ( +    id INTEGER AUTO_INCREMENT, +    client_id VARCHAR(255) NOT NULL, +    subject CHAR(36) NOT NULL, +	created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, +	expires_at TIMESTAMP NULL DEFAULT NULL, +	revoked BOOLEAN NOT NULL DEFAULT FALSE, +    scopes TEXT NOT NULL, +    audience TEXT NULL, +    PRIMARY KEY (id), +    CONSTRAINT oauth2_consent_preconfiguration_subjct_fkey +        FOREIGN KEY(subject) +            REFERENCES user_opaque_identifier(identifier) ON UPDATE RESTRICT ON DELETE RESTRICT +); + +INSERT INTO oauth2_consent_preconfiguration (client_id, subject, created_at, expires_at, scopes, audience) +SELECT client_id, subject, responded_at, expires_at, granted_scopes, granted_audience +FROM oauth2_consent_session +WHERE expires_at IS NOT NULL AND responded_at IS NOT NULL +ORDER BY responded_at; + +DROP TABLE oauth2_authorization_code_session; +DROP TABLE oauth2_access_token_session; +DROP TABLE oauth2_refresh_token_session; +DROP TABLE oauth2_pkce_request_session; +DROP TABLE oauth2_openid_connect_session; +DROP TABLE oauth2_consent_session; + +CREATE TABLE oauth2_consent_session ( +    id INTEGER AUTO_INCREMENT, +    challenge_id CHAR(36) NOT NULL, +    client_id VARCHAR(255) NOT NULL, +    subject CHAR(36) NOT NULL, +    authorized BOOLEAN NOT NULL DEFAULT FALSE, +    granted BOOLEAN NOT NULL DEFAULT FALSE, +    requested_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, +    responded_at TIMESTAMP NULL DEFAULT NULL, +    form_data TEXT NOT NULL, +    requested_scopes TEXT NOT NULL, +    granted_scopes TEXT NOT NULL, +    requested_audience TEXT NULL, +    granted_audience TEXT NULL, +    preconfiguration INTEGER NULL DEFAULT NULL, +    PRIMARY KEY (id), +    CONSTRAINT oauth2_consent_session_subject_fkey +        FOREIGN KEY (subject) +            REFERENCES user_opaque_identifier(identifier) ON UPDATE RESTRICT ON DELETE RESTRICT, +    CONSTRAINT oauth2_consent_session_preconfiguration_fkey +        FOREIGN KEY (preconfiguration) +            REFERENCES oauth2_consent_preconfiguration(id) ON UPDATE CASCADE ON DELETE CASCADE +); + +CREATE UNIQUE INDEX oauth2_consent_session_challenge_id_key ON oauth2_consent_session (challenge_id); + +CREATE TABLE oauth2_authorization_code_session ( +    id INTEGER AUTO_INCREMENT, +    challenge_id CHAR(36) NOT NULL, +    request_id VARCHAR(40) NOT NULL, +    client_id VARCHAR(255) NOT NULL, +    signature VARCHAR(255) NOT NULL, +    subject CHAR(36) NOT NULL, +    requested_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, +    requested_scopes TEXT NOT NULL, +    granted_scopes TEXT NOT NULL, +    requested_audience TEXT NULL, +    granted_audience TEXT NULL, +    active BOOLEAN NOT NULL DEFAULT FALSE, +    revoked BOOLEAN NOT NULL DEFAULT FALSE, +    form_data TEXT NOT NULL, +    session_data BLOB NOT NULL, +    PRIMARY KEY (id), +    CONSTRAINT oauth2_authorization_code_session_challenge_id_fkey +        FOREIGN KEY (challenge_id) +            REFERENCES oauth2_consent_session(challenge_id) ON UPDATE CASCADE ON DELETE CASCADE, +    CONSTRAINT oauth2_authorization_code_session_subject_fkey +        FOREIGN KEY (subject) +            REFERENCES user_opaque_identifier(identifier) ON UPDATE RESTRICT ON DELETE RESTRICT +); + +CREATE INDEX oauth2_authorization_code_session_request_id_idx ON oauth2_authorization_code_session (request_id); +CREATE INDEX oauth2_authorization_code_session_client_id_idx ON oauth2_authorization_code_session (client_id); +CREATE INDEX oauth2_authorization_code_session_client_id_subject_idx ON oauth2_authorization_code_session (client_id, subject); + +CREATE TABLE oauth2_access_token_session ( +    id INTEGER AUTO_INCREMENT, +    challenge_id CHAR(36) NOT NULL, +    request_id VARCHAR(40) NOT NULL, +    client_id VARCHAR(255) NOT NULL, +    signature VARCHAR(255) NOT NULL, +    subject CHAR(36) NOT NULL, +    requested_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, +    requested_scopes TEXT NOT NULL, +    granted_scopes TEXT NOT NULL, +    requested_audience TEXT NULL, +    granted_audience TEXT NULL, +    active BOOLEAN NOT NULL DEFAULT FALSE, +    revoked BOOLEAN NOT NULL DEFAULT FALSE, +    form_data TEXT NOT NULL, +    session_data BLOB NOT NULL, +    PRIMARY KEY (id), +    CONSTRAINT oauth2_access_token_session_challenge_id_fkey +        FOREIGN KEY(challenge_id) +            REFERENCES oauth2_consent_session(challenge_id) ON UPDATE CASCADE ON DELETE CASCADE, +    CONSTRAINT oauth2_access_token_session_subject_fkey +        FOREIGN KEY(subject) +            REFERENCES user_opaque_identifier(identifier) ON UPDATE RESTRICT ON DELETE RESTRICT +); + +CREATE INDEX oauth2_access_token_session_request_id_idx ON oauth2_access_token_session (request_id); +CREATE INDEX oauth2_access_token_session_client_id_idx ON oauth2_access_token_session (client_id); +CREATE INDEX oauth2_access_token_session_client_id_subject_idx ON oauth2_access_token_session (client_id, subject); + +CREATE TABLE oauth2_refresh_token_session ( +    id INTEGER AUTO_INCREMENT, +    challenge_id CHAR(36) NOT NULL, +    request_id VARCHAR(40) NOT NULL, +    client_id VARCHAR(255) NOT NULL, +    signature VARCHAR(255) NOT NULL, +    subject CHAR(36) NOT NULL, +    requested_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, +    requested_scopes TEXT NOT NULL, +    granted_scopes TEXT NOT NULL, +    requested_audience TEXT NULL, +    granted_audience TEXT NULL, +    active BOOLEAN NOT NULL DEFAULT FALSE, +    revoked BOOLEAN NOT NULL DEFAULT FALSE, +    form_data TEXT NOT NULL, +    session_data BLOB NOT NULL, +    PRIMARY KEY (id), +    CONSTRAINT oauth2_refresh_token_session_challenge_id_fkey +        FOREIGN KEY(challenge_id) +            REFERENCES oauth2_consent_session(challenge_id) ON UPDATE CASCADE ON DELETE CASCADE, +    CONSTRAINT oauth2_refresh_token_session_subject_fkey +        FOREIGN KEY(subject) +            REFERENCES user_opaque_identifier(identifier) ON UPDATE RESTRICT ON DELETE RESTRICT +); + +CREATE INDEX oauth2_refresh_token_session_request_id_idx ON oauth2_refresh_token_session (request_id); +CREATE INDEX oauth2_refresh_token_session_client_id_idx ON oauth2_refresh_token_session (client_id); +CREATE INDEX oauth2_refresh_token_session_client_id_subject_idx ON oauth2_refresh_token_session (client_id, subject); + +CREATE TABLE oauth2_pkce_request_session ( +    id INTEGER AUTO_INCREMENT, +    challenge_id CHAR(36) NOT NULL, +    request_id VARCHAR(40) NOT NULL, +    client_id VARCHAR(255) NOT NULL, +    signature VARCHAR(255) NOT NULL, +    subject CHAR(36) NOT NULL, +    requested_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, +    requested_scopes TEXT NOT NULL, +    granted_scopes TEXT NOT NULL, +    requested_audience TEXT NULL, +    granted_audience TEXT NULL, +    active BOOLEAN NOT NULL DEFAULT FALSE, +    revoked BOOLEAN NOT NULL DEFAULT FALSE, +    form_data TEXT NOT NULL, +    session_data BLOB NOT NULL, +    PRIMARY KEY (id), +    CONSTRAINT oauth2_pkce_request_session_challenge_id_fkey +        FOREIGN KEY(challenge_id) +            REFERENCES oauth2_consent_session(challenge_id) ON UPDATE CASCADE ON DELETE CASCADE, +    CONSTRAINT oauth2_pkce_request_session_subject_fkey +        FOREIGN KEY(subject) +            REFERENCES user_opaque_identifier(identifier) ON UPDATE RESTRICT ON DELETE RESTRICT +); + +CREATE INDEX oauth2_pkce_request_session_request_id_idx ON oauth2_pkce_request_session (request_id); +CREATE INDEX oauth2_pkce_request_session_client_id_idx ON oauth2_pkce_request_session (client_id); +CREATE INDEX oauth2_pkce_request_session_client_id_subject_idx ON oauth2_pkce_request_session (client_id, subject); + +CREATE TABLE oauth2_openid_connect_session ( +    id INTEGER AUTO_INCREMENT, +    challenge_id CHAR(36) NOT NULL, +    request_id VARCHAR(40) NOT NULL, +    client_id VARCHAR(255) NOT NULL, +    signature VARCHAR(255) NOT NULL, +    subject CHAR(36) NOT NULL, +    requested_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, +    requested_scopes TEXT NOT NULL, +    granted_scopes TEXT NOT NULL, +    requested_audience TEXT NULL, +    granted_audience TEXT NULL, +    active BOOLEAN NOT NULL DEFAULT FALSE, +    revoked BOOLEAN NOT NULL DEFAULT FALSE, +    form_data TEXT NOT NULL, +    session_data BLOB NOT NULL, +    PRIMARY KEY (id), +    CONSTRAINT oauth2_openid_connect_session_challenge_id_fkey +        FOREIGN KEY(challenge_id) +            REFERENCES oauth2_consent_session(challenge_id) ON UPDATE CASCADE ON DELETE CASCADE, +    CONSTRAINT oauth2_openid_connect_session_subject_fkey +        FOREIGN KEY(subject) +            REFERENCES user_opaque_identifier(identifier) ON UPDATE RESTRICT ON DELETE RESTRICT +); + +CREATE INDEX oauth2_openid_connect_session_request_id_idx ON oauth2_openid_connect_session (request_id); +CREATE INDEX oauth2_openid_connect_session_client_id_idx ON oauth2_openid_connect_session (client_id); +CREATE INDEX oauth2_openid_connect_session_client_id_subject_idx ON oauth2_openid_connect_session (client_id, subject); diff --git a/internal/storage/migrations/V0006.ConsentPreConfiguration.postgres.down.sql b/internal/storage/migrations/V0006.ConsentPreConfiguration.postgres.down.sql new file mode 100644 index 000000000..a180af297 --- /dev/null +++ b/internal/storage/migrations/V0006.ConsentPreConfiguration.postgres.down.sql @@ -0,0 +1,175 @@ +DROP TABLE oauth2_authorization_code_session; +DROP TABLE oauth2_access_token_session; +DROP TABLE oauth2_refresh_token_session; +DROP TABLE oauth2_pkce_request_session; +DROP TABLE oauth2_openid_connect_session; +DROP TABLE oauth2_consent_session; +DROP TABLE oauth2_consent_preconfiguration; + +CREATE TABLE oauth2_consent_session ( +    id SERIAL, +    challenge_id CHAR(36) NOT NULL, +    client_id VARCHAR(255) NOT NULL, +    subject CHAR(36) NULL DEFAULT NULL, +    authorized BOOLEAN NOT NULL DEFAULT FALSE, +    granted BOOLEAN NOT NULL DEFAULT FALSE, +    requested_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, +    responded_at TIMESTAMP WITH TIME ZONE NULL DEFAULT NULL, +    expires_at TIMESTAMP WITH TIME ZONE NULL DEFAULT NULL, +    form_data TEXT NOT NULL, +    requested_scopes TEXT NOT NULL, +    granted_scopes TEXT NOT NULL, +    requested_audience TEXT NULL DEFAULT '', +    granted_audience TEXT NULL DEFAULT '', +    PRIMARY KEY (id), +    CONSTRAINT oauth2_consent_session_subject_fkey +        FOREIGN KEY(subject) +            REFERENCES user_opaque_identifier(identifier) ON UPDATE RESTRICT ON DELETE RESTRICT +); + +CREATE UNIQUE INDEX oauth2_consent_session_challenge_id_key ON oauth2_consent_session (challenge_id); + +CREATE TABLE oauth2_authorization_code_session ( +    id SERIAL, +    challenge_id CHAR(36) NOT NULL, +    request_id VARCHAR(40) NOT NULL, +    client_id VARCHAR(255) NOT NULL, +    signature VARCHAR(255) NOT NULL, +    subject CHAR(36), +    requested_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, +    requested_scopes TEXT NOT NULL, +    granted_scopes TEXT NOT NULL, +    requested_audience TEXT NULL DEFAULT '', +    granted_audience TEXT NULL DEFAULT '', +    active BOOLEAN NOT NULL DEFAULT FALSE, +    revoked BOOLEAN NOT NULL DEFAULT FALSE, +    form_data TEXT NOT NULL, +    session_data BYTEA NOT NULL, +    PRIMARY KEY (id), +    CONSTRAINT oauth2_authorization_code_session_challenge_id_fkey +        FOREIGN KEY(challenge_id) +            REFERENCES oauth2_consent_session(challenge_id) ON UPDATE CASCADE ON DELETE CASCADE, +    CONSTRAINT oauth2_authorization_code_session_subject_fkey +        FOREIGN KEY(subject) +            REFERENCES user_opaque_identifier(identifier) ON UPDATE RESTRICT ON DELETE RESTRICT +); + +CREATE INDEX oauth2_authorization_code_session_request_id_idx ON oauth2_authorization_code_session (request_id); +CREATE INDEX oauth2_authorization_code_session_client_id_idx ON oauth2_authorization_code_session (client_id); +CREATE INDEX oauth2_authorization_code_session_client_id_subject_idx ON oauth2_authorization_code_session (client_id, subject); + +CREATE TABLE oauth2_access_token_session ( +    id SERIAL, +    challenge_id CHAR(36) NOT NULL, +    request_id VARCHAR(40) NOT NULL, +    client_id VARCHAR(255) NOT NULL, +    signature VARCHAR(255) NOT NULL, +    subject CHAR(36) NOT NULL, +    requested_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, +    requested_scopes TEXT NOT NULL, +    granted_scopes TEXT NOT NULL, +    requested_audience TEXT NULL DEFAULT '', +    granted_audience TEXT NULL DEFAULT '', +    active BOOLEAN NOT NULL DEFAULT FALSE, +    revoked BOOLEAN NOT NULL DEFAULT FALSE, +    form_data TEXT NOT NULL, +    session_data BYTEA NOT NULL, +    PRIMARY KEY (id), +    CONSTRAINT oauth2_access_token_session_challenge_id_fkey +        FOREIGN KEY(challenge_id) +            REFERENCES oauth2_consent_session(challenge_id) ON UPDATE CASCADE ON DELETE CASCADE, +    CONSTRAINT oauth2_access_token_session_subject_fkey +        FOREIGN KEY(subject) +            REFERENCES user_opaque_identifier(identifier) ON UPDATE RESTRICT ON DELETE RESTRICT +); + +CREATE INDEX oauth2_access_token_session_request_id_idx ON oauth2_access_token_session (request_id); +CREATE INDEX oauth2_access_token_session_client_id_idx ON oauth2_access_token_session (client_id); +CREATE INDEX oauth2_access_token_session_client_id_subject_idx ON oauth2_access_token_session (client_id, subject); + +CREATE TABLE oauth2_refresh_token_session ( +    id SERIAL, +    challenge_id CHAR(36) NOT NULL, +    request_id VARCHAR(40) NOT NULL, +    client_id VARCHAR(255) NOT NULL, +    signature VARCHAR(255) NOT NULL, +    subject CHAR(36) NOT NULL, +    requested_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, +    requested_scopes TEXT NOT NULL, +    granted_scopes TEXT NOT NULL, +    requested_audience TEXT NULL DEFAULT '', +    granted_audience TEXT NULL DEFAULT '', +    active BOOLEAN NOT NULL DEFAULT FALSE, +    revoked BOOLEAN NOT NULL DEFAULT FALSE, +    form_data TEXT NOT NULL, +    session_data BYTEA NOT NULL, +    PRIMARY KEY (id), +    CONSTRAINT oauth2_refresh_token_session_challenge_id_fkey +        FOREIGN KEY(challenge_id) +            REFERENCES oauth2_consent_session(challenge_id) ON UPDATE CASCADE ON DELETE CASCADE, +    CONSTRAINT oauth2_refresh_token_session_subject_fkey +        FOREIGN KEY(subject) +            REFERENCES user_opaque_identifier(identifier) ON UPDATE RESTRICT ON DELETE RESTRICT +); + +CREATE INDEX oauth2_refresh_token_session_request_id_idx ON oauth2_refresh_token_session (request_id); +CREATE INDEX oauth2_refresh_token_session_client_id_idx ON oauth2_refresh_token_session (client_id); +CREATE INDEX oauth2_refresh_token_session_client_id_subject_idx ON oauth2_refresh_token_session (client_id, subject); + +CREATE TABLE oauth2_pkce_request_session ( +    id SERIAL, +    challenge_id CHAR(36) NOT NULL, +    request_id VARCHAR(40) NOT NULL, +    client_id VARCHAR(255) NOT NULL, +    signature VARCHAR(255) NOT NULL, +    subject CHAR(36) NOT NULL, +    requested_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, +    requested_scopes TEXT NOT NULL, +    granted_scopes TEXT NOT NULL, +    requested_audience TEXT NULL DEFAULT '', +    granted_audience TEXT NULL DEFAULT '', +    active BOOLEAN NOT NULL DEFAULT FALSE, +    revoked BOOLEAN NOT NULL DEFAULT FALSE, +    form_data TEXT NOT NULL, +    session_data BYTEA NOT NULL, +    PRIMARY KEY (id), +    CONSTRAINT oauth2_pkce_request_session_challenge_id_fkey +        FOREIGN KEY(challenge_id) +            REFERENCES oauth2_consent_session(challenge_id) ON UPDATE CASCADE ON DELETE CASCADE, +    CONSTRAINT oauth2_pkce_request_session_subject_fkey +        FOREIGN KEY(subject) +            REFERENCES user_opaque_identifier(identifier) ON UPDATE RESTRICT ON DELETE RESTRICT +); + +CREATE INDEX oauth2_pkce_request_session_request_id_idx ON oauth2_pkce_request_session (request_id); +CREATE INDEX oauth2_pkce_request_session_client_id_idx ON oauth2_pkce_request_session (client_id); +CREATE INDEX oauth2_pkce_request_session_client_id_subject_idx ON oauth2_pkce_request_session (client_id, subject); + +CREATE TABLE oauth2_openid_connect_session ( +    id SERIAL, +    challenge_id CHAR(36) NOT NULL, +    request_id VARCHAR(40) NOT NULL, +    client_id VARCHAR(255) NOT NULL, +    signature VARCHAR(255) NOT NULL, +    subject CHAR(36) NOT NULL, +    requested_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, +    requested_scopes TEXT NOT NULL, +    granted_scopes TEXT NOT NULL, +    requested_audience TEXT NULL DEFAULT '', +    granted_audience TEXT NULL DEFAULT '', +    active BOOLEAN NOT NULL DEFAULT FALSE, +    revoked BOOLEAN NOT NULL DEFAULT FALSE, +    form_data TEXT NOT NULL, +    session_data BYTEA NOT NULL, +    PRIMARY KEY (id), +    CONSTRAINT oauth2_openid_connect_session_challenge_id_fkey +        FOREIGN KEY(challenge_id) +            REFERENCES oauth2_consent_session(challenge_id) ON UPDATE CASCADE ON DELETE CASCADE, +    CONSTRAINT oauth2_openid_connect_session_subject_fkey +        FOREIGN KEY(subject) +            REFERENCES user_opaque_identifier(identifier) ON UPDATE RESTRICT ON DELETE RESTRICT +); + +CREATE INDEX oauth2_openid_connect_session_request_id_idx ON oauth2_openid_connect_session (request_id); +CREATE INDEX oauth2_openid_connect_session_client_id_idx ON oauth2_openid_connect_session (client_id); +CREATE INDEX oauth2_openid_connect_session_client_id_subject_idx ON oauth2_openid_connect_session (client_id, subject); diff --git a/internal/storage/migrations/V0006.ConsentPreConfiguration.postgres.up.sql b/internal/storage/migrations/V0006.ConsentPreConfiguration.postgres.up.sql new file mode 100644 index 000000000..71a98616f --- /dev/null +++ b/internal/storage/migrations/V0006.ConsentPreConfiguration.postgres.up.sql @@ -0,0 +1,198 @@ +CREATE TABLE oauth2_consent_preconfiguration ( +    id SERIAL, +    client_id VARCHAR(255) NOT NULL, +    subject CHAR(36) NOT NULL, +	created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, +    expires_at TIMESTAMP WITH TIME ZONE NULL DEFAULT NULL, +	revoked BOOLEAN NOT NULL DEFAULT FALSE, +    scopes TEXT NOT NULL, +    audience TEXT NULL, +    PRIMARY KEY (id), +    CONSTRAINT oauth2_consent_preconfiguration_subjct_fkey +        FOREIGN KEY(subject) +            REFERENCES user_opaque_identifier(identifier) ON UPDATE RESTRICT ON DELETE RESTRICT +); + +INSERT INTO oauth2_consent_preconfiguration (client_id, subject, created_at, expires_at, scopes, audience) +SELECT client_id, subject, responded_at, expires_at, granted_scopes, granted_audience +FROM oauth2_consent_session +WHERE expires_at IS NOT NULL AND responded_at IS NOT NULL +ORDER BY responded_at; + +DROP TABLE oauth2_authorization_code_session; +DROP TABLE oauth2_access_token_session; +DROP TABLE oauth2_refresh_token_session; +DROP TABLE oauth2_pkce_request_session; +DROP TABLE oauth2_openid_connect_session; +DROP TABLE oauth2_consent_session; + +CREATE TABLE oauth2_consent_session ( +    id SERIAL, +    challenge_id CHAR(36) NOT NULL, +    client_id VARCHAR(255) NOT NULL, +    subject CHAR(36) NOT NULL, +    authorized BOOLEAN NOT NULL DEFAULT FALSE, +    granted BOOLEAN NOT NULL DEFAULT FALSE, +    requested_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, +    responded_at TIMESTAMP WITH TIME ZONE NULL DEFAULT NULL, +    form_data TEXT NOT NULL, +    requested_scopes TEXT NOT NULL, +    granted_scopes TEXT NOT NULL, +    requested_audience TEXT NULL, +    granted_audience TEXT NULL, +    preconfiguration INTEGER NULL DEFAULT NULL, +    PRIMARY KEY (id), +    CONSTRAINT oauth2_consent_session_subject_fkey +        FOREIGN KEY(subject) +            REFERENCES user_opaque_identifier(identifier) ON UPDATE RESTRICT ON DELETE RESTRICT, +    CONSTRAINT oauth2_consent_session_preconfiguration_fkey +        FOREIGN KEY(preconfiguration) +            REFERENCES oauth2_consent_preconfiguration(id) ON UPDATE CASCADE ON DELETE CASCADE +); + +CREATE UNIQUE INDEX oauth2_consent_session_challenge_id_key ON oauth2_consent_session (challenge_id); + +CREATE TABLE oauth2_authorization_code_session ( +    id SERIAL, +    challenge_id CHAR(36) NOT NULL, +    request_id VARCHAR(40) NOT NULL, +    client_id VARCHAR(255) NOT NULL, +    signature VARCHAR(255) NOT NULL, +    subject CHAR(36) NOT NULL, +    requested_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, +    requested_scopes TEXT NOT NULL, +    granted_scopes TEXT NOT NULL, +    requested_audience TEXT NULL DEFAULT '', +    granted_audience TEXT NULL DEFAULT '', +    active BOOLEAN NOT NULL DEFAULT FALSE, +    revoked BOOLEAN NOT NULL DEFAULT FALSE, +    form_data TEXT NOT NULL, +    session_data BYTEA NOT NULL, +    PRIMARY KEY (id), +    CONSTRAINT oauth2_authorization_code_session_challenge_id_fkey +        FOREIGN KEY(challenge_id) +            REFERENCES oauth2_consent_session(challenge_id) ON UPDATE CASCADE ON DELETE CASCADE, +    CONSTRAINT oauth2_authorization_code_session_subject_fkey +        FOREIGN KEY(subject) +            REFERENCES user_opaque_identifier(identifier) ON UPDATE RESTRICT ON DELETE RESTRICT +); + +CREATE INDEX oauth2_authorization_code_session_request_id_idx ON oauth2_authorization_code_session (request_id); +CREATE INDEX oauth2_authorization_code_session_client_id_idx ON oauth2_authorization_code_session (client_id); +CREATE INDEX oauth2_authorization_code_session_client_id_subject_idx ON oauth2_authorization_code_session (client_id, subject); + +CREATE TABLE oauth2_access_token_session ( +    id SERIAL, +    challenge_id CHAR(36) NOT NULL, +    request_id VARCHAR(40) NOT NULL, +    client_id VARCHAR(255) NOT NULL, +    signature VARCHAR(255) NOT NULL, +    subject CHAR(36) NOT NULL, +    requested_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, +    requested_scopes TEXT NOT NULL, +    granted_scopes TEXT NOT NULL, +    requested_audience TEXT NULL DEFAULT '', +    granted_audience TEXT NULL DEFAULT '', +    active BOOLEAN NOT NULL DEFAULT FALSE, +    revoked BOOLEAN NOT NULL DEFAULT FALSE, +    form_data TEXT NOT NULL, +    session_data BYTEA NOT NULL, +    PRIMARY KEY (id), +    CONSTRAINT oauth2_access_token_session_challenge_id_fkey +        FOREIGN KEY(challenge_id) +            REFERENCES oauth2_consent_session(challenge_id) ON UPDATE CASCADE ON DELETE CASCADE, +    CONSTRAINT oauth2_access_token_session_subject_fkey +        FOREIGN KEY(subject) +            REFERENCES user_opaque_identifier(identifier) ON UPDATE RESTRICT ON DELETE RESTRICT +); + +CREATE INDEX oauth2_access_token_session_request_id_idx ON oauth2_access_token_session (request_id); +CREATE INDEX oauth2_access_token_session_client_id_idx ON oauth2_access_token_session (client_id); +CREATE INDEX oauth2_access_token_session_client_id_subject_idx ON oauth2_access_token_session (client_id, subject); + +CREATE TABLE oauth2_refresh_token_session ( +    id SERIAL, +    challenge_id CHAR(36) NOT NULL, +    request_id VARCHAR(40) NOT NULL, +    client_id VARCHAR(255) NOT NULL, +    signature VARCHAR(255) NOT NULL, +    subject CHAR(36) NOT NULL, +    requested_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, +    requested_scopes TEXT NOT NULL, +    granted_scopes TEXT NOT NULL, +    requested_audience TEXT NULL DEFAULT '', +    granted_audience TEXT NULL DEFAULT '', +    active BOOLEAN NOT NULL DEFAULT FALSE, +    revoked BOOLEAN NOT NULL DEFAULT FALSE, +    form_data TEXT NOT NULL, +    session_data BYTEA NOT NULL, +    PRIMARY KEY (id), +    CONSTRAINT oauth2_refresh_token_session_challenge_id_fkey +        FOREIGN KEY(challenge_id) +            REFERENCES oauth2_consent_session(challenge_id) ON UPDATE CASCADE ON DELETE CASCADE, +    CONSTRAINT oauth2_refresh_token_session_subject_fkey +        FOREIGN KEY(subject) +            REFERENCES user_opaque_identifier(identifier) ON UPDATE RESTRICT ON DELETE RESTRICT +); + +CREATE INDEX oauth2_refresh_token_session_request_id_idx ON oauth2_refresh_token_session (request_id); +CREATE INDEX oauth2_refresh_token_session_client_id_idx ON oauth2_refresh_token_session (client_id); +CREATE INDEX oauth2_refresh_token_session_client_id_subject_idx ON oauth2_refresh_token_session (client_id, subject); + +CREATE TABLE oauth2_pkce_request_session ( +    id SERIAL, +    challenge_id CHAR(36) NOT NULL, +    request_id VARCHAR(40) NOT NULL, +    client_id VARCHAR(255) NOT NULL, +    signature VARCHAR(255) NOT NULL, +    subject CHAR(36) NOT NULL, +    requested_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, +    requested_scopes TEXT NOT NULL, +    granted_scopes TEXT NOT NULL, +    requested_audience TEXT NULL DEFAULT '', +    granted_audience TEXT NULL DEFAULT '', +    active BOOLEAN NOT NULL DEFAULT FALSE, +    revoked BOOLEAN NOT NULL DEFAULT FALSE, +    form_data TEXT NOT NULL, +    session_data BYTEA NOT NULL, +    PRIMARY KEY (id), +    CONSTRAINT oauth2_pkce_request_session_challenge_id_fkey +        FOREIGN KEY(challenge_id) +            REFERENCES oauth2_consent_session(challenge_id) ON UPDATE CASCADE ON DELETE CASCADE, +    CONSTRAINT oauth2_pkce_request_session_subject_fkey +        FOREIGN KEY(subject) +            REFERENCES user_opaque_identifier(identifier) ON UPDATE RESTRICT ON DELETE RESTRICT +); + +CREATE INDEX oauth2_pkce_request_session_request_id_idx ON oauth2_pkce_request_session (request_id); +CREATE INDEX oauth2_pkce_request_session_client_id_idx ON oauth2_pkce_request_session (client_id); +CREATE INDEX oauth2_pkce_request_session_client_id_subject_idx ON oauth2_pkce_request_session (client_id, subject); + +CREATE TABLE oauth2_openid_connect_session ( +    id SERIAL, +    challenge_id CHAR(36) NOT NULL, +    request_id VARCHAR(40) NOT NULL, +    client_id VARCHAR(255) NOT NULL, +    signature VARCHAR(255) NOT NULL, +    subject CHAR(36) NOT NULL, +    requested_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, +    requested_scopes TEXT NOT NULL, +    granted_scopes TEXT NOT NULL, +    requested_audience TEXT NULL DEFAULT '', +    granted_audience TEXT NULL DEFAULT '', +    active BOOLEAN NOT NULL DEFAULT FALSE, +    revoked BOOLEAN NOT NULL DEFAULT FALSE, +    form_data TEXT NOT NULL, +    session_data BYTEA NOT NULL, +    PRIMARY KEY (id), +    CONSTRAINT oauth2_openid_connect_session_challenge_id_fkey +        FOREIGN KEY(challenge_id) +            REFERENCES oauth2_consent_session(challenge_id) ON UPDATE CASCADE ON DELETE CASCADE, +    CONSTRAINT oauth2_openid_connect_session_subject_fkey +        FOREIGN KEY(subject) +            REFERENCES user_opaque_identifier(identifier) ON UPDATE RESTRICT ON DELETE RESTRICT +); + +CREATE INDEX oauth2_openid_connect_session_request_id_idx ON oauth2_openid_connect_session (request_id); +CREATE INDEX oauth2_openid_connect_session_client_id_idx ON oauth2_openid_connect_session (client_id); +CREATE INDEX oauth2_openid_connect_session_client_id_subject_idx ON oauth2_openid_connect_session (client_id, subject); diff --git a/internal/storage/migrations/V0006.ConsentPreConfiguration.sqlite.down.sql b/internal/storage/migrations/V0006.ConsentPreConfiguration.sqlite.down.sql new file mode 100644 index 000000000..e4bfaefeb --- /dev/null +++ b/internal/storage/migrations/V0006.ConsentPreConfiguration.sqlite.down.sql @@ -0,0 +1,175 @@ +DROP TABLE oauth2_authorization_code_session; +DROP TABLE oauth2_access_token_session; +DROP TABLE oauth2_refresh_token_session; +DROP TABLE oauth2_pkce_request_session; +DROP TABLE oauth2_openid_connect_session; +DROP TABLE oauth2_consent_session; +DROP TABLE oauth2_consent_preconfiguration; + +CREATE TABLE oauth2_consent_session ( +    id INTEGER, +    challenge_id CHAR(36) NOT NULL, +    client_id VARCHAR(255) NOT NULL, +    subject CHAR(36) NULL DEFAULT NULL, +    authorized BOOLEAN NOT NULL DEFAULT FALSE, +    granted BOOLEAN NOT NULL DEFAULT FALSE, +    requested_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, +    responded_at TIMESTAMP NULL DEFAULT NULL, +    expires_at TIMESTAMP NULL DEFAULT NULL, +    form_data TEXT NOT NULL, +    requested_scopes TEXT NOT NULL, +    granted_scopes TEXT NOT NULL, +	requested_audience TEXT NULL DEFAULT '', +	granted_audience TEXT NULL DEFAULT '', +    PRIMARY KEY (id), +    CONSTRAINT oauth2_consent_session_subject_fkey +        FOREIGN KEY(subject) +            REFERENCES user_opaque_identifier(identifier) ON UPDATE RESTRICT ON DELETE RESTRICT +); + +CREATE UNIQUE INDEX oauth2_consent_session_challenge_id_key ON oauth2_consent_session (challenge_id); + +CREATE TABLE oauth2_authorization_code_session ( +    id INTEGER, +    challenge_id CHAR(36) NOT NULL, +    request_id VARCHAR(40) NOT NULL, +    client_id VARCHAR(255) NOT NULL, +    signature VARCHAR(255) NOT NULL, +    subject CHAR(36) NOT NULL, +    requested_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, +    requested_scopes TEXT NOT NULL, +    granted_scopes TEXT NOT NULL, +    requested_audience TEXT NULL DEFAULT '', +    granted_audience TEXT NULL DEFAULT '', +    active BOOLEAN NOT NULL DEFAULT FALSE, +    revoked BOOLEAN NOT NULL DEFAULT FALSE, +    form_data TEXT NOT NULL, +    session_data BLOB NOT NULL, +    PRIMARY KEY (id), +    CONSTRAINT oauth2_authorization_code_session_challenge_id_fkey +        FOREIGN KEY(challenge_id) +            REFERENCES oauth2_consent_session(challenge_id) ON UPDATE CASCADE ON DELETE CASCADE, +    CONSTRAINT oauth2_authorization_code_session_subject_fkey +        FOREIGN KEY(subject) +            REFERENCES user_opaque_identifier(identifier) ON UPDATE RESTRICT ON DELETE RESTRICT +); + +CREATE INDEX oauth2_authorization_code_session_request_id_idx ON oauth2_authorization_code_session (request_id); +CREATE INDEX oauth2_authorization_code_session_client_id_idx ON oauth2_authorization_code_session (client_id); +CREATE INDEX oauth2_authorization_code_session_client_id_subject_idx ON oauth2_authorization_code_session (client_id, subject); + +CREATE TABLE IF NOT EXISTS oauth2_access_token_session ( +    id INTEGER, +    challenge_id CHAR(36) NOT NULL, +    request_id VARCHAR(40) NOT NULL, +    client_id VARCHAR(255) NOT NULL, +    signature VARCHAR(255) NOT NULL, +    subject CHAR(36) NOT NULL, +    requested_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, +    requested_scopes TEXT NOT NULL, +    granted_scopes TEXT NOT NULL, +    requested_audience TEXT NULL DEFAULT '', +    granted_audience TEXT NULL DEFAULT '', +    active BOOLEAN NOT NULL DEFAULT FALSE, +    revoked BOOLEAN NOT NULL DEFAULT FALSE, +    form_data TEXT NOT NULL, +    session_data BLOB NOT NULL, +    PRIMARY KEY (id), +    CONSTRAINT oauth2_access_token_session_challenge_id_fkey +        FOREIGN KEY(challenge_id) +            REFERENCES oauth2_consent_session(challenge_id) ON UPDATE CASCADE ON DELETE CASCADE, +    CONSTRAINT oauth2_access_token_session_subject_fkey +        FOREIGN KEY(subject) +            REFERENCES user_opaque_identifier(identifier) ON UPDATE RESTRICT ON DELETE RESTRICT +); + +CREATE INDEX oauth2_access_token_session_request_id_idx ON oauth2_access_token_session (request_id); +CREATE INDEX oauth2_access_token_session_client_id_idx ON oauth2_access_token_session (client_id); +CREATE INDEX oauth2_access_token_session_client_id_subject_idx ON oauth2_access_token_session (client_id, subject); + +CREATE TABLE IF NOT EXISTS oauth2_refresh_token_session ( +    id INTEGER, +    challenge_id CHAR(36) NOT NULL, +    request_id VARCHAR(40) NOT NULL, +    client_id VARCHAR(255) NOT NULL, +    signature VARCHAR(255) NOT NULL, +    subject CHAR(36) NOT NULL, +    requested_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, +    requested_scopes TEXT NOT NULL, +    granted_scopes TEXT NOT NULL, +    requested_audience TEXT NULL DEFAULT '', +    granted_audience TEXT NULL DEFAULT '', +    active BOOLEAN NOT NULL DEFAULT FALSE, +    revoked BOOLEAN NOT NULL DEFAULT FALSE, +    form_data TEXT NOT NULL, +    session_data BLOB NOT NULL, +    PRIMARY KEY (id), +    CONSTRAINT oauth2_refresh_token_session_challenge_id_fkey +        FOREIGN KEY(challenge_id) +            REFERENCES oauth2_consent_session(challenge_id) ON UPDATE CASCADE ON DELETE CASCADE, +    CONSTRAINT oauth2_refresh_token_session_subject_fkey +        FOREIGN KEY(subject) +            REFERENCES user_opaque_identifier(identifier) ON UPDATE RESTRICT ON DELETE RESTRICT +); + +CREATE INDEX oauth2_refresh_token_session_request_id_idx ON oauth2_refresh_token_session (request_id); +CREATE INDEX oauth2_refresh_token_session_client_id_idx ON oauth2_refresh_token_session (client_id); +CREATE INDEX oauth2_refresh_token_session_client_id_subject_idx ON oauth2_refresh_token_session (client_id, subject); + +CREATE TABLE oauth2_pkce_request_session ( +    id INTEGER, +    challenge_id CHAR(36) NOT NULL, +    request_id VARCHAR(40) NOT NULL, +    client_id VARCHAR(255) NOT NULL, +    signature VARCHAR(255) NOT NULL, +    subject CHAR(36) NOT NULL, +    requested_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, +    requested_scopes TEXT NOT NULL, +    granted_scopes TEXT NOT NULL, +    requested_audience TEXT NULL DEFAULT '', +    granted_audience TEXT NULL DEFAULT '', +    active BOOLEAN NOT NULL DEFAULT FALSE, +    revoked BOOLEAN NOT NULL DEFAULT FALSE, +    form_data TEXT NOT NULL, +    session_data BLOB NOT NULL, +    PRIMARY KEY (id), +    CONSTRAINT oauth2_pkce_request_session_challenge_id_fkey +        FOREIGN KEY(challenge_id) +            REFERENCES oauth2_consent_session(challenge_id) ON UPDATE CASCADE ON DELETE CASCADE, +    CONSTRAINT oauth2_pkce_request_session_subject_fkey +        FOREIGN KEY(subject) +            REFERENCES user_opaque_identifier(identifier) ON UPDATE RESTRICT ON DELETE RESTRICT +); + +CREATE INDEX oauth2_pkce_request_session_request_id_idx ON oauth2_pkce_request_session (request_id); +CREATE INDEX oauth2_pkce_request_session_client_id_idx ON oauth2_pkce_request_session (client_id); +CREATE INDEX oauth2_pkce_request_session_client_id_subject_idx ON oauth2_pkce_request_session (client_id, subject); + +CREATE TABLE oauth2_openid_connect_session ( +    id INTEGER, +    challenge_id CHAR(36) NOT NULL, +    request_id VARCHAR(40) NOT NULL, +    client_id VARCHAR(255) NOT NULL, +    signature VARCHAR(255) NOT NULL, +    subject CHAR(36) NOT NULL, +    requested_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, +    requested_scopes TEXT NOT NULL, +    granted_scopes TEXT NOT NULL, +    requested_audience TEXT NULL DEFAULT '', +    granted_audience TEXT NULL DEFAULT '', +    active BOOLEAN NOT NULL DEFAULT FALSE, +    revoked BOOLEAN NOT NULL DEFAULT FALSE, +    form_data TEXT NOT NULL, +    session_data BLOB NOT NULL, +    PRIMARY KEY (id), +    CONSTRAINT oauth2_openid_connect_session_challenge_id_fkey +        FOREIGN KEY(challenge_id) +            REFERENCES oauth2_consent_session(challenge_id) ON UPDATE CASCADE ON DELETE CASCADE, +    CONSTRAINT oauth2_openid_connect_session_subject_fkey +        FOREIGN KEY(subject) +            REFERENCES user_opaque_identifier(identifier) ON UPDATE RESTRICT ON DELETE RESTRICT +); + +CREATE INDEX oauth2_openid_connect_session_request_id_idx ON oauth2_openid_connect_session (request_id); +CREATE INDEX oauth2_openid_connect_session_client_id_idx ON oauth2_openid_connect_session (client_id); +CREATE INDEX oauth2_openid_connect_session_client_id_subject_idx ON oauth2_openid_connect_session (client_id, subject); diff --git a/internal/storage/migrations/V0006.ConsentPreConfiguration.sqlite.up.sql b/internal/storage/migrations/V0006.ConsentPreConfiguration.sqlite.up.sql new file mode 100644 index 000000000..a491e82df --- /dev/null +++ b/internal/storage/migrations/V0006.ConsentPreConfiguration.sqlite.up.sql @@ -0,0 +1,198 @@ +CREATE TABLE oauth2_consent_preconfiguration ( +    id INTEGER, +    client_id VARCHAR(255) NOT NULL, +    subject CHAR(36) NOT NULL, +	created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, +	expires_at TIMESTAMP NULL DEFAULT NULL, +	revoked BOOLEAN NOT NULL DEFAULT FALSE, +    scopes TEXT NOT NULL, +    audience TEXT NULL, +    PRIMARY KEY (id), +    CONSTRAINT oauth2_consent_preconfiguration_subjct_fkey +        FOREIGN KEY(subject) +            REFERENCES user_opaque_identifier(identifier) ON UPDATE RESTRICT ON DELETE RESTRICT +); + +INSERT INTO oauth2_consent_preconfiguration (client_id, subject, created_at, expires_at, scopes, audience) +SELECT client_id, subject, responded_at, expires_at, granted_scopes, granted_audience +FROM oauth2_consent_session +WHERE expires_at IS NOT NULL AND responded_at IS NOT NULL +ORDER BY responded_at; + +DROP TABLE oauth2_authorization_code_session; +DROP TABLE oauth2_access_token_session; +DROP TABLE oauth2_refresh_token_session; +DROP TABLE oauth2_pkce_request_session; +DROP TABLE oauth2_openid_connect_session; +DROP TABLE oauth2_consent_session; + +CREATE TABLE oauth2_consent_session ( +    id INTEGER, +    challenge_id CHAR(36) NOT NULL, +    client_id VARCHAR(255) NOT NULL, +    subject CHAR(36) NOT NULL, +    authorized BOOLEAN NOT NULL DEFAULT FALSE, +    granted BOOLEAN NOT NULL DEFAULT FALSE, +    requested_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, +    responded_at TIMESTAMP NULL DEFAULT NULL, +    form_data TEXT NOT NULL, +    requested_scopes TEXT NOT NULL, +    granted_scopes TEXT NOT NULL, +    requested_audience TEXT NULL DEFAULT '', +    granted_audience TEXT NULL DEFAULT '', +    preconfiguration INTEGER NULL DEFAULT NULL, +    PRIMARY KEY (id), +    CONSTRAINT oauth2_consent_session_subject_fkey +        FOREIGN KEY(subject) +            REFERENCES user_opaque_identifier(identifier) ON UPDATE RESTRICT ON DELETE RESTRICT, +    CONSTRAINT oauth2_consent_session_preconfiguration_fkey +        FOREIGN KEY(preconfiguration) +            REFERENCES oauth2_consent_preconfiguration(id) ON UPDATE CASCADE ON DELETE CASCADE +); + +CREATE UNIQUE INDEX oauth2_consent_session_challenge_id_key ON oauth2_consent_session (challenge_id); + +CREATE TABLE oauth2_authorization_code_session ( +    id INTEGER, +    challenge_id CHAR(36) NOT NULL, +    request_id VARCHAR(40) NOT NULL, +    client_id VARCHAR(255) NOT NULL, +    signature VARCHAR(255) NOT NULL, +    subject CHAR(36) NOT NULL, +    requested_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, +    requested_scopes TEXT NOT NULL, +    granted_scopes TEXT NOT NULL, +    requested_audience TEXT NULL DEFAULT '', +    granted_audience TEXT NULL DEFAULT '', +    active BOOLEAN NOT NULL DEFAULT FALSE, +    revoked BOOLEAN NOT NULL DEFAULT FALSE, +    form_data TEXT NOT NULL, +    session_data BLOB NOT NULL, +    PRIMARY KEY (id), +    CONSTRAINT oauth2_authorization_code_session_challenge_id_fkey +        FOREIGN KEY(challenge_id) +            REFERENCES oauth2_consent_session(challenge_id) ON UPDATE CASCADE ON DELETE CASCADE, +    CONSTRAINT oauth2_authorization_code_session_subject_fkey +        FOREIGN KEY(subject) +            REFERENCES user_opaque_identifier(identifier) ON UPDATE RESTRICT ON DELETE RESTRICT +); + +CREATE INDEX oauth2_authorization_code_session_request_id_idx ON oauth2_authorization_code_session (request_id); +CREATE INDEX oauth2_authorization_code_session_client_id_idx ON oauth2_authorization_code_session (client_id); +CREATE INDEX oauth2_authorization_code_session_client_id_subject_idx ON oauth2_authorization_code_session (client_id, subject); + +CREATE TABLE oauth2_access_token_session ( +    id INTEGER, +    challenge_id CHAR(36) NOT NULL, +    request_id VARCHAR(40) NOT NULL, +    client_id VARCHAR(255) NOT NULL, +    signature VARCHAR(255) NOT NULL, +    subject CHAR(36) NOT NULL, +    requested_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, +    requested_scopes TEXT NOT NULL, +    granted_scopes TEXT NOT NULL, +    requested_audience TEXT NULL DEFAULT '', +    granted_audience TEXT NULL DEFAULT '', +    active BOOLEAN NOT NULL DEFAULT FALSE, +    revoked BOOLEAN NOT NULL DEFAULT FALSE, +    form_data TEXT NOT NULL, +    session_data BLOB NOT NULL, +    PRIMARY KEY (id), +    CONSTRAINT oauth2_access_token_session_challenge_id_fkey +        FOREIGN KEY(challenge_id) +            REFERENCES oauth2_consent_session(challenge_id) ON UPDATE CASCADE ON DELETE CASCADE, +    CONSTRAINT oauth2_access_token_session_subject_fkey +        FOREIGN KEY(subject) +            REFERENCES user_opaque_identifier(identifier) ON UPDATE RESTRICT ON DELETE RESTRICT +); + +CREATE INDEX oauth2_access_token_session_request_id_idx ON oauth2_access_token_session (request_id); +CREATE INDEX oauth2_access_token_session_client_id_idx ON oauth2_access_token_session (client_id); +CREATE INDEX oauth2_access_token_session_client_id_subject_idx ON oauth2_access_token_session (client_id, subject); + +CREATE TABLE oauth2_refresh_token_session ( +    id INTEGER, +    challenge_id CHAR(36) NOT NULL, +    request_id VARCHAR(40) NOT NULL, +    client_id VARCHAR(255) NOT NULL, +    signature VARCHAR(255) NOT NULL, +    subject CHAR(36) NOT NULL, +    requested_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, +    requested_scopes TEXT NOT NULL, +    granted_scopes TEXT NOT NULL, +    requested_audience TEXT NULL DEFAULT '', +    granted_audience TEXT NULL DEFAULT '', +    active BOOLEAN NOT NULL DEFAULT FALSE, +    revoked BOOLEAN NOT NULL DEFAULT FALSE, +    form_data TEXT NOT NULL, +    session_data BLOB NOT NULL, +    PRIMARY KEY (id), +    CONSTRAINT oauth2_refresh_token_session_challenge_id_fkey +        FOREIGN KEY(challenge_id) +            REFERENCES oauth2_consent_session(challenge_id) ON UPDATE CASCADE ON DELETE CASCADE, +    CONSTRAINT oauth2_refresh_token_session_subject_fkey +        FOREIGN KEY(subject) +            REFERENCES user_opaque_identifier(identifier) ON UPDATE RESTRICT ON DELETE RESTRICT +); + +CREATE INDEX oauth2_refresh_token_session_request_id_idx ON oauth2_refresh_token_session (request_id); +CREATE INDEX oauth2_refresh_token_session_client_id_idx ON oauth2_refresh_token_session (client_id); +CREATE INDEX oauth2_refresh_token_session_client_id_subject_idx ON oauth2_refresh_token_session (client_id, subject); + +CREATE TABLE oauth2_pkce_request_session ( +    id INTEGER, +    challenge_id CHAR(36) NOT NULL, +    request_id VARCHAR(40) NOT NULL, +    client_id VARCHAR(255) NOT NULL, +    signature VARCHAR(255) NOT NULL, +    subject CHAR(36) NOT NULL, +    requested_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, +    requested_scopes TEXT NOT NULL, +    granted_scopes TEXT NOT NULL, +    requested_audience TEXT NULL DEFAULT '', +    granted_audience TEXT NULL DEFAULT '', +    active BOOLEAN NOT NULL DEFAULT FALSE, +    revoked BOOLEAN NOT NULL DEFAULT FALSE, +    form_data TEXT NOT NULL, +    session_data BLOB NOT NULL, +    PRIMARY KEY (id), +    CONSTRAINT oauth2_pkce_request_session_challenge_id_fkey +        FOREIGN KEY(challenge_id) +            REFERENCES oauth2_consent_session(challenge_id) ON UPDATE CASCADE ON DELETE CASCADE, +    CONSTRAINT oauth2_pkce_request_session_subject_fkey +        FOREIGN KEY(subject) +            REFERENCES user_opaque_identifier(identifier) ON UPDATE RESTRICT ON DELETE RESTRICT +); + +CREATE INDEX oauth2_pkce_request_session_request_id_idx ON oauth2_pkce_request_session (request_id); +CREATE INDEX oauth2_pkce_request_session_client_id_idx ON oauth2_pkce_request_session (client_id); +CREATE INDEX oauth2_pkce_request_session_client_id_subject_idx ON oauth2_pkce_request_session (client_id, subject); + +CREATE TABLE oauth2_openid_connect_session ( +    id INTEGER, +    challenge_id CHAR(36) NOT NULL, +    request_id VARCHAR(40) NOT NULL, +    client_id VARCHAR(255) NOT NULL, +    signature VARCHAR(255) NOT NULL, +    subject CHAR(36) NOT NULL, +    requested_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, +    requested_scopes TEXT NOT NULL, +    granted_scopes TEXT NOT NULL, +    requested_audience TEXT NULL DEFAULT '', +    granted_audience TEXT NULL DEFAULT '', +    active BOOLEAN NOT NULL DEFAULT FALSE, +    revoked BOOLEAN NOT NULL DEFAULT FALSE, +    form_data TEXT NOT NULL, +    session_data BLOB NOT NULL, +    PRIMARY KEY (id), +    CONSTRAINT oauth2_openid_connect_session_challenge_id_fkey +        FOREIGN KEY(challenge_id) +            REFERENCES oauth2_consent_session(challenge_id) ON UPDATE CASCADE ON DELETE CASCADE, +    CONSTRAINT oauth2_openid_connect_session_subject_fkey +        FOREIGN KEY(subject) +            REFERENCES user_opaque_identifier(identifier) ON UPDATE RESTRICT ON DELETE RESTRICT +); + +CREATE INDEX oauth2_openid_connect_session_request_id_idx ON oauth2_openid_connect_session (request_id); +CREATE INDEX oauth2_openid_connect_session_client_id_idx ON oauth2_openid_connect_session (client_id); +CREATE INDEX oauth2_openid_connect_session_client_id_subject_idx ON oauth2_openid_connect_session (client_id, subject); diff --git a/internal/storage/migrations_test.go b/internal/storage/migrations_test.go index 38fa84f42..f7c9df73e 100644 --- a/internal/storage/migrations_test.go +++ b/internal/storage/migrations_test.go @@ -7,11 +7,16 @@ import (  	"github.com/stretchr/testify/require"  ) +const ( +	// This is the latest schema version for the purpose of tests. +	LatestVersion = 6 +) +  func TestShouldObtainCorrectUpMigrations(t *testing.T) {  	ver, err := latestMigrationVersion(providerSQLite)  	require.NoError(t, err) -	assert.Equal(t, testLatestVersion, ver) +	assert.Equal(t, LatestVersion, ver)  	migrations, err := loadMigrations(providerSQLite, 0, ver)  	require.NoError(t, err) @@ -27,7 +32,7 @@ func TestShouldObtainCorrectDownMigrations(t *testing.T) {  	ver, err := latestMigrationVersion(providerSQLite)  	require.NoError(t, err) -	assert.Equal(t, testLatestVersion, ver) +	assert.Equal(t, LatestVersion, ver)  	migrations, err := loadMigrations(providerSQLite, ver, 0)  	require.NoError(t, err) diff --git a/internal/storage/provider.go b/internal/storage/provider.go index fdf415a89..ecfe104b0 100644 --- a/internal/storage/provider.go +++ b/internal/storage/provider.go @@ -2,6 +2,7 @@ package storage  import (  	"context" +	"database/sql"  	"time"  	"github.com/google/uuid" @@ -32,13 +33,13 @@ type Provider interface {  	FindIdentityVerification(ctx context.Context, jti string) (found bool, err error)  	SaveTOTPConfiguration(ctx context.Context, config model.TOTPConfiguration) (err error) -	UpdateTOTPConfigurationSignIn(ctx context.Context, id int, lastUsedAt *time.Time) (err error) +	UpdateTOTPConfigurationSignIn(ctx context.Context, id int, lastUsedAt sql.NullTime) (err error)  	DeleteTOTPConfiguration(ctx context.Context, username string) (err error)  	LoadTOTPConfiguration(ctx context.Context, username string) (config *model.TOTPConfiguration, err error)  	LoadTOTPConfigurations(ctx context.Context, limit, page int) (configs []model.TOTPConfiguration, err error)  	SaveWebauthnDevice(ctx context.Context, device model.WebauthnDevice) (err error) -	UpdateWebauthnDeviceSignIn(ctx context.Context, id int, rpid string, lastUsedAt *time.Time, signCount uint32, cloneWarning bool) (err error) +	UpdateWebauthnDeviceSignIn(ctx context.Context, id int, rpid string, lastUsedAt sql.NullTime, signCount uint32, cloneWarning bool) (err error)  	DeleteWebauthnDevice(ctx context.Context, kid string) (err error)  	DeleteWebauthnDeviceByUsername(ctx context.Context, username, description string) (err error)  	LoadWebauthnDevices(ctx context.Context, limit, page int) (devices []model.WebauthnDevice, err error) @@ -48,12 +49,14 @@ type Provider interface {  	DeletePreferredDuoDevice(ctx context.Context, username string) (err error)  	LoadPreferredDuoDevice(ctx context.Context, username string) (device *model.DuoDevice, err error) +	SaveOAuth2ConsentPreConfiguration(ctx context.Context, config model.OAuth2ConsentPreConfig) (insertedID int64, err error) +	LoadOAuth2ConsentPreConfigurations(ctx context.Context, clientID string, subject uuid.UUID) (rows *ConsentPreConfigRows, err error) +  	SaveOAuth2ConsentSession(ctx context.Context, consent model.OAuth2ConsentSession) (err error)  	SaveOAuth2ConsentSessionSubject(ctx context.Context, consent model.OAuth2ConsentSession) (err error)  	SaveOAuth2ConsentSessionResponse(ctx context.Context, consent model.OAuth2ConsentSession, rejection bool) (err error)  	SaveOAuth2ConsentSessionGranted(ctx context.Context, id int) (err error)  	LoadOAuth2ConsentSessionByChallengeID(ctx context.Context, challengeID uuid.UUID) (consent *model.OAuth2ConsentSession, err error) -	LoadOAuth2ConsentSessionsPreConfigured(ctx context.Context, clientID string, subject uuid.UUID) (rows *ConsentSessionRows, err error)  	SaveOAuth2Session(ctx context.Context, sessionType OAuth2SessionType, session model.OAuth2Session) (err error)  	RevokeOAuth2Session(ctx context.Context, sessionType OAuth2SessionType, signature string) (err error) diff --git a/internal/storage/sql_provider.go b/internal/storage/sql_provider.go index 6991e7021..7ee60ab5c 100644 --- a/internal/storage/sql_provider.go +++ b/internal/storage/sql_provider.go @@ -6,6 +6,7 @@ import (  	"database/sql"  	"errors"  	"fmt" +	"strings"  	"time"  	"github.com/google/uuid" @@ -73,6 +74,15 @@ func NewSQLProvider(config *schema.Configuration, name, driverName, dataSourceNa  		sqlSelectUserOpaqueIdentifiers:           fmt.Sprintf(queryFmtSelectUserOpaqueIdentifiers, tableUserOpaqueIdentifier),  		sqlSelectUserOpaqueIdentifierBySignature: fmt.Sprintf(queryFmtSelectUserOpaqueIdentifierBySignature, tableUserOpaqueIdentifier), +		sqlInsertOAuth2ConsentPreConfiguration:  fmt.Sprintf(queryFmtInsertOAuth2ConsentPreConfiguration, tableOAuth2ConsentPreConfiguration), +		sqlSelectOAuth2ConsentPreConfigurations: fmt.Sprintf(queryFmtSelectOAuth2ConsentPreConfigurations, tableOAuth2ConsentPreConfiguration), + +		sqlInsertOAuth2ConsentSession:              fmt.Sprintf(queryFmtInsertOAuth2ConsentSession, tableOAuth2ConsentSession), +		sqlUpdateOAuth2ConsentSessionSubject:       fmt.Sprintf(queryFmtUpdateOAuth2ConsentSessionSubject, tableOAuth2ConsentSession), +		sqlUpdateOAuth2ConsentSessionResponse:      fmt.Sprintf(queryFmtUpdateOAuth2ConsentSessionResponse, tableOAuth2ConsentSession), +		sqlUpdateOAuth2ConsentSessionGranted:       fmt.Sprintf(queryFmtUpdateOAuth2ConsentSessionGranted, tableOAuth2ConsentSession), +		sqlSelectOAuth2ConsentSessionByChallengeID: fmt.Sprintf(queryFmtSelectOAuth2ConsentSessionByChallengeID, tableOAuth2ConsentSession), +  		sqlInsertOAuth2AuthorizeCodeSession:                fmt.Sprintf(queryFmtInsertOAuth2Session, tableOAuth2AuthorizeCodeSession),  		sqlSelectOAuth2AuthorizeCodeSession:                fmt.Sprintf(queryFmtSelectOAuth2Session, tableOAuth2AuthorizeCodeSession),  		sqlRevokeOAuth2AuthorizeCodeSession:                fmt.Sprintf(queryFmtRevokeOAuth2Session, tableOAuth2AuthorizeCodeSession), @@ -108,13 +118,6 @@ func NewSQLProvider(config *schema.Configuration, name, driverName, dataSourceNa  		sqlDeactivateOAuth2OpenIDConnectSession:            fmt.Sprintf(queryFmtDeactivateOAuth2Session, tableOAuth2OpenIDConnectSession),  		sqlDeactivateOAuth2OpenIDConnectSessionByRequestID: fmt.Sprintf(queryFmtDeactivateOAuth2SessionByRequestID, tableOAuth2OpenIDConnectSession), -		sqlInsertOAuth2ConsentSession:               fmt.Sprintf(queryFmtInsertOAuth2ConsentSession, tableOAuth2ConsentSession), -		sqlUpdateOAuth2ConsentSessionSubject:        fmt.Sprintf(queryFmtUpdateOAuth2ConsentSessionSubject, tableOAuth2ConsentSession), -		sqlUpdateOAuth2ConsentSessionResponse:       fmt.Sprintf(queryFmtUpdateOAuth2ConsentSessionResponse, tableOAuth2ConsentSession), -		sqlUpdateOAuth2ConsentSessionGranted:        fmt.Sprintf(queryFmtUpdateOAuth2ConsentSessionGranted, tableOAuth2ConsentSession), -		sqlSelectOAuth2ConsentSessionByChallengeID:  fmt.Sprintf(queryFmtSelectOAuth2ConsentSessionByChallengeID, tableOAuth2ConsentSession), -		sqlSelectOAuth2ConsentSessionsPreConfigured: fmt.Sprintf(queryFmtSelectOAuth2ConsentSessionsPreConfigured, tableOAuth2ConsentSession), -  		sqlUpsertOAuth2BlacklistedJTI: fmt.Sprintf(queryFmtUpsertOAuth2BlacklistedJTI, tableOAuth2BlacklistedJTI),  		sqlSelectOAuth2BlacklistedJTI: fmt.Sprintf(queryFmtSelectOAuth2BlacklistedJTI, tableOAuth2BlacklistedJTI), @@ -202,6 +205,17 @@ type SQLProvider struct {  	sqlUpsertEncryptionValue string  	sqlSelectEncryptionValue string +	// Table: oauth2_consent_preconfiguration. +	sqlInsertOAuth2ConsentPreConfiguration  string +	sqlSelectOAuth2ConsentPreConfigurations string + +	// Table: oauth2_consent_session. +	sqlInsertOAuth2ConsentSession              string +	sqlUpdateOAuth2ConsentSessionSubject       string +	sqlUpdateOAuth2ConsentSessionResponse      string +	sqlUpdateOAuth2ConsentSessionGranted       string +	sqlSelectOAuth2ConsentSessionByChallengeID string +  	// Table: oauth2_authorization_code_session.  	sqlInsertOAuth2AuthorizeCodeSession                string  	sqlSelectOAuth2AuthorizeCodeSession                string @@ -242,14 +256,6 @@ type SQLProvider struct {  	sqlDeactivateOAuth2OpenIDConnectSession            string  	sqlDeactivateOAuth2OpenIDConnectSessionByRequestID string -	// Table: oauth2_consent_session. -	sqlInsertOAuth2ConsentSession               string -	sqlUpdateOAuth2ConsentSessionSubject        string -	sqlUpdateOAuth2ConsentSessionResponse       string -	sqlUpdateOAuth2ConsentSessionGranted        string -	sqlSelectOAuth2ConsentSessionByChallengeID  string -	sqlSelectOAuth2ConsentSessionsPreConfigured string -  	sqlUpsertOAuth2BlacklistedJTI string  	sqlSelectOAuth2BlacklistedJTI string @@ -404,8 +410,8 @@ func (p *SQLProvider) LoadUserOpaqueIdentifierBySignature(ctx context.Context, s  func (p *SQLProvider) SaveOAuth2ConsentSession(ctx context.Context, consent model.OAuth2ConsentSession) (err error) {  	if _, err = p.db.ExecContext(ctx, p.sqlInsertOAuth2ConsentSession,  		consent.ChallengeID, consent.ClientID, consent.Subject, consent.Authorized, consent.Granted, -		consent.RequestedAt, consent.RespondedAt, consent.ExpiresAt, consent.Form, -		consent.RequestedScopes, consent.GrantedScopes, consent.RequestedAudience, consent.GrantedAudience); err != nil { +		consent.RequestedAt, consent.RespondedAt, consent.Form, +		consent.RequestedScopes, consent.GrantedScopes, consent.RequestedAudience, consent.GrantedAudience, consent.PreConfiguration); err != nil {  		return fmt.Errorf("error inserting oauth2 consent session with challenge id '%s' for subject '%s': %w", consent.ChallengeID.String(), consent.Subject.UUID.String(), err)  	} @@ -423,7 +429,7 @@ func (p *SQLProvider) SaveOAuth2ConsentSessionSubject(ctx context.Context, conse  // SaveOAuth2ConsentSessionResponse updates an OAuth2.0 consent session with the response.  func (p *SQLProvider) SaveOAuth2ConsentSessionResponse(ctx context.Context, consent model.OAuth2ConsentSession, authorized bool) (err error) { -	if _, err = p.db.ExecContext(ctx, p.sqlUpdateOAuth2ConsentSessionResponse, authorized, consent.ExpiresAt, consent.GrantedScopes, consent.GrantedAudience, consent.ID); err != nil { +	if _, err = p.db.ExecContext(ctx, p.sqlUpdateOAuth2ConsentSessionResponse, authorized, consent.GrantedScopes, consent.GrantedAudience, consent.PreConfiguration, consent.ID); err != nil {  		return fmt.Errorf("error updating oauth2 consent session (authorized  '%t') with id '%d' and challenge id '%s' for subject '%s': %w", authorized, consent.ID, consent.ChallengeID, consent.Subject.UUID, err)  	} @@ -450,19 +456,43 @@ func (p *SQLProvider) LoadOAuth2ConsentSessionByChallengeID(ctx context.Context,  	return consent, nil  } -// LoadOAuth2ConsentSessionsPreConfigured returns an OAuth2.0 consents that are pre-configured given the consent signature. -func (p *SQLProvider) LoadOAuth2ConsentSessionsPreConfigured(ctx context.Context, clientID string, subject uuid.UUID) (rows *ConsentSessionRows, err error) { +// SaveOAuth2ConsentPreConfiguration inserts an OAuth2.0 consent pre-configuration. +func (p *SQLProvider) SaveOAuth2ConsentPreConfiguration(ctx context.Context, config model.OAuth2ConsentPreConfig) (insertedID int64, err error) { +	switch p.name { +	case providerPostgres: +		if err = p.db.GetContext(ctx, &insertedID, p.sqlInsertOAuth2ConsentPreConfiguration, +			config.ClientID, config.Subject, config.CreatedAt, config.ExpiresAt, +			config.Revoked, config.Scopes, config.Audience); err != nil { +			return -1, fmt.Errorf("error inserting oauth2 consent pre-configuration for subject '%s' with client id '%s' and scopes '%s': %w", config.Subject.String(), config.ClientID, strings.Join(config.Scopes, " "), err) +		} + +		return insertedID, nil +	default: +		var result sql.Result + +		if result, err = p.db.ExecContext(ctx, p.sqlInsertOAuth2ConsentPreConfiguration, +			config.ClientID, config.Subject, config.CreatedAt, config.ExpiresAt, +			config.Revoked, config.Scopes, config.Audience); err != nil { +			return -1, fmt.Errorf("error inserting oauth2 consent pre-configuration for subject '%s' with client id '%s' and scopes '%s': %w", config.Subject.String(), config.ClientID, strings.Join(config.Scopes, " "), err) +		} + +		return result.LastInsertId() +	} +} + +// LoadOAuth2ConsentPreConfigurations returns an OAuth2.0 consents pre-configurations given the consent signature. +func (p *SQLProvider) LoadOAuth2ConsentPreConfigurations(ctx context.Context, clientID string, subject uuid.UUID) (rows *ConsentPreConfigRows, err error) {  	var r *sqlx.Rows -	if r, err = p.db.QueryxContext(ctx, p.sqlSelectOAuth2ConsentSessionsPreConfigured, clientID, subject); err != nil { +	if r, err = p.db.QueryxContext(ctx, p.sqlSelectOAuth2ConsentPreConfigurations, clientID, subject); err != nil {  		if errors.Is(err, sql.ErrNoRows) { -			return &ConsentSessionRows{}, nil +			return &ConsentPreConfigRows{}, nil  		} -		return &ConsentSessionRows{}, fmt.Errorf("error selecting oauth2 consent session by signature with client id '%s' and subject '%s': %w", clientID, subject.String(), err) +		return &ConsentPreConfigRows{}, fmt.Errorf("error selecting oauth2 consent pre-configurations by signature with client id '%s' and subject '%s': %w", clientID, subject.String(), err)  	} -	return &ConsentSessionRows{rows: r}, nil +	return &ConsentPreConfigRows{rows: r}, nil  }  // SaveOAuth2Session saves a OAuth2Session to the database. @@ -724,7 +754,7 @@ func (p *SQLProvider) FindIdentityVerification(ctx context.Context, jti string)  	}  	switch { -	case verification.Consumed != nil: +	case verification.Consumed.Valid:  		return false, fmt.Errorf("the token has already been consumed")  	case verification.ExpiresAt.Before(time.Now()):  		return false, fmt.Errorf("the token expired %s ago", time.Since(verification.ExpiresAt)) @@ -750,7 +780,7 @@ func (p *SQLProvider) SaveTOTPConfiguration(ctx context.Context, config model.TO  }  // UpdateTOTPConfigurationSignIn updates a registered Webauthn devices sign in information. -func (p *SQLProvider) UpdateTOTPConfigurationSignIn(ctx context.Context, id int, lastUsedAt *time.Time) (err error) { +func (p *SQLProvider) UpdateTOTPConfigurationSignIn(ctx context.Context, id int, lastUsedAt sql.NullTime) (err error) {  	if _, err = p.db.ExecContext(ctx, p.sqlUpdateTOTPConfigRecordSignIn, lastUsedAt, id); err != nil {  		return fmt.Errorf("error updating TOTP configuration id %d: %w", id, err)  	} @@ -841,7 +871,7 @@ func (p *SQLProvider) SaveWebauthnDevice(ctx context.Context, device model.Webau  }  // UpdateWebauthnDeviceSignIn updates a registered Webauthn devices sign in information. -func (p *SQLProvider) UpdateWebauthnDeviceSignIn(ctx context.Context, id int, rpid string, lastUsedAt *time.Time, signCount uint32, cloneWarning bool) (err error) { +func (p *SQLProvider) UpdateWebauthnDeviceSignIn(ctx context.Context, id int, rpid string, lastUsedAt sql.NullTime, signCount uint32, cloneWarning bool) (err error) {  	if _, err = p.db.ExecContext(ctx, p.sqlUpdateWebauthnDeviceRecordSignIn, rpid, lastUsedAt, signCount, cloneWarning, id); err != nil {  		return fmt.Errorf("error updating Webauthn signin metadata for id '%x': %w", id, err)  	} diff --git a/internal/storage/sql_provider_backend_postgres.go b/internal/storage/sql_provider_backend_postgres.go index 06fc40c78..8a3ee7779 100644 --- a/internal/storage/sql_provider_backend_postgres.go +++ b/internal/storage/sql_provider_backend_postgres.go @@ -32,6 +32,7 @@ func NewPostgreSQLProvider(config *schema.Configuration) (provider *PostgreSQLPr  	provider.sqlUpsertPreferred2FAMethod = fmt.Sprintf(queryFmtUpsertPreferred2FAMethodPostgreSQL, tableUserPreferences)  	provider.sqlUpsertEncryptionValue = fmt.Sprintf(queryFmtUpsertEncryptionValuePostgreSQL, tableEncryption)  	provider.sqlUpsertOAuth2BlacklistedJTI = fmt.Sprintf(queryFmtUpsertOAuth2BlacklistedJTIPostgreSQL, tableOAuth2BlacklistedJTI) +	provider.sqlInsertOAuth2ConsentPreConfiguration = fmt.Sprintf(queryFmtInsertOAuth2ConsentPreConfigurationPostgreSQL, tableOAuth2ConsentPreConfiguration)  	// PostgreSQL requires rebinding of any query that contains a '?' placeholder to use the '$#' notation placeholders.  	provider.sqlFmtRenameTable = provider.db.Rebind(provider.sqlFmtRenameTable) @@ -77,12 +78,13 @@ func NewPostgreSQLProvider(config *schema.Configuration) (provider *PostgreSQLPr  	provider.sqlSelectEncryptionValue = provider.db.Rebind(provider.sqlSelectEncryptionValue) +	provider.sqlSelectOAuth2ConsentPreConfigurations = provider.db.Rebind(provider.sqlSelectOAuth2ConsentPreConfigurations) +  	provider.sqlInsertOAuth2ConsentSession = provider.db.Rebind(provider.sqlInsertOAuth2ConsentSession)  	provider.sqlUpdateOAuth2ConsentSessionSubject = provider.db.Rebind(provider.sqlUpdateOAuth2ConsentSessionSubject)  	provider.sqlUpdateOAuth2ConsentSessionResponse = provider.db.Rebind(provider.sqlUpdateOAuth2ConsentSessionResponse)  	provider.sqlUpdateOAuth2ConsentSessionGranted = provider.db.Rebind(provider.sqlUpdateOAuth2ConsentSessionGranted)  	provider.sqlSelectOAuth2ConsentSessionByChallengeID = provider.db.Rebind(provider.sqlSelectOAuth2ConsentSessionByChallengeID) -	provider.sqlSelectOAuth2ConsentSessionsPreConfigured = provider.db.Rebind(provider.sqlSelectOAuth2ConsentSessionsPreConfigured)  	provider.sqlInsertOAuth2AuthorizeCodeSession = provider.db.Rebind(provider.sqlInsertOAuth2AuthorizeCodeSession)  	provider.sqlRevokeOAuth2AuthorizeCodeSession = provider.db.Rebind(provider.sqlRevokeOAuth2AuthorizeCodeSession) diff --git a/internal/storage/sql_provider_queries.go b/internal/storage/sql_provider_queries.go index eedfca30a..327ab546f 100644 --- a/internal/storage/sql_provider_queries.go +++ b/internal/storage/sql_provider_queries.go @@ -234,22 +234,30 @@ const (  )  const ( -	queryFmtSelectOAuth2ConsentSessionByChallengeID = ` -		SELECT id, challenge_id, client_id, subject, authorized, granted, requested_at, responded_at, expires_at, -		form_data, requested_scopes, granted_scopes, requested_audience, granted_audience +	queryFmtSelectOAuth2ConsentPreConfigurations = ` +		SELECT id, client_id, subject, created_at, expires_at, revoked, scopes, audience  		FROM %s -		WHERE challenge_id = ?;` +		WHERE client_id = ? AND subject = ? AND +			  revoked = FALSE AND (expires_at IS NULL OR expires_at >= CURRENT_TIMESTAMP);` + +	queryFmtInsertOAuth2ConsentPreConfiguration = ` +		INSERT INTO %s (client_id, subject, created_at, expires_at, revoked, scopes, audience) +		VALUES(?, ?, ?, ?, ?, ?, ?);` -	queryFmtSelectOAuth2ConsentSessionsPreConfigured = ` -		SELECT id, challenge_id, client_id, subject, authorized, granted, requested_at, responded_at, expires_at, -		form_data, requested_scopes, granted_scopes, requested_audience, granted_audience +	queryFmtInsertOAuth2ConsentPreConfigurationPostgreSQL = ` +		INSERT INTO %s (client_id, subject, created_at, expires_at, revoked, scopes, audience) +		VALUES($1, $2, $3, $4, $5, $6, $7) +		RETURNING id;` + +	queryFmtSelectOAuth2ConsentSessionByChallengeID = ` +		SELECT id, challenge_id, client_id, subject, authorized, granted, requested_at, responded_at, +		form_data, requested_scopes, granted_scopes, requested_audience, granted_audience, preconfiguration  		FROM %s -		WHERE client_id = ? AND subject = ? AND -			  authorized = TRUE AND granted = TRUE AND expires_at IS NOT NULL AND expires_at >= CURRENT_TIMESTAMP;` +		WHERE challenge_id = ?;`  	queryFmtInsertOAuth2ConsentSession = ` -		INSERT INTO %s (challenge_id, client_id, subject, authorized, granted, requested_at, responded_at, expires_at, -		form_data, requested_scopes, granted_scopes, requested_audience, granted_audience) +		INSERT INTO %s (challenge_id, client_id, subject, authorized, granted, requested_at, responded_at, +		form_data, requested_scopes, granted_scopes, requested_audience, granted_audience, preconfiguration)  		VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);`  	queryFmtUpdateOAuth2ConsentSessionSubject = ` @@ -259,7 +267,7 @@ const (  	queryFmtUpdateOAuth2ConsentSessionResponse = `  		UPDATE %s -		SET authorized = ?, responded_at = CURRENT_TIMESTAMP, expires_at = ?, granted_scopes = ?, granted_audience = ? +		SET authorized = ?, responded_at = CURRENT_TIMESTAMP, granted_scopes = ?, granted_audience = ?, preconfiguration = ?  		WHERE id = ? AND responded_at IS NULL;`  	queryFmtUpdateOAuth2ConsentSessionGranted = ` diff --git a/internal/storage/sql_provider_schema_test.go b/internal/storage/sql_provider_schema_test.go index 8769e52bf..16352e451 100644 --- a/internal/storage/sql_provider_schema_test.go +++ b/internal/storage/sql_provider_schema_test.go @@ -31,98 +31,98 @@ func TestShouldReturnErrOnTargetSameAsCurrent(t *testing.T) {  func TestShouldReturnErrOnUpMigrationTargetVersionLessTHanCurrent(t *testing.T) {  	assert.EqualError(t, -		schemaMigrateChecks(providerPostgres, true, 0, testLatestVersion), -		fmt.Sprintf(ErrFmtMigrateUpTargetLessThanCurrent, 0, testLatestVersion)) +		schemaMigrateChecks(providerPostgres, true, 0, LatestVersion), +		fmt.Sprintf(ErrFmtMigrateUpTargetLessThanCurrent, 0, LatestVersion))  	assert.NoError(t, -		schemaMigrateChecks(providerPostgres, true, testLatestVersion, 0)) +		schemaMigrateChecks(providerPostgres, true, LatestVersion, 0))  	assert.EqualError(t, -		schemaMigrateChecks(providerSQLite, true, 0, testLatestVersion), -		fmt.Sprintf(ErrFmtMigrateUpTargetLessThanCurrent, 0, testLatestVersion)) +		schemaMigrateChecks(providerSQLite, true, 0, LatestVersion), +		fmt.Sprintf(ErrFmtMigrateUpTargetLessThanCurrent, 0, LatestVersion))  	assert.NoError(t, -		schemaMigrateChecks(providerSQLite, true, testLatestVersion, 0)) +		schemaMigrateChecks(providerSQLite, true, LatestVersion, 0))  	assert.EqualError(t, -		schemaMigrateChecks(providerMySQL, true, 0, testLatestVersion), -		fmt.Sprintf(ErrFmtMigrateUpTargetLessThanCurrent, 0, testLatestVersion)) +		schemaMigrateChecks(providerMySQL, true, 0, LatestVersion), +		fmt.Sprintf(ErrFmtMigrateUpTargetLessThanCurrent, 0, LatestVersion))  	assert.NoError(t, -		schemaMigrateChecks(providerMySQL, true, testLatestVersion, 0)) +		schemaMigrateChecks(providerMySQL, true, LatestVersion, 0))  }  func TestMigrationUpShouldReturnErrOnAlreadyLatest(t *testing.T) {  	assert.Equal(t,  		ErrSchemaAlreadyUpToDate, -		schemaMigrateChecks(providerPostgres, true, SchemaLatest, testLatestVersion)) +		schemaMigrateChecks(providerPostgres, true, SchemaLatest, LatestVersion))  	assert.Equal(t,  		ErrSchemaAlreadyUpToDate, -		schemaMigrateChecks(providerMySQL, true, SchemaLatest, testLatestVersion)) +		schemaMigrateChecks(providerMySQL, true, SchemaLatest, LatestVersion))  	assert.Equal(t,  		ErrSchemaAlreadyUpToDate, -		schemaMigrateChecks(providerSQLite, true, SchemaLatest, testLatestVersion)) +		schemaMigrateChecks(providerSQLite, true, SchemaLatest, LatestVersion))  }  func TestShouldReturnErrOnVersionDoesntExits(t *testing.T) {  	assert.EqualError(t, -		schemaMigrateChecks(providerPostgres, true, SchemaLatest-1, testLatestVersion), -		fmt.Sprintf(ErrFmtMigrateUpTargetGreaterThanLatest, SchemaLatest-1, testLatestVersion)) +		schemaMigrateChecks(providerPostgres, true, SchemaLatest-1, LatestVersion), +		fmt.Sprintf(ErrFmtMigrateUpTargetGreaterThanLatest, SchemaLatest-1, LatestVersion))  	assert.EqualError(t, -		schemaMigrateChecks(providerMySQL, true, SchemaLatest-1, testLatestVersion), -		fmt.Sprintf(ErrFmtMigrateUpTargetGreaterThanLatest, SchemaLatest-1, testLatestVersion)) +		schemaMigrateChecks(providerMySQL, true, SchemaLatest-1, LatestVersion), +		fmt.Sprintf(ErrFmtMigrateUpTargetGreaterThanLatest, SchemaLatest-1, LatestVersion))  	assert.EqualError(t, -		schemaMigrateChecks(providerSQLite, true, SchemaLatest-1, testLatestVersion), -		fmt.Sprintf(ErrFmtMigrateUpTargetGreaterThanLatest, SchemaLatest-1, testLatestVersion)) +		schemaMigrateChecks(providerSQLite, true, SchemaLatest-1, LatestVersion), +		fmt.Sprintf(ErrFmtMigrateUpTargetGreaterThanLatest, SchemaLatest-1, LatestVersion))  }  func TestMigrationDownShouldReturnErrOnTargetLessThanPre1(t *testing.T) {  	assert.EqualError(t, -		schemaMigrateChecks(providerSQLite, false, -4, testLatestVersion), +		schemaMigrateChecks(providerSQLite, false, -4, LatestVersion),  		fmt.Sprintf(ErrFmtMigrateDownTargetLessThanMinimum, -4))  	assert.EqualError(t, -		schemaMigrateChecks(providerMySQL, false, -2, testLatestVersion), +		schemaMigrateChecks(providerMySQL, false, -2, LatestVersion),  		fmt.Sprintf(ErrFmtMigrateDownTargetLessThanMinimum, -2))  	assert.EqualError(t, -		schemaMigrateChecks(providerPostgres, false, -2, testLatestVersion), +		schemaMigrateChecks(providerPostgres, false, -2, LatestVersion),  		fmt.Sprintf(ErrFmtMigrateDownTargetLessThanMinimum, -2))  	assert.NoError(t, -		schemaMigrateChecks(providerPostgres, false, -1, testLatestVersion)) +		schemaMigrateChecks(providerPostgres, false, -1, LatestVersion))  }  func TestMigrationDownShouldReturnErrOnTargetVersionGreaterThanCurrent(t *testing.T) {  	assert.EqualError(t, -		schemaMigrateChecks(providerSQLite, false, testLatestVersion, 0), -		fmt.Sprintf(ErrFmtMigrateDownTargetGreaterThanCurrent, testLatestVersion, 0)) +		schemaMigrateChecks(providerSQLite, false, LatestVersion, 0), +		fmt.Sprintf(ErrFmtMigrateDownTargetGreaterThanCurrent, LatestVersion, 0))  	assert.EqualError(t, -		schemaMigrateChecks(providerMySQL, false, testLatestVersion, 0), -		fmt.Sprintf(ErrFmtMigrateDownTargetGreaterThanCurrent, testLatestVersion, 0)) +		schemaMigrateChecks(providerMySQL, false, LatestVersion, 0), +		fmt.Sprintf(ErrFmtMigrateDownTargetGreaterThanCurrent, LatestVersion, 0))  	assert.EqualError(t, -		schemaMigrateChecks(providerPostgres, false, testLatestVersion, 0), -		fmt.Sprintf(ErrFmtMigrateDownTargetGreaterThanCurrent, testLatestVersion, 0)) +		schemaMigrateChecks(providerPostgres, false, LatestVersion, 0), +		fmt.Sprintf(ErrFmtMigrateDownTargetGreaterThanCurrent, LatestVersion, 0))  }  func TestShouldReturnErrWhenCurrentIsGreaterThanLatest(t *testing.T) {  	assert.EqualError(t,  		schemaMigrateChecks(providerPostgres, true, SchemaLatest-4, SchemaLatest-5), -		fmt.Sprintf(errFmtSchemaCurrentGreaterThanLatestKnown, testLatestVersion)) +		fmt.Sprintf(errFmtSchemaCurrentGreaterThanLatestKnown, LatestVersion))  	assert.EqualError(t,  		schemaMigrateChecks(providerMySQL, true, SchemaLatest-4, SchemaLatest-5), -		fmt.Sprintf(errFmtSchemaCurrentGreaterThanLatestKnown, testLatestVersion)) +		fmt.Sprintf(errFmtSchemaCurrentGreaterThanLatestKnown, LatestVersion))  	assert.EqualError(t,  		schemaMigrateChecks(providerSQLite, true, SchemaLatest-4, SchemaLatest-5), -		fmt.Sprintf(errFmtSchemaCurrentGreaterThanLatestKnown, testLatestVersion)) +		fmt.Sprintf(errFmtSchemaCurrentGreaterThanLatestKnown, LatestVersion))  }  func TestSchemaVersionToString(t *testing.T) { diff --git a/internal/storage/sql_rows.go b/internal/storage/sql_rows.go index 148dbf38d..f89c19121 100644 --- a/internal/storage/sql_rows.go +++ b/internal/storage/sql_rows.go @@ -8,13 +8,13 @@ import (  	"github.com/authelia/authelia/v4/internal/model"  ) -// ConsentSessionRows holds and assists with retrieving multiple model.OAuth2ConsentSession rows. -type ConsentSessionRows struct { +// ConsentPreConfigRows holds and assists with retrieving multiple model.OAuth2ConsentSession rows. +type ConsentPreConfigRows struct {  	rows *sqlx.Rows  }  // Next is the row iterator. -func (r *ConsentSessionRows) Next() bool { +func (r *ConsentPreConfigRows) Next() bool {  	if r.rows == nil {  		return false  	} @@ -23,7 +23,7 @@ func (r *ConsentSessionRows) Next() bool {  }  // Close the rows. -func (r *ConsentSessionRows) Close() (err error) { +func (r *ConsentPreConfigRows) Close() (err error) {  	if r.rows == nil {  		return nil  	} @@ -32,16 +32,16 @@ func (r *ConsentSessionRows) Close() (err error) {  }  // Get returns the *model.OAuth2ConsentSession or scan error. -func (r *ConsentSessionRows) Get() (consent *model.OAuth2ConsentSession, err error) { +func (r *ConsentPreConfigRows) Get() (config *model.OAuth2ConsentPreConfig, err error) {  	if r.rows == nil {  		return nil, sql.ErrNoRows  	} -	consent = &model.OAuth2ConsentSession{} +	config = &model.OAuth2ConsentPreConfig{} -	if err = r.rows.StructScan(consent); err != nil { +	if err = r.rows.StructScan(config); err != nil {  		return nil, err  	} -	return consent, nil +	return config, nil  } diff --git a/internal/suites/example/compose/oidc-client/docker-compose.yml b/internal/suites/example/compose/oidc-client/docker-compose.yml index 3a9ecc808..c9645d8a9 100644 --- a/internal/suites/example/compose/oidc-client/docker-compose.yml +++ b/internal/suites/example/compose/oidc-client/docker-compose.yml @@ -2,7 +2,7 @@  version: '3'  services:    oidc-client: -    image: ghcr.io/authelia/oidc-tester-app:master-01ff268 +    image: ghcr.io/authelia/oidc-tester-app:master-aeac7f4      command: /entrypoint.sh      depends_on:        - authelia-backend diff --git a/internal/suites/scenario_oidc_test.go b/internal/suites/scenario_oidc_test.go index f303d4615..65b867471 100644 --- a/internal/suites/scenario_oidc_test.go +++ b/internal/suites/scenario_oidc_test.go @@ -10,6 +10,8 @@ import (  	"github.com/stretchr/testify/assert"  	"github.com/stretchr/testify/suite" + +	"github.com/authelia/authelia/v4/internal/oidc"  )  type OIDCScenario struct { @@ -101,36 +103,44 @@ func (s *OIDCScenario) TestShouldAuthorizeAccessToOIDCApp() {  	rBase64 := regexp.MustCompile(`^[-_A-Za-z0-9+\\/]+([=]{0,3})$`)  	testCases := []struct { -		desc, elementID, elementText string -		pattern                      *regexp.Regexp +		desc, elementID string +		expected        any  	}{ -		{"welcome", "welcome", "Logged in as john!", nil}, -		{"at_hash", "claim-at_hash", "", rBase64}, -		{"jti", "claim-jti", "", rUUID}, -		{"iat", "claim-iat", "", rInteger}, -		{"nbf", "claim-nbf", "", rInteger}, -		{"rat", "claim-rat", "", rInteger}, -		{"expires", "claim-exp", "", rInteger}, -		{"amr", "claim-amr", "pwd, otp, mfa", nil}, -		{"acr", "claim-acr", "", nil}, -		{"issuer", "claim-iss", "https://login.example.com:8080", nil}, -		{"name", "claim-name", "John Doe", nil}, -		{"preferred_username", "claim-preferred_username", "john", nil}, -		{"groups", "claim-groups", "admins, dev", nil}, -		{"email", "claim-email", "john.doe@authelia.com", nil}, -		{"email_verified", "claim-email_verified", "", rBoolean}, +		{"welcome", "welcome", "Logged in as john!"}, +		{oidc.ClaimAccessTokenHash, "", rBase64}, +		{oidc.ClaimJWTID, "", rUUID}, +		{oidc.ClaimIssuedAt, "", rInteger}, +		{oidc.ClaimSubject, "", rUUID}, +		{oidc.ClaimNotBefore, "", rInteger}, +		{oidc.ClaimRequestedAt, "", rInteger}, +		{oidc.ClaimExpirationTime, "", rInteger}, +		{oidc.ClaimAuthenticationMethodsReference, "", "pwd, otp, mfa"}, +		{oidc.ClaimAuthenticationContextClassReference, "", ""}, +		{oidc.ClaimIssuer, "", "https://login.example.com:8080"}, +		{oidc.ClaimFullName, "", "John Doe"}, +		{oidc.ClaimPreferredUsername, "", "john"}, +		{oidc.ClaimGroups, "", "admins, dev"}, +		{oidc.ClaimPreferredEmail, "", "john.doe@authelia.com"}, +		{oidc.ClaimEmailVerified, "", rBoolean},  	} -	var text string +	var actual string  	for _, tc := range testCases {  		s.T().Run(fmt.Sprintf("check_claims/%s", tc.desc), func(t *testing.T) { -			text, err = s.WaitElementLocatedByID(t, s.Context(ctx), tc.elementID).Text() +			switch tc.elementID { +			case "": +				actual, err = s.WaitElementLocatedByID(t, s.Context(ctx), "claim-"+tc.desc).Text() +			default: +				actual, err = s.WaitElementLocatedByID(t, s.Context(ctx), tc.elementID).Text() +			} +  			assert.NoError(t, err) -			if tc.pattern == nil { -				assert.Equal(t, tc.elementText, text) -			} else { -				assert.Regexp(t, tc.pattern, text) +			switch expected := tc.expected.(type) { +			case *regexp.Regexp: +				assert.Regexp(t, expected, actual) +			default: +				assert.Equal(t, expected, actual)  			}  		})  	} diff --git a/web/src/constants/SearchParams.ts b/web/src/constants/SearchParams.ts new file mode 100644 index 000000000..c23f1a2f6 --- /dev/null +++ b/web/src/constants/SearchParams.ts @@ -0,0 +1 @@ +export const Identifier = "id"; diff --git a/web/src/hooks/Consent.ts b/web/src/hooks/Consent.ts deleted file mode 100644 index 08300c6e6..000000000 --- a/web/src/hooks/Consent.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { useRemoteCall } from "@hooks/RemoteCall"; -import { getConsentResponse } from "@services/Consent"; - -export function useConsentResponse() { -    return useRemoteCall(getConsentResponse, []); -} diff --git a/web/src/hooks/ConsentID.ts b/web/src/hooks/ConsentID.ts deleted file mode 100644 index b780fc601..000000000 --- a/web/src/hooks/ConsentID.ts +++ /dev/null @@ -1,8 +0,0 @@ -import queryString from "query-string"; -import { useLocation } from "react-router-dom"; - -export function useConsentID() { -    const location = useLocation(); -    const queryParams = queryString.parse(location.search); -    return queryParams && "consent_id" in queryParams ? (queryParams["consent_id"] as string) : undefined; -} diff --git a/web/src/hooks/Workflow.ts b/web/src/hooks/Workflow.ts index f4a56a4cf..659b5546d 100644 --- a/web/src/hooks/Workflow.ts +++ b/web/src/hooks/Workflow.ts @@ -1,8 +1,10 @@ -import queryString from "query-string"; -import { useLocation } from "react-router-dom"; +import { useSearchParams } from "react-router-dom"; -export function useWorkflow() { -    const location = useLocation(); -    const queryParams = queryString.parse(location.search); -    return queryParams && "workflow" in queryParams ? (queryParams["workflow"] as string) : undefined; +export function useWorkflow(): [string | undefined, string | undefined] { +    const [searchParams] = useSearchParams(); + +    const workflow = searchParams.get("workflow"); +    const id = searchParams.get("workflow_id"); + +    return [workflow === null ? undefined : workflow, id === null ? undefined : id];  } diff --git a/web/src/models/Webauthn.ts b/web/src/models/Webauthn.ts index bdbfb2f6a..bbd75503d 100644 --- a/web/src/models/Webauthn.ts +++ b/web/src/models/Webauthn.ts @@ -76,6 +76,8 @@ export interface PublicKeyCredentialJSON      clientExtensionResults: AuthenticationExtensionsClientOutputs;      response: AuthenticatorAssertionResponseJSON;      targetURL?: string; +    workflow?: string; +    workflowID?: string;  }  export enum AttestationResult { diff --git a/web/src/services/Consent.ts b/web/src/services/Consent.ts index a3a26380e..ed535f7d0 100644 --- a/web/src/services/Consent.ts +++ b/web/src/services/Consent.ts @@ -2,8 +2,8 @@ import { ConsentPath } from "@services/Api";  import { Get, Post } from "@services/Client";  interface ConsentPostRequestBody { +    id?: string;      client_id: string; -    consent_id?: string;      consent: boolean;      pre_configure: boolean;  } @@ -21,23 +21,23 @@ export interface ConsentGetResponseBody {  }  export function getConsentResponse(consentID: string) { -    return Get<ConsentGetResponseBody>(ConsentPath + "?consent_id=" + consentID); +    return Get<ConsentGetResponseBody>(ConsentPath + "?id=" + consentID);  } -export function acceptConsent(preConfigure: boolean, clientID: string, consentID?: string) { +export function acceptConsent(preConfigure: boolean, clientID: string, consentID: string | null) {      const body: ConsentPostRequestBody = { +        id: consentID === null ? undefined : consentID,          client_id: clientID, -        consent_id: consentID,          consent: true,          pre_configure: preConfigure,      };      return Post<ConsentPostResponseBody>(ConsentPath, body);  } -export function rejectConsent(clientID: string, consentID?: string) { +export function rejectConsent(clientID: string, consentID: string | null) {      const body: ConsentPostRequestBody = { +        id: consentID === null ? undefined : consentID,          client_id: clientID, -        consent_id: consentID,          consent: false,          pre_configure: false,      }; diff --git a/web/src/services/OneTimePassword.ts b/web/src/services/OneTimePassword.ts index 51e9f3164..ec66e9e83 100644 --- a/web/src/services/OneTimePassword.ts +++ b/web/src/services/OneTimePassword.ts @@ -2,21 +2,20 @@ import { CompleteTOTPSignInPath } from "@services/Api";  import { PostWithOptionalResponse } from "@services/Client";  import { SignInResponse } from "@services/SignIn"; -interface CompleteTOTPSigninBody { +interface CompleteTOTPSignInBody {      token: string;      targetURL?: string;      workflow?: string; +    workflowID?: string;  } -export function completeTOTPSignIn(passcode: string, targetURL?: string, workflow?: string) { -    const body: CompleteTOTPSigninBody = { token: `${passcode}` }; -    if (targetURL) { -        body.targetURL = targetURL; -    } - -    if (workflow) { -        body.workflow = workflow; -    } +export function completeTOTPSignIn(passcode: string, targetURL?: string, workflow?: string, workflowID?: string) { +    const body: CompleteTOTPSignInBody = { +        token: `${passcode}`, +        targetURL: targetURL, +        workflow: workflow, +        workflowID: workflowID, +    };      return PostWithOptionalResponse<SignInResponse>(CompleteTOTPSignInPath, body);  } diff --git a/web/src/services/PushNotification.ts b/web/src/services/PushNotification.ts index 2a7e40872..c24b08a50 100644 --- a/web/src/services/PushNotification.ts +++ b/web/src/services/PushNotification.ts @@ -5,20 +5,18 @@ import {  } from "@services/Api";  import { Get, PostWithOptionalResponse } from "@services/Client"; -interface CompletePushSigninBody { +interface CompletePushSignInBody {      targetURL?: string;      workflow?: string; +    workflowID?: string;  } -export function completePushNotificationSignIn(targetURL?: string, workflow?: string) { -    const body: CompletePushSigninBody = {}; -    if (targetURL) { -        body.targetURL = targetURL; -    } - -    if (workflow) { -        body.workflow = workflow; -    } +export function completePushNotificationSignIn(targetURL?: string, workflow?: string, workflowID?: string) { +    const body: CompletePushSignInBody = { +        targetURL: targetURL, +        workflow: workflow, +        workflowID: workflowID, +    };      return PostWithOptionalResponse<DuoSignInResponse>(CompletePushNotificationSignInPath, body);  } diff --git a/web/src/services/Webauthn.ts b/web/src/services/Webauthn.ts index 176fd840d..7dc2de6ce 100644 --- a/web/src/services/Webauthn.ts +++ b/web/src/services/Webauthn.ts @@ -131,6 +131,8 @@ function encodeAttestationPublicKeyCredential(  function encodeAssertionPublicKeyCredential(      credential: PublicKeyCredential,      targetURL: string | undefined, +    workflow: string | undefined, +    workflowID: string | undefined,  ): PublicKeyCredentialJSON {      const response = credential.response as AuthenticatorAssertionResponse; @@ -154,6 +156,8 @@ function encodeAssertionPublicKeyCredential(              userHandle: userHandle,          },          targetURL: targetURL, +        workflow: workflow, +        workflowID: workflowID,      };  } @@ -319,8 +323,10 @@ async function postAttestationPublicKeyCredentialResult(  export async function postAssertionPublicKeyCredentialResult(      credential: PublicKeyCredential,      targetURL: string | undefined, +    workflow?: string, +    workflowID?: string,  ): Promise<AxiosResponse<ServiceResponse<SignInResponse>>> { -    const credentialJSON = encodeAssertionPublicKeyCredential(credential, targetURL); +    const credentialJSON = encodeAssertionPublicKeyCredential(credential, targetURL, workflow, workflowID);      return axios.post<ServiceResponse<SignInResponse>>(WebauthnAssertionPath, credentialJSON);  } @@ -353,7 +359,11 @@ export async function performAttestationCeremony(token: string): Promise<Attesta      return AttestationResult.Failure;  } -export async function performAssertionCeremony(targetURL: string | undefined): Promise<AssertionResult> { +export async function performAssertionCeremony( +    targetURL?: string, +    workflow?: string, +    workflowID?: string, +): Promise<AssertionResult> {      const assertionRequestOpts = await getAssertionRequestOptions();      if (assertionRequestOpts.status !== 200 || assertionRequestOpts.options == null) { @@ -368,7 +378,12 @@ export async function performAssertionCeremony(targetURL: string | undefined): P          return AssertionResult.Failure;      } -    const response = await postAssertionPublicKeyCredentialResult(assertionResult.credential, targetURL); +    const response = await postAssertionPublicKeyCredentialResult( +        assertionResult.credential, +        targetURL, +        workflow, +        workflowID, +    );      if (response.data.status === "OK" && response.status === 200) {          return AssertionResult.Success; diff --git a/web/src/views/LoginPortal/ConsentView/ConsentView.tsx b/web/src/views/LoginPortal/ConsentView/ConsentView.tsx index 1eb3f6625..642d14fce 100644 --- a/web/src/views/LoginPortal/ConsentView/ConsentView.tsx +++ b/web/src/views/LoginPortal/ConsentView/ConsentView.tsx @@ -16,10 +16,10 @@ import {  } from "@mui/material";  import makeStyles from "@mui/styles/makeStyles";  import { useTranslation } from "react-i18next"; -import { useNavigate } from "react-router-dom"; +import { useNavigate, useSearchParams } from "react-router-dom";  import { IndexRoute } from "@constants/Routes"; -import { useConsentID } from "@hooks/ConsentID"; +import { Identifier } from "@constants/SearchParams";  import { useNotifications } from "@hooks/NotificationsContext";  import { useRedirector } from "@hooks/Redirector";  import { useUserInfoGET } from "@hooks/UserInfo"; @@ -50,8 +50,9 @@ const ConsentView = function (props: Props) {      const styles = useStyles();      const { t: translate } = useTranslation();      const navigate = useNavigate(); +    const [searchParams] = useSearchParams();      const redirect = useRedirector(); -    const consentID = useConsentID(); +    const consentID = searchParams.get(Identifier);      const { createErrorNotification, resetNotification } = useNotifications();      const [response, setResponse] = useState<ConsentGetResponseBody | undefined>(undefined);      const [error, setError] = useState<any>(undefined); @@ -68,7 +69,7 @@ const ConsentView = function (props: Props) {      }, [fetchUserInfo]);      useEffect(() => { -        if (consentID) { +        if (consentID !== null) {              getConsentResponse(consentID)                  .then((r) => {                      setResponse(r); diff --git a/web/src/views/LoginPortal/FirstFactor/FirstFactorForm.tsx b/web/src/views/LoginPortal/FirstFactor/FirstFactorForm.tsx index 5c03eec52..f6ed2a055 100644 --- a/web/src/views/LoginPortal/FirstFactor/FirstFactorForm.tsx +++ b/web/src/views/LoginPortal/FirstFactor/FirstFactorForm.tsx @@ -34,7 +34,7 @@ const FirstFactorForm = function (props: Props) {      const navigate = useNavigate();      const redirectionURL = useRedirectionURL();      const requestMethod = useRequestMethod(); -    const workflow = useWorkflow(); +    const [workflow] = useWorkflow();      const loginChannel = useMemo(() => new BroadcastChannel<boolean>("login"), []);      const [rememberMe, setRememberMe] = useState(false); diff --git a/web/src/views/LoginPortal/LoginPortal.tsx b/web/src/views/LoginPortal/LoginPortal.tsx index bb1d7daf0..bcb66c86b 100644 --- a/web/src/views/LoginPortal/LoginPortal.tsx +++ b/web/src/views/LoginPortal/LoginPortal.tsx @@ -1,6 +1,6 @@  import React, { Fragment, ReactNode, useCallback, useEffect, useState } from "react"; -import { Route, Routes, useLocation, useNavigate } from "react-router-dom"; +import { Route, Routes, useLocation, useNavigate, useSearchParams } from "react-router-dom";  import {      AuthenticatedRoute, @@ -14,10 +14,8 @@ import { useConfiguration } from "@hooks/Configuration";  import { useNotifications } from "@hooks/NotificationsContext";  import { useRedirectionURL } from "@hooks/RedirectionURL";  import { useRedirector } from "@hooks/Redirector"; -import { useRequestMethod } from "@hooks/RequestMethod";  import { useAutheliaState } from "@hooks/State";  import { useUserInfoPOST } from "@hooks/UserInfo"; -import { useWorkflow } from "@hooks/Workflow";  import { SecondFactorMethod } from "@models/Methods";  import { checkSafeRedirection } from "@services/SafeRedirection";  import { AuthenticationLevel } from "@services/State"; @@ -41,8 +39,6 @@ const LoginPortal = function (props: Props) {      const navigate = useNavigate();      const location = useLocation();      const redirectionURL = useRedirectionURL(); -    const requestMethod = useRequestMethod(); -    const workflow = useWorkflow();      const { createErrorNotification } = useNotifications();      const [firstFactorDisabled, setFirstFactorDisabled] = useState(true);      const [broadcastRedirect, setBroadcastRedirect] = useState(false); @@ -51,16 +47,23 @@ const LoginPortal = function (props: Props) {      const [state, fetchState, , fetchStateError] = useAutheliaState();      const [userInfo, fetchUserInfo, , fetchUserInfoError] = useUserInfoPOST();      const [configuration, fetchConfiguration, , fetchConfigurationError] = useConfiguration(); +    const [searchParams] = useSearchParams();      const redirect = useCallback( -        (pathname: string, search?: string) => { -            if (search) { -                navigate({ pathname: pathname, search: search }); +        ( +            pathname: string, +            preserveSearchParams: boolean = true, +            searchParamsOverride: URLSearchParams | undefined = undefined, +        ) => { +            if (searchParamsOverride && URLSearchParamsHasValues(searchParamsOverride)) { +                navigate({ pathname: pathname, search: `?${searchParamsOverride.toString()}` }); +            } else if (preserveSearchParams && URLSearchParamsHasValues(searchParams)) { +                navigate({ pathname: pathname, search: `?${searchParams.toString()}` });              } else {                  navigate({ pathname: pathname });              }          }, -        [navigate], +        [navigate, searchParams],      );      // Fetch the state when portal is mounted. @@ -132,25 +135,19 @@ const LoginPortal = function (props: Props) {                  return;              } -            const search = redirectionURL -                ? `?rd=${encodeURIComponent(redirectionURL)}${requestMethod ? `&rm=${requestMethod}` : ""}${ -                      workflow ? `&workflow=${workflow}` : "" -                  }` -                : undefined; -              if (state.authentication_level === AuthenticationLevel.Unauthenticated) {                  setFirstFactorDisabled(false); -                redirect(IndexRoute, search); +                redirect(IndexRoute);              } else if (state.authentication_level >= AuthenticationLevel.OneFactor && userInfo && configuration) {                  if (configuration.available_methods.size === 0) { -                    redirect(AuthenticatedRoute); +                    redirect(AuthenticatedRoute, false);                  } else {                      if (userInfo.method === SecondFactorMethod.Webauthn) { -                        redirect(`${SecondFactorRoute}${SecondFactorWebauthnSubRoute}`, search); +                        redirect(`${SecondFactorRoute}${SecondFactorWebauthnSubRoute}`);                      } else if (userInfo.method === SecondFactorMethod.MobilePush) { -                        redirect(`${SecondFactorRoute}${SecondFactorPushSubRoute}`, search); +                        redirect(`${SecondFactorRoute}${SecondFactorPushSubRoute}`);                      } else { -                        redirect(`${SecondFactorRoute}${SecondFactorTOTPSubRoute}`, search); +                        redirect(`${SecondFactorRoute}${SecondFactorTOTPSubRoute}`);                      }                  }              } @@ -158,8 +155,6 @@ const LoginPortal = function (props: Props) {      }, [          state,          redirectionURL, -        requestMethod, -        workflow,          redirect,          userInfo,          setFirstFactorDisabled, @@ -249,3 +244,7 @@ function ComponentOrLoading(props: ComponentOrLoadingProps) {          </Fragment>      );  } + +function URLSearchParamsHasValues(params?: URLSearchParams) { +    return params ? !params.entries().next().done : false; +} diff --git a/web/src/views/LoginPortal/SecondFactor/OneTimePasswordMethod.tsx b/web/src/views/LoginPortal/SecondFactor/OneTimePasswordMethod.tsx index 8f9309603..5cd60d08f 100644 --- a/web/src/views/LoginPortal/SecondFactor/OneTimePasswordMethod.tsx +++ b/web/src/views/LoginPortal/SecondFactor/OneTimePasswordMethod.tsx @@ -34,7 +34,7 @@ const OneTimePasswordMethod = function (props: Props) {          props.authenticationLevel === AuthenticationLevel.TwoFactor ? State.Success : State.Idle,      );      const redirectionURL = useRedirectionURL(); -    const workflow = useWorkflow(); +    const [workflow, workflowID] = useWorkflow();      const { t: translate } = useTranslation();      const { onSignInSuccess, onSignInError } = props; @@ -69,7 +69,7 @@ const OneTimePasswordMethod = function (props: Props) {          try {              setState(State.InProgress); -            const res = await completeTOTPSignIn(passcodeStr, redirectionURL, workflow); +            const res = await completeTOTPSignIn(passcodeStr, redirectionURL, workflow, workflowID);              setState(State.Success);              onSignInSuccessCallback(res ? res.redirect : undefined);          } catch (err) { @@ -84,6 +84,7 @@ const OneTimePasswordMethod = function (props: Props) {          passcode,          redirectionURL,          workflow, +        workflowID,          resp,          props.authenticationLevel,          props.registered, diff --git a/web/src/views/LoginPortal/SecondFactor/PushNotificationMethod.tsx b/web/src/views/LoginPortal/SecondFactor/PushNotificationMethod.tsx index ecccb345e..ea4b93890 100644 --- a/web/src/views/LoginPortal/SecondFactor/PushNotificationMethod.tsx +++ b/web/src/views/LoginPortal/SecondFactor/PushNotificationMethod.tsx @@ -45,7 +45,7 @@ const PushNotificationMethod = function (props: Props) {      const styles = useStyles();      const [state, setState] = useState(State.SignInInProgress);      const redirectionURL = useRedirectionURL(); -    const workflow = useWorkflow(); +    const [workflow, workflowID] = useWorkflow();      const mounted = useIsMountedRef();      const [enroll_url, setEnrollUrl] = useState("");      const [devices, setDevices] = useState([] as SelectableDevice[]); @@ -95,7 +95,7 @@ const PushNotificationMethod = function (props: Props) {          try {              setState(State.SignInInProgress); -            const res = await completePushNotificationSignIn(redirectionURL, workflow); +            const res = await completePushNotificationSignIn(redirectionURL, workflow, workflowID);              // If the request was initiated and the user changed 2FA method in the meantime,              // the process is interrupted to avoid updating state of unmounted component.              if (!mounted.current) return; @@ -139,6 +139,7 @@ const PushNotificationMethod = function (props: Props) {          props.duoSelfEnrollment,          redirectionURL,          workflow, +        workflowID,          mounted,          onSignInErrorCallback,          onSignInSuccessCallback, diff --git a/web/src/views/LoginPortal/SecondFactor/WebauthnMethod.tsx b/web/src/views/LoginPortal/SecondFactor/WebauthnMethod.tsx index 6b42dd1c1..96fa00fcf 100644 --- a/web/src/views/LoginPortal/SecondFactor/WebauthnMethod.tsx +++ b/web/src/views/LoginPortal/SecondFactor/WebauthnMethod.tsx @@ -9,6 +9,7 @@ import LinearProgressBar from "@components/LinearProgressBar";  import { useIsMountedRef } from "@hooks/Mounted";  import { useRedirectionURL } from "@hooks/RedirectionURL";  import { useTimer } from "@hooks/Timer"; +import { useWorkflow } from "@hooks/Workflow";  import { AssertionResult } from "@models/Webauthn";  import { AuthenticationLevel } from "@services/State";  import { @@ -40,6 +41,7 @@ const WebauthnMethod = function (props: Props) {      const [state, setState] = useState(State.WaitTouch);      const styles = useStyles();      const redirectionURL = useRedirectionURL(); +    const [workflow, workflowID] = useWorkflow();      const mounted = useIsMountedRef();      const [timerPercent, triggerTimer] = useTimer(signInTimeout * 1000 - 500); @@ -112,7 +114,12 @@ const WebauthnMethod = function (props: Props) {              setState(State.InProgress); -            const response = await postAssertionPublicKeyCredentialResult(result.credential, redirectionURL); +            const response = await postAssertionPublicKeyCredentialResult( +                result.credential, +                redirectionURL, +                workflow, +                workflowID, +            );              if (response.data.status === "OK" && response.status === 200) {                  onSignInSuccessCallback(response.data.data ? response.data.data.redirect : undefined); @@ -135,6 +142,8 @@ const WebauthnMethod = function (props: Props) {          onSignInErrorCallback,          onSignInSuccessCallback,          redirectionURL, +        workflow, +        workflowID,          mounted,          triggerTimer,          props.authenticationLevel,  | 
