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 package handlers
// TOTPRegistrationAction is the string representation of the action for which the token has been produced. const (
const TOTPRegistrationAction = "RegisterTOTPDevice" // 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. // ActionU2FRegistration is the string representation of the action for which the token has been produced.
const U2FRegistrationAction = "RegisterU2FDevice" ActionU2FRegistration = "RegisterU2FDevice"
// ResetPasswordAction is the string representation of the action for which the token has been produced. // ActionResetPassword is the string representation of the action for which the token has been produced.
const ResetPasswordAction = "ResetPassword" 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. // HeaderAuthorization is the basic-auth HTTP header Authelia utilises with "auth=basic" query param.
const ProxyAuthorizationHeader = "Proxy-Authorization" HeaderAuthorization = "Authorization"
// AuthorizationHeader is the basic-auth HTTP header Authelia utilises with "auth=basic" query param. // HeaderSessionUsername is used as additional protection to validate a user for things like pam_exec.
const AuthorizationHeader = "Authorization" HeaderSessionUsername = "Session-Username"
// SessionUsernameHeader is used as additional protection to validate a user for things like pam_exec. headerRemoteUser = "Remote-User"
const SessionUsernameHeader = "Session-Username" headerRemoteName = "Remote-Name"
headerRemoteEmail = "Remote-Email"
const remoteUserHeader = "Remote-User" headerRemoteGroups = "Remote-Groups"
const remoteNameHeader = "Remote-Name" )
const remoteEmailHeader = "Remote-Email"
const remoteGroupsHeader = "Remote-Groups"
const ( const (
// Forbidden means the user is forbidden the access to a resource. // Forbidden means the user is forbidden the access to a resource.
@ -34,47 +36,56 @@ const (
Authorized authorizationMatching = iota Authorized authorizationMatching = iota
) )
const operationFailedMessage = "Operation failed." const (
const authenticationFailedMessage = "Authentication failed. Check your credentials." messageOperationFailed = "Operation failed."
const userBannedMessage = "Please retry in a few minutes." messageAuthenticationFailed = "Authentication failed. Check your credentials."
const unableToRegisterOneTimePasswordMessage = "Unable to set up one-time passwords." //nolint:gosec messageUserBanned = "Please retry in a few minutes."
const unableToRegisterSecurityKeyMessage = "Unable to register your security key." messageUnableToRegisterOneTimePassword = "Unable to set up one-time passwords." //nolint:gosec
const unableToResetPasswordMessage = "Unable to reset your password." messageUnableToRegisterSecurityKey = "Unable to register your security key."
const mfaValidationFailedMessage = "Authentication failed, please retry later." 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{ const (
"0000052D", "SynoNumber", "SynoMixedCase", "SynoExcludeNameDesc", "SynoSpecialChar", loginDelayMovingAverageWindow = 10
} loginDelayMinimumDelayMilliseconds = float64(250)
var ldapPasswordComplexityErrors = []string{ loginDelayMaximumRandomDelayMilliseconds = int64(85)
"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)
// OIDC constants. // OIDC constants.
const ( const (
oidcJWKsPath = "/api/oidc/jwks" pathOpenIDConnectJWKs = "/api/oidc/jwks"
oidcAuthorizePath = "/api/oidc/authorize" pathOpenIDConnectAuthorization = "/api/oidc/authorize"
oidcTokenPath = "/api/oidc/token" //nolint:gosec // This is not a hard coded credential, it's a path. pathOpenIDConnectToken = "/api/oidc/token" //nolint:gosec // This is not a hard coded credential, it's a path.
oidcIntrospectPath = "/api/oidc/introspect" pathOpenIDConnectIntrospection = "/api/oidc/introspect"
oidcRevokePath = "/api/oidc/revoke" pathOpenIDConnectRevocation = "/api/oidc/revoke"
oidcUserinfoPath = "/api/oidc/userinfo" pathOpenIDConnectUserinfo = "/api/oidc/userinfo"
// Note: If you change this const you must also do so in the frontend at web/src/services/Api.ts. // 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 ( const (
accept = "accept" accept = "accept"
reject = "reject" 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() mutex.Lock()
if successful { if successful {
(*execDurationMovingAverage)[*movingAverageCursor] = value (*execDurationMovingAverage)[*movingAverageCursor] = value
*movingAverageCursor = (*movingAverageCursor + 1) % movingAverageWindow *movingAverageCursor = (*movingAverageCursor + 1) % loginDelayMovingAverageWindow
} }
var sum int64 var sum int64
@ -26,12 +26,12 @@ func movingAverageIteration(value time.Duration, successful bool, movingAverageC
} }
mutex.Unlock() mutex.Unlock()
return float64(sum / movingAverageWindow) return float64(sum / loginDelayMovingAverageWindow)
} }
func calculateActualDelay(ctx *middlewares.AutheliaCtx, execDuration time.Duration, avgExecDurationMs float64, successful *bool) float64 { 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. randomDelayMs := float64(rand.Int63n(loginDelayMaximumRandomDelayMilliseconds)) //nolint:gosec // TODO: Consider use of crypto/rand, this should be benchmarked and measured first.
totalDelayMs := math.Max(avgExecDurationMs, msMinimumDelay1FA) + randomDelayMs totalDelayMs := math.Max(avgExecDurationMs, loginDelayMinimumDelayMilliseconds) + randomDelayMs
actualDelayMs := math.Max(totalDelayMs-float64(execDuration.Milliseconds()), 1.0) 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)) 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. // FirstFactorPost is the handler performing the first factory.
//nolint:gocyclo // TODO: Consider refactoring time permitting. //nolint:gocyclo // TODO: Consider refactoring time permitting.
func FirstFactorPost(msInitialDelay time.Duration, delayEnabled bool) middlewares.RequestHandler { func FirstFactorPost(msInitialDelay time.Duration, delayEnabled bool) middlewares.RequestHandler {
var execDurationMovingAverage = make([]time.Duration, movingAverageWindow) var execDurationMovingAverage = make([]time.Duration, loginDelayMovingAverageWindow)
var movingAverageCursor = 0 var movingAverageCursor = 0
@ -73,7 +73,7 @@ func FirstFactorPost(msInitialDelay time.Duration, delayEnabled bool) middleware
err := ctx.ParseBody(&bodyJSON) err := ctx.ParseBody(&bodyJSON)
if err != nil { if err != nil {
handleAuthenticationUnauthorized(ctx, err, authenticationFailedMessage) handleAuthenticationUnauthorized(ctx, err, messageAuthenticationFailed)
return return
} }
@ -81,11 +81,11 @@ func FirstFactorPost(msInitialDelay time.Duration, delayEnabled bool) middleware
if err != nil { if err != nil {
if err == regulation.ErrUserIsBanned { 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 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 return
} }
@ -99,7 +99,7 @@ func FirstFactorPost(msInitialDelay time.Duration, delayEnabled bool) middleware
ctx.Logger.Errorf("Unable to mark authentication: %s", err.Error()) 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 return
} }
@ -111,7 +111,7 @@ func FirstFactorPost(msInitialDelay time.Duration, delayEnabled bool) middleware
ctx.Logger.Errorf("Unable to mark authentication: %s", err.Error()) 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 return
} }
@ -120,7 +120,7 @@ func FirstFactorPost(msInitialDelay time.Duration, delayEnabled bool) middleware
err = ctx.Providers.Regulator.Mark(bodyJSON.Username, true) err = ctx.Providers.Regulator.Mark(bodyJSON.Username, true)
if err != nil { 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 return
} }
@ -134,14 +134,14 @@ func FirstFactorPost(msInitialDelay time.Duration, delayEnabled bool) middleware
err = ctx.SaveSession(newSession) err = ctx.SaveSession(newSession)
if err != nil { 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 return
} }
err = ctx.Providers.SessionProvider.RegenerateSession(ctx.RequestCtx) err = ctx.Providers.SessionProvider.RegenerateSession(ctx.RequestCtx)
if err != nil { 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 return
} }
@ -152,7 +152,7 @@ func FirstFactorPost(msInitialDelay time.Duration, delayEnabled bool) middleware
if keepMeLoggedIn { if keepMeLoggedIn {
err = ctx.Providers.SessionProvider.UpdateExpiration(ctx.RequestCtx, ctx.Providers.SessionProvider.RememberMe) err = ctx.Providers.SessionProvider.UpdateExpiration(ctx.RequestCtx, ctx.Providers.SessionProvider.RememberMe)
if err != nil { 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 return
} }
} }
@ -161,7 +161,7 @@ func FirstFactorPost(msInitialDelay time.Duration, delayEnabled bool) middleware
userDetails, err := ctx.Providers.UserProvider.GetDetails(bodyJSON.Username) userDetails, err := ctx.Providers.UserProvider.GetDetails(bodyJSON.Username)
if err != nil { 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 return
} }
@ -175,7 +175,7 @@ func FirstFactorPost(msInitialDelay time.Duration, delayEnabled bool) middleware
err = ctx.SaveSession(userSession) err = ctx.SaveSession(userSession)
if err != nil { 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 return
} }

