mirror of https://github.com/authelia/authelia.git
611 lines
23 KiB
Go
611 lines
23 KiB
Go
package handlers
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
oauthelia2 "authelia.com/provider/oauth2"
|
|
"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"
|
|
)
|
|
|
|
// NewCookieSessionAuthnStrategy creates a new CookieSessionAuthnStrategy.
|
|
func NewCookieSessionAuthnStrategy(refresh schema.RefreshIntervalDuration) *CookieSessionAuthnStrategy {
|
|
return &CookieSessionAuthnStrategy{
|
|
refresh: refresh,
|
|
}
|
|
}
|
|
|
|
// NewHeaderAuthorizationAuthnStrategy creates a new HeaderAuthnStrategy using the Authorization and WWW-Authenticate
|
|
// headers, and the 407 Proxy Auth Required response.
|
|
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(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(schemes ...string) *HeaderAuthnStrategy {
|
|
return &HeaderAuthnStrategy{
|
|
authn: AuthnTypeProxyAuthorization,
|
|
headerAuthorize: headerProxyAuthorization,
|
|
headerAuthenticate: headerWWWAuthenticate,
|
|
handleAuthenticate: true,
|
|
statusAuthenticate: fasthttp.StatusUnauthorized,
|
|
schemes: model.NewAuthorizationSchemes(schemes...),
|
|
}
|
|
}
|
|
|
|
// NewHeaderLegacyAuthnStrategy creates a new HeaderLegacyAuthnStrategy.
|
|
func NewHeaderLegacyAuthnStrategy() *HeaderLegacyAuthnStrategy {
|
|
return &HeaderLegacyAuthnStrategy{}
|
|
}
|
|
|
|
// CookieSessionAuthnStrategy is a session cookie AuthnStrategy.
|
|
type CookieSessionAuthnStrategy struct {
|
|
refresh schema.RefreshIntervalDuration
|
|
}
|
|
|
|
// Get returns the Authn information for this AuthnStrategy.
|
|
func (s *CookieSessionAuthnStrategy) Get(ctx *middlewares.AutheliaCtx, provider *session.Session, _ *authorization.Object) (authn *Authn, err error) {
|
|
var userSession session.UserSession
|
|
|
|
authn = &Authn{
|
|
Type: AuthnTypeCookie,
|
|
Level: authentication.NotAuthenticated,
|
|
Username: anonymous,
|
|
}
|
|
|
|
if userSession, err = provider.GetSession(ctx.RequestCtx); err != nil {
|
|
return authn, fmt.Errorf("failed to retrieve user session: %w", err)
|
|
}
|
|
|
|
if userSession.CookieDomain != provider.Config.Domain {
|
|
ctx.Logger.Warnf("Destroying session cookie as the cookie domain '%s' does not match the requests detected cookie domain '%s' which may be a sign a user tried to move this cookie from one domain to another", userSession.CookieDomain, provider.Config.Domain)
|
|
|
|
if err = provider.DestroySession(ctx.RequestCtx); err != nil {
|
|
ctx.Logger.WithError(err).Error("Error occurred trying to destroy the session cookie")
|
|
}
|
|
|
|
userSession = provider.NewDefaultUserSession()
|
|
|
|
if err = provider.SaveSession(ctx.RequestCtx, userSession); err != nil {
|
|
ctx.Logger.WithError(err).Error("Error occurred trying to save the new session cookie")
|
|
}
|
|
}
|
|
|
|
if invalid := handleAuthnCookieValidate(ctx, provider, &userSession, s.refresh); invalid {
|
|
if err = ctx.DestroySession(); err != nil {
|
|
ctx.Logger.WithError(err).Errorf("Unable to destroy user session")
|
|
}
|
|
|
|
userSession = provider.NewDefaultUserSession()
|
|
userSession.LastActivity = ctx.Clock.Now().Unix()
|
|
|
|
if err = provider.SaveSession(ctx.RequestCtx, userSession); err != nil {
|
|
ctx.Logger.WithError(err).Error("Unable to save updated user session")
|
|
}
|
|
|
|
return authn, nil
|
|
}
|
|
|
|
if err = provider.SaveSession(ctx.RequestCtx, userSession); err != nil {
|
|
ctx.Logger.WithError(err).Error("Unable to save updated user session")
|
|
}
|
|
|
|
return &Authn{
|
|
Username: friendlyUsername(userSession.Username),
|
|
Details: authentication.UserDetails{
|
|
Username: userSession.Username,
|
|
DisplayName: userSession.DisplayName,
|
|
Emails: userSession.Emails,
|
|
Groups: userSession.Groups,
|
|
},
|
|
Level: userSession.AuthenticationLevel,
|
|
Type: AuthnTypeCookie,
|
|
}, nil
|
|
}
|
|
|
|
// CanHandleUnauthorized returns true if this AuthnStrategy should handle Unauthorized requests.
|
|
func (s *CookieSessionAuthnStrategy) CanHandleUnauthorized() (handle bool) {
|
|
return false
|
|
}
|
|
|
|
// HeaderStrategy returns true if this AuthnStrategy is header based.
|
|
func (s *CookieSessionAuthnStrategy) HeaderStrategy() (header bool) {
|
|
return false
|
|
}
|
|
|
|
// HandleUnauthorized is the Unauthorized handler for the cookie AuthnStrategy.
|
|
func (s *CookieSessionAuthnStrategy) HandleUnauthorized(_ *middlewares.AutheliaCtx, _ *Authn, _ *url.URL) {
|
|
}
|
|
|
|
// HeaderAuthnStrategy is a header AuthnStrategy.
|
|
type HeaderAuthnStrategy struct {
|
|
authn AuthnType
|
|
headerAuthorize []byte
|
|
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, object *authorization.Object) (authn *Authn, err error) {
|
|
var value []byte
|
|
|
|
authn = &Authn{
|
|
Type: s.authn,
|
|
Level: authentication.NotAuthenticated,
|
|
Username: anonymous,
|
|
}
|
|
|
|
if value = ctx.Request.Header.PeekBytes(s.headerAuthorize); len(value) == 0 {
|
|
return authn, 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)
|
|
}
|
|
|
|
authn.Header.Authorization = authz
|
|
|
|
var (
|
|
username, clientID string
|
|
|
|
ccs bool
|
|
level authentication.Level
|
|
)
|
|
|
|
scheme := authn.Header.Authorization.Scheme()
|
|
|
|
if !s.schemes.Has(scheme) {
|
|
ctx.Logger.
|
|
WithFields(map[string]any{"scheme": authn.Header.Authorization.SchemeRaw(), "header": string(s.headerAuthorize)}).
|
|
Debug("Skipping header authorization as the scheme and header combination is unknown to this endpoint configuration")
|
|
|
|
return authn, nil
|
|
}
|
|
|
|
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:
|
|
ctx.Logger.
|
|
WithFields(map[string]any{"scheme": authn.Header.Authorization.SchemeRaw(), "header": string(s.headerAuthorize)}).
|
|
Debug("Skipping header authorization as the scheme is unknown to this endpoint configuration")
|
|
|
|
return authn, nil
|
|
}
|
|
|
|
if err != nil {
|
|
if errors.Is(err, errTokenIntent) {
|
|
return authn, nil
|
|
}
|
|
|
|
return authn, fmt.Errorf("failed to validate %s header with %s scheme: %w", s.headerAuthorize, scheme, err)
|
|
}
|
|
|
|
switch {
|
|
case ccs:
|
|
if len(clientID) == 0 {
|
|
return authn, fmt.Errorf("failed to determine client id from the %s header", s.headerAuthorize)
|
|
}
|
|
|
|
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.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
|
|
}
|
|
|
|
// HeaderStrategy returns true if this AuthnStrategy is header based.
|
|
func (s *HeaderAuthnStrategy) HeaderStrategy() (header bool) {
|
|
return true
|
|
}
|
|
|
|
// HandleUnauthorized is the Unauthorized handler for the header AuthnStrategy.
|
|
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 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)
|
|
}
|
|
}
|
|
|
|
// HeaderLegacyAuthnStrategy is a legacy header AuthnStrategy which can be switched based on the query parameters.
|
|
type HeaderLegacyAuthnStrategy struct{}
|
|
|
|
// Get returns the Authn information for this AuthnStrategy.
|
|
func (s *HeaderLegacyAuthnStrategy) Get(ctx *middlewares.AutheliaCtx, _ *session.Session, _ *authorization.Object) (authn *Authn, err error) {
|
|
var (
|
|
username, password string
|
|
value, header []byte
|
|
)
|
|
|
|
authn = &Authn{
|
|
Level: authentication.NotAuthenticated,
|
|
Username: anonymous,
|
|
}
|
|
|
|
if qryValueAuth := ctx.QueryArgs().PeekBytes(qryArgAuth); bytes.Equal(qryValueAuth, qryValueBasic) {
|
|
authn.Type = AuthnTypeAuthorization
|
|
header = headerAuthorization
|
|
} else {
|
|
authn.Type = AuthnTypeProxyAuthorization
|
|
header = headerProxyAuthorization
|
|
}
|
|
|
|
value = ctx.Request.Header.PeekBytes(header)
|
|
|
|
switch {
|
|
case value == nil && authn.Type == AuthnTypeAuthorization:
|
|
return authn, fmt.Errorf("header %s expected", headerAuthorization)
|
|
case value == nil:
|
|
return authn, nil
|
|
}
|
|
|
|
if username, password, err = headerAuthorizationParse(value); err != nil {
|
|
return authn, fmt.Errorf("failed to parse content of %s header: %w", header, err)
|
|
}
|
|
|
|
if username == "" || password == "" {
|
|
return authn, fmt.Errorf("failed to validate parsed credentials of %s header for user '%s': %w", header, username, err)
|
|
}
|
|
|
|
var (
|
|
valid bool
|
|
details *authentication.UserDetails
|
|
)
|
|
|
|
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", header, username, err)
|
|
}
|
|
|
|
if !valid {
|
|
return authn, fmt.Errorf("validated parsed credentials of %s header but they are not valid for user '%s': %w", header, username, err)
|
|
}
|
|
|
|
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.Level = authentication.OneFactor
|
|
|
|
return authn, nil
|
|
}
|
|
|
|
// CanHandleUnauthorized returns true if this AuthnStrategy should handle Unauthorized requests.
|
|
func (s *HeaderLegacyAuthnStrategy) CanHandleUnauthorized() (handle bool) {
|
|
return true
|
|
}
|
|
|
|
// HeaderStrategy returns true if this AuthnStrategy is header based.
|
|
func (s *HeaderLegacyAuthnStrategy) HeaderStrategy() (header bool) {
|
|
return true
|
|
}
|
|
|
|
// HandleUnauthorized is the Unauthorized handler for the Legacy header AuthnStrategy.
|
|
func (s *HeaderLegacyAuthnStrategy) HandleUnauthorized(ctx *middlewares.AutheliaCtx, authn *Authn, _ *url.URL) {
|
|
handleAuthzUnauthorizedAuthorizationBasic(ctx, authn)
|
|
}
|
|
|
|
func handleAuthnCookieValidate(ctx *middlewares.AutheliaCtx, provider *session.Session, userSession *session.UserSession, refresh schema.RefreshIntervalDuration) (invalid bool) {
|
|
isAnonymous := userSession.Username == ""
|
|
|
|
if isAnonymous && userSession.AuthenticationLevel != authentication.NotAuthenticated {
|
|
ctx.Logger.WithFields(map[string]any{"username": anonymous, "level": userSession.AuthenticationLevel.String()}).Errorf("Session for user has an invalid authentication level: this may be a sign of a compromise")
|
|
|
|
return true
|
|
}
|
|
|
|
if invalid = handleAuthnCookieValidateInactivity(ctx, provider, userSession, isAnonymous); invalid {
|
|
ctx.Logger.WithField("username", userSession.Username).Info("Session for user not marked as remembered has exceeded configured session inactivity")
|
|
|
|
return true
|
|
}
|
|
|
|
if invalid = handleSessionValidateRefresh(ctx, userSession, refresh); invalid {
|
|
return true
|
|
}
|
|
|
|
if username := ctx.Request.Header.PeekBytes(headerSessionUsername); username != nil && !strings.EqualFold(string(username), userSession.Username) {
|
|
ctx.Logger.WithField("username", userSession.Username).Warnf("Session for user does not match the Session-Username header with value '%s' which could be a sign of a cookie hijack", username)
|
|
|
|
return true
|
|
}
|
|
|
|
if !userSession.KeepMeLoggedIn {
|
|
userSession.LastActivity = ctx.Clock.Now().Unix()
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func handleAuthnCookieValidateInactivity(ctx *middlewares.AutheliaCtx, provider *session.Session, userSession *session.UserSession, isAnonymous bool) (invalid bool) {
|
|
if isAnonymous || userSession.KeepMeLoggedIn || int64(provider.Config.Inactivity.Seconds()) == 0 {
|
|
return false
|
|
}
|
|
|
|
ctx.Logger.WithField("username", userSession.Username).Tracef("Inactivity report for user. Current Time: %d, Last Activity: %d, Maximum Inactivity: %d.", ctx.Clock.Now().Unix(), userSession.LastActivity, int(provider.Config.Inactivity.Seconds()))
|
|
|
|
return time.Unix(userSession.LastActivity, 0).Add(provider.Config.Inactivity).Before(ctx.Clock.Now())
|
|
}
|
|
|
|
func handleSessionValidateRefresh(ctx *middlewares.AutheliaCtx, userSession *session.UserSession, refresh schema.RefreshIntervalDuration) (invalid bool) {
|
|
if refresh.Never() || userSession.IsAnonymous() {
|
|
return false
|
|
}
|
|
|
|
ctx.Logger.WithField("username", userSession.Username).Trace("Checking if we need check the authentication backend for an updated profile for user")
|
|
|
|
if !refresh.Always() && userSession.RefreshTTL.After(ctx.Clock.Now()) {
|
|
return false
|
|
}
|
|
|
|
ctx.Logger.WithField("username", userSession.Username).Debug("Checking the authentication backend for an updated profile for user")
|
|
|
|
var (
|
|
details *authentication.UserDetails
|
|
err error
|
|
)
|
|
|
|
if details, err = ctx.Providers.UserProvider.GetDetails(userSession.Username); err != nil {
|
|
if errors.Is(err, authentication.ErrUserNotFound) {
|
|
ctx.Logger.WithField("username", userSession.Username).Error("Error occurred while attempting to update user details for user: the user was not found indicating they were deleted, disabled, or otherwise no longer authorized to login")
|
|
|
|
return true
|
|
}
|
|
|
|
ctx.Logger.WithError(err).WithField("username", userSession.Username).Error("Error occurred while attempting to update user details for user")
|
|
|
|
return false
|
|
}
|
|
|
|
var (
|
|
diffEmails, diffGroups, diffDisplayName bool
|
|
)
|
|
|
|
diffEmails, diffGroups = utils.IsStringSlicesDifferent(userSession.Emails, details.Emails), utils.IsStringSlicesDifferent(userSession.Groups, details.Groups)
|
|
diffDisplayName = userSession.DisplayName != details.DisplayName
|
|
|
|
if !refresh.Always() {
|
|
userSession.RefreshTTL = ctx.Clock.Now().Add(refresh.Value())
|
|
}
|
|
|
|
if !diffEmails && !diffGroups && !diffDisplayName {
|
|
ctx.Logger.WithField("username", userSession.Username).Trace("Updated profile not detected for user")
|
|
|
|
return false
|
|
}
|
|
|
|
ctx.Logger.WithField("username", userSession.Username).Debug("Updated profile detected for user")
|
|
|
|
if ctx.Logger.Level >= logrus.TraceLevel {
|
|
generateVerifySessionHasUpToDateProfileTraceLogs(ctx, userSession, details)
|
|
}
|
|
|
|
userSession.Emails, userSession.Groups, userSession.DisplayName = details.Emails, details.Groups, details.DisplayName
|
|
|
|
return false
|
|
}
|
|
|
|
func handleVerifyGETAuthorizationBearer(ctx *middlewares.AutheliaCtx, authn *Authn, object *authorization.Object) (username, clientID string, ccs bool, level authentication.Level, err error) {
|
|
var at bool
|
|
|
|
if at, err = oidc.IsAccessToken(ctx, authn.Header.Authorization.Value()); !at {
|
|
if err != nil {
|
|
ctx.Logger.WithError(err).Debug("The bearer token does not appear to be a relevant access token")
|
|
} else {
|
|
ctx.Logger.Debug("The bearer token does not appear to be a relevant access token")
|
|
}
|
|
|
|
return "", "", false, authentication.NotAuthenticated, errTokenIntent
|
|
}
|
|
|
|
return handleVerifyGETAuthorizationBearerIntrospection(ctx, ctx.Providers.OpenIDConnect, authn, object)
|
|
}
|
|
|
|
func handleVerifyGETAuthorizationBearerIntrospection(ctx context.Context, provider AuthzBearerIntrospectionProvider, authn *Authn, object *authorization.Object) (username, clientID string, ccs bool, level authentication.Level, err error) {
|
|
var (
|
|
use oauthelia2.TokenUse
|
|
requester oauthelia2.AccessRequester
|
|
)
|
|
|
|
authn.Header.Error = &oauthelia2.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(), oauthelia2.AccessToken, oidc.NewSession(), oidc.ScopeAutheliaBearerAuthz); err != nil {
|
|
return "", "", false, authentication.NotAuthenticated, fmt.Errorf("error performing token introspection: %w", oauthelia2.ErrorToDebugRFC6749Error(err))
|
|
}
|
|
|
|
if use != oauthelia2.AccessToken {
|
|
authn.Header.Error = oauthelia2.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.GetRegisteredClient(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")
|
|
}
|
|
|
|
parts := strings.SplitN(string(value), " ", 2)
|
|
|
|
if len(parts) != 2 {
|
|
return "", "", fmt.Errorf("header is malformed: does not appear to have a scheme")
|
|
}
|
|
|
|
scheme := strings.ToLower(parts[0])
|
|
|
|
switch scheme {
|
|
case headerAuthorizationSchemeBasic:
|
|
if username, password, err = headerAuthorizationParseBasic(parts[1]); err != nil {
|
|
return username, password, fmt.Errorf("header is malformed: %w", err)
|
|
}
|
|
|
|
return username, password, nil
|
|
default:
|
|
return "", "", fmt.Errorf("header is malformed: unsupported scheme '%s': supported schemes '%s'", parts[0], strings.ToTitle(headerAuthorizationSchemeBasic))
|
|
}
|
|
}
|
|
|
|
func headerAuthorizationParseBasic(value string) (username, password string, err error) {
|
|
var content []byte
|
|
|
|
if content, err = base64.StdEncoding.DecodeString(value); err != nil {
|
|
return "", "", fmt.Errorf("could not decode credentials: %w", err)
|
|
}
|
|
|
|
strContent := string(content)
|
|
s := strings.IndexByte(strContent, ':')
|
|
|
|
if s < 1 {
|
|
return "", "", fmt.Errorf("format of header must be <user>:<password> but either doesn't have a colon or username")
|
|
}
|
|
|
|
return strContent[:s], strContent[s+1:], nil
|
|
}
|