mirror of
https://github.com/0rangebananaspy/authelia.git
synced 2024-09-14 22:47:21 +07:00
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:
parent
7a4779b08e
commit
911d71204f
|
@ -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",
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"},
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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))
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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&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&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&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())
|
||||||
|
|
|
@ -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)))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -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("://")
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
11
internal/server/options_handler.go
Normal file
11
internal/server/options_handler.go
Normal 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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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(
|
||||||
|
"&", "&",
|
||||||
|
"<", "<",
|
||||||
|
">", ">",
|
||||||
|
`"`, """,
|
||||||
|
"'", "'",
|
||||||
|
)
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user