View File

@ -454,16 +454,16 @@ func TestFirstFactorDelayCalculations(t *testing.T) {
for i := 0; i < 100; i++ { for i := 0; i < 100; i++ {
delay := calculateActualDelay(mock.Ctx, execDuration, avgExecDurationMs, &successful) delay := calculateActualDelay(mock.Ctx, execDuration, avgExecDurationMs, &successful)
assert.True(t, delay >= expectedMinimumDelayMs) assert.True(t, delay >= expectedMinimumDelayMs)
assert.True(t, delay <= expectedMinimumDelayMs+float64(msMaximumRandomDelay)) assert.True(t, delay <= expectedMinimumDelayMs+float64(loginDelayMaximumRandomDelayMilliseconds))
} }
execDuration = 5 * time.Millisecond execDuration = 5 * time.Millisecond
avgExecDurationMs = 5.0 avgExecDurationMs = 5.0
expectedMinimumDelayMs = msMinimumDelay1FA - float64(execDuration.Milliseconds()) expectedMinimumDelayMs = loginDelayMinimumDelayMilliseconds - float64(execDuration.Milliseconds())
for i := 0; i < 100; i++ { for i := 0; i < 100; i++ {
delay := calculateActualDelay(mock.Ctx, execDuration, avgExecDurationMs, &successful) delay := calculateActualDelay(mock.Ctx, execDuration, avgExecDurationMs, &successful)
assert.True(t, delay >= expectedMinimumDelayMs) 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) err := ctx.ParseBody(&body)
if err != nil { 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") ctx.Logger.Tracef("Attempting to destroy session")
err = ctx.Providers.SessionProvider.DestroySession(ctx.RequestCtx) err = ctx.Providers.SessionProvider.DestroySession(ctx.RequestCtx)
if err != nil { 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) redirectionURL, err := url.Parse(body.TargetURL)
@ -46,6 +46,6 @@ func LogoutPost(ctx *middlewares.AutheliaCtx) {
err = ctx.SetJSONBody(responseBody) err = ctx.SetJSONBody(responseBody)
if err != nil { 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{ wellKnown := oidc.WellKnownConfiguration{
Issuer: issuer, Issuer: issuer,
JWKSURI: fmt.Sprintf("%s%s", issuer, oidcJWKsPath), JWKSURI: fmt.Sprintf("%s%s", issuer, pathOpenIDConnectJWKs),
AuthorizationEndpoint: fmt.Sprintf("%s%s", issuer, oidcAuthorizePath), AuthorizationEndpoint: fmt.Sprintf("%s%s", issuer, pathOpenIDConnectAuthorization),
TokenEndpoint: fmt.Sprintf("%s%s", issuer, oidcTokenPath), TokenEndpoint: fmt.Sprintf("%s%s", issuer, pathOpenIDConnectToken),
RevocationEndpoint: fmt.Sprintf("%s%s", issuer, oidcRevokePath), RevocationEndpoint: fmt.Sprintf("%s%s", issuer, pathOpenIDConnectRevocation),
UserinfoEndpoint: fmt.Sprintf("%s%s", issuer, oidcUserinfoPath), UserinfoEndpoint: fmt.Sprintf("%s%s", issuer, pathOpenIDConnectUserinfo),
Algorithms: []string{"RS256"}, Algorithms: []string{"RS256"},
UserinfoAlgorithms: []string{"none", "RS256"}, UserinfoAlgorithms: []string{"none", "RS256"},

View File

@ -32,7 +32,7 @@ var SecondFactorTOTPIdentityStart = middlewares.IdentityVerificationStart(middle
MailTitle: "Register your mobile", MailTitle: "Register your mobile",
MailButtonContent: "Register", MailButtonContent: "Register",
TargetEndpoint: "/one-time-password/register", TargetEndpoint: "/one-time-password/register",
ActionClaim: TOTPRegistrationAction, ActionClaim: ActionTOTPRegistration,
IdentityRetrieverFunc: identityRetrieverFromSession, IdentityRetrieverFunc: identityRetrieverFromSession,
}) })
@ -45,13 +45,13 @@ func secondFactorTOTPIdentityFinish(ctx *middlewares.AutheliaCtx, username strin
}) })
if err != nil { 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 return
} }
err = ctx.Providers.StorageProvider.SaveTOTPSecret(username, key.Secret()) err = ctx.Providers.StorageProvider.SaveTOTPSecret(username, key.Secret())
if err != nil { 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 return
} }
@ -69,6 +69,6 @@ func secondFactorTOTPIdentityFinish(ctx *middlewares.AutheliaCtx, username strin
// SecondFactorTOTPIdentityFinish the handler for finishing the identity validation. // SecondFactorTOTPIdentityFinish the handler for finishing the identity validation.
var SecondFactorTOTPIdentityFinish = middlewares.IdentityVerificationFinish( var SecondFactorTOTPIdentityFinish = middlewares.IdentityVerificationFinish(
middlewares.IdentityVerificationFinishArgs{ middlewares.IdentityVerificationFinishArgs{
ActionClaim: TOTPRegistrationAction, ActionClaim: ActionTOTPRegistration,
IsTokenUserValidFunc: isTokenUserValidFor2FARegistration, IsTokenUserValidFunc: isTokenUserValidFor2FARegistration,
}, secondFactorTOTPIdentityFinish) }, secondFactorTOTPIdentityFinish)

View File

@ -19,18 +19,18 @@ var SecondFactorU2FIdentityStart = middlewares.IdentityVerificationStart(middlew
MailTitle: "Register your key", MailTitle: "Register your key",
MailButtonContent: "Register", MailButtonContent: "Register",
TargetEndpoint: "/security-key/register", TargetEndpoint: "/security-key/register",
ActionClaim: U2FRegistrationAction, ActionClaim: ActionU2FRegistration,
IdentityRetrieverFunc: identityRetrieverFromSession, IdentityRetrieverFunc: identityRetrieverFromSession,
}) })
func secondFactorU2FIdentityFinish(ctx *middlewares.AutheliaCtx, username string) { func secondFactorU2FIdentityFinish(ctx *middlewares.AutheliaCtx, username string) {
if ctx.XForwardedProto() == nil { if ctx.XForwardedProto() == nil {
ctx.Error(errMissingXForwardedProto, operationFailedMessage) ctx.Error(errMissingXForwardedProto, messageOperationFailed)
return return
} }
if ctx.XForwardedHost() == nil { if ctx.XForwardedHost() == nil {
ctx.Error(errMissingXForwardedHost, operationFailedMessage) ctx.Error(errMissingXForwardedHost, messageOperationFailed)
return return
} }
@ -42,7 +42,7 @@ func secondFactorU2FIdentityFinish(ctx *middlewares.AutheliaCtx, username string
challenge, err := u2f.NewChallenge(appID, trustedFacets) challenge, err := u2f.NewChallenge(appID, trustedFacets)
if err != nil { 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 return
} }
@ -52,7 +52,7 @@ func secondFactorU2FIdentityFinish(ctx *middlewares.AutheliaCtx, username string
err = ctx.SaveSession(userSession) err = ctx.SaveSession(userSession)
if err != nil { 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 return
} }
@ -65,6 +65,6 @@ func secondFactorU2FIdentityFinish(ctx *middlewares.AutheliaCtx, username string
// SecondFactorU2FIdentityFinish the handler for finishing the identity validation. // SecondFactorU2FIdentityFinish the handler for finishing the identity validation.
var SecondFactorU2FIdentityFinish = middlewares.IdentityVerificationFinish( var SecondFactorU2FIdentityFinish = middlewares.IdentityVerificationFinish(
middlewares.IdentityVerificationFinishArgs{ middlewares.IdentityVerificationFinishArgs{
ActionClaim: U2FRegistrationAction, ActionClaim: ActionU2FRegistration,
IsTokenUserValidFunc: isTokenUserValidFor2FARegistration, IsTokenUserValidFunc: isTokenUserValidFor2FARegistration,
}, secondFactorU2FIdentityFinish) }, secondFactorU2FIdentityFinish)

View File

@ -50,7 +50,7 @@ func createToken(secret string, username string, action string, expiresAt time.T
} }
func (s *HandlerRegisterU2FStep1Suite) TestShouldRaiseWhenXForwardedProtoIsMissing() { 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)) time.Now().Add(1*time.Minute))
s.mock.Ctx.Request.SetBodyString(fmt.Sprintf("{\"token\":\"%s\"}", token)) s.mock.Ctx.Request.SetBodyString(fmt.Sprintf("{\"token\":\"%s\"}", token))
@ -70,7 +70,7 @@ func (s *HandlerRegisterU2FStep1Suite) TestShouldRaiseWhenXForwardedProtoIsMissi
func (s *HandlerRegisterU2FStep1Suite) TestShouldRaiseWhenXForwardedHostIsMissing() { func (s *HandlerRegisterU2FStep1Suite) TestShouldRaiseWhenXForwardedHostIsMissing() {
s.mock.Ctx.Request.Header.Add("X-Forwarded-Proto", "http") 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)) time.Now().Add(1*time.Minute))
s.mock.Ctx.Request.SetBodyString(fmt.Sprintf("{\"token\":\"%s\"}", token)) 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) err := ctx.ParseBody(&responseBody)
if err != nil { 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() userSession := ctx.GetSession()
if userSession.U2FChallenge == nil { 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 return
} }
// Ensure the challenge is cleared if anything goes wrong. // 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) registration, err := u2f.Register(responseBody, *userSession.U2FChallenge, u2fConfig)
if err != nil { 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 return
} }
@ -48,7 +48,7 @@ func SecondFactorU2FRegister(ctx *middlewares.AutheliaCtx) {
err = ctx.Providers.StorageProvider.SaveU2FDeviceHandle(userSession.Username, registration.KeyHandle, publicKey) err = ctx.Providers.StorageProvider.SaveU2FDeviceHandle(userSession.Username, registration.KeyHandle, publicKey)
if err != nil { 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 return
} }

