summaryrefslogtreecommitdiff
path: root/internal/handlers/handler_authz_authn.go
diff options
context:
space:
mode:
authorJames Elliott <james-d-elliott@users.noreply.github.com>2024-03-05 20:11:16 +1100
committerGitHub <noreply@github.com>2024-03-05 19:11:16 +1000
commitfb50f1a70c66d96391a3e9cae5721c9c78c75d8d (patch)
treef49313d4452fbfb8072210c30d93602b81739a75 /internal/handlers/handler_authz_authn.go
parentc70c83f74593c1ed75c2195e2dba74a5dfcd30cc (diff)
feat: oauth2 authorization bearer (#6774)
This implements user authorization utilizing the OAuth 2.0 bearer scheme (i.e. RFC6750) for both the authorize code grant and client credentials grant. This effectively allows application "passwords" when used with the client credentials grant. Closes #2023, Closes #188. Signed-off-by: James Elliott <james-d-elliott@users.noreply.github.com>
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")