authelia/internal/handlers/handler_authz_authn.go

449 lines
16 KiB
Go

package handlers
import (
"bytes"
"encoding/base64"
"errors"
"fmt"
"net/url"
"strings"
"time"
"github.com/sirupsen/logrus"
"github.com/valyala/fasthttp"
"github.com/authelia/authelia/v4/internal/authentication"
"github.com/authelia/authelia/v4/internal/configuration/schema"
"github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/session"
"github.com/authelia/authelia/v4/internal/utils"
)
// NewCookieSessionAuthnStrategy creates a new CookieSessionAuthnStrategy.
func NewCookieSessionAuthnStrategy(refreshInterval time.Duration) *CookieSessionAuthnStrategy {
if refreshInterval < time.Second*0 {
return &CookieSessionAuthnStrategy{}
}
return &CookieSessionAuthnStrategy{
refreshEnabled: true,
refreshInterval: refreshInterval,
}
}
// NewHeaderAuthorizationAuthnStrategy creates a new HeaderAuthnStrategy using the Authorization and WWW-Authenticate
// headers, and the 407 Proxy Auth Required response.
func NewHeaderAuthorizationAuthnStrategy() *HeaderAuthnStrategy {
return &HeaderAuthnStrategy{
authn: AuthnTypeAuthorization,
headerAuthorize: headerAuthorization,
headerAuthenticate: headerWWWAuthenticate,
handleAuthenticate: true,
statusAuthenticate: fasthttp.StatusUnauthorized,
}
}
// NewHeaderProxyAuthorizationAuthnStrategy creates a new HeaderAuthnStrategy using the Proxy-Authorization and
// Proxy-Authenticate headers, and the 407 Proxy Auth Required response.
func NewHeaderProxyAuthorizationAuthnStrategy() *HeaderAuthnStrategy {
return &HeaderAuthnStrategy{
authn: AuthnTypeProxyAuthorization,
headerAuthorize: headerProxyAuthorization,
headerAuthenticate: headerProxyAuthenticate,
handleAuthenticate: true,
statusAuthenticate: fasthttp.StatusProxyAuthRequired,
}
}
// 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 {
return &HeaderAuthnStrategy{
authn: AuthnTypeProxyAuthorization,
headerAuthorize: headerProxyAuthorization,
headerAuthenticate: headerWWWAuthenticate,
handleAuthenticate: true,
statusAuthenticate: fasthttp.StatusUnauthorized,
}
}
// NewHeaderLegacyAuthnStrategy creates a new HeaderLegacyAuthnStrategy.
func NewHeaderLegacyAuthnStrategy() *HeaderLegacyAuthnStrategy {
return &HeaderLegacyAuthnStrategy{}
}
// CookieSessionAuthnStrategy is a session cookie AuthnStrategy.
type CookieSessionAuthnStrategy struct {
refreshEnabled bool
refreshInterval time.Duration
}
// Get returns the Authn information for this AuthnStrategy.
func (s *CookieSessionAuthnStrategy) Get(ctx *middlewares.AutheliaCtx, provider *session.Session) (authn Authn, err error) {
authn = Authn{
Type: AuthnTypeCookie,
Level: authentication.NotAuthenticated,
}
var userSession session.UserSession
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 := handleVerifyGETAuthnCookieValidate(ctx, provider, &userSession, s.refreshEnabled, s.refreshInterval); invalid {
if err = ctx.DestroySession(); err != nil {
ctx.Logger.Errorf("Unable to destroy user session: %+v", err)
}
userSession = provider.NewDefaultUserSession()
userSession.LastActivity = ctx.Clock.Now().Unix()
if err = provider.SaveSession(ctx.RequestCtx, userSession); err != nil {
ctx.Logger.Errorf("Unable to save updated user session: %+v", err)
}
return authn, nil
}
if err = provider.SaveSession(ctx.RequestCtx, userSession); err != nil {
ctx.Logger.Errorf("Unable to save updated user session: %+v", err)
}
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
}
// 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
}
// 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
)
authn = Authn{
Type: s.authn,
Level: authentication.NotAuthenticated,
}
if value = ctx.Request.Header.PeekBytes(s.headerAuthorize); 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", 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)
}
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", s.headerAuthorize, username, err)
}
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)
}
if details, err = ctx.Providers.UserProvider.GetDetails(username); err != nil {
if errors.Is(err, authentication.ErrUserNotFound) {
ctx.Logger.Errorf("Error occurred while attempting to get user details for user '%s': the user was not found indicating they were deleted, disabled, or otherwise no longer authorized to login", username)
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 *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) {
ctx.Logger.Debugf("Responding %d %s", s.statusAuthenticate, s.headerAuthenticate)
ctx.ReplyStatusCode(s.statusAuthenticate)
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) (authn Authn, err error) {
var (
username, password string
value, header []byte
)
authn = Authn{
Level: authentication.NotAuthenticated,
}
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.Errorf("Error occurred while attempting to get user details for user '%s': the user was not found indicating they were deleted, disabled, or otherwise no longer authorized to login", username)
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
}
// 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 handleVerifyGETAuthnCookieValidate(ctx *middlewares.AutheliaCtx, provider *session.Session, userSession *session.UserSession, profileRefreshEnabled bool, profileRefreshInterval time.Duration) (invalid bool) {
isAnonymous := userSession.Username == ""
if isAnonymous && userSession.AuthenticationLevel != authentication.NotAuthenticated {
ctx.Logger.Errorf("Session for anonymous user has an authentication level of '%s': this may be a sign of a compromise", userSession.AuthenticationLevel)
return true
}
if invalid = handleVerifyGETAuthnCookieValidateInactivity(ctx, provider, userSession, isAnonymous); invalid {
ctx.Logger.Infof("Session for user '%s' not marked as remembereded has exceeded configured session inactivity", userSession.Username)
return true
}
if invalid = handleVerifyGETAuthnCookieValidateUpdate(ctx, userSession, isAnonymous, profileRefreshEnabled, profileRefreshInterval); invalid {
return true
}
if username := ctx.Request.Header.PeekBytes(headerSessionUsername); username != nil && !strings.EqualFold(string(username), userSession.Username) {
ctx.Logger.Warnf("Session for user '%s' does not match the Session-Username header with value '%s' which could be a sign of a cookie hijack", userSession.Username, username)
return true
}
if !userSession.KeepMeLoggedIn {
userSession.LastActivity = ctx.Clock.Now().Unix()
}
return false
}
func handleVerifyGETAuthnCookieValidateInactivity(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.Tracef("Inactivity report for user '%s'. Current Time: %d, Last Activity: %d, Maximum Inactivity: %d.", userSession.Username, 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 handleVerifyGETAuthnCookieValidateUpdate(ctx *middlewares.AutheliaCtx, userSession *session.UserSession, isAnonymous, enabled bool, interval time.Duration) (invalid bool) {
if !enabled || isAnonymous {
return false
}
ctx.Logger.Tracef("Checking if we need check the authentication backend for an updated profile for user '%s'", userSession.Username)
if interval != schema.RefreshIntervalAlways && userSession.RefreshTTL.After(ctx.Clock.Now()) {
return false
}
ctx.Logger.Debugf("Checking the authentication backend for an updated profile for user '%s'", userSession.Username)
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.Errorf("Error occurred while attempting to update user details for user '%s': the user was not found indicating they were deleted, disabled, or otherwise no longer authorized to login", userSession.Username)
return true
}
ctx.Logger.Errorf("Error occurred while attempting to update user details for user '%s': %v", userSession.Username, err)
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 interval != schema.RefreshIntervalAlways {
userSession.RefreshTTL = ctx.Clock.Now().Add(interval)
}
if !diffEmails && !diffGroups && !diffDisplayName {
ctx.Logger.Tracef("Updated profile not detected for user '%s'", userSession.Username)
return false
}
ctx.Logger.Debugf("Updated profile detected for user '%s'", userSession.Username)
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 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
}