diff options
| author | James Elliott <james-d-elliott@users.noreply.github.com> | 2022-10-20 13:16:36 +1100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2022-10-20 13:16:36 +1100 |
| commit | 3aaca0604f2ed479d7f00fb5087ffed059f87a71 (patch) | |
| tree | dfa3a2622b3368fac30d5eee2956b4d5fcc5fa17 /internal/handlers/handler_oidc_authorization_consent.go | |
| parent | b1a6dae99ac5b34f065391e266ac1cd87bac5b14 (diff) | |
feat(oidc): implicit consent (#4080)
This adds multiple consent modes to OpenID Connect clients. Specifically it allows configuration of a new consent mode called implicit which never asks for user consent.
Diffstat (limited to 'internal/handlers/handler_oidc_authorization_consent.go')
| -rw-r--r-- | internal/handlers/handler_oidc_authorization_consent.go | 261 |
1 files changed, 91 insertions, 170 deletions
diff --git a/internal/handlers/handler_oidc_authorization_consent.go b/internal/handlers/handler_oidc_authorization_consent.go index 3fdfb25d1..04dfacbb1 100644 --- a/internal/handlers/handler_oidc_authorization_consent.go +++ b/internal/handlers/handler_oidc_authorization_consent.go @@ -1,6 +1,7 @@ package handlers import ( + "errors" "fmt" "net/http" "net/url" @@ -16,115 +17,68 @@ import ( "github.com/authelia/authelia/v4/internal/model" "github.com/authelia/authelia/v4/internal/oidc" "github.com/authelia/authelia/v4/internal/session" - "github.com/authelia/authelia/v4/internal/storage" "github.com/authelia/authelia/v4/internal/utils" ) -func handleOIDCAuthorizationConsent(ctx *middlewares.AutheliaCtx, rootURI string, client *oidc.Client, +func handleOIDCAuthorizationConsent(ctx *middlewares.AutheliaCtx, issuer *url.URL, client *oidc.Client, userSession session.UserSession, rw http.ResponseWriter, r *http.Request, requester fosite.AuthorizeRequester) (consent *model.OAuth2ConsentSession, handled bool) { var ( - issuer *url.URL subject uuid.UUID err error ) - if issuer, err = url.ParseRequestURI(rootURI); err != nil { - ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrServerError.WithHint("Could not safely determine the issuer.")) + var handler handlerAuthorizationConsent - return nil, true - } - - if !strings.HasSuffix(issuer.Path, "/") { - issuer.Path += "/" - } + switch { + case userSession.IsAnonymous(): + handler = handleOIDCAuthorizationConsentNotAuthenticated + case client.IsAuthenticationLevelSufficient(userSession.AuthenticationLevel): + if subject, err = ctx.Providers.OpenIDConnect.GetSubject(ctx, client.GetSectorIdentifier(), userSession.Username); err != nil { + ctx.Logger.Errorf(logFmtErrConsentCantGetSubject, requester.GetID(), client.GetID(), client.Consent, userSession.Username, client.GetSectorIdentifier(), err) - // This prevents the consent request from being generated until the authentication level is sufficient. - if !client.IsAuthenticationLevelSufficient(userSession.AuthenticationLevel) || userSession.Username == "" { - redirectURL := getOIDCAuthorizationRedirectURL(issuer, requester) - - ctx.Logger.Debugf("Authorization Request with id '%s' on client with id '%s' is being redirected due to insufficient authentication", requester.GetID(), client.GetID()) - - http.Redirect(rw, r, redirectURL.String(), http.StatusFound) - - return nil, true - } - - if subject, err = ctx.Providers.OpenIDConnect.Store.GetSubject(ctx, client.GetSectorIdentifier(), userSession.Username); err != nil { - ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' could not be processed: error occurred retrieving subject identifier for user '%s' and sector identifier '%s': %+v", requester.GetID(), client.GetID(), userSession.Username, client.GetSectorIdentifier(), err) - - ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrServerError.WithHint("Could not retrieve the subject.")) - - return nil, true - } - - var consentIDBytes []byte - - if consentIDBytes = ctx.QueryArgs().Peek("consent_id"); len(consentIDBytes) != 0 { - var consentID uuid.UUID - - if consentID, err = uuid.Parse(string(consentIDBytes)); err != nil { - ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrServerError.WithHint("Consent Session ID was Malformed.")) + ctx.Providers.OpenIDConnect.WriteAuthorizeError(rw, requester, oidc.ErrSubjectCouldNotLookup) return nil, true } - ctx.Logger.Debugf("Authorization Request with id '%s' on client with id '%s' proceeding to lookup consent by challenge id '%s'", requester.GetID(), client.GetID(), consentID) - - return handleOIDCAuthorizationConsentWithChallengeID(ctx, issuer, client, userSession, subject, consentID, rw, r, requester) - } - - return handleOIDCAuthorizationConsentGenerate(ctx, issuer, client, userSession, subject, rw, r, requester) -} - -func handleOIDCAuthorizationConsentWithChallengeID(ctx *middlewares.AutheliaCtx, issuer *url.URL, client *oidc.Client, - userSession session.UserSession, subject, challengeID uuid.UUID, - rw http.ResponseWriter, r *http.Request, requester fosite.AuthorizeRequester) (consent *model.OAuth2ConsentSession, handled bool) { - var ( - err error - ) - - if consent, err = ctx.Providers.StorageProvider.LoadOAuth2ConsentSessionByChallengeID(ctx, challengeID); err != nil { - ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' could not be processed: error occurred during consent session lookup: %+v", requester.GetID(), requester.GetClient().GetID(), err) - - ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrServerError.WithHint("Failed to lookup consent session.")) - - return nil, true - } - - if err = verifyOIDCUserAuthorizedForConsent(ctx, client, userSession, consent, subject); err != nil { - ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' could not process consent session with challenge id '%s': could not authorize the user user '%s' for this consent session: %v", requester.GetID(), client.GetID(), consent.ChallengeID, userSession.Username, err) + switch client.Consent.Mode { + case oidc.ClientConsentModeExplicit: + handler = handleOIDCAuthorizationConsentModeExplicit + case oidc.ClientConsentModeImplicit: + handler = handleOIDCAuthorizationConsentModeImplicit + case oidc.ClientConsentModePreConfigured: + handler = handleOIDCAuthorizationConsentModePreConfigured + default: + ctx.Logger.Errorf(logFmtErrConsentCantDetermineConsentMode, requester.GetID(), client.GetID()) - ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrServerError.WithHint("The user is not authorized to perform consent.")) - - return nil, true - } - - if consent.Responded() { - if consent.Granted { - ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' could not be processed: this consent session with challenge id '%s' was already granted", requester.GetID(), client.GetID(), consent.ChallengeID) - - ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrServerError.WithHint("Authorization already granted.")) + ctx.Providers.OpenIDConnect.WriteAuthorizeError(rw, requester, fosite.ErrServerError.WithHint("Could not determine the client consent mode.")) return nil, true } + default: + if subject, err = ctx.Providers.OpenIDConnect.GetSubject(ctx, client.GetSectorIdentifier(), userSession.Username); err != nil { + ctx.Logger.Errorf(logFmtErrConsentCantGetSubject, requester.GetID(), client.GetID(), client.Consent, userSession.Username, client.GetSectorIdentifier(), err) - ctx.Logger.Debugf("Authorization Request with id '%s' loaded consent session with id '%d' and challenge id '%s' for client id '%s' and subject '%s' and scopes '%s'", requester.GetID(), consent.ID, consent.ChallengeID, client.GetID(), consent.Subject.UUID, strings.Join(requester.GetRequestedScopes(), " ")) - - if consent.IsDenied() { - ctx.Logger.Warnf("Authorization Request with id '%s' and challenge id '%s' for client id '%s' and subject '%s' and scopes '%s' was not denied by the user durng the consent session", requester.GetID(), consent.ChallengeID, client.GetID(), consent.Subject.UUID, strings.Join(requester.GetRequestedScopes(), " ")) - - ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrAccessDenied) + ctx.Providers.OpenIDConnect.WriteAuthorizeError(rw, requester, oidc.ErrSubjectCouldNotLookup) return nil, true } - return consent, false + handler = handleOIDCAuthorizationConsentGenerate } - handleOIDCAuthorizationConsentRedirect(ctx, issuer, consent, client, userSession, rw, r, requester) + return handler(ctx, issuer, client, userSession, subject, rw, r, requester) +} - return consent, true +func handleOIDCAuthorizationConsentNotAuthenticated(_ *middlewares.AutheliaCtx, issuer *url.URL, _ *oidc.Client, + _ session.UserSession, _ uuid.UUID, + rw http.ResponseWriter, r *http.Request, requester fosite.AuthorizeRequester) (consent *model.OAuth2ConsentSession, handled bool) { + redirectionURL := handleOIDCAuthorizationConsentGetRedirectionURL(issuer, nil, requester) + + http.Redirect(rw, r, redirectionURL.String(), http.StatusFound) + + return nil, true } func handleOIDCAuthorizationConsentGenerate(ctx *middlewares.AutheliaCtx, issuer *url.URL, client *oidc.Client, @@ -134,36 +88,28 @@ func handleOIDCAuthorizationConsentGenerate(ctx *middlewares.AutheliaCtx, issuer err error ) - scopes, audience := getOIDCExpectedScopesAndAudienceFromRequest(requester) + ctx.Logger.Debugf(logFmtDbgConsentGenerate, requester.GetID(), client.GetID(), client.Consent) - if consent, err = getOIDCPreConfiguredConsent(ctx, client.GetID(), subject, scopes, audience); err != nil { - ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' had error looking up pre-configured consent sessions: %+v", requester.GetID(), requester.GetClient().GetID(), err) + if len(ctx.QueryArgs().PeekBytes(qryArgConsentID)) != 0 { + ctx.Logger.Errorf(logFmtErrConsentGenerateError, requester.GetID(), client.GetID(), client.Consent, "generating", errors.New("consent id value was present when it should be absent")) - ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrServerError.WithHint("Could not lookup the consent session.")) + ctx.Providers.OpenIDConnect.WriteAuthorizeError(rw, requester, oidc.ErrConsentCouldNotGenerate) return nil, true } - if consent != nil { - ctx.Logger.Debugf("Authorization Request with id '%s' on client with id '%s' successfully looked up pre-configured consent with challenge id '%s'", requester.GetID(), client.GetID(), consent.ChallengeID) - - return consent, false - } - - ctx.Logger.Debugf("Authorization Request with id '%s' on client with id '%s' proceeding to generate a new consent due to unsuccessful lookup of pre-configured consent", requester.GetID(), client.GetID()) - if consent, err = model.NewOAuth2ConsentSession(subject, requester); err != nil { - ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' could not be processed: error occurred generating consent: %+v", requester.GetID(), requester.GetClient().GetID(), err) + ctx.Logger.Errorf(logFmtErrConsentGenerateError, requester.GetID(), client.GetID(), client.Consent, "generating", err) - ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrServerError.WithHint("Could not generate the consent session.")) + ctx.Providers.OpenIDConnect.WriteAuthorizeError(rw, requester, oidc.ErrConsentCouldNotGenerate) return nil, true } if err = ctx.Providers.StorageProvider.SaveOAuth2ConsentSession(ctx, *consent); err != nil { - ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' could not be processed: error occurred saving consent session: %+v", requester.GetID(), client.GetID(), err) + ctx.Logger.Errorf(logFmtErrConsentGenerateError, requester.GetID(), client.GetID(), client.Consent, "saving", err) - ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrServerError.WithHint("Could not save the consent session.")) + ctx.Providers.OpenIDConnect.WriteAuthorizeError(rw, requester, oidc.ErrConsentCouldNotSave) return nil, true } @@ -179,70 +125,87 @@ func handleOIDCAuthorizationConsentRedirect(ctx *middlewares.AutheliaCtx, issuer if client.IsAuthenticationLevelSufficient(userSession.AuthenticationLevel) { location, _ = url.ParseRequestURI(issuer.String()) - location.Path = path.Join(location.Path, "/consent") + location.Path = path.Join(location.Path, oidc.EndpointPathConsent) query := location.Query() - query.Set("consent_id", consent.ChallengeID.String()) + query.Set(queryArgID, consent.ChallengeID.String()) location.RawQuery = query.Encode() - ctx.Logger.Debugf("Authorization Request with id '%s' on client with id '%s' authentication level '%s' is sufficient for client level '%s'", requester.GetID(), client.GetID(), authentication.LevelToString(userSession.AuthenticationLevel), authorization.LevelToString(client.Policy)) + ctx.Logger.Debugf(logFmtDbgConsentAuthenticationSufficiency, requester.GetID(), client.GetID(), client.Consent, authentication.LevelToString(userSession.AuthenticationLevel), "sufficient", authorization.LevelToString(client.Policy)) } else { - location = getOIDCAuthorizationRedirectURL(issuer, requester) + location = handleOIDCAuthorizationConsentGetRedirectionURL(issuer, consent, requester) - ctx.Logger.Debugf("Authorization Request with id '%s' on client with id '%s' authentication level '%s' is insufficient for client level '%s'", requester.GetID(), client.GetID(), authentication.LevelToString(userSession.AuthenticationLevel), authorization.LevelToString(client.Policy)) + ctx.Logger.Debugf(logFmtDbgConsentAuthenticationSufficiency, requester.GetID(), client.GetID(), client.Consent, authentication.LevelToString(userSession.AuthenticationLevel), "insufficient", authorization.LevelToString(client.Policy)) } - ctx.Logger.Debugf("Authorization Request with id '%s' on client with id '%s' is being redirected to '%s'", requester.GetID(), client.GetID(), location) + ctx.Logger.Debugf(logFmtDbgConsentRedirect, requester.GetID(), client.GetID(), client.Consent, location) http.Redirect(rw, r, location.String(), http.StatusFound) } -func verifyOIDCUserAuthorizedForConsent(ctx *middlewares.AutheliaCtx, client *oidc.Client, userSession session.UserSession, consent *model.OAuth2ConsentSession, subject uuid.UUID) (err error) { - var sid, csid uint32 +func handleOIDCAuthorizationConsentGetRedirectionURL(issuer *url.URL, consent *model.OAuth2ConsentSession, requester fosite.AuthorizeRequester) (redirectURL *url.URL) { + iss := issuer.String() - csid = consent.Subject.UUID.ID() + if !strings.HasSuffix(iss, "/") { + iss += "/" + } + + redirectURL, _ = url.ParseRequestURI(iss) + + query := redirectURL.Query() + query.Set(queryArgWorkflow, workflowOpenIDConnect) - if !consent.Subject.Valid || csid == 0 { - return fmt.Errorf("the consent subject is null for consent session with id '%d'", consent.ID) + switch { + case consent != nil: + query.Set(queryArgWorkflowID, consent.ChallengeID.String()) + case requester != nil: + rd, _ := url.ParseRequestURI(iss) + rd.Path = path.Join(rd.Path, oidc.EndpointPathAuthorization) + rd.RawQuery = requester.GetRequestForm().Encode() + + query.Set(queryArgRD, rd.String()) } + redirectURL.RawQuery = query.Encode() + + return redirectURL +} + +func verifyOIDCUserAuthorizedForConsent(ctx *middlewares.AutheliaCtx, client *oidc.Client, userSession session.UserSession, consent *model.OAuth2ConsentSession, subject uuid.UUID) (err error) { + var sid uint32 + if client == nil { - if client, err = ctx.Providers.OpenIDConnect.Store.GetFullClient(consent.ClientID); err != nil { + if client, err = ctx.Providers.OpenIDConnect.GetFullClient(consent.ClientID); err != nil { return fmt.Errorf("failed to retrieve client: %w", err) } } if sid = subject.ID(); sid == 0 { - if subject, err = ctx.Providers.OpenIDConnect.Store.GetSubject(ctx, client.GetSectorIdentifier(), userSession.Username); err != nil { + if subject, err = ctx.Providers.OpenIDConnect.GetSubject(ctx, client.GetSectorIdentifier(), userSession.Username); err != nil { return fmt.Errorf("failed to lookup subject: %w", err) } sid = subject.ID() } - if csid != sid { - return fmt.Errorf("the consent subject identifier '%s' isn't owned by user '%s' who has a subject identifier of '%s' with sector identifier '%s'", consent.Subject.UUID, userSession.Username, subject, client.GetSectorIdentifier()) - } - - return nil -} - -func getOIDCAuthorizationRedirectURL(issuer *url.URL, requester fosite.AuthorizeRequester) (redirectURL *url.URL) { - redirectURL, _ = url.ParseRequestURI(issuer.String()) - - authorizationURL, _ := url.ParseRequestURI(issuer.String()) + if !consent.Subject.Valid { + if sid == 0 { + return fmt.Errorf("the consent subject is null for consent session with id '%d' for anonymous user", consent.ID) + } - authorizationURL.Path = path.Join(authorizationURL.Path, oidc.AuthorizationPath) - authorizationURL.RawQuery = requester.GetRequestForm().Encode() + consent.Subject = uuid.NullUUID{UUID: subject, Valid: true} - query := redirectURL.Query() - query.Set("rd", authorizationURL.String()) - query.Set("workflow", workflowOpenIDConnect) + if err = ctx.Providers.StorageProvider.SaveOAuth2ConsentSessionSubject(ctx, *consent); err != nil { + return fmt.Errorf("failed to update the consent subject: %w", err) + } + } - redirectURL.RawQuery = query.Encode() + if consent.Subject.UUID.ID() != sid { + return fmt.Errorf("the consent subject identifier '%s' isn't owned by user '%s' who has a subject identifier of '%s' with sector identifier '%s'", consent.Subject.UUID, userSession.Username, subject, client.GetSectorIdentifier()) + } - return redirectURL + return nil } func getOIDCExpectedScopesAndAudienceFromRequest(requester fosite.Requester) (scopes, audience []string) { @@ -256,45 +219,3 @@ func getOIDCExpectedScopesAndAudience(clientID string, scopes, audience []string return scopes, audience } - -func getOIDCPreConfiguredConsent(ctx *middlewares.AutheliaCtx, clientID string, subject uuid.UUID, scopes, audience []string) (consent *model.OAuth2ConsentSession, err error) { - var ( - rows *storage.ConsentSessionRows - ) - - ctx.Logger.Debugf("Consent Session is being checked for pre-configuration with signature of client id '%s' and subject '%s'", clientID, subject) - - if rows, err = ctx.Providers.StorageProvider.LoadOAuth2ConsentSessionsPreConfigured(ctx, clientID, subject); err != nil { - ctx.Logger.Debugf("Consent Session checked for pre-configuration with signature of client id '%s' and subject '%s' failed with error during load: %+v", clientID, subject, err) - - return nil, err - } - - defer func() { - if err := rows.Close(); err != nil { - ctx.Logger.Errorf("Consent Session checked for pre-configuration with signature of client id '%s' and subject '%s' failed to close rows with error: %+v", clientID, subject, err) - } - }() - - for rows.Next() { - if consent, err = rows.Get(); err != nil { - ctx.Logger.Debugf("Consent Session checked for pre-configuration with signature of client id '%s' and subject '%s' failed with error during iteration: %+v", clientID, subject, err) - - return nil, err - } - - if consent.HasExactGrants(scopes, audience) && consent.CanGrant() { - break - } - } - - if consent != nil && consent.HasExactGrants(scopes, audience) && consent.CanGrant() { - ctx.Logger.Debugf("Consent Session checked for pre-configuration with signature of client id '%s' and subject '%s' found a result with challenge id '%s'", clientID, subject, consent.ChallengeID) - - return consent, nil - } - - ctx.Logger.Debugf("Consent Session checked for pre-configuration with signature of client id '%s' and subject '%s' did not find any results", clientID, subject) - - return nil, nil -} |
