From 911d71204f2f5846c7d677d049b3c8d26b6f0637 Mon Sep 17 00:00:00 2001 From: James Elliott Date: Thu, 22 Jul 2021 13:52:37 +1000 Subject: [PATCH] fix(handlers): handle xhr requests to /api/verify with 401 (#2189) This changes the way XML HTTP requests are handled on the verify endpoint so that they are redirected using a 401 instead of a 302/303. --- internal/handlers/const.go | 109 ++++++++++-------- internal/handlers/handler_firstfactor.go | 32 ++--- internal/handlers/handler_firstfactor_test.go | 6 +- internal/handlers/handler_logout.go | 6 +- internal/handlers/handler_oidc_wellknown.go | 10 +- internal/handlers/handler_register_totp.go | 8 +- .../handlers/handler_register_u2f_step1.go | 12 +- .../handler_register_u2f_step1_test.go | 4 +- .../handlers/handler_register_u2f_step2.go | 8 +- .../handlers/handler_reset_password_step1.go | 4 +- .../handlers/handler_reset_password_step2.go | 8 +- internal/handlers/handler_sign_duo.go | 8 +- internal/handlers/handler_sign_totp.go | 12 +- internal/handlers/handler_sign_u2f_step1.go | 14 +-- internal/handlers/handler_sign_u2f_step2.go | 12 +- internal/handlers/handler_user_info.go | 8 +- internal/handlers/handler_verify.go | 83 +++++++------ internal/handlers/handler_verify_test.go | 27 +++-- internal/handlers/oidc_register.go | 18 +-- internal/handlers/response.go | 6 +- internal/middlewares/authelia_context.go | 63 ++++++++-- internal/middlewares/const.go | 29 +++-- internal/middlewares/identity_verification.go | 36 +++--- internal/server/options_handler.go | 11 ++ internal/server/server.go | 2 + internal/suites/suite_standalone_test.go | 20 ++-- internal/utils/const.go | 9 ++ internal/utils/strings.go | 5 + 28 files changed, 343 insertions(+), 227 deletions(-) create mode 100644 internal/server/options_handler.go diff --git a/internal/handlers/const.go b/internal/handlers/const.go index 8d3ec9fc..4502e5c9 100644 --- a/internal/handlers/const.go +++ b/internal/handlers/const.go @@ -1,29 +1,31 @@ package handlers -// TOTPRegistrationAction is the string representation of the action for which the token has been produced. -const TOTPRegistrationAction = "RegisterTOTPDevice" +const ( + // ActionTOTPRegistration is the string representation of the action for which the token has been produced. + ActionTOTPRegistration = "RegisterTOTPDevice" -// U2FRegistrationAction is the string representation of the action for which the token has been produced. -const U2FRegistrationAction = "RegisterU2FDevice" + // ActionU2FRegistration is the string representation of the action for which the token has been produced. + ActionU2FRegistration = "RegisterU2FDevice" -// ResetPasswordAction is the string representation of the action for which the token has been produced. -const ResetPasswordAction = "ResetPassword" + // ActionResetPassword is the string representation of the action for which the token has been produced. + ActionResetPassword = "ResetPassword" +) -const authPrefix = "Basic " +const ( + // HeaderProxyAuthorization is the basic-auth HTTP header Authelia utilises. + HeaderProxyAuthorization = "Proxy-Authorization" -// ProxyAuthorizationHeader is the basic-auth HTTP header Authelia utilises. -const ProxyAuthorizationHeader = "Proxy-Authorization" + // HeaderAuthorization is the basic-auth HTTP header Authelia utilises with "auth=basic" query param. + HeaderAuthorization = "Authorization" -// AuthorizationHeader is the basic-auth HTTP header Authelia utilises with "auth=basic" query param. -const AuthorizationHeader = "Authorization" + // HeaderSessionUsername is used as additional protection to validate a user for things like pam_exec. + HeaderSessionUsername = "Session-Username" -// SessionUsernameHeader is used as additional protection to validate a user for things like pam_exec. -const SessionUsernameHeader = "Session-Username" - -const remoteUserHeader = "Remote-User" -const remoteNameHeader = "Remote-Name" -const remoteEmailHeader = "Remote-Email" -const remoteGroupsHeader = "Remote-Groups" + headerRemoteUser = "Remote-User" + headerRemoteName = "Remote-Name" + headerRemoteEmail = "Remote-Email" + headerRemoteGroups = "Remote-Groups" +) const ( // Forbidden means the user is forbidden the access to a resource. @@ -34,47 +36,56 @@ const ( Authorized authorizationMatching = iota ) -const operationFailedMessage = "Operation failed." -const authenticationFailedMessage = "Authentication failed. Check your credentials." -const userBannedMessage = "Please retry in a few minutes." -const unableToRegisterOneTimePasswordMessage = "Unable to set up one-time passwords." //nolint:gosec -const unableToRegisterSecurityKeyMessage = "Unable to register your security key." -const unableToResetPasswordMessage = "Unable to reset your password." -const mfaValidationFailedMessage = "Authentication failed, please retry later." +const ( + messageOperationFailed = "Operation failed." + messageAuthenticationFailed = "Authentication failed. Check your credentials." + messageUserBanned = "Please retry in a few minutes." + messageUnableToRegisterOneTimePassword = "Unable to set up one-time passwords." //nolint:gosec + messageUnableToRegisterSecurityKey = "Unable to register your security key." + messageUnableToResetPassword = "Unable to reset your password." + messageMFAValidationFailed = "Authentication failed, please retry later." +) -const ldapPasswordComplexityCode = "0000052D." +const ( + testInactivity = "10" + testRedirectionURL = "http://redirection.local" + testResultAllow = "allow" + testUsername = "john" +) -var ldapPasswordComplexityCodes = []string{ - "0000052D", "SynoNumber", "SynoMixedCase", "SynoExcludeNameDesc", "SynoSpecialChar", -} -var ldapPasswordComplexityErrors = []string{ - "LDAP Result Code 19 \"Constraint Violation\": Password fails quality checking policy", - "LDAP Result Code 19 \"Constraint Violation\": Password is too young to change", -} - -const testInactivity = "10" -const testRedirectionURL = "http://redirection.local" -const testResultAllow = "allow" -const testUsername = "john" - -const movingAverageWindow = 10 -const msMinimumDelay1FA = float64(250) -const msMaximumRandomDelay = int64(85) +const ( + loginDelayMovingAverageWindow = 10 + loginDelayMinimumDelayMilliseconds = float64(250) + loginDelayMaximumRandomDelayMilliseconds = int64(85) +) // OIDC constants. const ( - oidcJWKsPath = "/api/oidc/jwks" - oidcAuthorizePath = "/api/oidc/authorize" - oidcTokenPath = "/api/oidc/token" //nolint:gosec // This is not a hard coded credential, it's a path. - oidcIntrospectPath = "/api/oidc/introspect" - oidcRevokePath = "/api/oidc/revoke" - oidcUserinfoPath = "/api/oidc/userinfo" + pathOpenIDConnectJWKs = "/api/oidc/jwks" + pathOpenIDConnectAuthorization = "/api/oidc/authorize" + pathOpenIDConnectToken = "/api/oidc/token" //nolint:gosec // This is not a hard coded credential, it's a path. + pathOpenIDConnectIntrospection = "/api/oidc/introspect" + pathOpenIDConnectRevocation = "/api/oidc/revoke" + pathOpenIDConnectUserinfo = "/api/oidc/userinfo" // Note: If you change this const you must also do so in the frontend at web/src/services/Api.ts. - oidcConsentPath = "/api/oidc/consent" + pathOpenIDConnectConsent = "/api/oidc/consent" ) const ( accept = "accept" reject = "reject" ) + +const authPrefix = "Basic " + +const ldapPasswordComplexityCode = "0000052D." + +var ldapPasswordComplexityCodes = []string{ + "0000052D", "SynoNumber", "SynoMixedCase", "SynoExcludeNameDesc", "SynoSpecialChar", +} + +var ldapPasswordComplexityErrors = []string{ + "LDAP Result Code 19 \"Constraint Violation\": Password fails quality checking policy", + "LDAP Result Code 19 \"Constraint Violation\": Password is too young to change", +} diff --git a/internal/handlers/handler_firstfactor.go b/internal/handlers/handler_firstfactor.go index 4e2f0803..f8391964 100644 --- a/internal/handlers/handler_firstfactor.go +++ b/internal/handlers/handler_firstfactor.go @@ -16,7 +16,7 @@ func movingAverageIteration(value time.Duration, successful bool, movingAverageC mutex.Lock() if successful { (*execDurationMovingAverage)[*movingAverageCursor] = value - *movingAverageCursor = (*movingAverageCursor + 1) % movingAverageWindow + *movingAverageCursor = (*movingAverageCursor + 1) % loginDelayMovingAverageWindow } var sum int64 @@ -26,12 +26,12 @@ func movingAverageIteration(value time.Duration, successful bool, movingAverageC } mutex.Unlock() - return float64(sum / movingAverageWindow) + return float64(sum / loginDelayMovingAverageWindow) } func calculateActualDelay(ctx *middlewares.AutheliaCtx, execDuration time.Duration, avgExecDurationMs float64, successful *bool) float64 { - randomDelayMs := float64(rand.Int63n(msMaximumRandomDelay)) //nolint:gosec // TODO: Consider use of crypto/rand, this should be benchmarked and measured first. - totalDelayMs := math.Max(avgExecDurationMs, msMinimumDelay1FA) + randomDelayMs + randomDelayMs := float64(rand.Int63n(loginDelayMaximumRandomDelayMilliseconds)) //nolint:gosec // TODO: Consider use of crypto/rand, this should be benchmarked and measured first. + totalDelayMs := math.Max(avgExecDurationMs, loginDelayMinimumDelayMilliseconds) + randomDelayMs actualDelayMs := math.Max(totalDelayMs-float64(execDuration.Milliseconds()), 1.0) ctx.Logger.Tracef("attempt successful: %t, exec duration: %d, avg execution duration: %d, random delay ms: %d, total delay ms: %d, actual delay ms: %d", *successful, execDuration.Milliseconds(), int64(avgExecDurationMs), int64(randomDelayMs), int64(totalDelayMs), int64(actualDelayMs)) @@ -48,7 +48,7 @@ func delayToPreventTimingAttacks(ctx *middlewares.AutheliaCtx, requestTime time. // FirstFactorPost is the handler performing the first factory. //nolint:gocyclo // TODO: Consider refactoring time permitting. func FirstFactorPost(msInitialDelay time.Duration, delayEnabled bool) middlewares.RequestHandler { - var execDurationMovingAverage = make([]time.Duration, movingAverageWindow) + var execDurationMovingAverage = make([]time.Duration, loginDelayMovingAverageWindow) var movingAverageCursor = 0 @@ -73,7 +73,7 @@ func FirstFactorPost(msInitialDelay time.Duration, delayEnabled bool) middleware err := ctx.ParseBody(&bodyJSON) if err != nil { - handleAuthenticationUnauthorized(ctx, err, authenticationFailedMessage) + handleAuthenticationUnauthorized(ctx, err, messageAuthenticationFailed) return } @@ -81,11 +81,11 @@ func FirstFactorPost(msInitialDelay time.Duration, delayEnabled bool) middleware if err != nil { if err == regulation.ErrUserIsBanned { - handleAuthenticationUnauthorized(ctx, fmt.Errorf("User %s is banned until %s", bodyJSON.Username, bannedUntil), userBannedMessage) + handleAuthenticationUnauthorized(ctx, fmt.Errorf("User %s is banned until %s", bodyJSON.Username, bannedUntil), messageUserBanned) return } - handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to regulate authentication: %s", err.Error()), authenticationFailedMessage) + handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to regulate authentication: %s", err.Error()), messageAuthenticationFailed) return } @@ -99,7 +99,7 @@ func FirstFactorPost(msInitialDelay time.Duration, delayEnabled bool) middleware ctx.Logger.Errorf("Unable to mark authentication: %s", err.Error()) } - handleAuthenticationUnauthorized(ctx, fmt.Errorf("Error while checking password for user %s: %s", bodyJSON.Username, err.Error()), authenticationFailedMessage) + handleAuthenticationUnauthorized(ctx, fmt.Errorf("Error while checking password for user %s: %s", bodyJSON.Username, err.Error()), messageAuthenticationFailed) return } @@ -111,7 +111,7 @@ func FirstFactorPost(msInitialDelay time.Duration, delayEnabled bool) middleware ctx.Logger.Errorf("Unable to mark authentication: %s", err.Error()) } - handleAuthenticationUnauthorized(ctx, fmt.Errorf("Credentials are wrong for user %s", bodyJSON.Username), authenticationFailedMessage) + handleAuthenticationUnauthorized(ctx, fmt.Errorf("Credentials are wrong for user %s", bodyJSON.Username), messageAuthenticationFailed) return } @@ -120,7 +120,7 @@ func FirstFactorPost(msInitialDelay time.Duration, delayEnabled bool) middleware err = ctx.Providers.Regulator.Mark(bodyJSON.Username, true) if err != nil { - handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to mark authentication: %s", err.Error()), authenticationFailedMessage) + handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to mark authentication: %s", err.Error()), messageAuthenticationFailed) return } @@ -134,14 +134,14 @@ func FirstFactorPost(msInitialDelay time.Duration, delayEnabled bool) middleware err = ctx.SaveSession(newSession) if err != nil { - handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to reset the session for user %s: %s", bodyJSON.Username, err.Error()), authenticationFailedMessage) + handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to reset the session for user %s: %s", bodyJSON.Username, err.Error()), messageAuthenticationFailed) return } err = ctx.Providers.SessionProvider.RegenerateSession(ctx.RequestCtx) if err != nil { - handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to regenerate session for user %s: %s", bodyJSON.Username, err.Error()), authenticationFailedMessage) + handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to regenerate session for user %s: %s", bodyJSON.Username, err.Error()), messageAuthenticationFailed) return } @@ -152,7 +152,7 @@ func FirstFactorPost(msInitialDelay time.Duration, delayEnabled bool) middleware if keepMeLoggedIn { err = ctx.Providers.SessionProvider.UpdateExpiration(ctx.RequestCtx, ctx.Providers.SessionProvider.RememberMe) if err != nil { - handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to update expiration timer for user %s: %s", bodyJSON.Username, err.Error()), authenticationFailedMessage) + handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to update expiration timer for user %s: %s", bodyJSON.Username, err.Error()), messageAuthenticationFailed) return } } @@ -161,7 +161,7 @@ func FirstFactorPost(msInitialDelay time.Duration, delayEnabled bool) middleware userDetails, err := ctx.Providers.UserProvider.GetDetails(bodyJSON.Username) if err != nil { - handleAuthenticationUnauthorized(ctx, fmt.Errorf("Error while retrieving details from user %s: %s", bodyJSON.Username, err.Error()), authenticationFailedMessage) + handleAuthenticationUnauthorized(ctx, fmt.Errorf("Error while retrieving details from user %s: %s", bodyJSON.Username, err.Error()), messageAuthenticationFailed) return } @@ -175,7 +175,7 @@ func FirstFactorPost(msInitialDelay time.Duration, delayEnabled bool) middleware err = ctx.SaveSession(userSession) if err != nil { - handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to save session of user %s", bodyJSON.Username), authenticationFailedMessage) + handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to save session of user %s", bodyJSON.Username), messageAuthenticationFailed) return } diff --git a/internal/handlers/handler_firstfactor_test.go b/internal/handlers/handler_firstfactor_test.go index 0c005e00..1642c731 100644 --- a/internal/handlers/handler_firstfactor_test.go +++ b/internal/handlers/handler_firstfactor_test.go @@ -454,16 +454,16 @@ func TestFirstFactorDelayCalculations(t *testing.T) { for i := 0; i < 100; i++ { delay := calculateActualDelay(mock.Ctx, execDuration, avgExecDurationMs, &successful) assert.True(t, delay >= expectedMinimumDelayMs) - assert.True(t, delay <= expectedMinimumDelayMs+float64(msMaximumRandomDelay)) + assert.True(t, delay <= expectedMinimumDelayMs+float64(loginDelayMaximumRandomDelayMilliseconds)) } execDuration = 5 * time.Millisecond avgExecDurationMs = 5.0 - expectedMinimumDelayMs = msMinimumDelay1FA - float64(execDuration.Milliseconds()) + expectedMinimumDelayMs = loginDelayMinimumDelayMilliseconds - float64(execDuration.Milliseconds()) for i := 0; i < 100; i++ { delay := calculateActualDelay(mock.Ctx, execDuration, avgExecDurationMs, &successful) assert.True(t, delay >= expectedMinimumDelayMs) - assert.True(t, delay <= expectedMinimumDelayMs+float64(msMaximumRandomDelay)) + assert.True(t, delay <= expectedMinimumDelayMs+float64(loginDelayMaximumRandomDelayMilliseconds)) } } diff --git a/internal/handlers/handler_logout.go b/internal/handlers/handler_logout.go index f9b54eb2..885aeb12 100644 --- a/internal/handlers/handler_logout.go +++ b/internal/handlers/handler_logout.go @@ -25,14 +25,14 @@ func LogoutPost(ctx *middlewares.AutheliaCtx) { err := ctx.ParseBody(&body) if err != nil { - ctx.Error(fmt.Errorf("Unable to parse body during logout: %s", err), operationFailedMessage) + ctx.Error(fmt.Errorf("Unable to parse body during logout: %s", err), messageOperationFailed) } ctx.Logger.Tracef("Attempting to destroy session") err = ctx.Providers.SessionProvider.DestroySession(ctx.RequestCtx) if err != nil { - ctx.Error(fmt.Errorf("Unable to destroy session during logout: %s", err), operationFailedMessage) + ctx.Error(fmt.Errorf("Unable to destroy session during logout: %s", err), messageOperationFailed) } redirectionURL, err := url.Parse(body.TargetURL) @@ -46,6 +46,6 @@ func LogoutPost(ctx *middlewares.AutheliaCtx) { err = ctx.SetJSONBody(responseBody) if err != nil { - ctx.Error(fmt.Errorf("Unable to set body during logout: %s", err), operationFailedMessage) + ctx.Error(fmt.Errorf("Unable to set body during logout: %s", err), messageOperationFailed) } } diff --git a/internal/handlers/handler_oidc_wellknown.go b/internal/handlers/handler_oidc_wellknown.go index 95b3ecdf..977baa37 100644 --- a/internal/handlers/handler_oidc_wellknown.go +++ b/internal/handlers/handler_oidc_wellknown.go @@ -22,12 +22,12 @@ func oidcWellKnown(ctx *middlewares.AutheliaCtx) { wellKnown := oidc.WellKnownConfiguration{ Issuer: issuer, - JWKSURI: fmt.Sprintf("%s%s", issuer, oidcJWKsPath), + JWKSURI: fmt.Sprintf("%s%s", issuer, pathOpenIDConnectJWKs), - AuthorizationEndpoint: fmt.Sprintf("%s%s", issuer, oidcAuthorizePath), - TokenEndpoint: fmt.Sprintf("%s%s", issuer, oidcTokenPath), - RevocationEndpoint: fmt.Sprintf("%s%s", issuer, oidcRevokePath), - UserinfoEndpoint: fmt.Sprintf("%s%s", issuer, oidcUserinfoPath), + AuthorizationEndpoint: fmt.Sprintf("%s%s", issuer, pathOpenIDConnectAuthorization), + TokenEndpoint: fmt.Sprintf("%s%s", issuer, pathOpenIDConnectToken), + RevocationEndpoint: fmt.Sprintf("%s%s", issuer, pathOpenIDConnectRevocation), + UserinfoEndpoint: fmt.Sprintf("%s%s", issuer, pathOpenIDConnectUserinfo), Algorithms: []string{"RS256"}, UserinfoAlgorithms: []string{"none", "RS256"}, diff --git a/internal/handlers/handler_register_totp.go b/internal/handlers/handler_register_totp.go index bfa68475..3a7fd0b5 100644 --- a/internal/handlers/handler_register_totp.go +++ b/internal/handlers/handler_register_totp.go @@ -32,7 +32,7 @@ var SecondFactorTOTPIdentityStart = middlewares.IdentityVerificationStart(middle MailTitle: "Register your mobile", MailButtonContent: "Register", TargetEndpoint: "/one-time-password/register", - ActionClaim: TOTPRegistrationAction, + ActionClaim: ActionTOTPRegistration, IdentityRetrieverFunc: identityRetrieverFromSession, }) @@ -45,13 +45,13 @@ func secondFactorTOTPIdentityFinish(ctx *middlewares.AutheliaCtx, username strin }) if err != nil { - ctx.Error(fmt.Errorf("Unable to generate TOTP key: %s", err), unableToRegisterOneTimePasswordMessage) + ctx.Error(fmt.Errorf("Unable to generate TOTP key: %s", err), messageUnableToRegisterOneTimePassword) return } err = ctx.Providers.StorageProvider.SaveTOTPSecret(username, key.Secret()) if err != nil { - ctx.Error(fmt.Errorf("Unable to save TOTP secret in DB: %s", err), unableToRegisterOneTimePasswordMessage) + ctx.Error(fmt.Errorf("Unable to save TOTP secret in DB: %s", err), messageUnableToRegisterOneTimePassword) return } @@ -69,6 +69,6 @@ func secondFactorTOTPIdentityFinish(ctx *middlewares.AutheliaCtx, username strin // SecondFactorTOTPIdentityFinish the handler for finishing the identity validation. var SecondFactorTOTPIdentityFinish = middlewares.IdentityVerificationFinish( middlewares.IdentityVerificationFinishArgs{ - ActionClaim: TOTPRegistrationAction, + ActionClaim: ActionTOTPRegistration, IsTokenUserValidFunc: isTokenUserValidFor2FARegistration, }, secondFactorTOTPIdentityFinish) diff --git a/internal/handlers/handler_register_u2f_step1.go b/internal/handlers/handler_register_u2f_step1.go index b1abb5db..c1a13a77 100644 --- a/internal/handlers/handler_register_u2f_step1.go +++ b/internal/handlers/handler_register_u2f_step1.go @@ -19,18 +19,18 @@ var SecondFactorU2FIdentityStart = middlewares.IdentityVerificationStart(middlew MailTitle: "Register your key", MailButtonContent: "Register", TargetEndpoint: "/security-key/register", - ActionClaim: U2FRegistrationAction, + ActionClaim: ActionU2FRegistration, IdentityRetrieverFunc: identityRetrieverFromSession, }) func secondFactorU2FIdentityFinish(ctx *middlewares.AutheliaCtx, username string) { if ctx.XForwardedProto() == nil { - ctx.Error(errMissingXForwardedProto, operationFailedMessage) + ctx.Error(errMissingXForwardedProto, messageOperationFailed) return } if ctx.XForwardedHost() == nil { - ctx.Error(errMissingXForwardedHost, operationFailedMessage) + ctx.Error(errMissingXForwardedHost, messageOperationFailed) return } @@ -42,7 +42,7 @@ func secondFactorU2FIdentityFinish(ctx *middlewares.AutheliaCtx, username string challenge, err := u2f.NewChallenge(appID, trustedFacets) if err != nil { - ctx.Error(fmt.Errorf("Unable to generate new U2F challenge for registration: %s", err), operationFailedMessage) + ctx.Error(fmt.Errorf("Unable to generate new U2F challenge for registration: %s", err), messageOperationFailed) return } @@ -52,7 +52,7 @@ func secondFactorU2FIdentityFinish(ctx *middlewares.AutheliaCtx, username string err = ctx.SaveSession(userSession) if err != nil { - ctx.Error(fmt.Errorf("Unable to save U2F challenge in session: %s", err), operationFailedMessage) + ctx.Error(fmt.Errorf("Unable to save U2F challenge in session: %s", err), messageOperationFailed) return } @@ -65,6 +65,6 @@ func secondFactorU2FIdentityFinish(ctx *middlewares.AutheliaCtx, username string // SecondFactorU2FIdentityFinish the handler for finishing the identity validation. var SecondFactorU2FIdentityFinish = middlewares.IdentityVerificationFinish( middlewares.IdentityVerificationFinishArgs{ - ActionClaim: U2FRegistrationAction, + ActionClaim: ActionU2FRegistration, IsTokenUserValidFunc: isTokenUserValidFor2FARegistration, }, secondFactorU2FIdentityFinish) diff --git a/internal/handlers/handler_register_u2f_step1_test.go b/internal/handlers/handler_register_u2f_step1_test.go index edfd8af4..b309b6b7 100644 --- a/internal/handlers/handler_register_u2f_step1_test.go +++ b/internal/handlers/handler_register_u2f_step1_test.go @@ -50,7 +50,7 @@ func createToken(secret string, username string, action string, expiresAt time.T } func (s *HandlerRegisterU2FStep1Suite) TestShouldRaiseWhenXForwardedProtoIsMissing() { - token := createToken(s.mock.Ctx.Configuration.JWTSecret, "john", U2FRegistrationAction, + token := createToken(s.mock.Ctx.Configuration.JWTSecret, "john", ActionU2FRegistration, time.Now().Add(1*time.Minute)) s.mock.Ctx.Request.SetBodyString(fmt.Sprintf("{\"token\":\"%s\"}", token)) @@ -70,7 +70,7 @@ func (s *HandlerRegisterU2FStep1Suite) TestShouldRaiseWhenXForwardedProtoIsMissi func (s *HandlerRegisterU2FStep1Suite) TestShouldRaiseWhenXForwardedHostIsMissing() { s.mock.Ctx.Request.Header.Add("X-Forwarded-Proto", "http") - token := createToken(s.mock.Ctx.Configuration.JWTSecret, "john", U2FRegistrationAction, + token := createToken(s.mock.Ctx.Configuration.JWTSecret, "john", ActionU2FRegistration, time.Now().Add(1*time.Minute)) s.mock.Ctx.Request.SetBodyString(fmt.Sprintf("{\"token\":\"%s\"}", token)) diff --git a/internal/handlers/handler_register_u2f_step2.go b/internal/handlers/handler_register_u2f_step2.go index 10ec3e4b..30846634 100644 --- a/internal/handlers/handler_register_u2f_step2.go +++ b/internal/handlers/handler_register_u2f_step2.go @@ -16,13 +16,13 @@ func SecondFactorU2FRegister(ctx *middlewares.AutheliaCtx) { err := ctx.ParseBody(&responseBody) if err != nil { - ctx.Error(fmt.Errorf("Unable to parse response body: %v", err), unableToRegisterSecurityKeyMessage) + ctx.Error(fmt.Errorf("Unable to parse response body: %v", err), messageUnableToRegisterSecurityKey) } userSession := ctx.GetSession() if userSession.U2FChallenge == nil { - ctx.Error(fmt.Errorf("U2F registration has not been initiated yet"), unableToRegisterSecurityKeyMessage) + ctx.Error(fmt.Errorf("U2F registration has not been initiated yet"), messageUnableToRegisterSecurityKey) return } // Ensure the challenge is cleared if anything goes wrong. @@ -38,7 +38,7 @@ func SecondFactorU2FRegister(ctx *middlewares.AutheliaCtx) { registration, err := u2f.Register(responseBody, *userSession.U2FChallenge, u2fConfig) if err != nil { - ctx.Error(fmt.Errorf("Unable to verify U2F registration: %v", err), unableToRegisterSecurityKeyMessage) + ctx.Error(fmt.Errorf("Unable to verify U2F registration: %v", err), messageUnableToRegisterSecurityKey) return } @@ -48,7 +48,7 @@ func SecondFactorU2FRegister(ctx *middlewares.AutheliaCtx) { err = ctx.Providers.StorageProvider.SaveU2FDeviceHandle(userSession.Username, registration.KeyHandle, publicKey) if err != nil { - ctx.Error(fmt.Errorf("Unable to register U2F device for user %s: %v", userSession.Username, err), unableToRegisterSecurityKeyMessage) + ctx.Error(fmt.Errorf("Unable to register U2F device for user %s: %v", userSession.Username, err), messageUnableToRegisterSecurityKey) return } diff --git a/internal/handlers/handler_reset_password_step1.go b/internal/handlers/handler_reset_password_step1.go index ce67a8c6..b7001782 100644 --- a/internal/handlers/handler_reset_password_step1.go +++ b/internal/handlers/handler_reset_password_step1.go @@ -38,7 +38,7 @@ var ResetPasswordIdentityStart = middlewares.IdentityVerificationStart(middlewar MailTitle: "Reset your password", MailButtonContent: "Reset", TargetEndpoint: "/reset-password/step2", - ActionClaim: ResetPasswordAction, + ActionClaim: ActionResetPassword, IdentityRetrieverFunc: identityRetrieverFromStorage, }) @@ -57,4 +57,4 @@ func resetPasswordIdentityFinish(ctx *middlewares.AutheliaCtx, username string) // ResetPasswordIdentityFinish the handler for finishing the identity validation. var ResetPasswordIdentityFinish = middlewares.IdentityVerificationFinish( - middlewares.IdentityVerificationFinishArgs{ActionClaim: ResetPasswordAction}, resetPasswordIdentityFinish) + middlewares.IdentityVerificationFinishArgs{ActionClaim: ActionResetPassword}, resetPasswordIdentityFinish) diff --git a/internal/handlers/handler_reset_password_step2.go b/internal/handlers/handler_reset_password_step2.go index 6c9df059..c7972314 100644 --- a/internal/handlers/handler_reset_password_step2.go +++ b/internal/handlers/handler_reset_password_step2.go @@ -15,7 +15,7 @@ func ResetPasswordPost(ctx *middlewares.AutheliaCtx) { // otherwise PasswordReset would not be set to true. We can improve the security of this check by making the // request expire at some point because here it only expires when the cookie expires. if userSession.PasswordResetUsername == nil { - ctx.Error(fmt.Errorf("No identity verification process has been initiated"), unableToResetPasswordMessage) + ctx.Error(fmt.Errorf("No identity verification process has been initiated"), messageUnableToResetPassword) return } @@ -23,7 +23,7 @@ func ResetPasswordPost(ctx *middlewares.AutheliaCtx) { err := ctx.ParseBody(&requestBody) if err != nil { - ctx.Error(err, unableToResetPasswordMessage) + ctx.Error(err, messageUnableToResetPassword) return } @@ -35,7 +35,7 @@ func ResetPasswordPost(ctx *middlewares.AutheliaCtx) { utils.IsStringInSliceContains(err.Error(), ldapPasswordComplexityErrors): ctx.Error(fmt.Errorf("%s", err), ldapPasswordComplexityCode) default: - ctx.Error(fmt.Errorf("%s", err), unableToResetPasswordMessage) + ctx.Error(fmt.Errorf("%s", err), messageUnableToResetPassword) } return @@ -48,7 +48,7 @@ func ResetPasswordPost(ctx *middlewares.AutheliaCtx) { err = ctx.SaveSession(userSession) if err != nil { - ctx.Error(fmt.Errorf("Unable to update password reset state: %s", err), operationFailedMessage) + ctx.Error(fmt.Errorf("Unable to update password reset state: %s", err), messageOperationFailed) return } diff --git a/internal/handlers/handler_sign_duo.go b/internal/handlers/handler_sign_duo.go index 6be431a9..ab94e136 100644 --- a/internal/handlers/handler_sign_duo.go +++ b/internal/handlers/handler_sign_duo.go @@ -15,7 +15,7 @@ func SecondFactorDuoPost(duoAPI duo.API) middlewares.RequestHandler { err := ctx.ParseBody(&requestBody) if err != nil { - handleAuthenticationUnauthorized(ctx, err, mfaValidationFailedMessage) + handleAuthenticationUnauthorized(ctx, err, messageMFAValidationFailed) return } @@ -37,7 +37,7 @@ func SecondFactorDuoPost(duoAPI duo.API) middlewares.RequestHandler { duoResponse, err := duoAPI.Call(values, ctx) if err != nil { - handleAuthenticationUnauthorized(ctx, fmt.Errorf("Duo API errored: %s", err), mfaValidationFailedMessage) + handleAuthenticationUnauthorized(ctx, fmt.Errorf("Duo API errored: %s", err), messageMFAValidationFailed) return } @@ -60,7 +60,7 @@ func SecondFactorDuoPost(duoAPI duo.API) middlewares.RequestHandler { err = ctx.Providers.SessionProvider.RegenerateSession(ctx.RequestCtx) if err != nil { - handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to regenerate session for user %s: %s", userSession.Username, err), mfaValidationFailedMessage) + handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to regenerate session for user %s: %s", userSession.Username, err), messageMFAValidationFailed) return } @@ -68,7 +68,7 @@ func SecondFactorDuoPost(duoAPI duo.API) middlewares.RequestHandler { err = ctx.SaveSession(userSession) if err != nil { - handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to update authentication level with Duo: %s", err), mfaValidationFailedMessage) + handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to update authentication level with Duo: %s", err), messageMFAValidationFailed) return } diff --git a/internal/handlers/handler_sign_totp.go b/internal/handlers/handler_sign_totp.go index 92b330a9..20fcbc89 100644 --- a/internal/handlers/handler_sign_totp.go +++ b/internal/handlers/handler_sign_totp.go @@ -13,7 +13,7 @@ func SecondFactorTOTPPost(totpVerifier TOTPVerifier) middlewares.RequestHandler err := ctx.ParseBody(&requestBody) if err != nil { - handleAuthenticationUnauthorized(ctx, err, mfaValidationFailedMessage) + handleAuthenticationUnauthorized(ctx, err, messageMFAValidationFailed) return } @@ -21,25 +21,25 @@ func SecondFactorTOTPPost(totpVerifier TOTPVerifier) middlewares.RequestHandler secret, err := ctx.Providers.StorageProvider.LoadTOTPSecret(userSession.Username) if err != nil { - handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to load TOTP secret: %s", err), mfaValidationFailedMessage) + handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to load TOTP secret: %s", err), messageMFAValidationFailed) return } isValid, err := totpVerifier.Verify(requestBody.Token, secret) if err != nil { - handleAuthenticationUnauthorized(ctx, fmt.Errorf("Error occurred during OTP validation for user %s: %s", userSession.Username, err), mfaValidationFailedMessage) + handleAuthenticationUnauthorized(ctx, fmt.Errorf("Error occurred during OTP validation for user %s: %s", userSession.Username, err), messageMFAValidationFailed) return } if !isValid { - handleAuthenticationUnauthorized(ctx, fmt.Errorf("Wrong passcode during TOTP validation for user %s", userSession.Username), mfaValidationFailedMessage) + handleAuthenticationUnauthorized(ctx, fmt.Errorf("Wrong passcode during TOTP validation for user %s", userSession.Username), messageMFAValidationFailed) return } err = ctx.Providers.SessionProvider.RegenerateSession(ctx.RequestCtx) if err != nil { - handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to regenerate session for user %s: %s", userSession.Username, err), mfaValidationFailedMessage) + handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to regenerate session for user %s: %s", userSession.Username, err), messageMFAValidationFailed) return } @@ -47,7 +47,7 @@ func SecondFactorTOTPPost(totpVerifier TOTPVerifier) middlewares.RequestHandler err = ctx.SaveSession(userSession) if err != nil { - handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to update the authentication level with TOTP: %s", err), mfaValidationFailedMessage) + handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to update the authentication level with TOTP: %s", err), messageMFAValidationFailed) return } diff --git a/internal/handlers/handler_sign_u2f_step1.go b/internal/handlers/handler_sign_u2f_step1.go index caf660a2..7c83bfd5 100644 --- a/internal/handlers/handler_sign_u2f_step1.go +++ b/internal/handlers/handler_sign_u2f_step1.go @@ -14,12 +14,12 @@ import ( // SecondFactorU2FSignGet handler for initiating a signing request. func SecondFactorU2FSignGet(ctx *middlewares.AutheliaCtx) { if ctx.XForwardedProto() == nil { - ctx.Error(errMissingXForwardedProto, mfaValidationFailedMessage) + ctx.Error(errMissingXForwardedProto, messageMFAValidationFailed) return } if ctx.XForwardedHost() == nil { - ctx.Error(errMissingXForwardedHost, mfaValidationFailedMessage) + ctx.Error(errMissingXForwardedHost, messageMFAValidationFailed) return } @@ -29,7 +29,7 @@ func SecondFactorU2FSignGet(ctx *middlewares.AutheliaCtx) { challenge, err := u2f.NewChallenge(appID, trustedFacets) if err != nil { - handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to create U2F challenge: %s", err), mfaValidationFailedMessage) + handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to create U2F challenge: %s", err), messageMFAValidationFailed) return } @@ -38,11 +38,11 @@ func SecondFactorU2FSignGet(ctx *middlewares.AutheliaCtx) { if err != nil { if err == storage.ErrNoU2FDeviceHandle { - handleAuthenticationUnauthorized(ctx, fmt.Errorf("No device handle found for user %s", userSession.Username), mfaValidationFailedMessage) + handleAuthenticationUnauthorized(ctx, fmt.Errorf("No device handle found for user %s", userSession.Username), messageMFAValidationFailed) return } - handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to retrieve U2F device handle: %s", err), mfaValidationFailedMessage) + handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to retrieve U2F device handle: %s", err), messageMFAValidationFailed) return } @@ -63,7 +63,7 @@ func SecondFactorU2FSignGet(ctx *middlewares.AutheliaCtx) { err = ctx.SaveSession(userSession) if err != nil { - handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to save U2F challenge and registration in session: %s", err), mfaValidationFailedMessage) + handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to save U2F challenge and registration in session: %s", err), messageMFAValidationFailed) return } @@ -71,7 +71,7 @@ func SecondFactorU2FSignGet(ctx *middlewares.AutheliaCtx) { err = ctx.SetJSONBody(signRequest) if err != nil { - handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to set sign request in body: %s", err), mfaValidationFailedMessage) + handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to set sign request in body: %s", err), messageMFAValidationFailed) return } } diff --git a/internal/handlers/handler_sign_u2f_step2.go b/internal/handlers/handler_sign_u2f_step2.go index eba199e7..54dcd5f6 100644 --- a/internal/handlers/handler_sign_u2f_step2.go +++ b/internal/handlers/handler_sign_u2f_step2.go @@ -13,18 +13,18 @@ func SecondFactorU2FSignPost(u2fVerifier U2FVerifier) middlewares.RequestHandler err := ctx.ParseBody(&requestBody) if err != nil { - ctx.Error(err, mfaValidationFailedMessage) + ctx.Error(err, messageMFAValidationFailed) return } userSession := ctx.GetSession() if userSession.U2FChallenge == nil { - handleAuthenticationUnauthorized(ctx, fmt.Errorf("U2F signing has not been initiated yet (no challenge)"), mfaValidationFailedMessage) + handleAuthenticationUnauthorized(ctx, fmt.Errorf("U2F signing has not been initiated yet (no challenge)"), messageMFAValidationFailed) return } if userSession.U2FRegistration == nil { - handleAuthenticationUnauthorized(ctx, fmt.Errorf("U2F signing has not been initiated yet (no registration)"), mfaValidationFailedMessage) + handleAuthenticationUnauthorized(ctx, fmt.Errorf("U2F signing has not been initiated yet (no registration)"), messageMFAValidationFailed) return } @@ -35,14 +35,14 @@ func SecondFactorU2FSignPost(u2fVerifier U2FVerifier) middlewares.RequestHandler *userSession.U2FChallenge) if err != nil { - ctx.Error(err, mfaValidationFailedMessage) + ctx.Error(err, messageMFAValidationFailed) return } err = ctx.Providers.SessionProvider.RegenerateSession(ctx.RequestCtx) if err != nil { - handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to regenerate session for user %s: %s", userSession.Username, err), mfaValidationFailedMessage) + handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to regenerate session for user %s: %s", userSession.Username, err), messageMFAValidationFailed) return } @@ -50,7 +50,7 @@ func SecondFactorU2FSignPost(u2fVerifier U2FVerifier) middlewares.RequestHandler err = ctx.SaveSession(userSession) if err != nil { - handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to update authentication level with U2F: %s", err), mfaValidationFailedMessage) + handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to update authentication level with U2F: %s", err), messageMFAValidationFailed) return } diff --git a/internal/handlers/handler_user_info.go b/internal/handlers/handler_user_info.go index 1fb454f0..ecc3c58a 100644 --- a/internal/handlers/handler_user_info.go +++ b/internal/handlers/handler_user_info.go @@ -87,7 +87,7 @@ func UserInfoGet(ctx *middlewares.AutheliaCtx) { errors := loadInfo(userSession.Username, ctx.Providers.StorageProvider, &userInfo, ctx.Logger) if len(errors) > 0 { - ctx.Error(fmt.Errorf("Unable to load user information"), operationFailedMessage) + ctx.Error(fmt.Errorf("Unable to load user information"), messageOperationFailed) return } @@ -110,12 +110,12 @@ func MethodPreferencePost(ctx *middlewares.AutheliaCtx) { err := ctx.ParseBody(&bodyJSON) if err != nil { - ctx.Error(err, operationFailedMessage) + ctx.Error(err, messageOperationFailed) return } if !utils.IsStringInSlice(bodyJSON.Method, authentication.PossibleMethods) { - ctx.Error(fmt.Errorf("Unknown method '%s', it should be one of %s", bodyJSON.Method, strings.Join(authentication.PossibleMethods, ", ")), operationFailedMessage) + ctx.Error(fmt.Errorf("Unknown method '%s', it should be one of %s", bodyJSON.Method, strings.Join(authentication.PossibleMethods, ", ")), messageOperationFailed) return } @@ -124,7 +124,7 @@ func MethodPreferencePost(ctx *middlewares.AutheliaCtx) { err = ctx.Providers.StorageProvider.SavePreferred2FAMethod(userSession.Username, bodyJSON.Method) if err != nil { - ctx.Error(fmt.Errorf("Unable to save new preferred 2FA method: %s", err), operationFailedMessage) + ctx.Error(fmt.Errorf("Unable to save new preferred 2FA method: %s", err), messageOperationFailed) return } diff --git a/internal/handlers/handler_verify.go b/internal/handlers/handler_verify.go index 2fbbc926..07fea270 100644 --- a/internal/handlers/handler_verify.go +++ b/internal/handlers/handler_verify.go @@ -116,14 +116,14 @@ func verifyBasicAuth(header string, auth []byte, targetURL url.URL, ctx *middlew // setForwardedHeaders set the forwarded User, Groups, Name and Email headers. func setForwardedHeaders(headers *fasthttp.ResponseHeader, username, name string, groups, emails []string) { if username != "" { - headers.Set(remoteUserHeader, username) - headers.Set(remoteGroupsHeader, strings.Join(groups, ",")) - headers.Set(remoteNameHeader, name) + headers.Set(headerRemoteUser, username) + headers.Set(headerRemoteGroups, strings.Join(groups, ",")) + headers.Set(headerRemoteName, name) if emails != nil { - headers.Set(remoteEmailHeader, emails[0]) + headers.Set(headerRemoteEmail, emails[0]) } else { - headers.Set(remoteEmailHeader, "") + headers.Set(headerRemoteEmail, "") } } } @@ -193,8 +193,17 @@ func verifySessionCookie(ctx *middlewares.AutheliaCtx, targetURL *url.URL, userS } func handleUnauthorized(ctx *middlewares.AutheliaCtx, targetURL fmt.Stringer, isBasicAuth bool, username string, method []byte) { - friendlyUsername := "" - if username != "" { + var ( + statusCode int + redirectionURL string + friendlyUsername string + friendlyRequestMethod string + ) + + switch username { + case "": + friendlyUsername = "" + default: friendlyUsername = username } @@ -212,33 +221,39 @@ func handleUnauthorized(ctx *middlewares.AutheliaCtx, targetURL fmt.Stringer, is rd := string(ctx.QueryArgs().Peek("rd")) rm := string(method) - friendlyMethod := "unknown" - - if rm != "" { - friendlyMethod = rm + switch rm { + case "": + friendlyRequestMethod = "unknown" + default: + friendlyRequestMethod = rm } if rd != "" { - redirectionURL := "" - - if rm != "" { - redirectionURL = fmt.Sprintf("%s?rd=%s&rm=%s", rd, url.QueryEscape(targetURL.String()), rm) - } else { - redirectionURL = fmt.Sprintf("%s?rd=%s", rd, url.QueryEscape(targetURL.String())) - } - - ctx.Logger.Infof("Access to %s (method %s) is not authorized to user %s, redirecting to %s", targetURL.String(), friendlyMethod, friendlyUsername, redirectionURL) - switch rm { - case fasthttp.MethodGet, fasthttp.MethodHead, "": - ctx.Redirect(redirectionURL, 302) - ctx.SetBodyString(fmt.Sprintf("Found. Redirecting to %s", redirectionURL)) + case "": + redirectionURL = fmt.Sprintf("%s?rd=%s", rd, url.QueryEscape(targetURL.String())) default: - ctx.Redirect(redirectionURL, 303) - ctx.SetBodyString(fmt.Sprintf("See Other. Redirecting to %s", redirectionURL)) + redirectionURL = fmt.Sprintf("%s?rd=%s&rm=%s", rd, url.QueryEscape(targetURL.String()), rm) } + } + + switch { + case ctx.IsXHR() || !ctx.AcceptsMIME("text/html") || rd == "": + statusCode = fasthttp.StatusUnauthorized + default: + switch rm { + case fasthttp.MethodGet, fasthttp.MethodOptions, "": + statusCode = fasthttp.StatusFound + default: + statusCode = fasthttp.StatusSeeOther + } + } + + if redirectionURL != "" { + ctx.Logger.Infof("Access to %s (method %s) is not authorized to user %s, responding with status code %d with location redirect to %s", targetURL.String(), friendlyRequestMethod, friendlyUsername, statusCode, redirectionURL) + ctx.SpecialRedirect(redirectionURL, statusCode) } else { - ctx.Logger.Infof("Access to %s (method %s) is not authorized to user %s, sending 401 response", targetURL.String(), friendlyMethod, friendlyUsername) + ctx.Logger.Infof("Access to %s (method %s) is not authorized to user %s, responding with status code %d", targetURL.String(), friendlyRequestMethod, friendlyUsername, statusCode) ctx.ReplyUnauthorized() } } @@ -386,9 +401,9 @@ func getProfileRefreshSettings(cfg schema.AuthenticationBackendConfiguration) (r } func verifyAuth(ctx *middlewares.AutheliaCtx, targetURL *url.URL, refreshProfile bool, refreshProfileInterval time.Duration) (isBasicAuth bool, username, name string, groups, emails []string, authLevel authentication.Level, err error) { - authHeader := ProxyAuthorizationHeader + authHeader := HeaderProxyAuthorization if bytes.Equal(ctx.QueryArgs().Peek("auth"), []byte("basic")) { - authHeader = AuthorizationHeader + authHeader = HeaderAuthorization isBasicAuth = true } @@ -408,7 +423,7 @@ func verifyAuth(ctx *middlewares.AutheliaCtx, targetURL *url.URL, refreshProfile userSession := ctx.GetSession() username, name, groups, emails, authLevel, err = verifySessionCookie(ctx, targetURL, &userSession, refreshProfile, refreshProfileInterval) - sessionUsername := ctx.Request.Header.Peek(SessionUsernameHeader) + sessionUsername := ctx.Request.Header.Peek(HeaderSessionUsername) if sessionUsername != nil && !strings.EqualFold(string(sessionUsername), username) { ctx.Logger.Warnf("Possible cookie hijack or attempt to bypass security detected destroying the session and sending 401 response") @@ -417,10 +432,10 @@ func verifyAuth(ctx *middlewares.AutheliaCtx, targetURL *url.URL, refreshProfile ctx.Logger.Error( fmt.Errorf( "Unable to destroy user session after handler could not match them to their %s header: %s", - SessionUsernameHeader, err)) + HeaderSessionUsername, err)) } - err = fmt.Errorf("Could not match user %s to their %s header with a value of %s when visiting %s", username, SessionUsernameHeader, sessionUsername, targetURL.String()) + err = fmt.Errorf("Could not match user %s to their %s header with a value of %s when visiting %s", username, HeaderSessionUsername, sessionUsername, targetURL.String()) } return @@ -465,7 +480,7 @@ func VerifyGet(cfg schema.AuthenticationBackendConfiguration) middlewares.Reques ctx.Logger.Error(fmt.Sprintf("Error caught when verifying user authorization: %s", err)) if err := updateActivityTimestamp(ctx, isBasicAuth, username); err != nil { - ctx.Error(fmt.Errorf("Unable to update last activity: %s", err), operationFailedMessage) + ctx.Error(fmt.Errorf("Unable to update last activity: %s", err), messageOperationFailed) return } @@ -488,7 +503,7 @@ func VerifyGet(cfg schema.AuthenticationBackendConfiguration) middlewares.Reques } if err := updateActivityTimestamp(ctx, isBasicAuth, username); err != nil { - ctx.Error(fmt.Errorf("Unable to update last activity: %s", err), operationFailedMessage) + ctx.Error(fmt.Errorf("Unable to update last activity: %s", err), messageOperationFailed) } } } diff --git a/internal/handlers/handler_verify_test.go b/internal/handlers/handler_verify_test.go index 9cee1641..cc4b4f6d 100644 --- a/internal/handlers/handler_verify_test.go +++ b/internal/handlers/handler_verify_test.go @@ -85,20 +85,20 @@ func TestShouldRaiseWhenXForwardedURIIsNotParsable(t *testing.T) { // Test parseBasicAuth. func TestShouldRaiseWhenHeaderDoesNotContainBasicPrefix(t *testing.T) { - _, _, err := parseBasicAuth(ProxyAuthorizationHeader, "alzefzlfzemjfej==") + _, _, err := parseBasicAuth(HeaderProxyAuthorization, "alzefzlfzemjfej==") assert.Error(t, err) assert.Equal(t, "Basic prefix not found in Proxy-Authorization header", err.Error()) } func TestShouldRaiseWhenCredentialsAreNotInBase64(t *testing.T) { - _, _, err := parseBasicAuth(ProxyAuthorizationHeader, "Basic alzefzlfzemjfej==") + _, _, err := parseBasicAuth(HeaderProxyAuthorization, "Basic alzefzlfzemjfej==") assert.Error(t, err) assert.Equal(t, "illegal base64 data at input byte 16", err.Error()) } func TestShouldRaiseWhenCredentialsAreNotInCorrectForm(t *testing.T) { // The decoded format should be user:password. - _, _, err := parseBasicAuth(ProxyAuthorizationHeader, "Basic am9obiBwYXNzd29yZA==") + _, _, err := parseBasicAuth(HeaderProxyAuthorization, "Basic am9obiBwYXNzd29yZA==") assert.Error(t, err) assert.Equal(t, "Format of Proxy-Authorization header must be user:password", err.Error()) } @@ -112,7 +112,7 @@ func TestShouldUseProvidedHeaderName(t *testing.T) { func TestShouldReturnUsernameAndPassword(t *testing.T) { // the decoded format should be user:password. - user, password, err := parseBasicAuth(ProxyAuthorizationHeader, "Basic am9objpwYXNzd29yZA==") + user, password, err := parseBasicAuth(HeaderProxyAuthorization, "Basic am9objpwYXNzd29yZA==") assert.NoError(t, err) assert.Equal(t, "john", user) assert.Equal(t, "password", password) @@ -177,7 +177,7 @@ func TestShouldVerifyWrongCredentials(t *testing.T) { Return(false, nil) url, _ := url.ParseRequestURI("https://test.example.com") - _, _, _, _, _, err := verifyBasicAuth(ProxyAuthorizationHeader, []byte("Basic am9objpwYXNzd29yZA=="), *url, mock.Ctx) + _, _, _, _, _, err := verifyBasicAuth(HeaderProxyAuthorization, []byte("Basic am9objpwYXNzd29yZA=="), *url, mock.Ctx) assert.Error(t, err) } @@ -718,10 +718,10 @@ func TestShouldRedirectWhenSessionInactiveForTooLongAndRDParamProvided(t *testin mock.Ctx.QueryArgs().Add("rd", "https://login.example.com") mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com") mock.Ctx.Request.Header.Set("X-Forwarded-Method", "GET") - + mock.Ctx.Request.Header.Set("Accept", "text/html; charset=utf-8") VerifyGet(verifyGetCfg)(mock.Ctx) - assert.Equal(t, "Found. Redirecting to https://login.example.com?rd=https%3A%2F%2Ftwo-factor.example.com&rm=GET", + assert.Equal(t, "Found", string(mock.Ctx.Response.Body())) assert.Equal(t, 302, mock.Ctx.Response.StatusCode()) @@ -737,20 +737,22 @@ func TestShouldRedirectWithCorrectStatusCodeBasedOnRequestMethod(t *testing.T) { mock.Ctx.QueryArgs().Add("rd", "https://login.example.com") mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com") mock.Ctx.Request.Header.Set("X-Forwarded-Method", "GET") + mock.Ctx.Request.Header.Set("Accept", "text/html; charset=utf-8") VerifyGet(verifyGetCfg)(mock.Ctx) - assert.Equal(t, "Found. Redirecting to https://login.example.com?rd=https%3A%2F%2Ftwo-factor.example.com&rm=GET", + assert.Equal(t, "Found", string(mock.Ctx.Response.Body())) assert.Equal(t, 302, mock.Ctx.Response.StatusCode()) mock.Ctx.QueryArgs().Add("rd", "https://login.example.com") mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com") mock.Ctx.Request.Header.Set("X-Forwarded-Method", "POST") + mock.Ctx.Request.Header.Set("Accept", "text/html; charset=utf-8") VerifyGet(verifyGetCfg)(mock.Ctx) - assert.Equal(t, "See Other. Redirecting to https://login.example.com?rd=https%3A%2F%2Ftwo-factor.example.com&rm=POST", + assert.Equal(t, "See Other", string(mock.Ctx.Response.Body())) assert.Equal(t, 303, mock.Ctx.Response.StatusCode()) } @@ -801,12 +803,13 @@ func TestShouldURLEncodeRedirectionURLParameter(t *testing.T) { require.NoError(t, err) mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com") + mock.Ctx.Request.Header.Set("Accept", "text/html; charset=utf-8") mock.Ctx.Request.SetHost("mydomain.com") mock.Ctx.Request.SetRequestURI("/?rd=https://auth.mydomain.com") VerifyGet(verifyGetCfg)(mock.Ctx) - assert.Equal(t, "Found. Redirecting to https://auth.mydomain.com?rd=https%3A%2F%2Ftwo-factor.example.com", + assert.Equal(t, "Found", string(mock.Ctx.Response.Body())) } @@ -1209,7 +1212,7 @@ func TestShouldCheckValidSessionUsernameHeaderAndReturn200(t *testing.T) { require.NoError(t, err) mock.Ctx.Request.Header.Set("X-Original-URL", "https://one-factor.example.com") - mock.Ctx.Request.Header.Set(SessionUsernameHeader, testUsername) + mock.Ctx.Request.Header.Set(HeaderSessionUsername, testUsername) VerifyGet(verifyGetCfg)(mock.Ctx) assert.Equal(t, expectedStatusCode, mock.Ctx.Response.StatusCode()) @@ -1233,7 +1236,7 @@ func TestShouldCheckInvalidSessionUsernameHeaderAndReturn401(t *testing.T) { require.NoError(t, err) mock.Ctx.Request.Header.Set("X-Original-URL", "https://one-factor.example.com") - mock.Ctx.Request.Header.Set(SessionUsernameHeader, "root") + mock.Ctx.Request.Header.Set(HeaderSessionUsername, "root") VerifyGet(verifyGetCfg)(mock.Ctx) assert.Equal(t, expectedStatusCode, mock.Ctx.Response.StatusCode()) diff --git a/internal/handlers/oidc_register.go b/internal/handlers/oidc_register.go index a3811dfa..bb1a51fe 100644 --- a/internal/handlers/oidc_register.go +++ b/internal/handlers/oidc_register.go @@ -11,22 +11,22 @@ func RegisterOIDC(router *router.Router, middleware middlewares.RequestHandlerBr // TODO: Add OPTIONS handler. router.GET("/.well-known/openid-configuration", middleware(oidcWellKnown)) - router.GET(oidcConsentPath, middleware(oidcConsent)) + router.GET(pathOpenIDConnectConsent, middleware(oidcConsent)) - router.POST(oidcConsentPath, middleware(oidcConsentPOST)) + router.POST(pathOpenIDConnectConsent, middleware(oidcConsentPOST)) - router.GET(oidcJWKsPath, middleware(oidcJWKs)) + router.GET(pathOpenIDConnectJWKs, middleware(oidcJWKs)) - router.GET(oidcAuthorizePath, middleware(middlewares.NewHTTPToAutheliaHandlerAdaptor(oidcAuthorize))) + router.GET(pathOpenIDConnectAuthorization, middleware(middlewares.NewHTTPToAutheliaHandlerAdaptor(oidcAuthorize))) // TODO: Add OPTIONS handler. - router.POST(oidcTokenPath, middleware(middlewares.NewHTTPToAutheliaHandlerAdaptor(oidcToken))) + router.POST(pathOpenIDConnectToken, middleware(middlewares.NewHTTPToAutheliaHandlerAdaptor(oidcToken))) - router.POST(oidcIntrospectPath, middleware(middlewares.NewHTTPToAutheliaHandlerAdaptor(oidcIntrospect))) + router.POST(pathOpenIDConnectIntrospection, middleware(middlewares.NewHTTPToAutheliaHandlerAdaptor(oidcIntrospect))) - router.GET(oidcUserinfoPath, middleware(middlewares.NewHTTPToAutheliaHandlerAdaptor(oidcUserinfo))) - router.POST(oidcUserinfoPath, middleware(middlewares.NewHTTPToAutheliaHandlerAdaptor(oidcUserinfo))) + router.GET(pathOpenIDConnectUserinfo, middleware(middlewares.NewHTTPToAutheliaHandlerAdaptor(oidcUserinfo))) + router.POST(pathOpenIDConnectUserinfo, middleware(middlewares.NewHTTPToAutheliaHandlerAdaptor(oidcUserinfo))) // TODO: Add OPTIONS handler. - router.POST(oidcRevokePath, middleware(middlewares.NewHTTPToAutheliaHandlerAdaptor(oidcRevoke))) + router.POST(pathOpenIDConnectRevocation, middleware(middlewares.NewHTTPToAutheliaHandlerAdaptor(oidcRevoke))) } diff --git a/internal/handlers/response.go b/internal/handlers/response.go index 5463f715..d2d4072f 100644 --- a/internal/handlers/response.go +++ b/internal/handlers/response.go @@ -25,7 +25,7 @@ func handleOIDCWorkflowResponse(ctx *middlewares.AutheliaCtx) { uri, err := ctx.ForwardedProtoHost() if err != nil { ctx.Logger.Errorf("%v", err) - handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to get forward facing URI"), authenticationFailedMessage) + handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to get forward facing URI"), messageAuthenticationFailed) return } @@ -64,7 +64,7 @@ func Handle1FAResponse(ctx *middlewares.AutheliaCtx, targetURI, requestMethod st targetURL, err := url.ParseRequestURI(targetURI) if err != nil { - ctx.Error(fmt.Errorf("Unable to parse target URL %s: %s", targetURI, err), authenticationFailedMessage) + ctx.Error(fmt.Errorf("Unable to parse target URL %s: %s", targetURI, err), messageAuthenticationFailed) return } @@ -128,7 +128,7 @@ func Handle2FAResponse(ctx *middlewares.AutheliaCtx, targetURI string) { targetURL, err := url.ParseRequestURI(targetURI) if err != nil { - ctx.Error(fmt.Errorf("Unable to parse target URL: %s", err), mfaValidationFailedMessage) + ctx.Error(fmt.Errorf("Unable to parse target URL: %s", err), messageMFAValidationFailed) return } diff --git a/internal/middlewares/authelia_context.go b/internal/middlewares/authelia_context.go index ec75e60f..e638886b 100644 --- a/internal/middlewares/authelia_context.go +++ b/internal/middlewares/authelia_context.go @@ -43,7 +43,7 @@ func AutheliaMiddleware(configuration schema.Configuration, providers Providers) return func(ctx *fasthttp.RequestCtx) { autheliaCtx, err := NewAutheliaCtx(ctx, configuration, providers) if err != nil { - autheliaCtx.Error(err, operationFailedMessage) + autheliaCtx.Error(err, messageOperationFailed) return } @@ -60,7 +60,7 @@ func (c *AutheliaCtx) Error(err error, message string) { c.Logger.Error(marshalErr) } - c.SetContentType("application/json") + c.SetContentType(contentTypeApplicationJSON) c.SetBody(b) c.Logger.Error(err) } @@ -73,7 +73,7 @@ func (c *AutheliaCtx) ReplyError(err error, message string) { c.Logger.Error(marshalErr) } - c.SetContentType("application/json") + c.SetContentType(contentTypeApplicationJSON) c.SetBody(b) c.Logger.Debug(err) } @@ -95,22 +95,22 @@ func (c *AutheliaCtx) ReplyBadRequest() { // XForwardedProto return the content of the X-Forwarded-Proto header. func (c *AutheliaCtx) XForwardedProto() []byte { - return c.RequestCtx.Request.Header.Peek(xForwardedProtoHeader) + return c.RequestCtx.Request.Header.Peek(headerXForwardedProto) } // XForwardedMethod return the content of the X-Forwarded-Method header. func (c *AutheliaCtx) XForwardedMethod() []byte { - return c.RequestCtx.Request.Header.Peek(xForwardedMethodHeader) + return c.RequestCtx.Request.Header.Peek(headerXForwardedMethod) } // XForwardedHost return the content of the X-Forwarded-Host header. func (c *AutheliaCtx) XForwardedHost() []byte { - return c.RequestCtx.Request.Header.Peek(xForwardedHostHeader) + return c.RequestCtx.Request.Header.Peek(headerXForwardedHost) } // XForwardedURI return the content of the X-Forwarded-URI header. func (c *AutheliaCtx) XForwardedURI() []byte { - return c.RequestCtx.Request.Header.Peek(xForwardedURIHeader) + return c.RequestCtx.Request.Header.Peek(headerXForwardedURI) } // ForwardedProtoHost gets the X-Forwarded-Proto and X-Forwarded-Host headers and forms them into a URL. @@ -133,7 +133,7 @@ func (c AutheliaCtx) ForwardedProtoHost() (string, error) { // XOriginalURL return the content of the X-Original-URL header. func (c *AutheliaCtx) XOriginalURL() []byte { - return c.RequestCtx.Request.Header.Peek(xOriginalURLHeader) + return c.RequestCtx.Request.Header.Peek(headerXOriginalURL) } // GetSession return the user session. Any update will be saved in cache. @@ -154,7 +154,7 @@ func (c *AutheliaCtx) SaveSession(userSession session.UserSession) error { // ReplyOK is a helper method to reply ok. func (c *AutheliaCtx) ReplyOK() { - c.SetContentType(applicationJSONContentType) + c.SetContentType(contentTypeApplicationJSON) c.SetBody(okMessageBytes) } @@ -186,7 +186,7 @@ func (c *AutheliaCtx) SetJSONBody(value interface{}) error { return fmt.Errorf("Unable to marshal JSON body") } - c.SetContentType("application/json") + c.SetContentType(contentTypeApplicationJSON) c.SetBody(b) return nil @@ -249,3 +249,46 @@ func (c *AutheliaCtx) GetOriginalURL() (*url.URL, error) { return parsedURL, nil } + +// IsXHR returns true if the request is a XMLHttpRequest. +func (c AutheliaCtx) IsXHR() (xhr bool) { + requestedWith := c.Request.Header.Peek(headerXRequestedWith) + + return requestedWith != nil && string(requestedWith) == headerValueXRequestedWithXHR +} + +// AcceptsMIME takes a mime type and returns true if the request accepts that type or the wildcard type. +func (c AutheliaCtx) AcceptsMIME(mime string) (acceptsMime bool) { + accepts := strings.Split(string(c.Request.Header.Peek("Accept")), ",") + + for i, accept := range accepts { + mimeType := strings.Trim(strings.SplitN(accept, ";", 2)[0], " ") + if mimeType == mime || (i == 0 && mimeType == "*/*") { + return true + } + } + + return false +} + +// SpecialRedirect performs a redirect similar to fasthttp.RequestCtx except it allows statusCode 401 and includes body +// content in the form of a link to the location. +func (c *AutheliaCtx) SpecialRedirect(uri string, statusCode int) { + if statusCode < fasthttp.StatusMovedPermanently || (statusCode > fasthttp.StatusSeeOther && statusCode != fasthttp.StatusTemporaryRedirect && statusCode != fasthttp.StatusPermanentRedirect && statusCode != fasthttp.StatusUnauthorized) { + statusCode = fasthttp.StatusFound + } + + c.SetContentType(contentTypeTextHTML) + c.SetStatusCode(statusCode) + + u := fasthttp.AcquireURI() + + c.URI().CopyTo(u) + u.Update(uri) + + c.Response.Header.SetBytesV("Location", u.FullURI()) + + c.SetBodyString(fmt.Sprintf("%s", utils.StringHTMLEscape(string(u.FullURI())), fasthttp.StatusMessage(statusCode))) + + fasthttp.ReleaseURI(u) +} diff --git a/internal/middlewares/const.go b/internal/middlewares/const.go index 858c9b70..1d404a70 100644 --- a/internal/middlewares/const.go +++ b/internal/middlewares/const.go @@ -2,19 +2,30 @@ package middlewares const jwtIssuer = "Authelia" -const xForwardedProtoHeader = "X-Forwarded-Proto" -const xForwardedMethodHeader = "X-Forwarded-Method" -const xForwardedHostHeader = "X-Forwarded-Host" -const xForwardedURIHeader = "X-Forwarded-URI" +const ( + headerXForwardedProto = "X-Forwarded-Proto" + headerXForwardedMethod = "X-Forwarded-Method" + headerXForwardedHost = "X-Forwarded-Host" + headerXForwardedURI = "X-Forwarded-URI" + headerXOriginalURL = "X-Original-URL" + headerXRequestedWith = "X-Requested-With" +) -const xOriginalURLHeader = "X-Original-URL" +const ( + headerValueXRequestedWithXHR = "XMLHttpRequest" +) -const applicationJSONContentType = "application/json" +const ( + contentTypeApplicationJSON = "application/json" + contentTypeTextHTML = "text/html" +) var okMessageBytes = []byte("{\"status\":\"OK\"}") -const operationFailedMessage = "Operation failed" -const identityVerificationTokenAlreadyUsedMessage = "The identity verification token has already been used" -const identityVerificationTokenHasExpiredMessage = "The identity verification token has expired" +const ( + messageOperationFailed = "Operation failed" + messageIdentityVerificationTokenAlreadyUsed = "The identity verification token has already been used" + messageIdentityVerificationTokenHasExpired = "The identity verification token has expired" +) var protoHostSeparator = []byte("://") diff --git a/internal/middlewares/identity_verification.go b/internal/middlewares/identity_verification.go index aaafa636..b40ae5e9 100644 --- a/internal/middlewares/identity_verification.go +++ b/internal/middlewares/identity_verification.go @@ -41,19 +41,19 @@ func IdentityVerificationStart(args IdentityVerificationStartArgs) RequestHandle ss, err := token.SignedString([]byte(ctx.Configuration.JWTSecret)) if err != nil { - ctx.Error(err, operationFailedMessage) + ctx.Error(err, messageOperationFailed) return } err = ctx.Providers.StorageProvider.SaveIdentityVerificationToken(ss) if err != nil { - ctx.Error(err, operationFailedMessage) + ctx.Error(err, messageOperationFailed) return } uri, err := ctx.ForwardedProtoHost() if err != nil { - ctx.Error(err, operationFailedMessage) + ctx.Error(err, messageOperationFailed) return } @@ -76,7 +76,7 @@ func IdentityVerificationStart(args IdentityVerificationStartArgs) RequestHandle err = templates.HTMLEmailTemplate.Execute(bufHTML, htmlParams) if err != nil { - ctx.Error(err, operationFailedMessage) + ctx.Error(err, messageOperationFailed) return } } @@ -89,7 +89,7 @@ func IdentityVerificationStart(args IdentityVerificationStartArgs) RequestHandle err = templates.PlainTextEmailTemplate.Execute(bufText, textParams) if err != nil { - ctx.Error(err, operationFailedMessage) + ctx.Error(err, messageOperationFailed) return } @@ -99,7 +99,7 @@ func IdentityVerificationStart(args IdentityVerificationStartArgs) RequestHandle err = ctx.Providers.Notifier.Send(identity.Email, args.MailTitle, bufText.String(), bufHTML.String()) if err != nil { - ctx.Error(err, operationFailedMessage) + ctx.Error(err, messageOperationFailed) return } @@ -117,25 +117,25 @@ func IdentityVerificationFinish(args IdentityVerificationFinishArgs, next func(c err := json.Unmarshal(b, &finishBody) if err != nil { - ctx.Error(err, operationFailedMessage) + ctx.Error(err, messageOperationFailed) return } if finishBody.Token == "" { - ctx.Error(fmt.Errorf("No token provided"), operationFailedMessage) + ctx.Error(fmt.Errorf("No token provided"), messageOperationFailed) return } found, err := ctx.Providers.StorageProvider.FindIdentityVerificationToken(finishBody.Token) if err != nil { - ctx.Error(err, operationFailedMessage) + ctx.Error(err, messageOperationFailed) return } if !found { ctx.Error(fmt.Errorf("Token is not in DB, it might have already been used"), - identityVerificationTokenAlreadyUsedMessage) + messageIdentityVerificationTokenAlreadyUsed) return } @@ -148,44 +148,44 @@ func IdentityVerificationFinish(args IdentityVerificationFinishArgs, next func(c if ve, ok := err.(*jwt.ValidationError); ok { switch { case ve.Errors&jwt.ValidationErrorMalformed != 0: - ctx.Error(fmt.Errorf("Cannot parse token"), operationFailedMessage) + ctx.Error(fmt.Errorf("Cannot parse token"), messageOperationFailed) return case ve.Errors&(jwt.ValidationErrorExpired|jwt.ValidationErrorNotValidYet) != 0: // Token is either expired or not active yet - ctx.Error(fmt.Errorf("Token expired"), identityVerificationTokenHasExpiredMessage) + ctx.Error(fmt.Errorf("Token expired"), messageIdentityVerificationTokenHasExpired) return default: - ctx.Error(fmt.Errorf("Cannot handle this token: %s", ve), operationFailedMessage) + ctx.Error(fmt.Errorf("Cannot handle this token: %s", ve), messageOperationFailed) return } } - ctx.Error(err, operationFailedMessage) + ctx.Error(err, messageOperationFailed) return } claims, ok := token.Claims.(*IdentityVerificationClaim) if !ok { - ctx.Error(fmt.Errorf("Wrong type of claims (%T != *middlewares.IdentityVerificationClaim)", claims), operationFailedMessage) + ctx.Error(fmt.Errorf("Wrong type of claims (%T != *middlewares.IdentityVerificationClaim)", claims), messageOperationFailed) return } // Verify that the action claim in the token is the one expected for the given endpoint. if claims.Action != args.ActionClaim { - ctx.Error(fmt.Errorf("This token has not been generated for this kind of action"), operationFailedMessage) + ctx.Error(fmt.Errorf("This token has not been generated for this kind of action"), messageOperationFailed) return } if args.IsTokenUserValidFunc != nil && !args.IsTokenUserValidFunc(ctx, claims.Username) { - ctx.Error(fmt.Errorf("This token has not been generated for this user"), operationFailedMessage) + ctx.Error(fmt.Errorf("This token has not been generated for this user"), messageOperationFailed) return } // TODO(c.michaud): find a way to garbage collect unused tokens. err = ctx.Providers.StorageProvider.RemoveIdentityVerificationToken(finishBody.Token) if err != nil { - ctx.Error(err, operationFailedMessage) + ctx.Error(err, messageOperationFailed) return } diff --git a/internal/server/options_handler.go b/internal/server/options_handler.go new file mode 100644 index 00000000..3bc04d5b --- /dev/null +++ b/internal/server/options_handler.go @@ -0,0 +1,11 @@ +package server + +import ( + "github.com/valyala/fasthttp" + + "github.com/authelia/authelia/internal/middlewares" +) + +func handleOPTIONS(ctx *middlewares.AutheliaCtx) { + ctx.SetStatusCode(fasthttp.StatusNoContent) +} diff --git a/internal/server/server.go b/internal/server/server.go index 479aa1e4..a602c78c 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -43,6 +43,8 @@ func registerRoutes(configuration schema.Configuration, providers middlewares.Pr r := router.New() r.GET("/", serveIndexHandler) + r.OPTIONS("/", autheliaMiddleware(handleOPTIONS)) + r.GET("/api/", serveSwaggerHandler) r.GET("/api/"+apiFile, serveSwaggerAPIHandler) diff --git a/internal/suites/suite_standalone_test.go b/internal/suites/suite_standalone_test.go index 7fc1f876..08e3807b 100644 --- a/internal/suites/suite_standalone_test.go +++ b/internal/suites/suite_standalone_test.go @@ -14,6 +14,7 @@ import ( "github.com/stretchr/testify/suite" "github.com/authelia/authelia/internal/storage" + "github.com/authelia/authelia/internal/utils" ) type StandaloneWebDriverSuite struct { @@ -110,6 +111,7 @@ func (s *StandaloneSuite) TestShouldRespectMethodsACL() { req.Header.Set("X-Forwarded-Proto", "https") req.Header.Set("X-Forwarded-Host", fmt.Sprintf("secure.%s", BaseDomain)) req.Header.Set("X-Forwarded-URI", "/") + req.Header.Set("Accept", "text/html; charset=utf8") client := NewHTTPClient() res, err := client.Do(req) @@ -119,7 +121,7 @@ func (s *StandaloneSuite) TestShouldRespectMethodsACL() { s.Assert().NoError(err) urlEncodedAdminURL := url.QueryEscape(SecureBaseURL + "/") - s.Assert().Equal(fmt.Sprintf("Found. Redirecting to %s?rd=%s&rm=GET", GetLoginBaseURL(), urlEncodedAdminURL), string(body)) + s.Assert().Equal(fmt.Sprintf("Found", utils.StringHTMLEscape(fmt.Sprintf("%s/?rd=%s&rm=GET", GetLoginBaseURL(), urlEncodedAdminURL))), string(body)) req.Header.Set("X-Forwarded-Method", "OPTIONS") @@ -135,6 +137,7 @@ func (s *StandaloneSuite) TestShouldRespondWithCorrectStatusCode() { req.Header.Set("X-Forwarded-Proto", "https") req.Header.Set("X-Forwarded-Host", fmt.Sprintf("secure.%s", BaseDomain)) req.Header.Set("X-Forwarded-URI", "/") + req.Header.Set("Accept", "text/html; charset=utf8") client := NewHTTPClient() res, err := client.Do(req) @@ -144,7 +147,7 @@ func (s *StandaloneSuite) TestShouldRespondWithCorrectStatusCode() { s.Assert().NoError(err) urlEncodedAdminURL := url.QueryEscape(SecureBaseURL + "/") - s.Assert().Equal(fmt.Sprintf("Found. Redirecting to %s?rd=%s&rm=GET", GetLoginBaseURL(), urlEncodedAdminURL), string(body)) + s.Assert().Equal(fmt.Sprintf("Found", utils.StringHTMLEscape(fmt.Sprintf("%s/?rd=%s&rm=GET", GetLoginBaseURL(), urlEncodedAdminURL))), string(body)) req.Header.Set("X-Forwarded-Method", "POST") @@ -155,15 +158,16 @@ func (s *StandaloneSuite) TestShouldRespondWithCorrectStatusCode() { s.Assert().NoError(err) urlEncodedAdminURL = url.QueryEscape(SecureBaseURL + "/") - s.Assert().Equal(fmt.Sprintf("See Other. Redirecting to %s?rd=%s&rm=POST", GetLoginBaseURL(), urlEncodedAdminURL), string(body)) + s.Assert().Equal(fmt.Sprintf("See Other", utils.StringHTMLEscape(fmt.Sprintf("%s/?rd=%s&rm=POST", GetLoginBaseURL(), urlEncodedAdminURL))), string(body)) } // Standard case using nginx. -func (s *StandaloneSuite) TestShouldVerifyAPIVerifyUnauthorize() { +func (s *StandaloneSuite) TestShouldVerifyAPIVerifyUnauthorized() { req, err := http.NewRequest("GET", fmt.Sprintf("%s/api/verify", AutheliaBaseURL), nil) s.Assert().NoError(err) req.Header.Set("X-Forwarded-Proto", "https") req.Header.Set("X-Original-URL", AdminBaseURL) + req.Header.Set("Accept", "text/html; charset=utf8") client := NewHTTPClient() res, err := client.Do(req) @@ -171,7 +175,7 @@ func (s *StandaloneSuite) TestShouldVerifyAPIVerifyUnauthorize() { s.Assert().Equal(res.StatusCode, 401) body, err := ioutil.ReadAll(res.Body) s.Assert().NoError(err) - s.Assert().Equal(string(body), "Unauthorized") + s.Assert().Equal("Unauthorized", string(body)) } // Standard case using Kubernetes. @@ -180,6 +184,7 @@ func (s *StandaloneSuite) TestShouldVerifyAPIVerifyRedirectFromXOriginalURL() { s.Assert().NoError(err) req.Header.Set("X-Forwarded-Proto", "https") req.Header.Set("X-Original-URL", AdminBaseURL) + req.Header.Set("Accept", "text/html; charset=utf8") client := NewHTTPClient() res, err := client.Do(req) @@ -189,7 +194,7 @@ func (s *StandaloneSuite) TestShouldVerifyAPIVerifyRedirectFromXOriginalURL() { s.Assert().NoError(err) urlEncodedAdminURL := url.QueryEscape(AdminBaseURL) - s.Assert().Equal(fmt.Sprintf("Found. Redirecting to %s?rd=%s", GetLoginBaseURL(), urlEncodedAdminURL), string(body)) + s.Assert().Equal(fmt.Sprintf("Found", utils.StringHTMLEscape(fmt.Sprintf("%s/?rd=%s", GetLoginBaseURL(), urlEncodedAdminURL))), string(body)) } func (s *StandaloneSuite) TestShouldVerifyAPIVerifyRedirectFromXOriginalHostURI() { @@ -198,6 +203,7 @@ func (s *StandaloneSuite) TestShouldVerifyAPIVerifyRedirectFromXOriginalHostURI( req.Header.Set("X-Forwarded-Proto", "https") req.Header.Set("X-Forwarded-Host", "secure.example.com:8080") req.Header.Set("X-Forwarded-URI", "/") + req.Header.Set("Accept", "text/html; charset=utf8") client := NewHTTPClient() res, err := client.Do(req) @@ -207,7 +213,7 @@ func (s *StandaloneSuite) TestShouldVerifyAPIVerifyRedirectFromXOriginalHostURI( s.Assert().NoError(err) urlEncodedAdminURL := url.QueryEscape(SecureBaseURL + "/") - s.Assert().Equal(fmt.Sprintf("Found. Redirecting to %s?rd=%s", GetLoginBaseURL(), urlEncodedAdminURL), string(body)) + s.Assert().Equal(fmt.Sprintf("Found", utils.StringHTMLEscape(fmt.Sprintf("%s/?rd=%s", GetLoginBaseURL(), urlEncodedAdminURL))), string(body)) } func (s *StandaloneSuite) TestStandaloneWebDriverScenario() { diff --git a/internal/utils/const.go b/internal/utils/const.go index 5190484f..bf4c2fe7 100644 --- a/internal/utils/const.go +++ b/internal/utils/const.go @@ -3,6 +3,7 @@ package utils import ( "errors" "regexp" + "strings" "time" ) @@ -54,3 +55,11 @@ var AlphaNumericCharacters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQ // ErrTLSVersionNotSupported returned when an unknown TLS version supplied. var ErrTLSVersionNotSupported = errors.New("supplied TLS version isn't supported") + +var htmlEscaper = strings.NewReplacer( + "&", "&", + "<", "<", + ">", ">", + `"`, """, + "'", "'", +) diff --git a/internal/utils/strings.go b/internal/utils/strings.go index 781b0e5f..ae2835aa 100644 --- a/internal/utils/strings.go +++ b/internal/utils/strings.go @@ -139,3 +139,8 @@ func RandomString(n int, characters []rune) (randomString string) { return string(b) } + +// StringHTMLEscape escapes chars for a HTML body. +func StringHTMLEscape(input string) (output string) { + return htmlEscaper.Replace(input) +}