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 : but either doesn't have a colon or username") } return strContent[:s], strContent[s+1:], nil }