View File

@ -38,7 +38,7 @@ var ResetPasswordIdentityStart = middlewares.IdentityVerificationStart(middlewar
MailTitle: "Reset your password", MailTitle: "Reset your password",
MailButtonContent: "Reset", MailButtonContent: "Reset",
TargetEndpoint: "/reset-password/step2", TargetEndpoint: "/reset-password/step2",
ActionClaim: ResetPasswordAction, ActionClaim: ActionResetPassword,
IdentityRetrieverFunc: identityRetrieverFromStorage, IdentityRetrieverFunc: identityRetrieverFromStorage,
}) })
@ -57,4 +57,4 @@ func resetPasswordIdentityFinish(ctx *middlewares.AutheliaCtx, username string)
// ResetPasswordIdentityFinish the handler for finishing the identity validation. // ResetPasswordIdentityFinish the handler for finishing the identity validation.
var ResetPasswordIdentityFinish = middlewares.IdentityVerificationFinish( 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 // 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. // request expire at some point because here it only expires when the cookie expires.
if userSession.PasswordResetUsername == nil { 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 return
} }
@ -23,7 +23,7 @@ func ResetPasswordPost(ctx *middlewares.AutheliaCtx) {
err := ctx.ParseBody(&requestBody) err := ctx.ParseBody(&requestBody)
if err != nil { if err != nil {
ctx.Error(err, unableToResetPasswordMessage) ctx.Error(err, messageUnableToResetPassword)
return return
} }
@ -35,7 +35,7 @@ func ResetPasswordPost(ctx *middlewares.AutheliaCtx) {
utils.IsStringInSliceContains(err.Error(), ldapPasswordComplexityErrors): utils.IsStringInSliceContains(err.Error(), ldapPasswordComplexityErrors):
ctx.Error(fmt.Errorf("%s", err), ldapPasswordComplexityCode) ctx.Error(fmt.Errorf("%s", err), ldapPasswordComplexityCode)
default: default:
ctx.Error(fmt.Errorf("%s", err), unableToResetPasswordMessage) ctx.Error(fmt.Errorf("%s", err), messageUnableToResetPassword)
} }
return return
@ -48,7 +48,7 @@ func ResetPasswordPost(ctx *middlewares.AutheliaCtx) {
err = ctx.SaveSession(userSession) err = ctx.SaveSession(userSession)
if err != nil { 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 return
} }

