summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJames Elliott <james-d-elliott@users.noreply.github.com>2025-02-22 22:03:33 +1100
committerGitHub <noreply@github.com>2025-02-22 11:03:33 +0000
commite7d387ed9169dcdb4e8171db8ed20ec6ef376e0a (patch)
tree96bfca916ad1e25c7f960e98cd7e3d0af8f3fdd3
parent111344eaea4fd0c32ce58a181b94414ae639fe2b (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>
-rw-r--r--docs/content/integration/openid-connect/introduction.md3
-rw-r--r--docs/static/schemas/v4.39/json-schema/configuration.json4
-rw-r--r--internal/configuration/schema/identity_providers.go4
-rw-r--r--internal/configuration/validator/const.go4
-rw-r--r--internal/configuration/validator/identity_providers_test.go6
-rw-r--r--internal/handlers/handler_oauth_authorization_claims.go12
-rw-r--r--internal/handlers/handler_oauth_device_authorization.go138
-rw-r--r--internal/handlers/handler_oidc_authorization_consent.go32
-rw-r--r--internal/handlers/handler_oidc_authorization_consent_explicit.go16
-rw-r--r--internal/handlers/handler_oidc_authorization_consent_implicit.go26
-rw-r--r--internal/handlers/handler_oidc_authorization_consent_pre_configured.go36
-rw-r--r--internal/handlers/types.go2
-rw-r--r--internal/mocks/storage.go72
-rw-r--r--internal/model/oidc.go119
-rw-r--r--internal/oidc/config_test.go3
-rw-r--r--internal/oidc/const.go18
-rw-r--r--internal/oidc/discovery.go2
-rw-r--r--internal/oidc/discovery_test.go5
-rw-r--r--internal/oidc/provider.go46
-rw-r--r--internal/oidc/store.go55
-rw-r--r--internal/server/handlers.go10
-rw-r--r--internal/storage/const.go1
-rw-r--r--internal/storage/migrations/mysql/V0018.OAuth2DeviceCode.down.sql1
-rw-r--r--internal/storage/migrations/mysql/V0018.OAuth2DeviceCode.up.sql32
-rw-r--r--internal/storage/migrations/postgres/V0018.OAuth2DeviceCode.down.sql1
-rw-r--r--internal/storage/migrations/postgres/V0018.OAuth2DeviceCode.up.sql32
-rw-r--r--internal/storage/migrations/sqlite/V0018.OAuth2DeviceCode.down.sql1
-rw-r--r--internal/storage/migrations/sqlite/V0018.OAuth2DeviceCode.up.sql30
-rw-r--r--internal/storage/migrations_test.go2
-rw-r--r--internal/storage/provider.go21
-rw-r--r--internal/storage/sql_provider.go81
-rw-r--r--internal/storage/sql_provider_backend_postgres.go6
-rw-r--r--internal/storage/sql_provider_queries.go25
-rw-r--r--internal/storage/types.go5
-rw-r--r--web/src/constants/Routes.ts1
-rw-r--r--web/src/hooks/OpenIDConnect.ts15
-rw-r--r--web/src/views/ConsentPortal/OpenIDConnectConsentPortal/OpenIDConnectConsentDeviceAuthorizationFormView.tsx60
-rw-r--r--web/src/views/ConsentPortal/OpenIDConnectConsentPortal/OpenIDConnectConsentPortal.tsx15
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>
);
};