summaryrefslogtreecommitdiff
path: root/internal/handlers/handler_oidc_authorization_consent.go
diff options
context:
space:
mode:
authorJames Elliott <james-d-elliott@users.noreply.github.com>2022-10-20 13:16:36 +1100
committerGitHub <noreply@github.com>2022-10-20 13:16:36 +1100
commit3aaca0604f2ed479d7f00fb5087ffed059f87a71 (patch)
treedfa3a2622b3368fac30d5eee2956b4d5fcc5fa17 /internal/handlers/handler_oidc_authorization_consent.go
parentb1a6dae99ac5b34f065391e266ac1cd87bac5b14 (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.go261
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
-}