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.
This commit is contained in:
James Elliott 2021-07-22 13:52:37 +10:00 committed by GitHub
parent 7a4779b08e
commit 911d71204f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 343 additions and 227 deletions

View File

@ -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",
}

View File

@ -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
}

View File

@ -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))
}
}

View File

@ -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)
}
}

View File

@ -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"},

View File

@ -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)

View File

@ -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)

View File

@ -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))

View File

@ -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
}

View File

@ -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)

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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 := "<anonymous>"
if username != "" {
var (
statusCode int
redirectionURL string
friendlyUsername string
friendlyRequestMethod string
)
switch username {
case "":
friendlyUsername = "<anonymous>"
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)
}
}
}

View File

@ -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, "<a href=\"https://login.example.com/?rd=https%3A%2F%2Ftwo-factor.example.com&amp;rm=GET\">Found</a>",
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, "<a href=\"https://login.example.com/?rd=https%3A%2F%2Ftwo-factor.example.com&amp;rm=GET\">Found</a>",
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, "<a href=\"https://login.example.com/?rd=https%3A%2F%2Ftwo-factor.example.com&amp;rm=POST\">See Other</a>",
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, "<a href=\"https://auth.mydomain.com/?rd=https%3A%2F%2Ftwo-factor.example.com\">Found</a>",
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())

View File

@ -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)))
}

View File

@ -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
}

View File

@ -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("<a href=\"%s\">%s</a>", utils.StringHTMLEscape(string(u.FullURI())), fasthttp.StatusMessage(statusCode)))
fasthttp.ReleaseURI(u)
}

View File

@ -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("://")

View File

@ -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
}

View File

@ -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)
}

View File

@ -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)

View File

@ -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("<a href=\"%s\">Found</a>", 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("<a href=\"%s\">Found</a>", 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("<a href=\"%s\">See Other</a>", 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("<a href=\"%s\">Found</a>", 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("<a href=\"%s\">Found</a>", utils.StringHTMLEscape(fmt.Sprintf("%s/?rd=%s", GetLoginBaseURL(), urlEncodedAdminURL))), string(body))
}
func (s *StandaloneSuite) TestStandaloneWebDriverScenario() {

View File

@ -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(
"&", "&amp;",
"<", "&lt;",
">", "&gt;",
`"`, "&#34;",
"'", "&#39;",
)

View File

@ -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)
}