View File

@ -15,7 +15,7 @@ func SecondFactorDuoPost(duoAPI duo.API) middlewares.RequestHandler {
err := ctx.ParseBody(&requestBody) err := ctx.ParseBody(&requestBody)
if err != nil { if err != nil {
handleAuthenticationUnauthorized(ctx, err, mfaValidationFailedMessage) handleAuthenticationUnauthorized(ctx, err, messageMFAValidationFailed)
return return
} }
@ -37,7 +37,7 @@ func SecondFactorDuoPost(duoAPI duo.API) middlewares.RequestHandler {
duoResponse, err := duoAPI.Call(values, ctx) duoResponse, err := duoAPI.Call(values, ctx)
if err != nil { if err != nil {
handleAuthenticationUnauthorized(ctx, fmt.Errorf("Duo API errored: %s", err), mfaValidationFailedMessage) handleAuthenticationUnauthorized(ctx, fmt.Errorf("Duo API errored: %s", err), messageMFAValidationFailed)
return return
} }
@ -60,7 +60,7 @@ func SecondFactorDuoPost(duoAPI duo.API) middlewares.RequestHandler {
err = ctx.Providers.SessionProvider.RegenerateSession(ctx.RequestCtx) err = ctx.Providers.SessionProvider.RegenerateSession(ctx.RequestCtx)
if err != nil { 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 return
} }
@ -68,7 +68,7 @@ func SecondFactorDuoPost(duoAPI duo.API) middlewares.RequestHandler {
err = ctx.SaveSession(userSession) err = ctx.SaveSession(userSession)
if err != nil { 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 return
} }

View File

@ -13,7 +13,7 @@ func SecondFactorTOTPPost(totpVerifier TOTPVerifier) middlewares.RequestHandler
err := ctx.ParseBody(&requestBody) err := ctx.ParseBody(&requestBody)
if err != nil { if err != nil {
handleAuthenticationUnauthorized(ctx, err, mfaValidationFailedMessage) handleAuthenticationUnauthorized(ctx, err, messageMFAValidationFailed)
return return
} }
@ -21,25 +21,25 @@ func SecondFactorTOTPPost(totpVerifier TOTPVerifier) middlewares.RequestHandler
secret, err := ctx.Providers.StorageProvider.LoadTOTPSecret(userSession.Username) secret, err := ctx.Providers.StorageProvider.LoadTOTPSecret(userSession.Username)
if err != nil { 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 return
} }
isValid, err := totpVerifier.Verify(requestBody.Token, secret) isValid, err := totpVerifier.Verify(requestBody.Token, secret)
if err != nil { 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 return
} }
if !isValid { 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 return
} }
err = ctx.Providers.SessionProvider.RegenerateSession(ctx.RequestCtx) err = ctx.Providers.SessionProvider.RegenerateSession(ctx.RequestCtx)
if err != nil { 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 return
} }
@ -47,7 +47,7 @@ func SecondFactorTOTPPost(totpVerifier TOTPVerifier) middlewares.RequestHandler
err = ctx.SaveSession(userSession) err = ctx.SaveSession(userSession)
if err != nil { 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 return
} }

View File

@ -14,12 +14,12 @@ import (
// SecondFactorU2FSignGet handler for initiating a signing request. // SecondFactorU2FSignGet handler for initiating a signing request.
func SecondFactorU2FSignGet(ctx *middlewares.AutheliaCtx) { func SecondFactorU2FSignGet(ctx *middlewares.AutheliaCtx) {
if ctx.XForwardedProto() == nil { if ctx.XForwardedProto() == nil {
ctx.Error(errMissingXForwardedProto, mfaValidationFailedMessage) ctx.Error(errMissingXForwardedProto, messageMFAValidationFailed)
return return
} }
if ctx.XForwardedHost() == nil { if ctx.XForwardedHost() == nil {
ctx.Error(errMissingXForwardedHost, mfaValidationFailedMessage) ctx.Error(errMissingXForwardedHost, messageMFAValidationFailed)
return return
} }
@ -29,7 +29,7 @@ func SecondFactorU2FSignGet(ctx *middlewares.AutheliaCtx) {
challenge, err := u2f.NewChallenge(appID, trustedFacets) challenge, err := u2f.NewChallenge(appID, trustedFacets)
if err != nil { 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 return
} }
@ -38,11 +38,11 @@ func SecondFactorU2FSignGet(ctx *middlewares.AutheliaCtx) {
if err != nil { if err != nil {
if err == storage.ErrNoU2FDeviceHandle { 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 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 return
} }
@ -63,7 +63,7 @@ func SecondFactorU2FSignGet(ctx *middlewares.AutheliaCtx) {
err = ctx.SaveSession(userSession) err = ctx.SaveSession(userSession)
if err != nil { 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 return
} }
@ -71,7 +71,7 @@ func SecondFactorU2FSignGet(ctx *middlewares.AutheliaCtx) {
err = ctx.SetJSONBody(signRequest) err = ctx.SetJSONBody(signRequest)
if err != nil { 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 return
} }
} }

View File

@ -13,18 +13,18 @@ func SecondFactorU2FSignPost(u2fVerifier U2FVerifier) middlewares.RequestHandler
err := ctx.ParseBody(&requestBody) err := ctx.ParseBody(&requestBody)
if err != nil { if err != nil {
ctx.Error(err, mfaValidationFailedMessage) ctx.Error(err, messageMFAValidationFailed)
return return
} }
userSession := ctx.GetSession() userSession := ctx.GetSession()
if userSession.U2FChallenge == nil { 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 return
} }
if userSession.U2FRegistration == nil { 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 return
} }
@ -35,14 +35,14 @@ func SecondFactorU2FSignPost(u2fVerifier U2FVerifier) middlewares.RequestHandler
*userSession.U2FChallenge) *userSession.U2FChallenge)
if err != nil { if err != nil {
ctx.Error(err, mfaValidationFailedMessage) ctx.Error(err, messageMFAValidationFailed)
return return
} }
err = ctx.Providers.SessionProvider.RegenerateSession(ctx.RequestCtx) err = ctx.Providers.SessionProvider.RegenerateSession(ctx.RequestCtx)
if err != nil { 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 return
} }
@ -50,7 +50,7 @@ func SecondFactorU2FSignPost(u2fVerifier U2FVerifier) middlewares.RequestHandler
err = ctx.SaveSession(userSession) err = ctx.SaveSession(userSession)
if err != nil { 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 return
} }

View File

