summaryrefslogtreecommitdiff
path: root/internal/handlers/handler_authz_authn.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/handlers/handler_authz_authn.go')
-rw-r--r--internal/handlers/handler_authz_authn.go210
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")