diff options
Diffstat (limited to 'internal/handlers/handler_authz_authn.go')
| -rw-r--r-- | internal/handlers/handler_authz_authn.go | 210 |
1 files changed, 175 insertions, 35 deletions
diff --git a/internal/handlers/handler_authz_authn.go b/internal/handlers/handler_authz_authn.go index 853e98a5c..5a8b80224 100644 --- a/internal/handlers/handler_authz_authn.go +++ b/internal/handlers/handler_authz_authn.go @@ -2,6 +2,7 @@ package handlers import ( "bytes" + "context" "encoding/base64" "errors" "fmt" @@ -9,12 +10,16 @@ import ( "strings" "time" + "github.com/ory/fosite" "github.com/sirupsen/logrus" "github.com/valyala/fasthttp" "github.com/authelia/authelia/v4/internal/authentication" + "github.com/authelia/authelia/v4/internal/authorization" "github.com/authelia/authelia/v4/internal/configuration/schema" "github.com/authelia/authelia/v4/internal/middlewares" + "github.com/authelia/authelia/v4/internal/model" + "github.com/authelia/authelia/v4/internal/oidc" "github.com/authelia/authelia/v4/internal/session" "github.com/authelia/authelia/v4/internal/utils" ) @@ -28,38 +33,41 @@ func NewCookieSessionAuthnStrategy(refresh schema.RefreshIntervalDuration) *Cook // NewHeaderAuthorizationAuthnStrategy creates a new HeaderAuthnStrategy using the Authorization and WWW-Authenticate // headers, and the 407 Proxy Auth Required response. -func NewHeaderAuthorizationAuthnStrategy() *HeaderAuthnStrategy { +func NewHeaderAuthorizationAuthnStrategy(schemes ...string) *HeaderAuthnStrategy { return &HeaderAuthnStrategy{ authn: AuthnTypeAuthorization, headerAuthorize: headerAuthorization, headerAuthenticate: headerWWWAuthenticate, handleAuthenticate: true, statusAuthenticate: fasthttp.StatusUnauthorized, + schemes: model.NewAuthorizationSchemes(schemes...), } } // NewHeaderProxyAuthorizationAuthnStrategy creates a new HeaderAuthnStrategy using the Proxy-Authorization and // Proxy-Authenticate headers, and the 407 Proxy Auth Required response. -func NewHeaderProxyAuthorizationAuthnStrategy() *HeaderAuthnStrategy { +func NewHeaderProxyAuthorizationAuthnStrategy(schemes ...string) *HeaderAuthnStrategy { return &HeaderAuthnStrategy{ authn: AuthnTypeProxyAuthorization, headerAuthorize: headerProxyAuthorization, headerAuthenticate: headerProxyAuthenticate, handleAuthenticate: true, statusAuthenticate: fasthttp.StatusProxyAuthRequired, + schemes: model.NewAuthorizationSchemes(schemes...), } } // NewHeaderProxyAuthorizationAuthRequestAuthnStrategy creates a new HeaderAuthnStrategy using the Proxy-Authorization // and WWW-Authenticate headers, and the 401 Proxy Auth Required response. This is a special AuthnStrategy for the // AuthRequest implementation. -func NewHeaderProxyAuthorizationAuthRequestAuthnStrategy() *HeaderAuthnStrategy { +func NewHeaderProxyAuthorizationAuthRequestAuthnStrategy(schemes ...string) *HeaderAuthnStrategy { return &HeaderAuthnStrategy{ authn: AuthnTypeProxyAuthorization, headerAuthorize: headerProxyAuthorization, headerAuthenticate: headerWWWAuthenticate, handleAuthenticate: true, statusAuthenticate: fasthttp.StatusUnauthorized, + schemes: model.NewAuthorizationSchemes(schemes...), } } @@ -74,10 +82,10 @@ type CookieSessionAuthnStrategy struct { } // Get returns the Authn information for this AuthnStrategy. -func (s *CookieSessionAuthnStrategy) Get(ctx *middlewares.AutheliaCtx, provider *session.Session) (authn Authn, err error) { +func (s *CookieSessionAuthnStrategy) Get(ctx *middlewares.AutheliaCtx, provider *session.Session, _ *authorization.Object) (authn *Authn, err error) { var userSession session.UserSession - authn = Authn{ + authn = &Authn{ Type: AuthnTypeCookie, Level: authentication.NotAuthenticated, Username: anonymous, @@ -120,7 +128,7 @@ func (s *CookieSessionAuthnStrategy) Get(ctx *middlewares.AutheliaCtx, provider ctx.Logger.WithError(err).Error("Unable to save updated user session") } - return Authn{ + return &Authn{ Username: friendlyUsername(userSession.Username), Details: authentication.UserDetails{ Username: userSession.Username, @@ -149,75 +157,118 @@ type HeaderAuthnStrategy struct { headerAuthenticate []byte handleAuthenticate bool statusAuthenticate int + schemes model.AuthorizationSchemes } // Get returns the Authn information for this AuthnStrategy. -func (s *HeaderAuthnStrategy) Get(ctx *middlewares.AutheliaCtx, _ *session.Session) (authn Authn, err error) { - var ( - username, password string - value []byte - ) +func (s *HeaderAuthnStrategy) Get(ctx *middlewares.AutheliaCtx, _ *session.Session, object *authorization.Object) (authn *Authn, err error) { + var value []byte - authn = Authn{ + authn = &Authn{ Type: s.authn, Level: authentication.NotAuthenticated, Username: anonymous, } - if value = ctx.Request.Header.PeekBytes(s.headerAuthorize); value == nil { + if value = ctx.Request.Header.PeekBytes(s.headerAuthorize); len(value) == 0 { return authn, nil } - if username, password, err = headerAuthorizationParse(value); err != nil { + authz := model.NewAuthorization() + + if err = authz.ParseBytes(value); err != nil { return authn, fmt.Errorf("failed to parse content of %s header: %w", s.headerAuthorize, err) } - if username == "" || password == "" { - return authn, fmt.Errorf("failed to validate parsed credentials of %s header for user '%s': %w", s.headerAuthorize, username, err) - } + authn.Header.Authorization = authz var ( - valid bool - details *authentication.UserDetails + username, clientID string + + ccs bool + level authentication.Level ) - if valid, err = ctx.Providers.UserProvider.CheckUserPassword(username, password); err != nil { - return authn, fmt.Errorf("failed to validate parsed credentials of %s header for user '%s': %w", s.headerAuthorize, username, err) + scheme := authn.Header.Authorization.Scheme() + + if !s.schemes.Has(scheme) { + return authn, fmt.Errorf("invalid scheme: scheme with name '%s' isn't available on this endpoint", scheme.String()) } - if !valid { - return authn, fmt.Errorf("validated parsed credentials of %s header but they are not valid for user '%s': %w", s.headerAuthorize, username, err) + switch scheme { + case model.AuthorizationSchemeBasic: + username, level, err = s.handleGetBasic(ctx, authn, object) + case model.AuthorizationSchemeBearer: + username, clientID, ccs, level, err = handleVerifyGETAuthorizationBearer(ctx, authn, object) + default: + err = fmt.Errorf("failed to parse content of %s header: the scheme '%s' is not known", s.headerAuthorize, authn.Header.Authorization.SchemeRaw()) } - if details, err = ctx.Providers.UserProvider.GetDetails(username); err != nil { - if errors.Is(err, authentication.ErrUserNotFound) { - ctx.Logger.WithField("username", username).Error("Error occurred while attempting to get user details for user: the user was not found indicating they were deleted, disabled, or otherwise no longer authorized to login") + if err != nil { + return authn, fmt.Errorf("failed to validate %s header with %s scheme: %w", s.headerAuthorize, scheme, err) + } - return authn, err + switch { + case ccs: + if len(clientID) == 0 { + return authn, fmt.Errorf("failed to determine client id from the %s header", s.headerAuthorize) } - return authn, fmt.Errorf("unable to retrieve details for user '%s': %w", username, err) + authn.ClientID = clientID + case len(username) == 0: + return authn, fmt.Errorf("failed to determine username from the %s header", s.headerAuthorize) + default: + var details *authentication.UserDetails + + if details, err = ctx.Providers.UserProvider.GetDetails(username); err != nil { + if errors.Is(err, authentication.ErrUserNotFound) { + ctx.Logger.WithField("username", username).Error("Error occurred while attempting to get user details for user: the user was not found indicating they were deleted, disabled, or otherwise no longer authorized to login") + + return authn, err + } + + return authn, fmt.Errorf("unable to retrieve details for user '%s': %w", username, err) + } + + authn.Username = friendlyUsername(details.Username) + authn.Details = *details } - authn.Username = friendlyUsername(details.Username) - authn.Details = *details - authn.Level = authentication.OneFactor + authn.Level = level return authn, nil } +func (s *HeaderAuthnStrategy) handleGetBasic(ctx *middlewares.AutheliaCtx, authn *Authn, _ *authorization.Object) (username string, level authentication.Level, err error) { + var ( + valid bool + ) + + if valid, err = ctx.Providers.UserProvider.CheckUserPassword(authn.Header.Authorization.Basic()); err != nil { + return "", authentication.NotAuthenticated, fmt.Errorf("failed to validate parsed credentials of %s header for user '%s': %w", s.headerAuthorize, authn.Header.Authorization.BasicUsername(), err) + } + + if !valid { + return "", authentication.NotAuthenticated, fmt.Errorf("validated parsed credentials of %s header but they are not valid for user '%s': %w", s.headerAuthorize, authn.Header.Authorization.BasicUsername(), err) + } + + return authn.Header.Authorization.BasicUsername(), authentication.OneFactor, nil +} + // CanHandleUnauthorized returns true if this AuthnStrategy should handle Unauthorized requests. func (s *HeaderAuthnStrategy) CanHandleUnauthorized() (handle bool) { return s.handleAuthenticate } // HandleUnauthorized is the Unauthorized handler for the header AuthnStrategy. -func (s *HeaderAuthnStrategy) HandleUnauthorized(ctx *middlewares.AutheliaCtx, _ *Authn, _ *url.URL) { +func (s *HeaderAuthnStrategy) HandleUnauthorized(ctx *middlewares.AutheliaCtx, authn *Authn, _ *url.URL) { ctx.Logger.Debugf("Responding %d %s", s.statusAuthenticate, s.headerAuthenticate) ctx.ReplyStatusCode(s.statusAuthenticate) - if s.headerAuthenticate != nil { + if authn.Header.Authorization != nil && authn.Header.Authorization.Scheme() == model.AuthorizationSchemeBearer && authn.Header.Error != nil { + ctx.Response.Header.SetBytesK(s.headerAuthenticate, fmt.Sprintf(`Bearer %s`, oidc.RFC6750Header(authn.Header.Realm, authn.Header.Scope, authn.Header.Error))) + } else if s.headerAuthenticate != nil { ctx.Response.Header.SetBytesKV(s.headerAuthenticate, headerValueAuthenticateBasic) } } @@ -226,13 +277,13 @@ func (s *HeaderAuthnStrategy) HandleUnauthorized(ctx *middlewares.AutheliaCtx, _ type HeaderLegacyAuthnStrategy struct{} // Get returns the Authn information for this AuthnStrategy. -func (s *HeaderLegacyAuthnStrategy) Get(ctx *middlewares.AutheliaCtx, _ *session.Session) (authn Authn, err error) { +func (s *HeaderLegacyAuthnStrategy) Get(ctx *middlewares.AutheliaCtx, _ *session.Session, _ *authorization.Object) (authn *Authn, err error) { var ( username, password string value, header []byte ) - authn = Authn{ + authn = &Authn{ Level: authentication.NotAuthenticated, Username: anonymous, } @@ -402,6 +453,95 @@ func handleVerifyGETAuthnCookieValidateRefresh(ctx *middlewares.AutheliaCtx, use return false } +func handleVerifyGETAuthorizationBearer(ctx *middlewares.AutheliaCtx, authn *Authn, object *authorization.Object) (username, clientID string, ccs bool, level authentication.Level, err error) { + if ctx.Providers.OpenIDConnect == nil || ctx.Configuration.IdentityProviders.OIDC == nil || !ctx.Configuration.IdentityProviders.OIDC.Discovery.BearerAuthorization { + return "", "", false, authentication.NotAuthenticated, fmt.Errorf("authorization bearer scheme requires an OpenID Connect 1.0 configuration but it's absent") + } + + if !ctx.Configuration.IdentityProviders.OIDC.Discovery.BearerAuthorization { + return "", "", false, authentication.NotAuthenticated, fmt.Errorf("authorization bearer scheme requires an OAuth 2.0 or OpenID Connect 1.0 client to be registered with the '%s' scope but there are none", oidc.ScopeAutheliaBearerAuthz) + } + + return handleVerifyGETAuthorizationBearerIntrospection(ctx, ctx.Providers.OpenIDConnect, authn, object) +} + +type AuthzBearerIntrospectionProvider interface { + GetFullClient(ctx context.Context, id string) (client oidc.Client, err error) + GetAudienceStrategy(ctx context.Context) (strategy fosite.AudienceMatchingStrategy) + IntrospectToken(ctx context.Context, token string, tokenUse fosite.TokenUse, session fosite.Session, scope ...string) (fosite.TokenUse, fosite.AccessRequester, error) +} + +func handleVerifyGETAuthorizationBearerIntrospection(ctx context.Context, provider AuthzBearerIntrospectionProvider, authn *Authn, object *authorization.Object) (username, clientID string, ccs bool, level authentication.Level, err error) { + var ( + use fosite.TokenUse + requester fosite.AccessRequester + ) + + authn.Header.Error = &fosite.RFC6749Error{ + ErrorField: "invalid_token", + DescriptionField: "The access token is expired, revoked, malformed, or invalid for other reasons. The client can obtain a new access token and try again.", + } + + if use, requester, err = provider.IntrospectToken(ctx, authn.Header.Authorization.Value(), fosite.AccessToken, oidc.NewSession(), oidc.ScopeAutheliaBearerAuthz); err != nil { + return "", "", false, authentication.NotAuthenticated, fmt.Errorf("error performing token introspection: %w", err) + } + + if use != fosite.AccessToken { + authn.Header.Error = fosite.ErrInvalidRequest + + return "", "", false, authentication.NotAuthenticated, fmt.Errorf("token is not an access token") + } + + audience := []string{object.URL.String()} + strategy := provider.GetAudienceStrategy(ctx) + + if err = strategy(requester.GetGrantedAudience(), audience); err != nil { + return "", "", false, authentication.NotAuthenticated, fmt.Errorf("token does not contain a valid audience for the url '%s' with the error: %w", audience[0], err) + } + + fsession := requester.GetSession() + + var ( + client oidc.Client + osession *oidc.Session + ok bool + ) + + if osession, ok = fsession.(*oidc.Session); !ok { + return "", "", false, authentication.NotAuthenticated, fmt.Errorf("introspection returned an invalid session type") + } + + if client, err = provider.GetFullClient(ctx, osession.ClientID); err != nil || client == nil { + return "", "", false, authentication.NotAuthenticated, fmt.Errorf("client id '%s' is not registered", osession.ClientID) + } + + if !client.GetScopes().Has(oidc.ScopeAutheliaBearerAuthz) { + return "", "", false, authentication.NotAuthenticated, fmt.Errorf("client id '%s' is registered but does not permit the '%s' scope", osession.ClientID, oidc.ScopeAutheliaBearerAuthz) + } + + if err = strategy(client.GetAudience(), audience); err != nil { + return "", "", false, authentication.NotAuthenticated, fmt.Errorf("client id '%s' is registered but does not permit an audience for the url '%s' with the error: %w", osession.ClientID, audience[0], err) + } + + if osession.DefaultSession == nil || osession.DefaultSession.Claims == nil { + return "", "", false, authentication.NotAuthenticated, fmt.Errorf("introspection returned a session missing required values") + } + + authn.Header.Error = nil + + if osession.ClientCredentials { + return "", osession.ClientID, true, authentication.OneFactor, nil + } + + if oidc.NewAuthenticationMethodsReferencesFromClaim(osession.DefaultSession.Claims.AuthenticationMethodsReferences).MultiFactorAuthentication() { + level = authentication.TwoFactor + } else { + level = authentication.OneFactor + } + + return osession.Username, "", false, level, nil +} + func headerAuthorizationParse(value []byte) (username, password string, err error) { if bytes.Equal(value, qryValueEmpty) { return "", "", fmt.Errorf("header is malformed: empty value") |