@ -87,7 +87,7 @@ func UserInfoGet(ctx *middlewares.AutheliaCtx) {
errors := loadInfo(userSession.Username, ctx.Providers.StorageProvider, &userInfo, ctx.Logger) errors := loadInfo(userSession.Username, ctx.Providers.StorageProvider, &userInfo, ctx.Logger)
if len(errors) > 0 { 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 return
} }
@ -110,12 +110,12 @@ func MethodPreferencePost(ctx *middlewares.AutheliaCtx) {
err := ctx.ParseBody(&bodyJSON) err := ctx.ParseBody(&bodyJSON)
if err != nil { if err != nil {
ctx.Error(err, operationFailedMessage) ctx.Error(err, messageOperationFailed)
return return
} }
if !utils.IsStringInSlice(bodyJSON.Method, authentication.PossibleMethods) { 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 return
} }
@ -124,7 +124,7 @@ func MethodPreferencePost(ctx *middlewares.AutheliaCtx) {
err = ctx.Providers.StorageProvider.SavePreferred2FAMethod(userSession.Username, bodyJSON.Method) err = ctx.Providers.StorageProvider.SavePreferred2FAMethod(userSession.Username, bodyJSON.Method)
if err != nil { 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 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. // setForwardedHeaders set the forwarded User, Groups, Name and Email headers.
func setForwardedHeaders(headers *fasthttp.ResponseHeader, username, name string, groups, emails []string) { func setForwardedHeaders(headers *fasthttp.ResponseHeader, username, name string, groups, emails []string) {
if username != "" { if username != "" {
headers.Set(remoteUserHeader, username) headers.Set(headerRemoteUser, username)
headers.Set(remoteGroupsHeader, strings.Join(groups, ",")) headers.Set(headerRemoteGroups, strings.Join(groups, ","))
headers.Set(remoteNameHeader, name) headers.Set(headerRemoteName, name)
if emails != nil { if emails != nil {
headers.Set(remoteEmailHeader, emails[0]) headers.Set(headerRemoteEmail, emails[0])
} else { } 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) { func handleUnauthorized(ctx *middlewares.AutheliaCtx, targetURL fmt.Stringer, isBasicAuth bool, username string, method []byte) {
friendlyUsername := "<anonymous>" var (
if username != "" { statusCode int
redirectionURL string
friendlyUsername string
friendlyRequestMethod string
)
switch username {
case "":
friendlyUsername = "<anonymous>"
default:
friendlyUsername = username friendlyUsername = username
} }
@ -212,33 +221,39 @@ func handleUnauthorized(ctx *middlewares.AutheliaCtx, targetURL fmt.Stringer, is
rd := string(ctx.QueryArgs().Peek("rd")) rd := string(ctx.QueryArgs().Peek("rd"))
rm := string(method) rm := string(method)
friendlyMethod := "unknown" switch rm {
case "":
if rm != "" { friendlyRequestMethod = "unknown"
friendlyMethod = rm default:
friendlyRequestMethod = rm
} }
if rd != "" { 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 { switch rm {
case fasthttp.MethodGet, fasthttp.MethodHead, "": case "":
ctx.Redirect(redirectionURL, 302) redirectionURL = fmt.Sprintf("%s?rd=%s", rd, url.QueryEscape(targetURL.String()))
ctx.SetBodyString(fmt.Sprintf("Found. Redirecting to %s", redirectionURL))
default: default:
ctx.Redirect(redirectionURL, 303) redirectionURL = fmt.Sprintf("%s?rd=%s&rm=%s", rd, url.QueryEscape(targetURL.String()), rm)
ctx.SetBodyString(fmt.Sprintf("See Other. Redirecting to %s", redirectionURL))
} }
}
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 { } 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() 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) { 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")) { if bytes.Equal(ctx.QueryArgs().Peek("auth"), []byte("basic")) {
authHeader = AuthorizationHeader authHeader = HeaderAuthorization
isBasicAuth = true isBasicAuth = true
} }
@ -408,7 +423,7 @@ func verifyAuth(ctx *middlewares.AutheliaCtx, targetURL *url.URL, refreshProfile
userSession := ctx.GetSession() userSession := ctx.GetSession()
username, name, groups, emails, authLevel, err = verifySessionCookie(ctx, targetURL, &userSession, refreshProfile, refreshProfileInterval) 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) { 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") 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( ctx.Logger.Error(
fmt.Errorf( fmt.Errorf(
"Unable to destroy user session after handler could not match them to their %s header: %s", "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 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)) ctx.Logger.Error(fmt.Sprintf("Error caught when verifying user authorization: %s", err))
if err := updateActivityTimestamp(ctx, isBasicAuth, username); err != nil { 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 return
} }
@ -488,7 +503,7 @@ func VerifyGet(cfg schema.AuthenticationBackendConfiguration) middlewares.Reques
} }
if err := updateActivityTimestamp(ctx, isBasicAuth, username); err != nil { 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. // Test parseBasicAuth.
func TestShouldRaiseWhenHeaderDoesNotContainBasicPrefix(t *testing.T) { func TestShouldRaiseWhenHeaderDoesNotContainBasicPrefix(t *testing.T) {
_, _, err := parseBasicAuth(ProxyAuthorizationHeader, "alzefzlfzemjfej==") _, _, err := parseBasicAuth(HeaderProxyAuthorization, "alzefzlfzemjfej==")
assert.Error(t, err) assert.Error(t, err)
assert.Equal(t, "Basic prefix not found in Proxy-Authorization header", err.Error()) assert.Equal(t, "Basic prefix not found in Proxy-Authorization header", err.Error())
} }
func TestShouldRaiseWhenCredentialsAreNotInBase64(t *testing.T) { func TestShouldRaiseWhenCredentialsAreNotInBase64(t *testing.T) {
_, _, err := parseBasicAuth(ProxyAuthorizationHeader, "Basic alzefzlfzemjfej==") _, _, err := parseBasicAuth(HeaderProxyAuthorization, "Basic alzefzlfzemjfej==")
assert.Error(t, err) assert.Error(t, err)
assert.Equal(t, "illegal base64 data at input byte 16", err.Error()) assert.Equal(t, "illegal base64 data at input byte 16", err.Error())
} }
func TestShouldRaiseWhenCredentialsAreNotInCorrectForm(t *testing.T) { func TestShouldRaiseWhenCredentialsAreNotInCorrectForm(t *testing.T) {
// The decoded format should be user:password. // The decoded format should be user:password.
_, _, err := parseBasicAuth(ProxyAuthorizationHeader, "Basic am9obiBwYXNzd29yZA==") _, _, err := parseBasicAuth(HeaderProxyAuthorization, "Basic am9obiBwYXNzd29yZA==")
assert.Error(t, err) assert.Error(t, err)
assert.Equal(t, "Format of Proxy-Authorization header must be user:password", err.Error()) 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) { func TestShouldReturnUsernameAndPassword(t *testing.T) {
// the decoded format should be user:password. // 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.NoError(t, err)
assert.Equal(t, "john", user) assert.Equal(t, "john", user)
assert.Equal(t, "password", password) assert.Equal(t, "password", password)
@ -177,7 +177,7 @@ func TestShouldVerifyWrongCredentials(t *testing.T) {
Return(false, nil) Return(false, nil)
url, _ := url.ParseRequestURI("https://test.example.com") 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) assert.Error(t, err)
} }
@ -718,10 +718,10 @@ func TestShouldRedirectWhenSessionInactiveForTooLongAndRDParamProvided(t *testin
mock.Ctx.QueryArgs().Add("rd", "https://login.example.com") 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-Original-URL", "https://two-factor.example.com")
mock.Ctx.Request.Header.Set("X-Forwarded-Method", "GET") mock.Ctx.Request.Header.Set("X-Forwarded-Method", "GET")
mock.Ctx.Request.Header.Set("Accept", "text/html; charset=utf-8")
VerifyGet(verifyGetCfg)(mock.Ctx) 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())) string(mock.Ctx.Response.Body()))
assert.Equal(t, 302, mock.Ctx.Response.StatusCode()) 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.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-Original-URL", "https://two-factor.example.com")
mock.Ctx.Request.Header.Set("X-Forwarded-Method", "GET") mock.Ctx.Request.Header.Set("X-Forwarded-Method", "GET")
mock.Ctx.Request.Header.Set("Accept", "text/html; charset=utf-8")
VerifyGet(verifyGetCfg)(mock.Ctx) 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())) string(mock.Ctx.Response.Body()))
assert.Equal(t, 302, mock.Ctx.Response.StatusCode()) assert.Equal(t, 302, mock.Ctx.Response.StatusCode())
mock.Ctx.QueryArgs().Add("rd", "https://login.example.com") 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-Original-URL", "https://two-factor.example.com")
mock.Ctx.Request.Header.Set("X-Forwarded-Method", "POST") mock.Ctx.Request.Header.Set("X-Forwarded-Method", "POST")
mock.Ctx.Request.Header.Set("Accept", "text/html; charset=utf-8")
VerifyGet(verifyGetCfg)(mock.Ctx) 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())) string(mock.Ctx.Response.Body()))
assert.Equal(t, 303, mock.Ctx.Response.StatusCode()) assert.Equal(t, 303, mock.Ctx.Response.StatusCode())
} }
@ -801,12 +803,13 @@ func TestShouldURLEncodeRedirectionURLParameter(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com") 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.SetHost("mydomain.com")
mock.Ctx.Request.SetRequestURI("/?rd=https://auth.mydomain.com") mock.Ctx.Request.SetRequestURI("/?rd=https://auth.mydomain.com")
VerifyGet(verifyGetCfg)(mock.Ctx) 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())) string(mock.Ctx.Response.Body()))
} }
@ -1209,7 +1212,7 @@ func TestShouldCheckValidSessionUsernameHeaderAndReturn200(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
mock.Ctx.Request.Header.Set("X-Original-URL", "https://one-factor.example.com") 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) VerifyGet(verifyGetCfg)(mock.Ctx)
assert.Equal(t, expectedStatusCode, mock.Ctx.Response.StatusCode()) assert.Equal(t, expectedStatusCode, mock.Ctx.Response.StatusCode())
@ -1233,7 +1236,7 @@ func TestShouldCheckInvalidSessionUsernameHeaderAndReturn401(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
mock.Ctx.Request.Header.Set("X-Original-URL", "https://one-factor.example.com") 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) VerifyGet(verifyGetCfg)(mock.Ctx)
assert.Equal(t, expectedStatusCode, mock.Ctx.Response.StatusCode()) 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. // TODO: Add OPTIONS handler.
router.GET("/.well-known/openid-configuration", middleware(oidcWellKnown)) 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. // 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.GET(pathOpenIDConnectUserinfo, middleware(middlewares.NewHTTPToAutheliaHandlerAdaptor(oidcUserinfo)))
router.POST(oidcUserinfoPath, middleware(middlewares.NewHTTPToAutheliaHandlerAdaptor(oidcUserinfo))) router.POST(pathOpenIDConnectUserinfo, middleware(middlewares.NewHTTPToAutheliaHandlerAdaptor(oidcUserinfo)))
// TODO: Add OPTIONS handler. // 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() uri, err := ctx.ForwardedProtoHost()
if err != nil { if err != nil {
ctx.Logger.Errorf("%v", err) 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 return
} }
@ -64,7 +64,7 @@ func Handle1FAResponse(ctx *middlewares.AutheliaCtx, targetURI, requestMethod st
targetURL, err := url.ParseRequestURI(targetURI) targetURL, err := url.ParseRequestURI(targetURI)
if err != nil { 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 return
} }
@ -128,7 +128,7 @@ func Handle2FAResponse(ctx *middlewares.AutheliaCtx, targetURI string) {
targetURL, err := url.ParseRequestURI(targetURI) targetURL, err := url.ParseRequestURI(targetURI)
if err != nil { 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 return
} }

