diff options
| author | James Elliott <james-d-elliott@users.noreply.github.com> | 2025-02-22 22:03:33 +1100 | 
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-02-22 11:03:33 +0000 | 
| commit | e7d387ed9169dcdb4e8171db8ed20ec6ef376e0a (patch) | |
| tree | 96bfca916ad1e25c7f960e98cd7e3d0af8f3fdd3 | |
| parent | 111344eaea4fd0c32ce58a181b94414ae639fe2b (diff) | |
feat(oidc): rfc8628 oauth 2.0 device code grant (#8082)
This implements RFC8628 OAuth 2.0 Device Authorization Grant and the accompanying OAuth 2.0 Device Code Flow.
Signed-off-by: James Elliott <james-d-elliott@users.noreply.github.com>
38 files changed, 857 insertions, 85 deletions
diff --git a/docs/content/integration/openid-connect/introduction.md b/docs/content/integration/openid-connect/introduction.md index 27f655fda..b87c1cdaa 100644 --- a/docs/content/integration/openid-connect/introduction.md +++ b/docs/content/integration/openid-connect/introduction.md @@ -162,7 +162,7 @@ field is both the required value for the `grant_type` parameter in the access /  |         [OAuth 2.0 Client Credentials]          |    Yes    |              `client_credentials`              | If this is the only grant type for a client then the `openid`, `offline`, and `offline_access` scopes are not allowed |  |              [OAuth 2.0 Implicit]               |    Yes    |                   `implicit`                   |                          This Grant Type has been deprecated and should not normally be used                          |  |            [OAuth 2.0 Refresh Token]            |    Yes    |                `refresh_token`                 |                 This Grant Type should only be used for clients which have the `offline_access` scope                 | -|             [OAuth 2.0 Device Code]             |    No     | `urn:ietf:params:oauth:grant-type:device_code` |                                                                                                                       | +|             [OAuth 2.0 Device Code]             |    Yes    | `urn:ietf:params:oauth:grant-type:device_code` |                                                                                                                       |  [OAuth 2.0 Authorization Code]: https://datatracker.ietf.org/doc/html/rfc6749#section-1.3.1  [OAuth 2.0 Implicit]: https://datatracker.ietf.org/doc/html/rfc6749#section-1.3.2 @@ -275,6 +275,7 @@ These endpoints implement OpenID Connect 1.0 Provider specifications.  |:-------------------------------:|:----------------------------------------------------------------------------------------------------------------------------------------------------:|:-------------------------------------:|  |       [JSON Web Key Set]        |               https://{{< sitevar name="subdomain-authelia" nojs="auth" >}}.{{< sitevar name="domain" nojs="example.com" >}}/jwks.json               |               jwks_uri                |  |         [Authorization]         |        https://{{< sitevar name="subdomain-authelia" nojs="auth" >}}.{{< sitevar name="domain" nojs="example.com" >}}/api/oidc/authorization         |        authorization_endpoint         | +|     [Device Authorization]      |     https://{{< sitevar name="subdomain-authelia" nojs="auth" >}}.{{< sitevar name="domain" nojs="example.com" >}}/api/oidc/device-authorization     |     device_authorization_endpoint     |  | [Pushed Authorization Requests] | https://{{< sitevar name="subdomain-authelia" nojs="auth" >}}.{{< sitevar name="domain" nojs="example.com" >}}/api/oidc/pushed-authorization-request | pushed_authorization_request_endpoint |  |             [Token]             |            https://{{< sitevar name="subdomain-authelia" nojs="auth" >}}.{{< sitevar name="domain" nojs="example.com" >}}/api/oidc/token             |            token_endpoint             |  |           [UserInfo]            |           https://{{< sitevar name="subdomain-authelia" nojs="auth" >}}.{{< sitevar name="domain" nojs="example.com" >}}/api/oidc/userinfo           |           userinfo_endpoint           | diff --git a/docs/static/schemas/v4.39/json-schema/configuration.json b/docs/static/schemas/v4.39/json-schema/configuration.json index 24e8968a8..b8e1bca92 100644 --- a/docs/static/schemas/v4.39/json-schema/configuration.json +++ b/docs/static/schemas/v4.39/json-schema/configuration.json @@ -1350,6 +1350,7 @@              "type": "string",              "enum": [                "authorization", +              "device-authorization",                "pushed-authorization-request",                "token",                "introspection", @@ -1487,7 +1488,8 @@                "authorization_code",                "implicit",                "refresh_token", -              "client_credentials" +              "client_credentials", +              "urn:ietf:params:oauth:grant-type:device_code"              ]            },            "type": "array", diff --git a/internal/configuration/schema/identity_providers.go b/internal/configuration/schema/identity_providers.go index d2583fca2..198f8f856 100644 --- a/internal/configuration/schema/identity_providers.go +++ b/internal/configuration/schema/identity_providers.go @@ -122,7 +122,7 @@ type IdentityProvidersOpenIDConnectLifespanToken struct {  // IdentityProvidersOpenIDConnectCORS represents an OpenID Connect 1.0 CORS config.  type IdentityProvidersOpenIDConnectCORS struct { -	Endpoints      []string   `koanf:"endpoints" json:"endpoints" jsonschema:"uniqueItems,enum=authorization,enum=pushed-authorization-request,enum=token,enum=introspection,enum=revocation,enum=userinfo,title=Endpoints" jsonschema_description:"List of endpoints to enable CORS handling for."` +	Endpoints      []string   `koanf:"endpoints" json:"endpoints" jsonschema:"uniqueItems,enum=authorization,enum=device-authorization,enum=pushed-authorization-request,enum=token,enum=introspection,enum=revocation,enum=userinfo,title=Endpoints" jsonschema_description:"List of endpoints to enable CORS handling for."`  	AllowedOrigins []*url.URL `koanf:"allowed_origins" json:"allowed_origins" jsonschema:"format=uri,title=Allowed Origins" jsonschema_description:"List of arbitrary allowed origins for CORS requests."`  	AllowedOriginsFromClientRedirectURIs bool `koanf:"allowed_origins_from_client_redirect_uris" json:"allowed_origins_from_client_redirect_uris" jsonschema:"default=false,title=Allowed Origins From Client Redirect URIs" jsonschema_description:"Automatically include the redirect URIs from the registered clients."` @@ -141,7 +141,7 @@ type IdentityProvidersOpenIDConnectClient struct {  	Audience      []string `koanf:"audience" json:"audience" jsonschema:"uniqueItems,title=Audience" jsonschema_description:"List of authorized audiences."`  	Scopes        []string `koanf:"scopes" json:"scopes" jsonschema:"required,enum=openid,enum=offline_access,enum=groups,enum=email,enum=profile,enum=authelia.bearer.authz,uniqueItems,title=Scopes" jsonschema_description:"The Scopes this client is allowed request and be granted."` -	GrantTypes    []string `koanf:"grant_types" json:"grant_types" jsonschema:"enum=authorization_code,enum=implicit,enum=refresh_token,enum=client_credentials,uniqueItems,title=Grant Types" jsonschema_description:"The Grant Types this client is allowed to use for the protected endpoints."` +	GrantTypes    []string `koanf:"grant_types" json:"grant_types" jsonschema:"enum=authorization_code,enum=implicit,enum=refresh_token,enum=client_credentials,enum=urn:ietf:params:oauth:grant-type:device_code,uniqueItems,title=Grant Types" jsonschema_description:"The Grant Types this client is allowed to use for the protected endpoints."`  	ResponseTypes []string `koanf:"response_types" json:"response_types" jsonschema:"enum=code,enum=id_token token,enum=id_token,enum=token,enum=code token,enum=code id_token,enum=code id_token token,uniqueItems,title=Response Types" jsonschema_description:"The Response Types the client is authorized to request."`  	ResponseModes []string `koanf:"response_modes" json:"response_modes" jsonschema:"enum=form_post,enum=form_post.jwt,enum=query,enum=query.jwt,enum=fragment,enum=fragment.jwt,enum=jwt,uniqueItems,title=Response Modes" jsonschema_description:"The Response Modes this client is authorized request."` diff --git a/internal/configuration/validator/const.go b/internal/configuration/validator/const.go index 4feea1f72..a3cbf3dfb 100644 --- a/internal/configuration/validator/const.go +++ b/internal/configuration/validator/const.go @@ -568,7 +568,7 @@ var (  )  var ( -	validOIDCCORSEndpoints = []string{oidc.EndpointAuthorization, oidc.EndpointPushedAuthorizationRequest, oidc.EndpointToken, oidc.EndpointIntrospection, oidc.EndpointRevocation, oidc.EndpointUserinfo} +	validOIDCCORSEndpoints = []string{oidc.EndpointAuthorization, oidc.EndpointDeviceAuthorization, oidc.EndpointPushedAuthorizationRequest, oidc.EndpointToken, oidc.EndpointIntrospection, oidc.EndpointRevocation, oidc.EndpointUserinfo}  	validOIDCReservedClaims                  = []string{oidc.ClaimJWTID, oidc.ClaimSessionID, oidc.ClaimAuthorizedParty, oidc.ClaimClientIdentifier, oidc.ClaimScope, oidc.ClaimScopeNonStandard, oidc.ClaimIssuer, oidc.ClaimSubject, oidc.ClaimAudience, oidc.ClaimSessionID, oidc.ClaimStateHash, oidc.ClaimCodeHash, oidc.ClaimIssuedAt, oidc.ClaimUpdatedAt, oidc.ClaimRequestedAt, oidc.ClaimNotBefore, oidc.ClaimExpirationTime, oidc.ClaimAuthenticationTime, oidc.ClaimAuthenticationMethodsReference, oidc.ClaimAuthenticationContextClassReference, oidc.ClaimNonce}  	validOIDCClientClaims                    = []string{oidc.ClaimFullName, oidc.ClaimGivenName, oidc.ClaimFamilyName, oidc.ClaimMiddleName, oidc.ClaimNickname, oidc.ClaimPreferredUsername, oidc.ClaimProfile, oidc.ClaimPicture, oidc.ClaimWebsite, oidc.ClaimEmail, oidc.ClaimEmailVerified, oidc.ClaimGender, oidc.ClaimBirthdate, oidc.ClaimZoneinfo, oidc.ClaimLocale, oidc.ClaimPhoneNumber, oidc.ClaimPhoneNumberVerified, oidc.ClaimAddress, oidc.ClaimGroups, oidc.ClaimEmailAlts} @@ -579,7 +579,7 @@ var (  	validOIDCClientResponseTypesImplicitFlow = []string{oidc.ResponseTypeImplicitFlowIDToken, oidc.ResponseTypeImplicitFlowToken, oidc.ResponseTypeImplicitFlowBoth}  	validOIDCClientResponseTypesHybridFlow   = []string{oidc.ResponseTypeHybridFlowIDToken, oidc.ResponseTypeHybridFlowToken, oidc.ResponseTypeHybridFlowBoth}  	validOIDCClientResponseTypesRefreshToken = []string{oidc.ResponseTypeAuthorizationCodeFlow, oidc.ResponseTypeHybridFlowIDToken, oidc.ResponseTypeHybridFlowToken, oidc.ResponseTypeHybridFlowBoth} -	validOIDCClientGrantTypes                = []string{oidc.GrantTypeAuthorizationCode, oidc.GrantTypeImplicit, oidc.GrantTypeClientCredentials, oidc.GrantTypeRefreshToken} +	validOIDCClientGrantTypes                = []string{oidc.GrantTypeAuthorizationCode, oidc.GrantTypeImplicit, oidc.GrantTypeClientCredentials, oidc.GrantTypeRefreshToken, oidc.GrantTypeDeviceCode}  	validOIDCClientTokenEndpointAuthMethods                = []string{oidc.ClientAuthMethodNone, oidc.ClientAuthMethodClientSecretPost, oidc.ClientAuthMethodClientSecretBasic, oidc.ClientAuthMethodPrivateKeyJWT, oidc.ClientAuthMethodClientSecretJWT}  	validOIDCClientTokenEndpointAuthMethodsConfidential    = []string{oidc.ClientAuthMethodClientSecretPost, oidc.ClientAuthMethodClientSecretBasic, oidc.ClientAuthMethodPrivateKeyJWT} diff --git a/internal/configuration/validator/identity_providers_test.go b/internal/configuration/validator/identity_providers_test.go index dc2273681..d21eadfa7 100644 --- a/internal/configuration/validator/identity_providers_test.go +++ b/internal/configuration/validator/identity_providers_test.go @@ -118,7 +118,7 @@ func TestShouldRaiseErrorWhenCORSEndpointsNotValid(t *testing.T) {  	require.Len(t, validator.Errors(), 1) -	assert.EqualError(t, validator.Errors()[0], "identity_providers: oidc: cors: option 'endpoints' contains an invalid value 'invalid_endpoint': must be one of 'authorization', 'pushed-authorization-request', 'token', 'introspection', 'revocation', or 'userinfo'") +	assert.EqualError(t, validator.Errors()[0], "identity_providers: oidc: cors: option 'endpoints' contains an invalid value 'invalid_endpoint': must be one of 'authorization', 'device-authorization', 'pushed-authorization-request', 'token', 'introspection', 'revocation', or 'userinfo'")  }  func TestShouldRaiseErrorWhenOIDCPKCEEnforceValueInvalid(t *testing.T) { @@ -674,7 +674,7 @@ func TestShouldRaiseErrorWhenOIDCClientConfiguredWithBadGrantTypes(t *testing.T)  	ValidateIdentityProviders(NewValidateCtx(), config, validator)  	require.Len(t, validator.Errors(), 1) -	assert.EqualError(t, validator.Errors()[0], "identity_providers: oidc: clients: client 'good_id': option 'grant_types' must only have the values 'authorization_code', 'implicit', 'client_credentials', or 'refresh_token' but the values 'bad_grant_type' are present") +	assert.EqualError(t, validator.Errors()[0], "identity_providers: oidc: clients: client 'good_id': option 'grant_types' must only have the values 'authorization_code', 'implicit', 'client_credentials', 'refresh_token', or 'urn:ietf:params:oauth:grant-type:device_code' but the values 'bad_grant_type' are present")  }  func TestShouldNotErrorOnCertificateValid(t *testing.T) { @@ -1380,7 +1380,7 @@ func TestValidateOIDCClients(t *testing.T) {  			},  			nil,  			[]string{ -				"identity_providers: oidc: clients: client 'test': option 'grant_types' must only have the values 'authorization_code', 'implicit', 'client_credentials', or 'refresh_token' but the values 'invalid' are present", +				"identity_providers: oidc: clients: client 'test': option 'grant_types' must only have the values 'authorization_code', 'implicit', 'client_credentials', 'refresh_token', or 'urn:ietf:params:oauth:grant-type:device_code' but the values 'invalid' are present",  			},  		},  		{ diff --git a/internal/handlers/handler_oauth_authorization_claims.go b/internal/handlers/handler_oauth_authorization_claims.go index 53d5c387c..7b8ab22fe 100644 --- a/internal/handlers/handler_oauth_authorization_claims.go +++ b/internal/handlers/handler_oauth_authorization_claims.go @@ -13,14 +13,14 @@ import (  	"github.com/authelia/authelia/v4/internal/session"  ) -func handleOAuth2AuthorizationClaims(ctx *middlewares.AutheliaCtx, rw http.ResponseWriter, _ *http.Request, flow string, userSession session.UserSession, details *authentication.UserDetailsExtended, client oidc.Client, requester oauthelia2.AuthorizeRequester, issuer *url.URL, consent *model.OAuth2ConsentSession, extra map[string]any) (requests *oidc.ClaimsRequests, handled bool) { +func handleOAuth2AuthorizationClaims(ctx *middlewares.AutheliaCtx, rw http.ResponseWriter, _ *http.Request, flow string, userSession session.UserSession, details *authentication.UserDetailsExtended, client oidc.Client, requester oauthelia2.Requester, issuer *url.URL, consent *model.OAuth2ConsentSession, extra map[string]any) (requests *oidc.ClaimsRequests, handled bool) {  	var err error  	if requester.GetRequestedScopes().Has(oidc.ScopeOpenID) {  		if requests, err = oidc.NewClaimRequests(requester.GetRequestForm()); err != nil {  			ctx.Logger.WithError(err).Errorf("%s Request with id '%s' on client with id '%s' could not be processed: error occurred parsing the claims parameter", flow, requester.GetID(), client.GetID()) -			ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, err) +			ctx.Providers.OpenIDConnect.WriteDynamicAuthorizeError(ctx, rw, requester, err)  			return nil, true  		} @@ -31,7 +31,7 @@ func handleOAuth2AuthorizationClaims(ctx *middlewares.AutheliaCtx, rw http.Respo  		if err = claimsStrategy.ValidateClaimsRequests(ctx, scopeStrategy, client, requests); err != nil {  			ctx.Logger.WithError(oauthelia2.ErrorToDebugRFC6749Error(err)).Errorf("%s Request with id '%s' on client with id '%s' could not be processed: the client requested claims were not permitted.", flow, requester.GetID(), client.GetID()) -			ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, oauthelia2.ErrAccessDenied.WithHint("The requested subject was not the same subject that attempted to authorize the request.")) +			ctx.Providers.OpenIDConnect.WriteDynamicAuthorizeError(ctx, rw, requester, oauthelia2.ErrAccessDenied.WithHint("The requested subject was not the same subject that attempted to authorize the request."))  			return nil, true  		} @@ -39,7 +39,7 @@ func handleOAuth2AuthorizationClaims(ctx *middlewares.AutheliaCtx, rw http.Respo  		if requested, ok := requests.MatchesIssuer(issuer); !ok {  			ctx.Logger.Errorf("%s Request with id '%s' on client with id '%s' could not be processed: the client requested issuer '%s' but the issuer for the token will be '%s' instead", flow, requester.GetID(), client.GetID(), requested, issuer.String()) -			ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, oauthelia2.ErrAccessDenied.WithHint("The requested issuer was not the same issuer that attempted to authorize the request.")) +			ctx.Providers.OpenIDConnect.WriteDynamicAuthorizeError(ctx, rw, requester, oauthelia2.ErrAccessDenied.WithHint("The requested issuer was not the same issuer that attempted to authorize the request."))  			return nil, true  		} @@ -47,7 +47,7 @@ func handleOAuth2AuthorizationClaims(ctx *middlewares.AutheliaCtx, rw http.Respo  		if requested, ok := requests.MatchesSubject(consent.Subject.UUID.String()); !ok {  			ctx.Logger.Errorf("%s Request with id '%s' on client with id '%s' could not be processed: the client requested subject '%s' but the subject value for '%s' is '%s' for the '%s' sector identifier", flow, requester.GetID(), client.GetID(), requested, userSession.Username, consent.Subject.UUID, client.GetSectorIdentifierURI()) -			ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, oauthelia2.ErrAccessDenied.WithHint("The requested subject was not the same subject that attempted to authorize the request.")) +			ctx.Providers.OpenIDConnect.WriteDynamicAuthorizeError(ctx, rw, requester, oauthelia2.ErrAccessDenied.WithHint("The requested subject was not the same subject that attempted to authorize the request."))  			return nil, true  		} @@ -57,7 +57,7 @@ func handleOAuth2AuthorizationClaims(ctx *middlewares.AutheliaCtx, rw http.Respo  		if err = claimsStrategy.PopulateIDTokenClaims(ctx, scopeStrategy, client, requester.GetGrantedScopes(), oauthelia2.Arguments(consent.GrantedClaims), requests.GetIDTokenRequests(), details, ctx.Clock.Now(), nil, extra); err != nil {  			ctx.Logger.Errorf("%s Response for Request with id '%s' on client with id '%s' could not be created: %s", flow, requester.GetID(), client.GetID(), oauthelia2.ErrorToDebugRFC6749Error(err)) -			ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, err) +			ctx.Providers.OpenIDConnect.WriteDynamicAuthorizeError(ctx, rw, requester, err)  			return nil, true  		} diff --git a/internal/handlers/handler_oauth_device_authorization.go b/internal/handlers/handler_oauth_device_authorization.go new file mode 100644 index 000000000..3e217b55f --- /dev/null +++ b/internal/handlers/handler_oauth_device_authorization.go @@ -0,0 +1,138 @@ +package handlers + +import ( +	"errors" +	"net/http" +	"net/url" + +	oauthelia2 "authelia.com/provider/oauth2" +	"authelia.com/provider/oauth2/x/errorsx" + +	"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" +) + +func OAuthDeviceAuthorizationPOST(ctx *middlewares.AutheliaCtx, rw http.ResponseWriter, req *http.Request) { +	var ( +		request  oauthelia2.DeviceAuthorizeRequester +		response oauthelia2.DeviceAuthorizeResponder + +		err error +	) + +	if request, err = ctx.Providers.OpenIDConnect.NewRFC862DeviceAuthorizeRequest(ctx, req); err != nil { +		ctx.Logger.Errorf("Device Authorization Request failed with error: %s", oauthelia2.ErrorToDebugRFC6749Error(err)) + +		errorsx.WriteJSONError(rw, req, err) + +		return +	} + +	if response, err = ctx.Providers.OpenIDConnect.NewRFC862DeviceAuthorizeResponse(ctx, request, oidc.NewSession()); err != nil { +		ctx.Logger.Errorf("Device Authorization Request with id '%s' on client with id '%s'  failed with error: %s", request.GetID(), request.GetClient().GetID(), oauthelia2.ErrorToDebugRFC6749Error(err)) + +		errorsx.WriteJSONError(rw, req, err) + +		return +	} + +	ctx.Providers.OpenIDConnect.WriteRFC862DeviceAuthorizeResponse(ctx, rw, request, response) +} + +func OAuthDeviceAuthorizationPUT(ctx *middlewares.AutheliaCtx, rw http.ResponseWriter, r *http.Request) { +	var ( +		requester oauthelia2.DeviceAuthorizeRequester +		responder oauthelia2.DeviceUserAuthorizeResponder +		client    oidc.Client + +		err error +	) + +	if requester, err = ctx.Providers.OpenIDConnect.NewRFC8628UserAuthorizeRequest(ctx, r); err != nil { +		ctx.Logger.Errorf("Device Authorization Request failed with error: %s", oauthelia2.ErrorToDebugRFC6749Error(err)) + +		ctx.Providers.OpenIDConnect.WriteRFC8628UserAuthorizeError(ctx, rw, requester, err) + +		return +	} + +	clientID := requester.GetClient().GetID() + +	ctx.Logger.Debugf("Device Authorization Request with id '%s' on client with id '%s' is being processed", requester.GetID(), clientID) + +	if client, err = ctx.Providers.OpenIDConnect.GetRegisteredClient(ctx, clientID); err != nil { +		if errors.Is(err, oauthelia2.ErrNotFound) { +			ctx.Logger.Errorf("Device 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("Device Authorization Request with id '%s' on client with id '%s' could not be processed: failed to find client: %s", requester.GetID(), clientID, oauthelia2.ErrorToDebugRFC6749Error(err)) +		} + +		ctx.Providers.OpenIDConnect.WriteRFC8628UserAuthorizeError(ctx, rw, requester, err) + +		return +	} + +	var ( +		userSession session.UserSession +		consent     *model.OAuth2ConsentSession +		issuer      *url.URL +		handled     bool +	) + +	if userSession, err = ctx.GetSession(); err != nil { +		ctx.Logger.Errorf("Device Authorization Request with id '%s' on client with id '%s' could not be processed: error occurred obtaining session information: %+v", requester.GetID(), client.GetID(), err) + +		ctx.Providers.OpenIDConnect.WriteRFC8628UserAuthorizeError(ctx, rw, requester, oauthelia2.ErrServerError.WithHint("Could not obtain the user session.")) + +		return +	} + +	issuer = ctx.RootURL() + +	if consent, handled = handleOIDCAuthorizationConsent(ctx, issuer, client, userSession, rw, r, requester); handled { +		return +	} + +	var details *authentication.UserDetailsExtended + +	if details, err = ctx.Providers.UserProvider.GetDetailsExtended(userSession.Username); err != nil { +		ctx.Logger.WithError(err).Errorf("Device Authorization Request with id '%s' on client with id '%s' could not be processed: error occurred retrieving user details for '%s' from the backend", requester.GetID(), client.GetID(), userSession.Username) + +		ctx.Providers.OpenIDConnect.WriteRFC8628UserAuthorizeError(ctx, rw, requester, oauthelia2.ErrServerError.WithHint("Could not obtain the users details.")) + +		return +	} + +	var requests *oidc.ClaimsRequests + +	extra := map[string]any{} + +	if requests, handled = handleOAuth2AuthorizationClaims(ctx, rw, r, "Device Authorization", userSession, details, client, requester, issuer, consent, extra); handled { +		return +	} + +	ctx.Logger.Debugf("Device Authorization Request with id '%s' on client with id '%s' was successfully processed, proceeding to build Authorization Response", requester.GetID(), clientID) + +	session := oidc.NewSessionWithRequester(ctx, issuer, ctx.Providers.OpenIDConnect.KeyManager.GetKeyID(ctx, client.GetIDTokenSignedResponseKeyID(), client.GetIDTokenSignedResponseAlg()), details.Username, userSession.AuthenticationMethodRefs.MarshalRFC8176(), extra, userSession.LastAuthenticatedTime(), consent, requester, requests) + +	if responder, err = ctx.Providers.OpenIDConnect.NewRFC8628UserAuthorizeResponse(ctx, requester, session); err != nil { +		ctx.Logger.Errorf("Device Authorization Request with id '%s' failed with error: %s", requester.GetID(), oauthelia2.ErrorToDebugRFC6749Error(err)) + +		ctx.Providers.OpenIDConnect.WriteRFC8628UserAuthorizeError(ctx, rw, requester, err) + +		return +	} + +	if err = ctx.Providers.StorageProvider.SaveOAuth2ConsentSessionGranted(ctx, consent.ID); err != nil { +		ctx.Logger.Errorf("Device 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.WriteRFC8628UserAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotSave) + +		return +	} + +	ctx.Providers.OpenIDConnect.WriteRFC8628UserAuthorizeResponse(ctx, rw, requester, responder) +} diff --git a/internal/handlers/handler_oidc_authorization_consent.go b/internal/handlers/handler_oidc_authorization_consent.go index 44d13cfba..68836aa3c 100644 --- a/internal/handlers/handler_oidc_authorization_consent.go +++ b/internal/handlers/handler_oidc_authorization_consent.go @@ -20,7 +20,7 @@ import (  func handleOIDCAuthorizationConsent(ctx *middlewares.AutheliaCtx, issuer *url.URL, client oidc.Client,  	userSession session.UserSession, -	rw http.ResponseWriter, r *http.Request, requester oauthelia2.AuthorizeRequester) (consent *model.OAuth2ConsentSession, handled bool) { +	rw http.ResponseWriter, r *http.Request, requester oauthelia2.Requester) (consent *model.OAuth2ConsentSession, handled bool) {  	var (  		subject uuid.UUID  		err     error @@ -38,7 +38,7 @@ func handleOIDCAuthorizationConsent(ctx *middlewares.AutheliaCtx, issuer *url.UR  		if subject, err = ctx.Providers.OpenIDConnect.GetSubject(ctx, client.GetSectorIdentifierURI(), userSession.Username); err != nil {  			ctx.Logger.Errorf(logFmtErrConsentCantGetSubject, requester.GetID(), client.GetID(), client.GetConsentPolicy(), userSession.Username, client.GetSectorIdentifierURI(), err) -			ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, oidc.ErrSubjectCouldNotLookup) +			ctx.Providers.OpenIDConnect.WriteDynamicAuthorizeError(ctx, rw, requester, oidc.ErrSubjectCouldNotLookup)  			return nil, true  		} @@ -53,21 +53,21 @@ func handleOIDCAuthorizationConsent(ctx *middlewares.AutheliaCtx, issuer *url.UR  		default:  			ctx.Logger.Errorf(logFmtErrConsentCantDetermineConsentMode, requester.GetID(), client.GetID()) -			ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, oauthelia2.ErrServerError.WithHint("Could not determine the client consent mode.")) +			ctx.Providers.OpenIDConnect.WriteDynamicAuthorizeError(ctx, rw, requester, oauthelia2.ErrServerError.WithHint("Could not determine the client consent mode."))  			return nil, true  		}  	case level == authorization.Denied:  		ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' using policy '%s' could not be processed: the user '%s' is not authorized to use this client", requester.GetID(), client.GetID(), policy.Name, userSession.Username) -		ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, oidc.ErrClientAuthorizationUserAccessDenied) +		ctx.Providers.OpenIDConnect.WriteDynamicAuthorizeError(ctx, rw, requester, oidc.ErrClientAuthorizationUserAccessDenied)  		return nil, true  	default:  		if subject, err = ctx.Providers.OpenIDConnect.GetSubject(ctx, client.GetSectorIdentifierURI(), userSession.Username); err != nil {  			ctx.Logger.Errorf(logFmtErrConsentCantGetSubject, requester.GetID(), client.GetID(), client.GetConsentPolicy(), userSession.Username, client.GetSectorIdentifierURI(), err) -			ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, oidc.ErrSubjectCouldNotLookup) +			ctx.Providers.OpenIDConnect.WriteDynamicAuthorizeError(ctx, rw, requester, oidc.ErrSubjectCouldNotLookup)  			return nil, true  		} @@ -80,13 +80,13 @@ func handleOIDCAuthorizationConsent(ctx *middlewares.AutheliaCtx, issuer *url.UR  func handleOIDCAuthorizationConsentNotAuthenticated(ctx *middlewares.AutheliaCtx, issuer *url.URL, client oidc.Client,  	_ session.UserSession, _ uuid.UUID, -	rw http.ResponseWriter, r *http.Request, requester oauthelia2.AuthorizeRequester) (consent *model.OAuth2ConsentSession, handled bool) { +	rw http.ResponseWriter, r *http.Request, requester oauthelia2.Requester) (consent *model.OAuth2ConsentSession, handled bool) {  	var err error  	if consent, err = handleOpenIDConnectNewConsentSession(uuid.UUID{}, requester, ctx.Providers.OpenIDConnect.GetPushedAuthorizeRequestURIPrefix(ctx)); err != nil {  		ctx.Logger.Errorf(logFmtErrConsentGenerateError, requester.GetID(), client.GetID(), client.GetConsentPolicy(), "generating", err) -		ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotGenerate) +		ctx.Providers.OpenIDConnect.WriteDynamicAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotGenerate)  		return nil, true  	} @@ -94,7 +94,7 @@ func handleOIDCAuthorizationConsentNotAuthenticated(ctx *middlewares.AutheliaCtx  	if err = ctx.Providers.StorageProvider.SaveOAuth2ConsentSession(ctx, consent); err != nil {  		ctx.Logger.Errorf(logFmtErrConsentGenerateError, requester.GetID(), client.GetID(), client.GetConsentPolicy(), "saving", err) -		ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotSave) +		ctx.Providers.OpenIDConnect.WriteDynamicAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotSave)  		return nil, true  	} @@ -110,7 +110,7 @@ func handleOIDCAuthorizationConsentNotAuthenticated(ctx *middlewares.AutheliaCtx  func handleOIDCAuthorizationConsentGenerate(ctx *middlewares.AutheliaCtx, issuer *url.URL, client oidc.Client,  	userSession session.UserSession, subject uuid.UUID, -	rw http.ResponseWriter, r *http.Request, requester oauthelia2.AuthorizeRequester) (consent *model.OAuth2ConsentSession, handled bool) { +	rw http.ResponseWriter, r *http.Request, requester oauthelia2.Requester) (consent *model.OAuth2ConsentSession, handled bool) {  	var (  		err error  	) @@ -120,7 +120,7 @@ func handleOIDCAuthorizationConsentGenerate(ctx *middlewares.AutheliaCtx, issuer  	if len(ctx.QueryArgs().PeekBytes(qryArgConsentID)) != 0 {  		ctx.Logger.Errorf(logFmtErrConsentGenerateError, requester.GetID(), client.GetID(), client.GetConsentPolicy(), "generating", errors.New("consent id value was present when it should be absent")) -		ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotGenerate) +		ctx.Providers.OpenIDConnect.WriteDynamicAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotGenerate)  		return nil, true  	} @@ -128,7 +128,7 @@ func handleOIDCAuthorizationConsentGenerate(ctx *middlewares.AutheliaCtx, issuer  	if consent, err = handleOpenIDConnectNewConsentSession(subject, requester, ctx.Providers.OpenIDConnect.GetPushedAuthorizeRequestURIPrefix(ctx)); err != nil {  		ctx.Logger.Errorf(logFmtErrConsentGenerateError, requester.GetID(), client.GetID(), client.GetConsentPolicy(), "generating", err) -		ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotGenerate) +		ctx.Providers.OpenIDConnect.WriteDynamicAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotGenerate)  		return nil, true  	} @@ -136,7 +136,7 @@ func handleOIDCAuthorizationConsentGenerate(ctx *middlewares.AutheliaCtx, issuer  	if err = ctx.Providers.StorageProvider.SaveOAuth2ConsentSession(ctx, consent); err != nil {  		ctx.Logger.Errorf(logFmtErrConsentGenerateError, requester.GetID(), client.GetID(), client.GetConsentPolicy(), "saving", err) -		ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotSave) +		ctx.Providers.OpenIDConnect.WriteDynamicAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotSave)  		return nil, true  	} @@ -155,7 +155,7 @@ func handleOIDCAuthorizationConsentGenerate(ctx *middlewares.AutheliaCtx, issuer  }  func handleOIDCAuthorizationConsentRedirect(ctx *middlewares.AutheliaCtx, issuer *url.URL, consent *model.OAuth2ConsentSession, client oidc.Client, -	userSession session.UserSession, rw http.ResponseWriter, r *http.Request, requester oauthelia2.AuthorizeRequester) { +	userSession session.UserSession, rw http.ResponseWriter, r *http.Request, requester oauthelia2.Requester) {  	var location *url.URL  	if client.IsAuthenticationLevelSufficient(userSession.AuthenticationLevel, authorization.Subject{Username: userSession.Username, Groups: userSession.Groups, IP: ctx.RemoteIP()}) { @@ -181,7 +181,7 @@ func handleOIDCAuthorizationConsentRedirect(ctx *middlewares.AutheliaCtx, issuer  	http.Redirect(rw, r, location.String(), http.StatusFound)  } -func handleOIDCPushedAuthorizeConsent(ctx *middlewares.AutheliaCtx, requester oauthelia2.AuthorizeRequester, form url.Values) { +func handleOIDCPushedAuthorizeConsent(ctx *middlewares.AutheliaCtx, requester oauthelia2.Requester, form url.Values) {  	if !oidc.IsPushedAuthorizedRequest(requester, ctx.Providers.OpenIDConnect.GetPushedAuthorizeRequestURIPrefix(ctx)) {  		return  	} @@ -202,7 +202,7 @@ func handleOIDCPushedAuthorizeConsent(ctx *middlewares.AutheliaCtx, requester oa  	}  } -func handleOIDCAuthorizationConsentPromptLoginRedirect(ctx *middlewares.AutheliaCtx, issuer *url.URL, client oidc.Client, userSession session.UserSession, rw http.ResponseWriter, r *http.Request, requester oauthelia2.AuthorizeRequester, consent *model.OAuth2ConsentSession) { +func handleOIDCAuthorizationConsentPromptLoginRedirect(ctx *middlewares.AutheliaCtx, issuer *url.URL, client oidc.Client, userSession session.UserSession, rw http.ResponseWriter, r *http.Request, requester oauthelia2.Requester, consent *model.OAuth2ConsentSession) {  	ctx.Logger.WithFields(map[string]any{"requested_at": consent.RequestedAt, "authenticated_at": userSession.LastAuthenticatedTime()}).Debugf("Authorization Request with id '%s' on client with id '%s' is being redirected for reauthentication: prompt type login was requested", requester.GetID(), client.GetID())  	handleOIDCPushedAuthorizeConsent(ctx, requester, r.Form) @@ -236,7 +236,7 @@ func handleOIDCAuthorizationConsentGetRedirectionURL(_ *middlewares.AutheliaCtx,  	return redirectURL  } -func handleOpenIDConnectNewConsentSession(subject uuid.UUID, requester oauthelia2.AuthorizeRequester, prefixPAR string) (consent *model.OAuth2ConsentSession, err error) { +func handleOpenIDConnectNewConsentSession(subject uuid.UUID, requester oauthelia2.Requester, prefixPAR string) (consent *model.OAuth2ConsentSession, err error) {  	if oidc.IsPushedAuthorizedRequest(requester, prefixPAR) {  		form := url.Values{} diff --git a/internal/handlers/handler_oidc_authorization_consent_explicit.go b/internal/handlers/handler_oidc_authorization_consent_explicit.go index 24a5dd79c..0c2a2e012 100644 --- a/internal/handlers/handler_oidc_authorization_consent_explicit.go +++ b/internal/handlers/handler_oidc_authorization_consent_explicit.go @@ -15,7 +15,7 @@ import (  func handleOIDCAuthorizationConsentModeExplicit(ctx *middlewares.AutheliaCtx, issuer *url.URL, client oidc.Client,  	userSession session.UserSession, subject uuid.UUID, -	rw http.ResponseWriter, r *http.Request, requester oauthelia2.AuthorizeRequester) (consent *model.OAuth2ConsentSession, handled bool) { +	rw http.ResponseWriter, r *http.Request, requester oauthelia2.Requester) (consent *model.OAuth2ConsentSession, handled bool) {  	var (  		consentID uuid.UUID  		err       error @@ -30,7 +30,7 @@ func handleOIDCAuthorizationConsentModeExplicit(ctx *middlewares.AutheliaCtx, is  		if consentID, err = uuid.ParseBytes(bytesConsentID); err != nil {  			ctx.Logger.Errorf(logFmtErrConsentParseChallengeID, requester.GetID(), client.GetID(), client.GetConsentPolicy(), bytesConsentID, err) -			ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, oidc.ErrConsentMalformedChallengeID) +			ctx.Providers.OpenIDConnect.WriteDynamicAuthorizeError(ctx, rw, requester, oidc.ErrConsentMalformedChallengeID)  			return nil, true  		} @@ -41,7 +41,7 @@ func handleOIDCAuthorizationConsentModeExplicit(ctx *middlewares.AutheliaCtx, is  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 oauthelia2.AuthorizeRequester) (consent *model.OAuth2ConsentSession, handled bool) { +	rw http.ResponseWriter, r *http.Request, requester oauthelia2.Requester) (consent *model.OAuth2ConsentSession, handled bool) {  	var (  		err error  	) @@ -49,7 +49,7 @@ func handleOIDCAuthorizationConsentModeExplicitWithID(ctx *middlewares.AutheliaC  	if consentID == uuid.Nil {  		ctx.Logger.Errorf(logFmtErrConsentZeroID, requester.GetID(), client.GetID(), client.GetConsentPolicy()) -		ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotLookup) +		ctx.Providers.OpenIDConnect.WriteDynamicAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotLookup)  		return nil, true  	} @@ -57,7 +57,7 @@ func handleOIDCAuthorizationConsentModeExplicitWithID(ctx *middlewares.AutheliaC  	if consent, err = ctx.Providers.StorageProvider.LoadOAuth2ConsentSessionByChallengeID(ctx, consentID); err != nil {  		ctx.Logger.Errorf(logFmtErrConsentLookupLoadingSession, requester.GetID(), client.GetID(), client.GetConsentPolicy(), consentID, err) -		ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotLookup) +		ctx.Providers.OpenIDConnect.WriteDynamicAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotLookup)  		return nil, true  	} @@ -65,7 +65,7 @@ func handleOIDCAuthorizationConsentModeExplicitWithID(ctx *middlewares.AutheliaC  	if subject != consent.Subject.UUID {  		ctx.Logger.Errorf(logFmtErrConsentSessionSubjectNotAuthorized, requester.GetID(), client.GetID(), client.GetConsentPolicy(), consent.ChallengeID, userSession.Username, subject, consent.Subject.UUID) -		ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotLookup) +		ctx.Providers.OpenIDConnect.WriteDynamicAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotLookup)  		return nil, true  	} @@ -79,7 +79,7 @@ func handleOIDCAuthorizationConsentModeExplicitWithID(ctx *middlewares.AutheliaC  	if !consent.CanGrant() {  		ctx.Logger.Errorf(logFmtErrConsentCantGrant, requester.GetID(), client.GetID(), client.GetConsentPolicy(), consent.ChallengeID, "explicit") -		ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotPerform) +		ctx.Providers.OpenIDConnect.WriteDynamicAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotPerform)  		return nil, true  	} @@ -88,7 +88,7 @@ func handleOIDCAuthorizationConsentModeExplicitWithID(ctx *middlewares.AutheliaC  		if consent.Responded() {  			ctx.Logger.Errorf(logFmtErrConsentCantGrantRejected, requester.GetID(), client.GetID(), client.GetConsentPolicy(), consent.ChallengeID) -			ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, oauthelia2.ErrAccessDenied) +			ctx.Providers.OpenIDConnect.WriteDynamicAuthorizeError(ctx, rw, requester, oauthelia2.ErrAccessDenied)  			return nil, true  		} diff --git a/internal/handlers/handler_oidc_authorization_consent_implicit.go b/internal/handlers/handler_oidc_authorization_consent_implicit.go index 1d7ac9ef3..5531a4b66 100644 --- a/internal/handlers/handler_oidc_authorization_consent_implicit.go +++ b/internal/handlers/handler_oidc_authorization_consent_implicit.go @@ -15,7 +15,7 @@ import (  func handleOIDCAuthorizationConsentModeImplicit(ctx *middlewares.AutheliaCtx, issuer *url.URL, client oidc.Client,  	userSession session.UserSession, subject uuid.UUID, -	rw http.ResponseWriter, r *http.Request, requester oauthelia2.AuthorizeRequester) (consent *model.OAuth2ConsentSession, handled bool) { +	rw http.ResponseWriter, r *http.Request, requester oauthelia2.Requester) (consent *model.OAuth2ConsentSession, handled bool) {  	var (  		consentID uuid.UUID  		err       error @@ -28,7 +28,7 @@ func handleOIDCAuthorizationConsentModeImplicit(ctx *middlewares.AutheliaCtx, is  		if consentID, err = uuid.ParseBytes(bytesConsentID); err != nil {  			ctx.Logger.Errorf(logFmtErrConsentParseChallengeID, requester.GetID(), client.GetID(), client.GetConsentPolicy(), bytesConsentID, err) -			ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, oidc.ErrConsentMalformedChallengeID) +			ctx.Providers.OpenIDConnect.WriteDynamicAuthorizeError(ctx, rw, requester, oidc.ErrConsentMalformedChallengeID)  			return nil, true  		} @@ -39,7 +39,7 @@ func handleOIDCAuthorizationConsentModeImplicit(ctx *middlewares.AutheliaCtx, is  func handleOIDCAuthorizationConsentModeImplicitWithID(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 oauthelia2.AuthorizeRequester) (consent *model.OAuth2ConsentSession, handled bool) { +	rw http.ResponseWriter, r *http.Request, requester oauthelia2.Requester) (consent *model.OAuth2ConsentSession, handled bool) {  	var (  		err error  	) @@ -47,7 +47,7 @@ func handleOIDCAuthorizationConsentModeImplicitWithID(ctx *middlewares.AutheliaC  	if consentID == uuid.Nil {  		ctx.Logger.Errorf(logFmtErrConsentZeroID, requester.GetID(), client.GetID(), client.GetConsentPolicy()) -		ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotLookup) +		ctx.Providers.OpenIDConnect.WriteDynamicAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotLookup)  		return nil, true  	} @@ -55,7 +55,7 @@ func handleOIDCAuthorizationConsentModeImplicitWithID(ctx *middlewares.AutheliaC  	if consent, err = ctx.Providers.StorageProvider.LoadOAuth2ConsentSessionByChallengeID(ctx, consentID); err != nil {  		ctx.Logger.Errorf(logFmtErrConsentLookupLoadingSession, requester.GetID(), client.GetID(), client.GetConsentPolicy(), consentID, err) -		ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotLookup) +		ctx.Providers.OpenIDConnect.WriteDynamicAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotLookup)  		return nil, true  	} @@ -63,7 +63,7 @@ func handleOIDCAuthorizationConsentModeImplicitWithID(ctx *middlewares.AutheliaC  	if subject != consent.Subject.UUID {  		ctx.Logger.Errorf(logFmtErrConsentSessionSubjectNotAuthorized, requester.GetID(), client.GetID(), client.GetConsentPolicy(), consent.ChallengeID, userSession.Username, subject, consent.Subject.UUID) -		ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotLookup) +		ctx.Providers.OpenIDConnect.WriteDynamicAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotLookup)  		return nil, true  	} @@ -79,7 +79,7 @@ func handleOIDCAuthorizationConsentModeImplicitWithID(ctx *middlewares.AutheliaC  	if !consent.CanGrant() {  		ctx.Logger.Errorf(logFmtErrConsentCantGrant, requester.GetID(), client.GetID(), client.GetConsentPolicy(), consent.ChallengeID, "implicit") -		ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotPerform) +		ctx.Providers.OpenIDConnect.WriteDynamicAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotPerform)  		return nil, true  	} @@ -95,7 +95,7 @@ func handleOIDCAuthorizationConsentModeImplicitWithID(ctx *middlewares.AutheliaC  	if err = ctx.Providers.StorageProvider.SaveOAuth2ConsentSessionResponse(ctx, consent, false); err != nil {  		ctx.Logger.Errorf(logFmtErrConsentSaveSessionResponse, requester.GetID(), client.GetID(), client.GetConsentPolicy(), consent.ChallengeID, err) -		ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotSave) +		ctx.Providers.OpenIDConnect.WriteDynamicAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotSave)  		return nil, true  	} @@ -105,7 +105,7 @@ func handleOIDCAuthorizationConsentModeImplicitWithID(ctx *middlewares.AutheliaC  func handleOIDCAuthorizationConsentModeImplicitWithoutID(ctx *middlewares.AutheliaCtx, issuer *url.URL, client oidc.Client,  	userSession session.UserSession, subject uuid.UUID, -	rw http.ResponseWriter, r *http.Request, requester oauthelia2.AuthorizeRequester) (consent *model.OAuth2ConsentSession, handled bool) { +	rw http.ResponseWriter, r *http.Request, requester oauthelia2.Requester) (consent *model.OAuth2ConsentSession, handled bool) {  	var (  		err error  	) @@ -113,7 +113,7 @@ func handleOIDCAuthorizationConsentModeImplicitWithoutID(ctx *middlewares.Authel  	if consent, err = handleOpenIDConnectNewConsentSession(subject, requester, ctx.Providers.OpenIDConnect.GetPushedAuthorizeRequestURIPrefix(ctx)); err != nil {  		ctx.Logger.Errorf(logFmtErrConsentGenerateError, requester.GetID(), client.GetID(), client.GetConsentPolicy(), "generating", err) -		ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotGenerate) +		ctx.Providers.OpenIDConnect.WriteDynamicAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotGenerate)  		return nil, true  	} @@ -121,7 +121,7 @@ func handleOIDCAuthorizationConsentModeImplicitWithoutID(ctx *middlewares.Authel  	if err = ctx.Providers.StorageProvider.SaveOAuth2ConsentSession(ctx, consent); err != nil {  		ctx.Logger.Errorf(logFmtErrConsentSaveSession, requester.GetID(), client.GetID(), client.GetConsentPolicy(), consent.ChallengeID, err) -		ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotSave) +		ctx.Providers.OpenIDConnect.WriteDynamicAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotSave)  		return nil, true  	} @@ -131,7 +131,7 @@ func handleOIDCAuthorizationConsentModeImplicitWithoutID(ctx *middlewares.Authel  	if consent, err = ctx.Providers.StorageProvider.LoadOAuth2ConsentSessionByChallengeID(ctx, challenge); err != nil {  		ctx.Logger.Errorf(logFmtErrConsentSaveSession, requester.GetID(), client.GetID(), client.GetConsentPolicy(), challenge, err) -		ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotSave) +		ctx.Providers.OpenIDConnect.WriteDynamicAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotSave)  		return nil, true  	} @@ -155,7 +155,7 @@ func handleOIDCAuthorizationConsentModeImplicitWithoutID(ctx *middlewares.Authel  	if err = ctx.Providers.StorageProvider.SaveOAuth2ConsentSessionResponse(ctx, consent, false); err != nil {  		ctx.Logger.Errorf(logFmtErrConsentSaveSessionResponse, requester.GetID(), client.GetID(), client.GetConsentPolicy(), consent.ChallengeID, err) -		ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotSave) +		ctx.Providers.OpenIDConnect.WriteDynamicAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotSave)  		return nil, true  	} diff --git a/internal/handlers/handler_oidc_authorization_consent_pre_configured.go b/internal/handlers/handler_oidc_authorization_consent_pre_configured.go index 2dc3fc800..3f079771c 100644 --- a/internal/handlers/handler_oidc_authorization_consent_pre_configured.go +++ b/internal/handlers/handler_oidc_authorization_consent_pre_configured.go @@ -19,7 +19,7 @@ import (  func handleOIDCAuthorizationConsentModePreConfigured(ctx *middlewares.AutheliaCtx, issuer *url.URL, client oidc.Client,  	userSession session.UserSession, subject uuid.UUID, -	rw http.ResponseWriter, r *http.Request, requester oauthelia2.AuthorizeRequester) (consent *model.OAuth2ConsentSession, handled bool) { +	rw http.ResponseWriter, r *http.Request, requester oauthelia2.Requester) (consent *model.OAuth2ConsentSession, handled bool) {  	var (  		consentID uuid.UUID  		err       error @@ -34,7 +34,7 @@ func handleOIDCAuthorizationConsentModePreConfigured(ctx *middlewares.AutheliaCt  		if consentID, err = uuid.ParseBytes(bytesConsentID); err != nil {  			ctx.Logger.Errorf(logFmtErrConsentParseChallengeID, requester.GetID(), client.GetID(), client.GetConsentPolicy(), bytesConsentID, err) -			ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, oidc.ErrConsentMalformedChallengeID) +			ctx.Providers.OpenIDConnect.WriteDynamicAuthorizeError(ctx, rw, requester, oidc.ErrConsentMalformedChallengeID)  			return nil, true  		} @@ -45,7 +45,7 @@ func handleOIDCAuthorizationConsentModePreConfigured(ctx *middlewares.AutheliaCt  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 oauthelia2.AuthorizeRequester) (consent *model.OAuth2ConsentSession, handled bool) { +	rw http.ResponseWriter, r *http.Request, requester oauthelia2.Requester) (consent *model.OAuth2ConsentSession, handled bool) {  	var (  		config *model.OAuth2ConsentPreConfig  		err    error @@ -54,7 +54,7 @@ func handleOIDCAuthorizationConsentModePreConfiguredWithID(ctx *middlewares.Auth  	if consentID == uuid.Nil {  		ctx.Logger.Errorf(logFmtErrConsentZeroID, requester.GetID(), client.GetID(), client.GetConsentPolicy()) -		ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotLookup) +		ctx.Providers.OpenIDConnect.WriteDynamicAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotLookup)  		return nil, true  	} @@ -62,7 +62,7 @@ func handleOIDCAuthorizationConsentModePreConfiguredWithID(ctx *middlewares.Auth  	if consent, err = ctx.Providers.StorageProvider.LoadOAuth2ConsentSessionByChallengeID(ctx, consentID); err != nil {  		ctx.Logger.Errorf(logFmtErrConsentLookupLoadingSession, requester.GetID(), client.GetID(), client.GetConsentPolicy(), consentID, err) -		ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotLookup) +		ctx.Providers.OpenIDConnect.WriteDynamicAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotLookup)  		return nil, true  	} @@ -70,7 +70,7 @@ func handleOIDCAuthorizationConsentModePreConfiguredWithID(ctx *middlewares.Auth  	if subject != consent.Subject.UUID {  		ctx.Logger.Errorf(logFmtErrConsentSessionSubjectNotAuthorized, requester.GetID(), client.GetID(), client.GetConsentPolicy(), consent.ChallengeID, userSession.Username, subject, consent.Subject.UUID) -		ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotLookup) +		ctx.Providers.OpenIDConnect.WriteDynamicAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotLookup)  		return nil, true  	} @@ -84,7 +84,7 @@ func handleOIDCAuthorizationConsentModePreConfiguredWithID(ctx *middlewares.Auth  	if !consent.CanGrant() {  		ctx.Logger.Errorf(logFmtErrConsentCantGrantPreConf, requester.GetID(), client.GetID(), client.GetConsentPolicy(), consent.ChallengeID) -		ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotPerform) +		ctx.Providers.OpenIDConnect.WriteDynamicAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotPerform)  		return nil, true  	} @@ -92,7 +92,7 @@ func handleOIDCAuthorizationConsentModePreConfiguredWithID(ctx *middlewares.Auth  	if config, err = handleOIDCAuthorizationConsentModePreConfiguredGetPreConfig(ctx, client, subject, requester); err != nil {  		ctx.Logger.Errorf(logFmtErrConsentPreConfLookup, requester.GetID(), client.GetID(), client.GetConsentPolicy(), err) -		ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotLookup) +		ctx.Providers.OpenIDConnect.WriteDynamicAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotLookup)  		return nil, true  	} @@ -105,7 +105,7 @@ func handleOIDCAuthorizationConsentModePreConfiguredWithID(ctx *middlewares.Auth  		if err = ctx.Providers.StorageProvider.SaveOAuth2ConsentSessionResponse(ctx, consent, false); err != nil {  			ctx.Logger.Errorf(logFmtErrConsentSaveSessionResponse, requester.GetID(), client.GetID(), client.GetConsentPolicy(), consent.ChallengeID, err) -			ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotSave) +			ctx.Providers.OpenIDConnect.WriteDynamicAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotSave)  			return nil, true  		} @@ -117,7 +117,7 @@ func handleOIDCAuthorizationConsentModePreConfiguredWithID(ctx *middlewares.Auth  		if consent.Responded() {  			ctx.Logger.Errorf(logFmtErrConsentCantGrantRejected, requester.GetID(), client.GetID(), client.GetConsentPolicy(), consent.ChallengeID) -			ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, oauthelia2.ErrAccessDenied) +			ctx.Providers.OpenIDConnect.WriteDynamicAuthorizeError(ctx, rw, requester, oauthelia2.ErrAccessDenied)  			return nil, true  		} @@ -130,7 +130,7 @@ func handleOIDCAuthorizationConsentModePreConfiguredWithID(ctx *middlewares.Auth  	if requester.GetRequestForm().Get(oidc.FormParameterPrompt) == oidc.PromptNone {  		ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' could not be processed: the 'prompt' type of 'none' was requested but client is configured to require consent or pre-configured consent and the pre-configured consent was absent", requester.GetID(), client.GetID()) -		ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, oauthelia2.ErrConsentRequired) +		ctx.Providers.OpenIDConnect.WriteDynamicAuthorizeError(ctx, rw, requester, oauthelia2.ErrConsentRequired)  		return nil, true  	} @@ -140,7 +140,7 @@ func handleOIDCAuthorizationConsentModePreConfiguredWithID(ctx *middlewares.Auth  func handleOIDCAuthorizationConsentModePreConfiguredWithoutID(ctx *middlewares.AutheliaCtx, issuer *url.URL, client oidc.Client,  	userSession session.UserSession, subject uuid.UUID, -	rw http.ResponseWriter, r *http.Request, requester oauthelia2.AuthorizeRequester) (consent *model.OAuth2ConsentSession, handled bool) { +	rw http.ResponseWriter, r *http.Request, requester oauthelia2.Requester) (consent *model.OAuth2ConsentSession, handled bool) {  	var (  		config *model.OAuth2ConsentPreConfig  		err    error @@ -149,7 +149,7 @@ func handleOIDCAuthorizationConsentModePreConfiguredWithoutID(ctx *middlewares.A  	if config, err = handleOIDCAuthorizationConsentModePreConfiguredGetPreConfig(ctx, client, subject, requester); err != nil {  		ctx.Logger.Errorf(logFmtErrConsentPreConfLookup, requester.GetID(), client.GetID(), client.GetConsentPolicy(), err) -		ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotLookup) +		ctx.Providers.OpenIDConnect.WriteDynamicAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotLookup)  		return nil, true  	} @@ -158,7 +158,7 @@ func handleOIDCAuthorizationConsentModePreConfiguredWithoutID(ctx *middlewares.A  		if requester.GetRequestForm().Get(oidc.FormParameterPrompt) == oidc.PromptNone {  			ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' could not be processed: the 'prompt' type of 'none' was requested but client is configured to require consent or pre-configured consent and the pre-configured consent was absent", requester.GetID(), client.GetID()) -			ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, oauthelia2.ErrConsentRequired) +			ctx.Providers.OpenIDConnect.WriteDynamicAuthorizeError(ctx, rw, requester, oauthelia2.ErrConsentRequired)  			return nil, true  		} @@ -169,7 +169,7 @@ func handleOIDCAuthorizationConsentModePreConfiguredWithoutID(ctx *middlewares.A  	if consent, err = handleOpenIDConnectNewConsentSession(subject, requester, ctx.Providers.OpenIDConnect.GetPushedAuthorizeRequestURIPrefix(ctx)); err != nil {  		ctx.Logger.Errorf(logFmtErrConsentGenerateError, requester.GetID(), client.GetID(), client.GetConsentPolicy(), "generating", err) -		ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotGenerate) +		ctx.Providers.OpenIDConnect.WriteDynamicAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotGenerate)  		return nil, true  	} @@ -177,7 +177,7 @@ func handleOIDCAuthorizationConsentModePreConfiguredWithoutID(ctx *middlewares.A  	if err = ctx.Providers.StorageProvider.SaveOAuth2ConsentSession(ctx, consent); err != nil {  		ctx.Logger.Errorf(logFmtErrConsentSaveSession, requester.GetID(), client.GetID(), client.GetConsentPolicy(), consent.ChallengeID, err) -		ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotSave) +		ctx.Providers.OpenIDConnect.WriteDynamicAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotSave)  		return nil, true  	} @@ -185,7 +185,7 @@ func handleOIDCAuthorizationConsentModePreConfiguredWithoutID(ctx *middlewares.A  	if consent, err = ctx.Providers.StorageProvider.LoadOAuth2ConsentSessionByChallengeID(ctx, consent.ChallengeID); err != nil {  		ctx.Logger.Errorf(logFmtErrConsentSaveSession, requester.GetID(), client.GetID(), client.GetConsentPolicy(), consent.ChallengeID, err) -		ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotSave) +		ctx.Providers.OpenIDConnect.WriteDynamicAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotSave)  		return nil, true  	} @@ -205,7 +205,7 @@ func handleOIDCAuthorizationConsentModePreConfiguredWithoutID(ctx *middlewares.A  	if err = ctx.Providers.StorageProvider.SaveOAuth2ConsentSessionResponse(ctx, consent, false); err != nil {  		ctx.Logger.Errorf(logFmtErrConsentSaveSessionResponse, requester.GetID(), client.GetID(), client.GetConsentPolicy(), consent.ChallengeID, err) -		ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotSave) +		ctx.Providers.OpenIDConnect.WriteDynamicAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotSave)  		return nil, true  	} diff --git a/internal/handlers/types.go b/internal/handlers/types.go index 377b158b9..3e140acb6 100644 --- a/internal/handlers/types.go +++ b/internal/handlers/types.go @@ -199,4 +199,4 @@ 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 oauthelia2.AuthorizeRequester) (consent *model.OAuth2ConsentSession, handled bool) +	requester oauthelia2.Requester) (consent *model.OAuth2ConsentSession, handled bool) diff --git a/internal/mocks/storage.go b/internal/mocks/storage.go index fe70f4339..1834e9ea9 100644 --- a/internal/mocks/storage.go +++ b/internal/mocks/storage.go @@ -130,6 +130,20 @@ func (mr *MockStorageMockRecorder) ConsumeOneTimeCode(ctx, code any) *gomock.Cal  	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConsumeOneTimeCode", reflect.TypeOf((*MockStorage)(nil).ConsumeOneTimeCode), ctx, code)  } +// DeactivateOAuth2DeviceCodeSession mocks base method. +func (m *MockStorage) DeactivateOAuth2DeviceCodeSession(ctx context.Context, signature string) error { +	m.ctrl.T.Helper() +	ret := m.ctrl.Call(m, "DeactivateOAuth2DeviceCodeSession", ctx, signature) +	ret0, _ := ret[0].(error) +	return ret0 +} + +// DeactivateOAuth2DeviceCodeSession indicates an expected call of DeactivateOAuth2DeviceCodeSession. +func (mr *MockStorageMockRecorder) DeactivateOAuth2DeviceCodeSession(ctx, signature any) *gomock.Call { +	mr.mock.ctrl.T.Helper() +	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeactivateOAuth2DeviceCodeSession", reflect.TypeOf((*MockStorage)(nil).DeactivateOAuth2DeviceCodeSession), ctx, signature) +} +  // DeactivateOAuth2Session mocks base method.  func (m *MockStorage) DeactivateOAuth2Session(ctx context.Context, sessionType storage.OAuth2SessionType, signature string) error {  	m.ctrl.T.Helper() @@ -319,6 +333,36 @@ func (mr *MockStorageMockRecorder) LoadOAuth2ConsentSessionByChallengeID(ctx, ch  	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadOAuth2ConsentSessionByChallengeID", reflect.TypeOf((*MockStorage)(nil).LoadOAuth2ConsentSessionByChallengeID), ctx, challengeID)  } +// LoadOAuth2DeviceCodeSession mocks base method. +func (m *MockStorage) LoadOAuth2DeviceCodeSession(ctx context.Context, signature string) (*model.OAuth2DeviceCodeSession, error) { +	m.ctrl.T.Helper() +	ret := m.ctrl.Call(m, "LoadOAuth2DeviceCodeSession", ctx, signature) +	ret0, _ := ret[0].(*model.OAuth2DeviceCodeSession) +	ret1, _ := ret[1].(error) +	return ret0, ret1 +} + +// LoadOAuth2DeviceCodeSession indicates an expected call of LoadOAuth2DeviceCodeSession. +func (mr *MockStorageMockRecorder) LoadOAuth2DeviceCodeSession(ctx, signature any) *gomock.Call { +	mr.mock.ctrl.T.Helper() +	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadOAuth2DeviceCodeSession", reflect.TypeOf((*MockStorage)(nil).LoadOAuth2DeviceCodeSession), ctx, signature) +} + +// LoadOAuth2DeviceCodeSessionByUserCode mocks base method. +func (m *MockStorage) LoadOAuth2DeviceCodeSessionByUserCode(ctx context.Context, signature string) (*model.OAuth2DeviceCodeSession, error) { +	m.ctrl.T.Helper() +	ret := m.ctrl.Call(m, "LoadOAuth2DeviceCodeSessionByUserCode", ctx, signature) +	ret0, _ := ret[0].(*model.OAuth2DeviceCodeSession) +	ret1, _ := ret[1].(error) +	return ret0, ret1 +} + +// LoadOAuth2DeviceCodeSessionByUserCode indicates an expected call of LoadOAuth2DeviceCodeSessionByUserCode. +func (mr *MockStorageMockRecorder) LoadOAuth2DeviceCodeSessionByUserCode(ctx, signature any) *gomock.Call { +	mr.mock.ctrl.T.Helper() +	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadOAuth2DeviceCodeSessionByUserCode", reflect.TypeOf((*MockStorage)(nil).LoadOAuth2DeviceCodeSessionByUserCode), ctx, signature) +} +  // LoadOAuth2PARContext mocks base method.  func (m *MockStorage) LoadOAuth2PARContext(ctx context.Context, signature string) (*model.OAuth2PARContext, error) {  	m.ctrl.T.Helper() @@ -772,6 +816,20 @@ func (mr *MockStorageMockRecorder) SaveOAuth2ConsentSessionSubject(ctx, consent  	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveOAuth2ConsentSessionSubject", reflect.TypeOf((*MockStorage)(nil).SaveOAuth2ConsentSessionSubject), ctx, consent)  } +// SaveOAuth2DeviceCodeSession mocks base method. +func (m *MockStorage) SaveOAuth2DeviceCodeSession(ctx context.Context, session *model.OAuth2DeviceCodeSession) error { +	m.ctrl.T.Helper() +	ret := m.ctrl.Call(m, "SaveOAuth2DeviceCodeSession", ctx, session) +	ret0, _ := ret[0].(error) +	return ret0 +} + +// SaveOAuth2DeviceCodeSession indicates an expected call of SaveOAuth2DeviceCodeSession. +func (mr *MockStorageMockRecorder) SaveOAuth2DeviceCodeSession(ctx, session any) *gomock.Call { +	mr.mock.ctrl.T.Helper() +	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveOAuth2DeviceCodeSession", reflect.TypeOf((*MockStorage)(nil).SaveOAuth2DeviceCodeSession), ctx, session) +} +  // SaveOAuth2PARContext mocks base method.  func (m *MockStorage) SaveOAuth2PARContext(ctx context.Context, par model.OAuth2PARContext) error {  	m.ctrl.T.Helper() @@ -1060,6 +1118,20 @@ func (mr *MockStorageMockRecorder) StartupCheck() *gomock.Call {  	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartupCheck", reflect.TypeOf((*MockStorage)(nil).StartupCheck))  } +// UpdateOAuth2DeviceCodeSession mocks base method. +func (m *MockStorage) UpdateOAuth2DeviceCodeSession(ctx context.Context, signature string, status int, checked time.Time) error { +	m.ctrl.T.Helper() +	ret := m.ctrl.Call(m, "UpdateOAuth2DeviceCodeSession", ctx, signature, status, checked) +	ret0, _ := ret[0].(error) +	return ret0 +} + +// UpdateOAuth2DeviceCodeSession indicates an expected call of UpdateOAuth2DeviceCodeSession. +func (mr *MockStorageMockRecorder) UpdateOAuth2DeviceCodeSession(ctx, signature, status, checked any) *gomock.Call { +	mr.mock.ctrl.T.Helper() +	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateOAuth2DeviceCodeSession", reflect.TypeOf((*MockStorage)(nil).UpdateOAuth2DeviceCodeSession), ctx, signature, status, checked) +} +  // UpdateOAuth2PARContext mocks base method.  func (m *MockStorage) UpdateOAuth2PARContext(ctx context.Context, par model.OAuth2PARContext) error {  	m.ctrl.T.Helper() diff --git a/internal/model/oidc.go b/internal/model/oidc.go index d8aedbe01..9e6246d2c 100644 --- a/internal/model/oidc.go +++ b/internal/model/oidc.go @@ -103,6 +103,62 @@ func NewOAuth2SessionFromRequest(signature string, r oauthelia2.Requester) (sess  	}, nil  } +// NewOAuth2DeviceCodeSessionFromRequest creates a new OAuth2DeviceCodeSession from a signature and oauthelia2.Requester. +func NewOAuth2DeviceCodeSessionFromRequest(r oauthelia2.DeviceAuthorizeRequester) (session *OAuth2DeviceCodeSession, err error) { +	if r == nil { +		return nil, fmt.Errorf("failed to create new *model.OAuth2Session: the oauthelia2.Requester was nil") +	} + +	var ( +		subject     sql.NullString +		s           OpenIDSession +		ok          bool +		sessionData []byte +	) + +	s, ok = r.GetSession().(OpenIDSession) +	if !ok { +		return nil, fmt.Errorf("failed to create new *model.OAuth2DeviceCodeSession: the session type OpenIDSession was expected but the type '%T' was used", r.GetSession()) +	} + +	subject = sql.NullString{String: s.GetSubject()} + +	subject.Valid = len(subject.String) > 0 + +	if sessionData, err = json.Marshal(s); err != nil { +		return nil, fmt.Errorf("failed to create new *model.OAuth2DeviceCodeSession: an error was returned while attempting to marshal the session data to json: %w", err) +	} + +	requested, granted := r.GetRequestedScopes(), r.GetGrantedScopes() + +	if requested == nil { +		requested = oauthelia2.Arguments{} +	} + +	if granted == nil { +		granted = oauthelia2.Arguments{} +	} + +	return &OAuth2DeviceCodeSession{ +		RequestID:         r.GetID(), +		ClientID:          r.GetClient().GetID(), +		Signature:         r.GetDeviceCodeSignature(), +		UserCodeSignature: r.GetUserCodeSignature(), +		Status:            int(r.GetStatus()), +		Subject:           subject, +		RequestedAt:       r.GetRequestedAt(), +		CheckedAt:         r.GetLastChecked(), +		RequestedScopes:   StringSlicePipeDelimited(requested), +		GrantedScopes:     StringSlicePipeDelimited(granted), +		RequestedAudience: StringSlicePipeDelimited(r.GetRequestedAudience()), +		GrantedAudience:   StringSlicePipeDelimited(r.GetGrantedAudience()), +		Active:            true, +		Revoked:           false, +		Form:              r.GetRequestForm().Encode(), +		Session:           sessionData, +	}, nil +} +  // NewOAuth2PARContext creates a new Pushed Authorization Request Context as a OAuth2PARContext.  func NewOAuth2PARContext(contextID string, r oauthelia2.AuthorizeRequester) (context *OAuth2PARContext, err error) {  	var ( @@ -333,6 +389,69 @@ func (s *OAuth2Session) ToRequest(ctx context.Context, session oauthelia2.Sessio  	}, nil  } +// OAuth2DeviceCodeSession stores the Device Code Grant information. +type OAuth2DeviceCodeSession struct { +	ID                int                      `db:"id"` +	ChallengeID       uuid.NullUUID            `db:"challenge_id"` +	RequestID         string                   `db:"request_id"` +	ClientID          string                   `db:"client_id"` +	Signature         string                   `db:"signature"` +	UserCodeSignature string                   `db:"user_code_signature"` +	Status            int                      `db:"status"` +	Subject           sql.NullString           `db:"subject"` +	RequestedAt       time.Time                `db:"requested_at"` +	CheckedAt         time.Time                `db:"checked_at"` +	RequestedScopes   StringSlicePipeDelimited `db:"requested_scopes"` +	GrantedScopes     StringSlicePipeDelimited `db:"granted_scopes"` +	RequestedAudience StringSlicePipeDelimited `db:"requested_audience"` +	GrantedAudience   StringSlicePipeDelimited `db:"granted_audience"` +	Active            bool                     `db:"active"` +	Revoked           bool                     `db:"revoked"` +	Form              string                   `db:"form_data"` +	Session           []byte                   `db:"session_data"` +} + +// ToRequest converts an OAuth2Session into a oauthelia2.Request given a oauthelia2.Session and oauthelia2.Storage. +func (s *OAuth2DeviceCodeSession) ToRequest(ctx context.Context, session oauthelia2.Session, store oauthelia2.Storage) (request *oauthelia2.DeviceAuthorizeRequest, err error) { +	sessionData := s.Session + +	if session != nil { +		if err = json.Unmarshal(sessionData, session); err != nil { +			return nil, fmt.Errorf("error occurred while mapping OAuth 2.0 Session back to a DeviceAuthorizeRequest while trying to unmarshal the JSON session data: %w", err) +		} +	} + +	client, err := store.GetClient(ctx, s.ClientID) +	if err != nil { +		return nil, fmt.Errorf("error occurred while mapping OAuth 2.0 Session back to a DeviceAuthorizeRequest while trying to lookup the registered client: %w", err) +	} + +	values, err := url.ParseQuery(s.Form) +	if err != nil { +		return nil, fmt.Errorf("error occurred while mapping OAuth 2.0 Session back to a DeviceAuthorizeRequest while trying to parse the original form: %w", err) +	} + +	request = &oauthelia2.DeviceAuthorizeRequest{ +		Request: oauthelia2.Request{ +			ID:                s.RequestID, +			RequestedAt:       s.RequestedAt, +			Client:            client, +			RequestedScope:    oauthelia2.Arguments(s.RequestedScopes), +			GrantedScope:      oauthelia2.Arguments(s.GrantedScopes), +			RequestedAudience: oauthelia2.Arguments(s.RequestedAudience), +			GrantedAudience:   oauthelia2.Arguments(s.GrantedAudience), +			Form:              values, +			Session:           session, +		}, +		DeviceCodeSignature: s.Signature, +		UserCodeSignature:   s.UserCodeSignature, +		Status:              oauthelia2.DeviceAuthorizeStatus(s.Status), +		LastChecked:         s.CheckedAt, +	} + +	return request, nil +} +  // OAuth2PARContext holds relevant information about a Pushed Authorization Request in order to process the authorization.  type OAuth2PARContext struct {  	ID                   int                      `db:"id"` diff --git a/internal/oidc/config_test.go b/internal/oidc/config_test.go index 8787637dd..4655ee6cc 100644 --- a/internal/oidc/config_test.go +++ b/internal/oidc/config_test.go @@ -382,6 +382,5 @@ func TestMisc(t *testing.T) {  	assert.Equal(t, []string{"https://example.com/issuer", "https://example.com/issuer/api/oidc/token", "https://example.com/issuer/api/oidc/pushed-authorization-request"}, config.GetAllowedJWTAssertionAudiences(tctx)) -	assert.Equal(t, "https://example.com/issuer/api/oidc/device-code/user-verification", config.GetRFC8628UserVerificationURL(tctx)) -	assert.Equal(t, "https://example.com/issuer/api/oidc/device-code/user-verification", config.GetRFC8628UserVerificationURL(tctx)) +	assert.Equal(t, "https://example.com/issuer/consent/openid/device-authorization", config.GetRFC8628UserVerificationURL(tctx))  } diff --git a/internal/oidc/const.go b/internal/oidc/const.go index ef522c18d..57e59f691 100644 --- a/internal/oidc/const.go +++ b/internal/oidc/const.go @@ -108,6 +108,7 @@ const (  	GrantTypeRefreshToken      = valueRefreshToken  	GrantTypeAuthorizationCode = "authorization_code"  	GrantTypeClientCredentials = "client_credentials" +	GrantTypeDeviceCode        = "urn:ietf:params:oauth:grant-type:device_code"  )  // Client Auth Method strings. @@ -205,6 +206,7 @@ const (  // Endpoints.  const (  	EndpointAuthorization              = "authorization" +	EndpointDeviceAuthorization        = "device-authorization"  	EndpointToken                      = "token"  	EndpointUserinfo                   = "userinfo"  	EndpointIntrospection              = "introspection" @@ -235,21 +237,21 @@ const (  	EndpointPathConsentDecision = EndpointPathConsent + "/decision"  	EndpointPathConsentLogin    = EndpointPathConsent + "/login" +	EndpointPathRFC8628UserVerificationURL = EndpointPathConsent + "/" + EndpointDeviceAuthorization +  	EndpointPathWellKnownOpenIDConfiguration      = "/.well-known/openid-configuration"  	EndpointPathWellKnownOAuthAuthorizationServer = "/.well-known/oauth-authorization-server"  	EndpointPathJWKs                              = "/jwks.json"  	EndpointPathRoot = "/api/oidc" -	EndpointPathAuthorization = EndpointPathRoot + "/" + EndpointAuthorization -	EndpointPathToken         = EndpointPathRoot + "/" + EndpointToken -	EndpointPathUserinfo      = EndpointPathRoot + "/" + EndpointUserinfo -	EndpointPathIntrospection = EndpointPathRoot + "/" + EndpointIntrospection -	EndpointPathRevocation    = EndpointPathRoot + "/" + EndpointRevocation - +	EndpointPathAuthorization              = EndpointPathRoot + "/" + EndpointAuthorization +	EndpointPathToken                      = EndpointPathRoot + "/" + EndpointToken +	EndpointPathUserinfo                   = EndpointPathRoot + "/" + EndpointUserinfo +	EndpointPathIntrospection              = EndpointPathRoot + "/" + EndpointIntrospection +	EndpointPathRevocation                 = EndpointPathRoot + "/" + EndpointRevocation +	EndpointPathDeviceAuthorization        = EndpointPathRoot + "/" + EndpointDeviceAuthorization  	EndpointPathPushedAuthorizationRequest = EndpointPathRoot + "/" + EndpointPushedAuthorizationRequest - -	EndpointPathRFC8628UserVerificationURL = EndpointPathRoot + "/device-code/user-verification"  )  // Authentication Method Reference Values https://datatracker.ietf.org/doc/html/rfc8176 diff --git a/internal/oidc/discovery.go b/internal/oidc/discovery.go index e39ea394a..4d72c72aa 100644 --- a/internal/oidc/discovery.go +++ b/internal/oidc/discovery.go @@ -30,6 +30,7 @@ func NewOpenIDConnectWellKnownConfiguration(c *schema.IdentityProvidersOpenIDCon  					GrantTypeImplicit,  					GrantTypeClientCredentials,  					GrantTypeRefreshToken, +					GrantTypeDeviceCode,  				},  				ResponseModesSupported: []string{  					ResponseModeFormPost, @@ -138,6 +139,7 @@ func NewOpenIDConnectWellKnownConfiguration(c *schema.IdentityProvidersOpenIDCon  					ClientAuthMethodPrivateKeyJWT,  				},  			}, +			OAuth2DeviceAuthorizationGrantDiscoveryOptions: &OAuth2DeviceAuthorizationGrantDiscoveryOptions{},  			OAuth2JWTIntrospectionResponseDiscoveryOptions: &OAuth2JWTIntrospectionResponseDiscoveryOptions{  				IntrospectionSigningAlgValuesSupported: []string{  					SigningAlgRSAUsingSHA256, diff --git a/internal/oidc/discovery_test.go b/internal/oidc/discovery_test.go index 91c5cd6cf..77a30adf7 100644 --- a/internal/oidc/discovery_test.go +++ b/internal/oidc/discovery_test.go @@ -227,7 +227,7 @@ func TestNewOpenIDConnectProvider_GetOpenIDConnectWellKnownConfiguration(t *test  	assert.Contains(t, disco.RevocationEndpointAuthMethodsSupported, oidc.ClientAuthMethodNone)  	assert.Equal(t, []string{oidc.ClientAuthMethodClientSecretBasic, oidc.ClientAuthMethodClientSecretPost, oidc.ClientAuthMethodClientSecretJWT, oidc.ClientAuthMethodPrivateKeyJWT}, disco.IntrospectionEndpointAuthMethodsSupported) -	assert.Equal(t, []string{oidc.GrantTypeAuthorizationCode, oidc.GrantTypeImplicit, oidc.GrantTypeClientCredentials, oidc.GrantTypeRefreshToken}, disco.GrantTypesSupported) +	assert.Equal(t, []string{oidc.GrantTypeAuthorizationCode, oidc.GrantTypeImplicit, oidc.GrantTypeClientCredentials, oidc.GrantTypeRefreshToken, oidc.GrantTypeDeviceCode}, disco.GrantTypesSupported)  	assert.Equal(t, []string{oidc.SigningAlgHMACUsingSHA256, oidc.SigningAlgHMACUsingSHA384, oidc.SigningAlgHMACUsingSHA512, oidc.SigningAlgRSAUsingSHA256, oidc.SigningAlgRSAUsingSHA384, oidc.SigningAlgRSAUsingSHA512, oidc.SigningAlgECDSAUsingP256AndSHA256, oidc.SigningAlgECDSAUsingP384AndSHA384, oidc.SigningAlgECDSAUsingP521AndSHA512, oidc.SigningAlgRSAPSSUsingSHA256, oidc.SigningAlgRSAPSSUsingSHA384, oidc.SigningAlgRSAPSSUsingSHA512}, disco.RevocationEndpointAuthSigningAlgValuesSupported)  	assert.Equal(t, []string{oidc.SigningAlgHMACUsingSHA256, oidc.SigningAlgHMACUsingSHA384, oidc.SigningAlgHMACUsingSHA512, oidc.SigningAlgRSAUsingSHA256, oidc.SigningAlgRSAUsingSHA384, oidc.SigningAlgRSAUsingSHA512, oidc.SigningAlgECDSAUsingP256AndSHA256, oidc.SigningAlgECDSAUsingP384AndSHA384, oidc.SigningAlgECDSAUsingP521AndSHA512, oidc.SigningAlgRSAPSSUsingSHA256, oidc.SigningAlgRSAPSSUsingSHA384, oidc.SigningAlgRSAPSSUsingSHA512}, disco.TokenEndpointAuthSigningAlgValuesSupported)  	assert.Equal(t, []string{oidc.SigningAlgRSAUsingSHA256, oidc.SigningAlgNone}, disco.IDTokenSigningAlgValuesSupported) @@ -350,11 +350,12 @@ func TestNewOpenIDConnectProvider_GetOAuth2WellKnownConfiguration(t *testing.T)  	assert.Contains(t, disco.TokenEndpointAuthMethodsSupported, oidc.ClientAuthMethodPrivateKeyJWT)  	assert.Contains(t, disco.TokenEndpointAuthMethodsSupported, oidc.ClientAuthMethodNone) -	assert.Len(t, disco.GrantTypesSupported, 4) +	assert.Len(t, disco.GrantTypesSupported, 5)  	assert.Contains(t, disco.GrantTypesSupported, oidc.GrantTypeAuthorizationCode)  	assert.Contains(t, disco.GrantTypesSupported, oidc.GrantTypeImplicit)  	assert.Contains(t, disco.GrantTypesSupported, oidc.GrantTypeClientCredentials)  	assert.Contains(t, disco.GrantTypesSupported, oidc.GrantTypeRefreshToken) +	assert.Contains(t, disco.GrantTypesSupported, oidc.GrantTypeDeviceCode)  	assert.Len(t, disco.ClaimsSupported, 33)  	assert.Contains(t, disco.ClaimsSupported, oidc.ClaimAuthenticationMethodsReference) diff --git a/internal/oidc/provider.go b/internal/oidc/provider.go index 55b04872f..4ed6aa3ee 100644 --- a/internal/oidc/provider.go +++ b/internal/oidc/provider.go @@ -1,9 +1,15 @@  package oidc  import ( +	"encoding/json" +	"errors"  	"fmt" +	"net/http" +	"net/url"  	oauthelia2 "authelia.com/provider/oauth2" +	"authelia.com/provider/oauth2/i18n" +	"authelia.com/provider/oauth2/x/errorsx"  	"github.com/authelia/authelia/v4/internal/configuration/schema"  	"github.com/authelia/authelia/v4/internal/storage" @@ -41,6 +47,7 @@ func (p *OpenIDConnectProvider) GetOAuth2WellKnownConfiguration(issuer string) O  	options.JWKSURI = fmt.Sprintf("%s%s", issuer, EndpointPathJWKs)  	options.AuthorizationEndpoint = fmt.Sprintf("%s%s", issuer, EndpointPathAuthorization) +	options.DeviceAuthorizationEndpoint = fmt.Sprintf("%s%s", issuer, EndpointPathDeviceAuthorization)  	options.PushedAuthorizationRequestEndpoint = fmt.Sprintf("%s%s", issuer, EndpointPathPushedAuthorizationRequest)  	options.TokenEndpoint = fmt.Sprintf("%s%s", issuer, EndpointPathToken)  	options.IntrospectionEndpoint = fmt.Sprintf("%s%s", issuer, EndpointPathIntrospection) @@ -57,6 +64,7 @@ func (p *OpenIDConnectProvider) GetOpenIDConnectWellKnownConfiguration(issuer st  	options.JWKSURI = fmt.Sprintf("%s%s", issuer, EndpointPathJWKs)  	options.AuthorizationEndpoint = fmt.Sprintf("%s%s", issuer, EndpointPathAuthorization) +	options.DeviceAuthorizationEndpoint = fmt.Sprintf("%s%s", issuer, EndpointPathDeviceAuthorization)  	options.PushedAuthorizationRequestEndpoint = fmt.Sprintf("%s%s", issuer, EndpointPathPushedAuthorizationRequest)  	options.TokenEndpoint = fmt.Sprintf("%s%s", issuer, EndpointPathToken)  	options.UserinfoEndpoint = fmt.Sprintf("%s%s", issuer, EndpointPathUserinfo) @@ -65,3 +73,41 @@ func (p *OpenIDConnectProvider) GetOpenIDConnectWellKnownConfiguration(issuer st  	return options  } + +type bodyDeviceCodeUserAuthorizeRequest struct { +	ID       string `json:"id"` +	UserCode string `json:"user_code"` +} + +func (p *OpenIDConnectProvider) NewRFC8628UserAuthorizeRequest(ctx Context, req *http.Request) (requester oauthelia2.DeviceAuthorizeRequester, err error) { +	body := &bodyDeviceCodeUserAuthorizeRequest{} + +	if err = json.NewDecoder(req.Body).Decode(body); err != nil { +		return nil, errorsx.WithStack(oauthelia2.ErrInvalidRequest.WithHint("Unable to parse HTTP body, make sure to send a properly formatted JSON body.").WithWrap(err).WithDebugError(err)) +	} + +	request := oauthelia2.NewDeviceAuthorizeRequest() +	request.Lang = i18n.GetLangFromRequest(p.Config.GetMessageCatalog(ctx), req) + +	request.Form = url.Values{ +		"user_code": []string{body.UserCode}, +		"id":        []string{body.ID}, +	} + +	for _, h := range p.Config.GetRFC8628UserAuthorizeEndpointHandlers(ctx) { +		if err = h.HandleRFC8628UserAuthorizeEndpointRequest(ctx, request); err != nil && !errors.Is(err, oauthelia2.ErrUnknownRequest) { +			return nil, err +		} +	} + +	return request, nil +} + +func (p *OpenIDConnectProvider) WriteDynamicAuthorizeError(ctx Context, rw http.ResponseWriter, requester oauthelia2.Requester, err error) { +	switch r := requester.(type) { +	case oauthelia2.DeviceAuthorizeRequester: +		p.WriteRFC8628UserAuthorizeError(ctx, rw, r, err) +	case oauthelia2.AuthorizeRequester: +		p.WriteAuthorizeError(ctx, rw, r, err) +	} +} diff --git a/internal/oidc/store.go b/internal/oidc/store.go index 0fe04acfe..50e795368 100644 --- a/internal/oidc/store.go +++ b/internal/oidc/store.go @@ -11,6 +11,7 @@ import (  	oauthelia2 "authelia.com/provider/oauth2"  	"authelia.com/provider/oauth2/handler/oauth2"  	"authelia.com/provider/oauth2/handler/openid" +	"authelia.com/provider/oauth2/handler/rfc8628"  	ostorage "authelia.com/provider/oauth2/storage"  	"github.com/google/uuid" @@ -274,6 +275,59 @@ func (s *Store) GetOpenIDConnectSession(ctx context.Context, authorizeCode strin  	return s.loadRequesterBySignature(ctx, storage.OAuth2SessionTypeOpenIDConnect, authorizeCode, request.GetSession())  } +func (s *Store) CreateDeviceCodeSession(ctx context.Context, signature string, request oauthelia2.DeviceAuthorizeRequester) (err error) { +	session, err := model.NewOAuth2DeviceCodeSessionFromRequest(request) +	if err != nil { +		return err +	} + +	return s.provider.SaveOAuth2DeviceCodeSession(ctx, session) +} + +func (s *Store) UpdateDeviceCodeSession(ctx context.Context, signature string, request oauthelia2.DeviceAuthorizeRequester) (err error) { +	return s.provider.UpdateOAuth2DeviceCodeSession(ctx, signature, int(request.GetStatus()), request.GetLastChecked()) +} + +func (s *Store) GetDeviceCodeSession(ctx context.Context, signature string, session oauthelia2.Session) (request oauthelia2.DeviceAuthorizeRequester, err error) { +	data, err := s.provider.LoadOAuth2DeviceCodeSession(ctx, signature) +	if err != nil { +		return nil, err +	} + +	if !data.Active { +		return nil, oauthelia2.ErrInvalidatedDeviceCode +	} + +	r, err := data.ToRequest(ctx, session, s) +	if err != nil { +		return nil, err +	} + +	return r, nil +} + +func (s *Store) InvalidateDeviceCodeSession(ctx context.Context, signature string) (err error) { +	return s.provider.DeactivateOAuth2DeviceCodeSession(ctx, signature) +} + +func (s *Store) GetDeviceCodeSessionByUserCode(ctx context.Context, signature string, session oauthelia2.Session) (request oauthelia2.DeviceAuthorizeRequester, err error) { +	data, err := s.provider.LoadOAuth2DeviceCodeSessionByUserCode(ctx, signature) +	if err != nil { +		return nil, err +	} + +	if !data.Active { +		return nil, oauthelia2.ErrInvalidatedUserCode +	} + +	r, err := data.ToRequest(ctx, session, s) +	if err != nil { +		return nil, err +	} + +	return r, nil +} +  // CreatePARSession stores the pushed authorization request context. The requestURI is used to derive the key.  // This implements a portion of oauthelia2.PARStorage.  func (s *Store) CreatePARSession(ctx context.Context, requestURI string, request oauthelia2.AuthorizeRequester) (err error) { @@ -383,4 +437,5 @@ var (  	_ oauth2.AuthorizeCodeStorage        = (*Store)(nil)  	_ oauth2.TokenRevocationStorage      = (*Store)(nil)  	_ openid.OpenIDConnectRequestStorage = (*Store)(nil) +	_ rfc8628.Storage                    = (*Store)(nil)  ) diff --git a/internal/server/handlers.go b/internal/server/handlers.go index 8e16872d6..b26d42f29 100644 --- a/internal/server/handlers.go +++ b/internal/server/handlers.go @@ -375,6 +375,16 @@ func handleRouter(config *schema.Configuration, providers middlewares.Providers)  		r.GET("/api/oidc/authorize", authorization)  		r.POST("/api/oidc/authorize", authorization) +		policyCORSDeviceAuthorization := middlewares.NewCORSPolicyBuilder(). +			WithAllowedMethods(fasthttp.MethodOptions, fasthttp.MethodPost). +			WithAllowedOrigins(allowedOrigins...). +			WithEnabled(utils.IsStringInSlice(oidc.EndpointDeviceAuthorization, config.IdentityProviders.OIDC.CORS.Endpoints)). +			Build() + +		r.OPTIONS(oidc.EndpointPathDeviceAuthorization, policyCORSDeviceAuthorization.HandleOnlyOPTIONS) +		r.POST(oidc.EndpointPathDeviceAuthorization, middlewares.Wrap(middlewares.NewMetricsRequestOpenIDConnect(providers.Metrics, oidc.EndpointDeviceAuthorization), policyCORSDeviceAuthorization.Middleware(bridgeOIDC(middlewares.NewHTTPToAutheliaHandlerAdaptor(handlers.OAuthDeviceAuthorizationPOST))))) +		r.PUT(oidc.EndpointPathDeviceAuthorization, middlewares.Wrap(middlewares.NewMetricsRequestOpenIDConnect(providers.Metrics, oidc.EndpointDeviceAuthorization), bridgeOIDC(middlewares.NewHTTPToAutheliaHandlerAdaptor(handlers.OAuthDeviceAuthorizationPUT)))) +  		policyCORSPAR := middlewares.NewCORSPolicyBuilder().  			WithAllowedMethods(fasthttp.MethodOptions, fasthttp.MethodPost).  			WithAllowedOrigins(allowedOrigins...). diff --git a/internal/storage/const.go b/internal/storage/const.go index 84066fa23..fbd73ef51 100644 --- a/internal/storage/const.go +++ b/internal/storage/const.go @@ -22,6 +22,7 @@ const (  	tableOAuth2AccessTokenSession   = "oauth2_access_token_session" //nolint:gosec // This is not a hardcoded credential.  	tableOAuth2AuthorizeCodeSession = "oauth2_authorization_code_session" +	tableOAuth2DeviceCodeSession    = "oauth2_device_code_session"  	tableOAuth2OpenIDConnectSession = "oauth2_openid_connect_session"  	tableOAuth2PARContext           = "oauth2_par_context"  	tableOAuth2PKCERequestSession   = "oauth2_pkce_request_session" diff --git a/internal/storage/migrations/mysql/V0018.OAuth2DeviceCode.down.sql b/internal/storage/migrations/mysql/V0018.OAuth2DeviceCode.down.sql new file mode 100644 index 000000000..f1739171f --- /dev/null +++ b/internal/storage/migrations/mysql/V0018.OAuth2DeviceCode.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS oauth2_device_code_session; diff --git a/internal/storage/migrations/mysql/V0018.OAuth2DeviceCode.up.sql b/internal/storage/migrations/mysql/V0018.OAuth2DeviceCode.up.sql new file mode 100644 index 000000000..49f234cff --- /dev/null +++ b/internal/storage/migrations/mysql/V0018.OAuth2DeviceCode.up.sql @@ -0,0 +1,32 @@ +CREATE TABLE IF NOT EXISTS oauth2_device_code_session ( +    id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT, +    challenge_id CHAR(36) NULL DEFAULT NULL, +    request_id VARCHAR(40) NOT NULL, +    client_id VARCHAR(255) NOT NULL, +    signature VARCHAR(255) NOT NULL, +    user_code_signature VARCHAR(255) NOT NULL, +    status INTEGER NOT NULL, +    subject CHAR(36) NOT NULL, +    requested_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, +    checked_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 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; + +CREATE INDEX oauth2_device_code_session_request_id_idx ON oauth2_device_code_session (request_id); +CREATE INDEX oauth2_device_code_session_client_id_idx ON oauth2_device_code_session (client_id); +CREATE INDEX oauth2_device_code_session_client_id_subject_idx ON oauth2_device_code_session (client_id, subject); + +ALTER TABLE oauth2_device_code_session +    ADD CONSTRAINT oauth2_device_code_session_challenge_id_fkey +        FOREIGN KEY (challenge_id) +            REFERENCES oauth2_consent_session (challenge_id) ON UPDATE CASCADE ON DELETE CASCADE, +    ADD CONSTRAINT oauth2_device_code_session_subject_fkey +        FOREIGN KEY (subject) +            REFERENCES user_opaque_identifier (identifier) ON UPDATE RESTRICT ON DELETE RESTRICT; diff --git a/internal/storage/migrations/postgres/V0018.OAuth2DeviceCode.down.sql b/internal/storage/migrations/postgres/V0018.OAuth2DeviceCode.down.sql new file mode 100644 index 000000000..f1739171f --- /dev/null +++ b/internal/storage/migrations/postgres/V0018.OAuth2DeviceCode.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS oauth2_device_code_session; diff --git a/internal/storage/migrations/postgres/V0018.OAuth2DeviceCode.up.sql b/internal/storage/migrations/postgres/V0018.OAuth2DeviceCode.up.sql new file mode 100644 index 000000000..eea1dbfe1 --- /dev/null +++ b/internal/storage/migrations/postgres/V0018.OAuth2DeviceCode.up.sql @@ -0,0 +1,32 @@ +CREATE TABLE IF NOT EXISTS oauth2_device_code_session ( +    id SERIAL CONSTRAINT oauth2_device_code_session_pkey PRIMARY KEY, +    challenge_id CHAR(36) NOT NULL, +    request_id VARCHAR(40) NOT NULL, +    client_id VARCHAR(255) NOT NULL, +    signature VARCHAR(255) NOT NULL, +    user_code_signature VARCHAR(255) NOT NULL, +    status INTEGER NOT NULL, +    subject CHAR(36) NOT NULL, +    requested_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, +    checked_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 +); + +CREATE INDEX oauth2_device_code_session_request_id_idx ON oauth2_device_code_session (request_id); +CREATE INDEX oauth2_device_code_session_client_id_idx ON oauth2_device_code_session (client_id); +CREATE INDEX oauth2_device_code_session_client_id_subject_idx ON oauth2_device_code_session (client_id, subject); + +ALTER TABLE oauth2_device_code_session +    ADD CONSTRAINT oauth2_device_code_session_challenge_id_fkey +        FOREIGN KEY (challenge_id) +            REFERENCES oauth2_consent_session (challenge_id) ON UPDATE CASCADE ON DELETE CASCADE, +    ADD CONSTRAINT oauth2_device_code_session_subject_fkey +        FOREIGN KEY (subject) +            REFERENCES user_opaque_identifier (identifier) ON UPDATE RESTRICT ON DELETE RESTRICT; diff --git a/internal/storage/migrations/sqlite/V0018.OAuth2DeviceCode.down.sql b/internal/storage/migrations/sqlite/V0018.OAuth2DeviceCode.down.sql new file mode 100644 index 000000000..f1739171f --- /dev/null +++ b/internal/storage/migrations/sqlite/V0018.OAuth2DeviceCode.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS oauth2_device_code_session; diff --git a/internal/storage/migrations/sqlite/V0018.OAuth2DeviceCode.up.sql b/internal/storage/migrations/sqlite/V0018.OAuth2DeviceCode.up.sql new file mode 100644 index 000000000..bcd3be630 --- /dev/null +++ b/internal/storage/migrations/sqlite/V0018.OAuth2DeviceCode.up.sql @@ -0,0 +1,30 @@ +CREATE TABLE IF NOT EXISTS oauth2_device_code_session ( +    id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, +    challenge_id CHAR(36) NULL DEFAULT NULL, +    request_id VARCHAR(40) NOT NULL, +    client_id VARCHAR(255) NOT NULL, +    signature VARCHAR(255) NOT NULL, +    user_code_signature VARCHAR(255) NOT NULL, +    status INTEGER NOT NULL, +    subject CHAR(36) NULL DEFAULT NULL, +    requested_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, +    checked_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, +    CONSTRAINT oauth2_device_code_session_challenge_id_fkey +        FOREIGN KEY (challenge_id) +            REFERENCES oauth2_consent_session (challenge_id) ON UPDATE CASCADE ON DELETE CASCADE, +    CONSTRAINT oauth2_device_code_session_subject_fkey +        FOREIGN KEY (subject) +            REFERENCES user_opaque_identifier (identifier) ON UPDATE CASCADE ON DELETE RESTRICT +); + +CREATE INDEX oauth2_device_code_session_request_id_idx ON oauth2_device_code_session (request_id); +CREATE INDEX oauth2_device_code_session_client_id_idx ON oauth2_device_code_session (client_id); +CREATE INDEX oauth2_device_code_session_client_id_subject_idx ON oauth2_device_code_session (client_id, subject); diff --git a/internal/storage/migrations_test.go b/internal/storage/migrations_test.go index 32c1dcc82..fc2af4777 100644 --- a/internal/storage/migrations_test.go +++ b/internal/storage/migrations_test.go @@ -11,7 +11,7 @@ import (  const (  	// This is the latest schema version for the purpose of tests. -	LatestVersion = 17 +	LatestVersion = 18  )  func TestShouldObtainCorrectMigrations(t *testing.T) { diff --git a/internal/storage/provider.go b/internal/storage/provider.go index b30bf45b5..34c50940d 100644 --- a/internal/storage/provider.go +++ b/internal/storage/provider.go @@ -236,6 +236,27 @@ type Provider interface {  	LoadOAuth2Session(ctx context.Context, sessionType OAuth2SessionType, signature string) (session *model.OAuth2Session, err error)  	/* +		Implementation for OAuth2.0 Device Code Sessions. +	*/ + +	// SaveOAuth2DeviceCodeSession saves an OAuth2.0 device code session to the storage provider. +	SaveOAuth2DeviceCodeSession(ctx context.Context, session *model.OAuth2DeviceCodeSession) (err error) + +	// UpdateOAuth2DeviceCodeSession updates an OAuth2.0 device code session in the storage provider. +	UpdateOAuth2DeviceCodeSession(ctx context.Context, signature string, status int, checked time.Time) (err error) + +	// DeactivateOAuth2DeviceCodeSession marks an OAuth2.0 device code session as inactive in the storage provider. +	DeactivateOAuth2DeviceCodeSession(ctx context.Context, signature string) (err error) + +	// LoadOAuth2DeviceCodeSession loads an OAuth2.0 device code session from the storage provider given the signature +	// of the device code. +	LoadOAuth2DeviceCodeSession(ctx context.Context, signature string) (session *model.OAuth2DeviceCodeSession, err error) + +	// LoadOAuth2DeviceCodeSessionByUserCode loads an OAuth2.0 device code session from the storage provider given the +	// signature of a user code. +	LoadOAuth2DeviceCodeSessionByUserCode(ctx context.Context, signature string) (session *model.OAuth2DeviceCodeSession, err error) + +	/*  		Implementation for OAuth2.0 PAR Contexts.  	*/ diff --git a/internal/storage/sql_provider.go b/internal/storage/sql_provider.go index c458722be..c2c0e7621 100644 --- a/internal/storage/sql_provider.go +++ b/internal/storage/sql_provider.go @@ -121,6 +121,12 @@ func NewSQLProvider(config *schema.Configuration, name, driverName, dataSourceNa  		sqlDeactivateOAuth2AuthorizeCodeSession:            fmt.Sprintf(queryFmtDeactivateOAuth2Session, tableOAuth2AuthorizeCodeSession),  		sqlDeactivateOAuth2AuthorizeCodeSessionByRequestID: fmt.Sprintf(queryFmtDeactivateOAuth2SessionByRequestID, tableOAuth2AuthorizeCodeSession), +		sqlInsertOAuth2DeviceCodeSession:           fmt.Sprintf(queryFmtInsertOAuth2DeviceCodeSession, tableOAuth2DeviceCodeSession), +		sqlSelectOAuth2DeviceCodeSession:           fmt.Sprintf(queryFmtSelectOAuth2DeviceCodeSession, tableOAuth2DeviceCodeSession), +		sqlUpdateOAuth2DeviceCodeSession:           fmt.Sprintf(queryFmtUpdateOAuth2DeviceCodeSession, tableOAuth2DeviceCodeSession), +		sqlDeactivateOAuth2DeviceCodeSession:       fmt.Sprintf(queryFmtDeactivateOAuth2Session, tableOAuth2DeviceCodeSession), +		sqlSelectOAuth2DeviceCodeSessionByUserCode: fmt.Sprintf(queryFmtSelectOAuth2DeviceCodeSessionByUserCode, tableOAuth2DeviceCodeSession), +  		sqlInsertOAuth2OpenIDConnectSession:                fmt.Sprintf(queryFmtInsertOAuth2Session, tableOAuth2OpenIDConnectSession),  		sqlSelectOAuth2OpenIDConnectSession:                fmt.Sprintf(queryFmtSelectOAuth2Session, tableOAuth2OpenIDConnectSession),  		sqlRevokeOAuth2OpenIDConnectSession:                fmt.Sprintf(queryFmtRevokeOAuth2Session, tableOAuth2OpenIDConnectSession), @@ -263,6 +269,13 @@ type SQLProvider struct {  	sqlDeactivateOAuth2AuthorizeCodeSession            string  	sqlDeactivateOAuth2AuthorizeCodeSessionByRequestID string +	// Table: oauth2_device_code_session. +	sqlInsertOAuth2DeviceCodeSession           string +	sqlSelectOAuth2DeviceCodeSession           string +	sqlUpdateOAuth2DeviceCodeSession           string +	sqlDeactivateOAuth2DeviceCodeSession       string +	sqlSelectOAuth2DeviceCodeSessionByUserCode string +  	// Table: oauth2_access_token_session.  	sqlInsertOAuth2AccessTokenSession                string  	sqlSelectOAuth2AccessTokenSession                string @@ -1234,6 +1247,74 @@ func (p *SQLProvider) LoadOAuth2Session(ctx context.Context, sessionType OAuth2S  	return session, nil  } +func (p *SQLProvider) SaveOAuth2DeviceCodeSession(ctx context.Context, session *model.OAuth2DeviceCodeSession) (err error) { +	if session.Session, err = p.encrypt(session.Session); err != nil { +		return fmt.Errorf("error encrypting oauth2 device code session data for session with signature '%s' for subject '%s' and request id '%s': %w", session.Subject.String, session.Signature, session.RequestID, err) +	} + +	_, err = p.db.ExecContext(ctx, p.sqlInsertOAuth2DeviceCodeSession, +		session.ChallengeID, session.RequestID, session.ClientID, session.Signature, session.UserCodeSignature, +		session.Status, session.Subject, session.RequestedAt, session.CheckedAt, +		session.RequestedScopes, session.GrantedScopes, +		session.RequestedAudience, session.GrantedAudience, +		session.Active, session.Revoked, session.Form, session.Session) + +	if err != nil { +		return fmt.Errorf("error inserting oauth2 device code session with device code signature '%s' and user code signature '%s' for subject '%s' and request id '%s': %w", session.Signature, session.UserCodeSignature, session.Subject.String, session.RequestID, err) +	} + +	return nil +} + +func (p *SQLProvider) UpdateOAuth2DeviceCodeSession(ctx context.Context, signature string, status int, checked time.Time) (err error) { +	_, err = p.db.ExecContext(ctx, p.sqlUpdateOAuth2DeviceCodeSession, +		checked, status, signature) + +	if err != nil { +		return fmt.Errorf("error updating oauth2 device code session data with device code signature '%s': %w", signature, err) +	} + +	return nil +} + +func (p *SQLProvider) DeactivateOAuth2DeviceCodeSession(ctx context.Context, signature string) (err error) { +	_, err = p.db.ExecContext(ctx, p.sqlDeactivateOAuth2DeviceCodeSession, signature) + +	if err != nil { +		return fmt.Errorf("error deactivating oauth2 device code session with device code signature '%s': %w", signature, err) +	} + +	return nil +} + +func (p *SQLProvider) LoadOAuth2DeviceCodeSession(ctx context.Context, signature string) (session *model.OAuth2DeviceCodeSession, err error) { +	session = &model.OAuth2DeviceCodeSession{} + +	if err = p.db.GetContext(ctx, session, p.sqlSelectOAuth2DeviceCodeSession, signature); err != nil { +		return nil, fmt.Errorf("error selecting oauth2 device code session with device code signature '%s': %w", signature, err) +	} + +	if session.Session, err = p.decrypt(session.Session); err != nil { +		return nil, fmt.Errorf("error decrypting the oauth2 device code session data with device code signature '%s' for subject '%s' and request id '%s': %w", signature, session.Subject.String, session.RequestID, err) +	} + +	return session, nil +} + +func (p *SQLProvider) LoadOAuth2DeviceCodeSessionByUserCode(ctx context.Context, signature string) (session *model.OAuth2DeviceCodeSession, err error) { +	session = &model.OAuth2DeviceCodeSession{} + +	if err = p.db.GetContext(ctx, session, p.sqlSelectOAuth2DeviceCodeSessionByUserCode, signature); err != nil { +		return nil, fmt.Errorf("error selecting oauth2 device code session with user code signature '%s': %w", signature, err) +	} + +	if session.Session, err = p.decrypt(session.Session); err != nil { +		return nil, fmt.Errorf("error decrypting the oauth2 device code session data with user code signature '%s' for subject '%s' and request id '%s': %w", signature, session.Subject.String, session.RequestID, err) +	} + +	return session, nil +} +  // SaveOAuth2PARContext save an OAuth2.0 PAR context to the storage provider.  func (p *SQLProvider) SaveOAuth2PARContext(ctx context.Context, par model.OAuth2PARContext) (err error) {  	if par.Session, err = p.encrypt(par.Session); err != nil { diff --git a/internal/storage/sql_provider_backend_postgres.go b/internal/storage/sql_provider_backend_postgres.go index 2886d387e..44aaedb62 100644 --- a/internal/storage/sql_provider_backend_postgres.go +++ b/internal/storage/sql_provider_backend_postgres.go @@ -120,6 +120,12 @@ func NewPostgreSQLProvider(config *schema.Configuration, caCertPool *x509.CertPo  	provider.sqlDeactivateOAuth2AuthorizeCodeSessionByRequestID = provider.db.Rebind(provider.sqlDeactivateOAuth2AuthorizeCodeSessionByRequestID)  	provider.sqlSelectOAuth2AuthorizeCodeSession = provider.db.Rebind(provider.sqlSelectOAuth2AuthorizeCodeSession) +	provider.sqlInsertOAuth2DeviceCodeSession = provider.db.Rebind(provider.sqlInsertOAuth2DeviceCodeSession) +	provider.sqlSelectOAuth2DeviceCodeSession = provider.db.Rebind(provider.sqlSelectOAuth2DeviceCodeSession) +	provider.sqlUpdateOAuth2DeviceCodeSession = provider.db.Rebind(provider.sqlUpdateOAuth2DeviceCodeSession) +	provider.sqlDeactivateOAuth2DeviceCodeSession = provider.db.Rebind(provider.sqlDeactivateOAuth2DeviceCodeSession) +	provider.sqlSelectOAuth2DeviceCodeSessionByUserCode = provider.db.Rebind(provider.sqlSelectOAuth2DeviceCodeSessionByUserCode) +  	provider.sqlInsertOAuth2OpenIDConnectSession = provider.db.Rebind(provider.sqlInsertOAuth2OpenIDConnectSession)  	provider.sqlRevokeOAuth2OpenIDConnectSession = provider.db.Rebind(provider.sqlRevokeOAuth2OpenIDConnectSession)  	provider.sqlRevokeOAuth2OpenIDConnectSessionByRequestID = provider.db.Rebind(provider.sqlRevokeOAuth2OpenIDConnectSessionByRequestID) diff --git a/internal/storage/sql_provider_queries.go b/internal/storage/sql_provider_queries.go index 8fe0787c0..f386954e9 100644 --- a/internal/storage/sql_provider_queries.go +++ b/internal/storage/sql_provider_queries.go @@ -388,6 +388,31 @@ const (  		SET active = FALSE  		WHERE request_id = ?;` +	queryFmtSelectOAuth2DeviceCodeSession = ` +		SELECT id, challenge_id, request_id, client_id, signature, user_code_signature, status, subject, +		requested_at, checked_at, requested_scopes, granted_scopes, requested_audience, granted_audience, +		active, revoked, form_data, session_data +		FROM %s +		WHERE signature = ? AND revoked = FALSE;` + +	queryFmtSelectOAuth2DeviceCodeSessionByUserCode = ` +		SELECT id, challenge_id, request_id, client_id, signature, user_code_signature, status, subject, +		requested_at, checked_at, requested_scopes, granted_scopes, requested_audience, granted_audience, +		active, revoked, form_data, session_data +		FROM %s +		WHERE user_code_signature = ? AND revoked = FALSE;` + +	queryFmtInsertOAuth2DeviceCodeSession = ` +		INSERT INTO %s (challenge_id, request_id, client_id, signature, user_code_signature, status, subject, +		requested_at, checked_at, requested_scopes, granted_scopes, requested_audience, granted_audience, +		active, revoked, form_data, session_data) +		VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);` + +	queryFmtUpdateOAuth2DeviceCodeSession = ` +		UPDATE %s +		SET checked_at = ?, status = ? +		WHERE signature = ?;` +  	queryFmtSelectOAuth2PARContext = `  		SELECT id, signature, request_id, client_id, requested_at, scopes, audience,  		handled_response_types, response_mode, response_mode_default, revoked, diff --git a/internal/storage/types.go b/internal/storage/types.go index 8ce91fbda..aeff19fbd 100644 --- a/internal/storage/types.go +++ b/internal/storage/types.go @@ -111,6 +111,7 @@ type OAuth2SessionType int  const (  	OAuth2SessionTypeAccessToken OAuth2SessionType = iota  	OAuth2SessionTypeAuthorizeCode +	OAuth2SessionTypeDeviceAuthorizeCode  	OAuth2SessionTypeOpenIDConnect  	OAuth2SessionTypePAR  	OAuth2SessionTypePKCEChallenge @@ -124,6 +125,8 @@ func (s OAuth2SessionType) String() string {  		return "access token"  	case OAuth2SessionTypeAuthorizeCode:  		return "authorization code" +	case OAuth2SessionTypeDeviceAuthorizeCode: +		return "device code"  	case OAuth2SessionTypeOpenIDConnect:  		return "openid connect"  	case OAuth2SessionTypePAR: @@ -144,6 +147,8 @@ func (s OAuth2SessionType) Table() string {  		return tableOAuth2AccessTokenSession  	case OAuth2SessionTypeAuthorizeCode:  		return tableOAuth2AuthorizeCodeSession +	case OAuth2SessionTypeDeviceAuthorizeCode: +		return tableOAuth2DeviceCodeSession  	case OAuth2SessionTypeOpenIDConnect:  		return tableOAuth2OpenIDConnectSession  	case OAuth2SessionTypePAR: diff --git a/web/src/constants/Routes.ts b/web/src/constants/Routes.ts index aed6356a0..27cca105d 100644 --- a/web/src/constants/Routes.ts +++ b/web/src/constants/Routes.ts @@ -19,3 +19,4 @@ export const ConsentRoute: string = "/consent";  export const ConsentOpenIDSubRoute: string = "/openid";  export const ConsentLoginSubRoute: string = "/login";  export const ConsentDecisionSubRoute: string = "/decision"; +export const ConsentOpenIDDeviceAuthorizationSubRoute: string = "/device-authorization"; diff --git a/web/src/hooks/OpenIDConnect.ts b/web/src/hooks/OpenIDConnect.ts new file mode 100644 index 000000000..bcbafafa7 --- /dev/null +++ b/web/src/hooks/OpenIDConnect.ts @@ -0,0 +1,15 @@ +import { useLocation } from "react-router-dom"; + +export function useUserCode() { +    const { hash } = useLocation(); + +    let raw = hash; + +    if (raw.startsWith("#")) { +        raw = raw.slice(1); +    } + +    const parameters = new URLSearchParams(raw); + +    return parameters.get("user_code"); +} diff --git a/web/src/views/ConsentPortal/OpenIDConnectConsentPortal/OpenIDConnectConsentDeviceAuthorizationFormView.tsx b/web/src/views/ConsentPortal/OpenIDConnectConsentPortal/OpenIDConnectConsentDeviceAuthorizationFormView.tsx new file mode 100644 index 000000000..2e5e5e358 --- /dev/null +++ b/web/src/views/ConsentPortal/OpenIDConnectConsentPortal/OpenIDConnectConsentDeviceAuthorizationFormView.tsx @@ -0,0 +1,60 @@ +import React, { useEffect, useState } from "react"; + +import { Button, FormControl } from "@mui/material"; +import Grid from "@mui/material/Grid2"; +import TextField from "@mui/material/TextField"; +import { useTranslation } from "react-i18next"; + +import { useUserCode } from "@hooks/OpenIDConnect"; +import LoginLayout from "@layouts/LoginLayout"; +import { UserInfo } from "@models/UserInfo"; +import { AutheliaState } from "@services/State"; + +export interface Props { +    userInfo: UserInfo; +    state: AutheliaState; +} + +const OpenIDConnectConsentDeviceAuthorizationFormView: React.FC<Props> = (props: Props) => { +    const { t: translate } = useTranslation(); + +    const [code, setCode] = useState(""); + +    const userCode = useUserCode(); + +    useEffect(() => { +        if (userCode === null || userCode === "") { +            return; +        } + +        setCode(userCode); +    }, [userCode]); + +    return ( +        <LoginLayout id="consent-stage" title={translate("Confirm the Code")}> +            <FormControl id={"form-consent-openid-device-code-authorization"}> +                <Grid container spacing={2}> +                    <Grid size={{ xs: 12 }}> +                        <TextField +                            id="user-code" +                            label={translate("Code")} +                            variant="outlined" +                            required +                            value={code} +                            fullWidth +                            onChange={(v) => setCode(v.target.value)} +                            autoCapitalize="none" +                        /> +                    </Grid> +                    <Grid size={{ xs: 12 }}> +                        <Button id="confirm-button" variant="contained" color="primary" fullWidth> +                            {translate("Confirm")} +                        </Button> +                    </Grid> +                </Grid> +            </FormControl> +        </LoginLayout> +    ); +}; + +export default OpenIDConnectConsentDeviceAuthorizationFormView; diff --git a/web/src/views/ConsentPortal/OpenIDConnectConsentPortal/OpenIDConnectConsentPortal.tsx b/web/src/views/ConsentPortal/OpenIDConnectConsentPortal/OpenIDConnectConsentPortal.tsx index f61241b02..38904582d 100644 --- a/web/src/views/ConsentPortal/OpenIDConnectConsentPortal/OpenIDConnectConsentPortal.tsx +++ b/web/src/views/ConsentPortal/OpenIDConnectConsentPortal/OpenIDConnectConsentPortal.tsx @@ -2,12 +2,19 @@ import React, { lazy } from "react";  import { Route, Routes } from "react-router-dom"; -import { ConsentDecisionSubRoute, ConsentLoginSubRoute } from "@constants/Routes"; +import { +    ConsentDecisionSubRoute, +    ConsentLoginSubRoute, +    ConsentOpenIDDeviceAuthorizationSubRoute, +} from "@constants/Routes";  import { UserInfo } from "@models/UserInfo";  import { AutheliaState } from "@services/State";  const OpenIDConnectConsentDecisionFormView = lazy(      () => import("@views/ConsentPortal/OpenIDConnectConsentPortal/OpenIDConnectConsentDecisionFormView"),  ); +const OpenIDConnectConsentDeviceAuthorizationFormView = lazy( +    () => import("@views/ConsentPortal/OpenIDConnectConsentPortal/OpenIDConnectConsentDeviceAuthorizationFormView"), +);  const OpenIDConnectConsentLoginFormView = lazy(      () => import("@views/ConsentPortal/OpenIDConnectConsentPortal/OpenIDConnectConsentLoginFormView"),  ); @@ -28,6 +35,12 @@ const OpenIDConnectConsentPortal: React.FC<Props> = (props: Props) => {                  path={ConsentDecisionSubRoute}                  element={<OpenIDConnectConsentDecisionFormView userInfo={props.userInfo} state={props.state} />}              /> +            <Route +                path={ConsentOpenIDDeviceAuthorizationSubRoute} +                element={ +                    <OpenIDConnectConsentDeviceAuthorizationFormView userInfo={props.userInfo} state={props.state} /> +                } +            />          </Routes>      );  };  | 