View File

@ -43,7 +43,7 @@ func AutheliaMiddleware(configuration schema.Configuration, providers Providers)
return func(ctx *fasthttp.RequestCtx) { return func(ctx *fasthttp.RequestCtx) {
autheliaCtx, err := NewAutheliaCtx(ctx, configuration, providers) autheliaCtx, err := NewAutheliaCtx(ctx, configuration, providers)
if err != nil { if err != nil {
autheliaCtx.Error(err, operationFailedMessage) autheliaCtx.Error(err, messageOperationFailed)
return return
} }
@ -60,7 +60,7 @@ func (c *AutheliaCtx) Error(err error, message string) {
c.Logger.Error(marshalErr) c.Logger.Error(marshalErr)
} }
c.SetContentType("application/json") c.SetContentType(contentTypeApplicationJSON)
c.SetBody(b) c.SetBody(b)
c.Logger.Error(err) c.Logger.Error(err)
} }
@ -73,7 +73,7 @@ func (c *AutheliaCtx) ReplyError(err error, message string) {
c.Logger.Error(marshalErr) c.Logger.Error(marshalErr)
} }
c.SetContentType("application/json") c.SetContentType(contentTypeApplicationJSON)
c.SetBody(b) c.SetBody(b)
c.Logger.Debug(err) c.Logger.Debug(err)
} }
@ -95,22 +95,22 @@ func (c *AutheliaCtx) ReplyBadRequest() {
// XForwardedProto return the content of the X-Forwarded-Proto header. // XForwardedProto return the content of the X-Forwarded-Proto header.
func (c *AutheliaCtx) XForwardedProto() []byte { 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. // XForwardedMethod return the content of the X-Forwarded-Method header.
func (c *AutheliaCtx) XForwardedMethod() []byte { 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. // XForwardedHost return the content of the X-Forwarded-Host header.
func (c *AutheliaCtx) XForwardedHost() []byte { 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. // XForwardedURI return the content of the X-Forwarded-URI header.
func (c *AutheliaCtx) XForwardedURI() []byte { 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. // 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. // XOriginalURL return the content of the X-Original-URL header.
func (c *AutheliaCtx) XOriginalURL() []byte { 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. // 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. // ReplyOK is a helper method to reply ok.
func (c *AutheliaCtx) ReplyOK() { func (c *AutheliaCtx) ReplyOK() {
c.SetContentType(applicationJSONContentType) c.SetContentType(contentTypeApplicationJSON)
c.SetBody(okMessageBytes) c.SetBody(okMessageBytes)
} }
@ -186,7 +186,7 @@ func (c *AutheliaCtx) SetJSONBody(value interface{}) error {
return fmt.Errorf("Unable to marshal JSON body") return fmt.Errorf("Unable to marshal JSON body")
} }
c.SetContentType("application/json") c.SetContentType(contentTypeApplicationJSON)
c.SetBody(b) c.SetBody(b)
return nil return nil
@ -249,3 +249,46 @@ func (c *AutheliaCtx) GetOriginalURL() (*url.URL, error) {
return parsedURL, nil 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 jwtIssuer = "Authelia"
const xForwardedProtoHeader = "X-Forwarded-Proto" const (
const xForwardedMethodHeader = "X-Forwarded-Method" headerXForwardedProto = "X-Forwarded-Proto"
const xForwardedHostHeader = "X-Forwarded-Host" headerXForwardedMethod = "X-Forwarded-Method"
const xForwardedURIHeader = "X-Forwarded-URI" 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\"}") var okMessageBytes = []byte("{\"status\":\"OK\"}")
const operationFailedMessage = "Operation failed" const (
const identityVerificationTokenAlreadyUsedMessage = "The identity verification token has already been used" messageOperationFailed = "Operation failed"
const identityVerificationTokenHasExpiredMessage = "The identity verification token has expired" messageIdentityVerificationTokenAlreadyUsed = "The identity verification token has already been used"
messageIdentityVerificationTokenHasExpired = "The identity verification token has expired"
)
var protoHostSeparator = []byte("://") var protoHostSeparator = []byte("://")

View File

@ -41,19 +41,19 @@ func IdentityVerificationStart(args IdentityVerificationStartArgs) RequestHandle
ss, err := token.SignedString([]byte(ctx.Configuration.JWTSecret)) ss, err := token.SignedString([]byte(ctx.Configuration.JWTSecret))
if err != nil { if err != nil {
ctx.Error(err, operationFailedMessage) ctx.Error(err, messageOperationFailed)
return return
} }
err = ctx.Providers.StorageProvider.SaveIdentityVerificationToken(ss) err = ctx.Providers.StorageProvider.SaveIdentityVerificationToken(ss)
if err != nil { if err != nil {
ctx.Error(err, operationFailedMessage) ctx.Error(err, messageOperationFailed)
return return
} }
uri, err := ctx.ForwardedProtoHost() uri, err := ctx.ForwardedProtoHost()
if err != nil { if err != nil {
ctx.Error(err, operationFailedMessage) ctx.Error(err, messageOperationFailed)
return return
} }
@ -76,7 +76,7 @@ func IdentityVerificationStart(args IdentityVerificationStartArgs) RequestHandle
err = templates.HTMLEmailTemplate.Execute(bufHTML, htmlParams) err = templates.HTMLEmailTemplate.Execute(bufHTML, htmlParams)
if err != nil { if err != nil {
ctx.Error(err, operationFailedMessage) ctx.Error(err, messageOperationFailed)
return return
} }
} }
@ -89,7 +89,7 @@ func IdentityVerificationStart(args IdentityVerificationStartArgs) RequestHandle
err = templates.PlainTextEmailTemplate.Execute(bufText, textParams) err = templates.PlainTextEmailTemplate.Execute(bufText, textParams)
if err != nil { if err != nil {
ctx.Error(err, operationFailedMessage) ctx.Error(err, messageOperationFailed)
return return
} }
@ -99,7 +99,7 @@ func IdentityVerificationStart(args IdentityVerificationStartArgs) RequestHandle
err = ctx.Providers.Notifier.Send(identity.Email, args.MailTitle, bufText.String(), bufHTML.String()) err = ctx.Providers.Notifier.Send(identity.Email, args.MailTitle, bufText.String(), bufHTML.String())
if err != nil { if err != nil {
ctx.Error(err, operationFailedMessage) ctx.Error(err, messageOperationFailed)
return return
} }
@ -117,25 +117,25 @@ func IdentityVerificationFinish(args IdentityVerificationFinishArgs, next func(c
err := json.Unmarshal(b, &finishBody) err := json.Unmarshal(b, &finishBody)
if err != nil { if err != nil {
ctx.Error(err, operationFailedMessage) ctx.Error(err, messageOperationFailed)
return return
} }
if finishBody.Token == "" { if finishBody.Token == "" {
ctx.Error(fmt.Errorf("No token provided"), operationFailedMessage) ctx.Error(fmt.Errorf("No token provided"), messageOperationFailed)
return return
} }
found, err := ctx.Providers.StorageProvider.FindIdentityVerificationToken(finishBody.Token) found, err := ctx.Providers.StorageProvider.FindIdentityVerificationToken(finishBody.Token)
if err != nil { if err != nil {
ctx.Error(err, operationFailedMessage) ctx.Error(err, messageOperationFailed)
return return
} }
if !found { if !found {
ctx.Error(fmt.Errorf("Token is not in DB, it might have already been used"), ctx.Error(fmt.Errorf("Token is not in DB, it might have already been used"),
identityVerificationTokenAlreadyUsedMessage) messageIdentityVerificationTokenAlreadyUsed)
return return
} }
@ -148,44 +148,44 @@ func IdentityVerificationFinish(args IdentityVerificationFinishArgs, next func(c
if ve, ok := err.(*jwt.ValidationError); ok { if ve, ok := err.(*jwt.ValidationError); ok {
switch { switch {
case ve.Errors&jwt.ValidationErrorMalformed != 0: case ve.Errors&jwt.ValidationErrorMalformed != 0:
ctx.Error(fmt.Errorf("Cannot parse token"), operationFailedMessage) ctx.Error(fmt.Errorf("Cannot parse token"), messageOperationFailed)
return return
case ve.Errors&(jwt.ValidationErrorExpired|jwt.ValidationErrorNotValidYet) != 0: case ve.Errors&(jwt.ValidationErrorExpired|jwt.ValidationErrorNotValidYet) != 0:
// Token is either expired or not active yet // Token is either expired or not active yet
ctx.Error(fmt.Errorf("Token expired"), identityVerificationTokenHasExpiredMessage) ctx.Error(fmt.Errorf("Token expired"), messageIdentityVerificationTokenHasExpired)
return return
default: default:
ctx.Error(fmt.Errorf("Cannot handle this token: %s", ve), operationFailedMessage) ctx.Error(fmt.Errorf("Cannot handle this token: %s", ve), messageOperationFailed)
return return
} }
} }
ctx.Error(err, operationFailedMessage) ctx.Error(err, messageOperationFailed)
return return
} }
claims, ok := token.Claims.(*IdentityVerificationClaim) claims, ok := token.Claims.(*IdentityVerificationClaim)
if !ok { 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 return
} }
// Verify that the action claim in the token is the one expected for the given endpoint. // Verify that the action claim in the token is the one expected for the given endpoint.
if claims.Action != args.ActionClaim { 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 return
} }
if args.IsTokenUserValidFunc != nil && !args.IsTokenUserValidFunc(ctx, claims.Username) { 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 return
} }
// TODO(c.michaud): find a way to garbage collect unused tokens. // TODO(c.michaud): find a way to garbage collect unused tokens.
err = ctx.Providers.StorageProvider.RemoveIdentityVerificationToken(finishBody.Token) err = ctx.Providers.StorageProvider.RemoveIdentityVerificationToken(finishBody.Token)
if err != nil { if err != nil {
ctx.Error(err, operationFailedMessage) ctx.Error(err, messageOperationFailed)
return 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 := router.New()
r.GET("/", serveIndexHandler) r.GET("/", serveIndexHandler)
r.OPTIONS("/", autheliaMiddleware(handleOPTIONS))
r.GET("/api/", serveSwaggerHandler) r.GET("/api/", serveSwaggerHandler)
r.GET("/api/"+apiFile, serveSwaggerAPIHandler) r.GET("/api/"+apiFile, serveSwaggerAPIHandler)

View File

@ -14,6 +14,7 @@ import (
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"github.com/authelia/authelia/internal/storage" "github.com/authelia/authelia/internal/storage"
"github.com/authelia/authelia/internal/utils"
) )
type StandaloneWebDriverSuite struct { type StandaloneWebDriverSuite struct {
@ -110,6 +111,7 @@ func (s *StandaloneSuite) TestShouldRespectMethodsACL() {
req.Header.Set("X-Forwarded-Proto", "https") req.Header.Set("X-Forwarded-Proto", "https")
req.Header.Set("X-Forwarded-Host", fmt.Sprintf("secure.%s", BaseDomain)) req.Header.Set("X-Forwarded-Host", fmt.Sprintf("secure.%s", BaseDomain))
req.Header.Set("X-Forwarded-URI", "/") req.Header.Set("X-Forwarded-URI", "/")
req.Header.Set("Accept", "text/html; charset=utf8")
client := NewHTTPClient() client := NewHTTPClient()
res, err := client.Do(req) res, err := client.Do(req)
@ -119,7 +121,7 @@ func (s *StandaloneSuite) TestShouldRespectMethodsACL() {
s.Assert().NoError(err) s.Assert().NoError(err)
urlEncodedAdminURL := url.QueryEscape(SecureBaseURL + "/") 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") 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-Proto", "https")
req.Header.Set("X-Forwarded-Host", fmt.Sprintf("secure.%s", BaseDomain)) req.Header.Set("X-Forwarded-Host", fmt.Sprintf("secure.%s", BaseDomain))
req.Header.Set("X-Forwarded-URI", "/") req.Header.Set("X-Forwarded-URI", "/")
req.Header.Set("Accept", "text/html; charset=utf8")
client := NewHTTPClient() client := NewHTTPClient()
res, err := client.Do(req) res, err := client.Do(req)
@ -144,7 +147,7 @@ func (s *StandaloneSuite) TestShouldRespondWithCorrectStatusCode() {
s.Assert().NoError(err) s.Assert().NoError(err)
urlEncodedAdminURL := url.QueryEscape(SecureBaseURL + "/") 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") req.Header.Set("X-Forwarded-Method", "POST")
@ -155,15 +158,16 @@ func (s *StandaloneSuite) TestShouldRespondWithCorrectStatusCode() {
s.Assert().NoError(err) s.Assert().NoError(err)
urlEncodedAdminURL = url.QueryEscape(SecureBaseURL + "/") 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. // Standard case using nginx.
func (s *StandaloneSuite) TestShouldVerifyAPIVerifyUnauthorize() { func (s *StandaloneSuite) TestShouldVerifyAPIVerifyUnauthorized() {
req, err := http.NewRequest("GET", fmt.Sprintf("%s/api/verify", AutheliaBaseURL), nil) req, err := http.NewRequest("GET", fmt.Sprintf("%s/api/verify", AutheliaBaseURL), nil)
s.Assert().NoError(err) s.Assert().NoError(err)
req.Header.Set("X-Forwarded-Proto", "https") req.Header.Set("X-Forwarded-Proto", "https")
req.Header.Set("X-Original-URL", AdminBaseURL) req.Header.Set("X-Original-URL", AdminBaseURL)
req.Header.Set("Accept", "text/html; charset=utf8")
client := NewHTTPClient() client := NewHTTPClient()
res, err := client.Do(req) res, err := client.Do(req)
@ -171,7 +175,7 @@ func (s *StandaloneSuite) TestShouldVerifyAPIVerifyUnauthorize() {
s.Assert().Equal(res.StatusCode, 401) s.Assert().Equal(res.StatusCode, 401)
body, err := ioutil.ReadAll(res.Body) body, err := ioutil.ReadAll(res.Body)
s.Assert().NoError(err) s.Assert().NoError(err)
s.Assert().Equal(string(body), "Unauthorized") s.Assert().Equal("Unauthorized", string(body))
} }
// Standard case using Kubernetes. // Standard case using Kubernetes.
@ -180,6 +184,7 @@ func (s *StandaloneSuite) TestShouldVerifyAPIVerifyRedirectFromXOriginalURL() {
s.Assert().NoError(err) s.Assert().NoError(err)
req.Header.Set("X-Forwarded-Proto", "https") req.Header.Set("X-Forwarded-Proto", "https")
req.Header.Set("X-Original-URL", AdminBaseURL) req.Header.Set("X-Original-URL", AdminBaseURL)
req.Header.Set("Accept", "text/html; charset=utf8")
client := NewHTTPClient() client := NewHTTPClient()
res, err := client.Do(req) res, err := client.Do(req)
@ -189,7 +194,7 @@ func (s *StandaloneSuite) TestShouldVerifyAPIVerifyRedirectFromXOriginalURL() {
s.Assert().NoError(err) s.Assert().NoError(err)
urlEncodedAdminURL := url.QueryEscape(AdminBaseURL) 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() { func (s *StandaloneSuite) TestShouldVerifyAPIVerifyRedirectFromXOriginalHostURI() {
@ -198,6 +203,7 @@ func (s *StandaloneSuite) TestShouldVerifyAPIVerifyRedirectFromXOriginalHostURI(
req.Header.Set("X-Forwarded-Proto", "https") req.Header.Set("X-Forwarded-Proto", "https")
req.Header.Set("X-Forwarded-Host", "secure.example.com:8080") req.Header.Set("X-Forwarded-Host", "secure.example.com:8080")
req.Header.Set("X-Forwarded-URI", "/") req.Header.Set("X-Forwarded-URI", "/")
req.Header.Set("Accept", "text/html; charset=utf8")
client := NewHTTPClient() client := NewHTTPClient()
res, err := client.Do(req) res, err := client.Do(req)
@ -207,7 +213,7 @@ func (s *StandaloneSuite) TestShouldVerifyAPIVerifyRedirectFromXOriginalHostURI(
s.Assert().NoError(err) s.Assert().NoError(err)
urlEncodedAdminURL := url.QueryEscape(SecureBaseURL + "/") 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() { func (s *StandaloneSuite) TestStandaloneWebDriverScenario() {

View File

@ -3,6 +3,7 @@ package utils
import ( import (
"errors" "errors"
"regexp" "regexp"
"strings"
"time" "time"
) )
@ -54,3 +55,11 @@ var AlphaNumericCharacters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQ
// ErrTLSVersionNotSupported returned when an unknown TLS version supplied. // ErrTLSVersionNotSupported returned when an unknown TLS version supplied.
var ErrTLSVersionNotSupported = errors.New("supplied TLS version isn't supported") 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) return string(b)
} }
// StringHTMLEscape escapes chars for a HTML body.
func StringHTMLEscape(input string) (output string) {
return htmlEscaper.Replace(input)
}