[FIX] Fix default redirection URL not taken into account (#600)
* Remove unused mongo docker-compose file. * Default redirection URL was not taken into account. * Fix possible storage options in config template. * Remove useless checks in u2f registration endpoints. * Add default redirection url in config of duo suite. * Fix log line in response handler of 2FA methods. * Fix integration tests. Co-authored-by: Amir Zarrinkafsh <nightah@me.com>pull/606/head^2
parent
05592cbe2d
commit
ea9b408b70
|
@ -245,7 +245,7 @@ regulation:
|
||||||
|
|
||||||
# Configuration of the storage backend used to store data and secrets.
|
# Configuration of the storage backend used to store data and secrets.
|
||||||
#
|
#
|
||||||
# You must use only an available configuration: local, sql
|
# You must use only an available configuration: local, mysql, postgres
|
||||||
storage:
|
storage:
|
||||||
# The directory where the DB files will be saved
|
# The directory where the DB files will be saved
|
||||||
## local:
|
## local:
|
||||||
|
|
|
@ -2,6 +2,7 @@ package validator
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
"github.com/authelia/authelia/internal/configuration/schema"
|
"github.com/authelia/authelia/internal/configuration/schema"
|
||||||
)
|
)
|
||||||
|
@ -23,6 +24,13 @@ func Validate(configuration *schema.Configuration, validator *schema.StructValid
|
||||||
configuration.LogsLevel = defaultLogsLevel
|
configuration.LogsLevel = defaultLogsLevel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if configuration.DefaultRedirectionURL != "" {
|
||||||
|
_, err := url.ParseRequestURI(configuration.DefaultRedirectionURL)
|
||||||
|
if err != nil {
|
||||||
|
validator.Push(fmt.Errorf("Unable to parse default redirection url"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if configuration.JWTSecret == "" {
|
if configuration.JWTSecret == "" {
|
||||||
validator.Push(fmt.Errorf("Provide a JWT secret using `jwt_secret` key"))
|
validator.Push(fmt.Errorf("Provide a JWT secret using `jwt_secret` key"))
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"github.com/authelia/authelia/internal/middlewares"
|
"github.com/authelia/authelia/internal/middlewares"
|
||||||
"github.com/authelia/authelia/internal/regulation"
|
"github.com/authelia/authelia/internal/regulation"
|
||||||
"github.com/authelia/authelia/internal/session"
|
"github.com/authelia/authelia/internal/session"
|
||||||
|
"github.com/authelia/authelia/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FirstFactorPost is the handler performing the first factory.
|
// FirstFactorPost is the handler performing the first factory.
|
||||||
|
@ -78,7 +79,7 @@ func FirstFactorPost(ctx *middlewares.AutheliaCtx) {
|
||||||
|
|
||||||
// set the cookie to expire in 1 year if "Remember me" was ticked.
|
// set the cookie to expire in 1 year if "Remember me" was ticked.
|
||||||
if *bodyJSON.KeepMeLoggedIn {
|
if *bodyJSON.KeepMeLoggedIn {
|
||||||
err = ctx.Providers.SessionProvider.UpdateExpiration(ctx.RequestCtx, time.Duration(31556952 * time.Second))
|
err = ctx.Providers.SessionProvider.UpdateExpiration(ctx.RequestCtx, time.Duration(31556952*time.Second))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Error(fmt.Errorf("Unable to update expiration timer for user %s: %s", bodyJSON.Username, err), authenticationFailedMessage)
|
ctx.Error(fmt.Errorf("Unable to update expiration timer for user %s: %s", bodyJSON.Username, err), authenticationFailedMessage)
|
||||||
return
|
return
|
||||||
|
@ -124,7 +125,7 @@ func FirstFactorPost(ctx *middlewares.AutheliaCtx) {
|
||||||
|
|
||||||
ctx.Logger.Debugf("Required level for the URL %s is %d", targetURL.String(), requiredLevel)
|
ctx.Logger.Debugf("Required level for the URL %s is %d", targetURL.String(), requiredLevel)
|
||||||
|
|
||||||
safeRedirection := isRedirectionSafe(*targetURL, ctx.Configuration.Session.Domain)
|
safeRedirection := utils.IsRedirectionSafe(*targetURL, ctx.Configuration.Session.Domain)
|
||||||
|
|
||||||
if safeRedirection && requiredLevel <= authorization.OneFactor {
|
if safeRedirection && requiredLevel <= authorization.OneFactor {
|
||||||
ctx.Logger.Debugf("Redirection is safe, redirecting...")
|
ctx.Logger.Debugf("Redirection is safe, redirecting...")
|
||||||
|
|
|
@ -45,14 +45,7 @@ func secondFactorU2FIdentityFinish(ctx *middlewares.AutheliaCtx, username string
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
request := u2f.NewWebRegisterRequest(challenge, []u2f.Registration{})
|
ctx.SetJSONBody(u2f.NewWebRegisterRequest(challenge, []u2f.Registration{}))
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
ctx.Error(fmt.Errorf("Unable to generate new U2F request for registration: %s", err), operationFailedMessage)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.SetJSONBody(request)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SecondFactorU2FIdentityFinish the handler for finishing the identity validation
|
// SecondFactorU2FIdentityFinish the handler for finishing the identity validation
|
||||||
|
|
|
@ -33,11 +33,6 @@ func SecondFactorU2FRegister(ctx *middlewares.AutheliaCtx) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
ctx.Error(fmt.Errorf("Unable to marshal U2F registration data: %v", err), unableToRegisterSecurityKeyMessage)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Logger.Debugf("Register U2F device for user %s", userSession.Username)
|
ctx.Logger.Debugf("Register U2F device for user %s", userSession.Username)
|
||||||
|
|
||||||
publicKey := elliptic.Marshal(elliptic.P256(), registration.PubKey.X, registration.PubKey.Y)
|
publicKey := elliptic.Marshal(elliptic.P256(), registration.PubKey.X, registration.PubKey.Y)
|
||||||
|
|
|
@ -51,21 +51,6 @@ func SecondFactorDuoPost(duoAPI duo.API) middlewares.RequestHandler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if requestBody.TargetURL != "" {
|
HandleAuthResponse(ctx, requestBody.TargetURL)
|
||||||
targetURL, err := url.ParseRequestURI(requestBody.TargetURL)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
ctx.Error(fmt.Errorf("Unable to parse target URL: %s", err), mfaValidationFailedMessage)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if targetURL != nil && isRedirectionSafe(*targetURL, ctx.Configuration.Session.Domain) {
|
|
||||||
ctx.SetJSONBody(redirectResponse{Redirect: requestBody.TargetURL})
|
|
||||||
} else {
|
|
||||||
ctx.ReplyOK()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ctx.ReplyOK()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -92,6 +93,80 @@ func (s *SecondFactorDuoPostSuite) TestShouldCallDuoAPIAndFail() {
|
||||||
s.mock.Assert200KO(s.T(), "Authentication failed, please retry later.")
|
s.mock.Assert200KO(s.T(), "Authentication failed, please retry later.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SecondFactorDuoPostSuite) TestShouldRedirectUserToDefaultURL() {
|
||||||
|
duoMock := mocks.NewMockAPI(s.mock.Ctrl)
|
||||||
|
|
||||||
|
response := duo.Response{}
|
||||||
|
response.Response.Result = "allow"
|
||||||
|
|
||||||
|
duoMock.EXPECT().Call(gomock.Any()).Return(&response, nil)
|
||||||
|
|
||||||
|
s.mock.Ctx.Configuration.DefaultRedirectionURL = "http://redirection.local"
|
||||||
|
|
||||||
|
bodyBytes, err := json.Marshal(signDuoRequestBody{})
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.mock.Ctx.Request.SetBody(bodyBytes)
|
||||||
|
|
||||||
|
SecondFactorDuoPost(duoMock)(s.mock.Ctx)
|
||||||
|
s.mock.Assert200OK(s.T(), redirectResponse{
|
||||||
|
Redirect: "http://redirection.local",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SecondFactorDuoPostSuite) TestShouldNotReturnRedirectURL() {
|
||||||
|
duoMock := mocks.NewMockAPI(s.mock.Ctrl)
|
||||||
|
|
||||||
|
response := duo.Response{}
|
||||||
|
response.Response.Result = "allow"
|
||||||
|
|
||||||
|
duoMock.EXPECT().Call(gomock.Any()).Return(&response, nil)
|
||||||
|
|
||||||
|
bodyBytes, err := json.Marshal(signDuoRequestBody{})
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.mock.Ctx.Request.SetBody(bodyBytes)
|
||||||
|
|
||||||
|
SecondFactorDuoPost(duoMock)(s.mock.Ctx)
|
||||||
|
s.mock.Assert200OK(s.T(), nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SecondFactorDuoPostSuite) TestShouldRedirectUserToSafeTargetURL() {
|
||||||
|
duoMock := mocks.NewMockAPI(s.mock.Ctrl)
|
||||||
|
|
||||||
|
response := duo.Response{}
|
||||||
|
response.Response.Result = "allow"
|
||||||
|
|
||||||
|
duoMock.EXPECT().Call(gomock.Any()).Return(&response, nil)
|
||||||
|
|
||||||
|
bodyBytes, err := json.Marshal(signDuoRequestBody{
|
||||||
|
TargetURL: "https://mydomain.local",
|
||||||
|
})
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.mock.Ctx.Request.SetBody(bodyBytes)
|
||||||
|
|
||||||
|
SecondFactorDuoPost(duoMock)(s.mock.Ctx)
|
||||||
|
s.mock.Assert200OK(s.T(), redirectResponse{
|
||||||
|
Redirect: "https://mydomain.local",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SecondFactorDuoPostSuite) TestShouldNotRedirectToUnsafeURL() {
|
||||||
|
duoMock := mocks.NewMockAPI(s.mock.Ctrl)
|
||||||
|
|
||||||
|
response := duo.Response{}
|
||||||
|
response.Response.Result = "allow"
|
||||||
|
|
||||||
|
duoMock.EXPECT().Call(gomock.Any()).Return(&response, nil)
|
||||||
|
|
||||||
|
bodyBytes, err := json.Marshal(signDuoRequestBody{
|
||||||
|
TargetURL: "http://mydomain.local",
|
||||||
|
})
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.mock.Ctx.Request.SetBody(bodyBytes)
|
||||||
|
|
||||||
|
SecondFactorDuoPost(duoMock)(s.mock.Ctx)
|
||||||
|
s.mock.Assert200OK(s.T(), nil)
|
||||||
|
}
|
||||||
|
|
||||||
func TestRunSecondFactorDuoPostSuite(t *testing.T) {
|
func TestRunSecondFactorDuoPostSuite(t *testing.T) {
|
||||||
s := new(SecondFactorDuoPostSuite)
|
s := new(SecondFactorDuoPostSuite)
|
||||||
suite.Run(t, s)
|
suite.Run(t, s)
|
||||||
|
|
|
@ -2,58 +2,44 @@ package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
|
||||||
|
|
||||||
"github.com/authelia/authelia/internal/authentication"
|
"github.com/authelia/authelia/internal/authentication"
|
||||||
"github.com/authelia/authelia/internal/middlewares"
|
"github.com/authelia/authelia/internal/middlewares"
|
||||||
"github.com/pquerna/otp/totp"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// SecondFactorTOTPPost validate the TOTP passcode provided by the user.
|
// SecondFactorTOTPPost validate the TOTP passcode provided by the user.
|
||||||
func SecondFactorTOTPPost(ctx *middlewares.AutheliaCtx) {
|
func SecondFactorTOTPPost(totpVerifier TOTPVerifier) middlewares.RequestHandler {
|
||||||
bodyJSON := signTOTPRequestBody{}
|
return func(ctx *middlewares.AutheliaCtx) {
|
||||||
err := ctx.ParseBody(&bodyJSON)
|
bodyJSON := signTOTPRequestBody{}
|
||||||
|
err := ctx.ParseBody(&bodyJSON)
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
ctx.Error(err, mfaValidationFailedMessage)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
userSession := ctx.GetSession()
|
|
||||||
secret, err := ctx.Providers.StorageProvider.LoadTOTPSecret(userSession.Username)
|
|
||||||
if err != nil {
|
|
||||||
ctx.Error(fmt.Errorf("Unable to load TOTP secret: %s", err), mfaValidationFailedMessage)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
isValid := totp.Validate(bodyJSON.Token, secret)
|
|
||||||
|
|
||||||
if !isValid {
|
|
||||||
ctx.Error(fmt.Errorf("Wrong passcode during TOTP validation for user %s", userSession.Username), mfaValidationFailedMessage)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
userSession.AuthenticationLevel = authentication.TwoFactor
|
|
||||||
err = ctx.SaveSession(userSession)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
ctx.Error(fmt.Errorf("Unable to update the authentication level with TOTP: %s", err), mfaValidationFailedMessage)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if bodyJSON.TargetURL != "" {
|
|
||||||
targetURL, err := url.ParseRequestURI(bodyJSON.TargetURL)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Error(fmt.Errorf("Unable to parse URL with TOTP: %s", err), mfaValidationFailedMessage)
|
ctx.Error(err, mfaValidationFailedMessage)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if targetURL != nil && isRedirectionSafe(*targetURL, ctx.Configuration.Session.Domain) {
|
userSession := ctx.GetSession()
|
||||||
ctx.SetJSONBody(redirectResponse{bodyJSON.TargetURL})
|
secret, err := ctx.Providers.StorageProvider.LoadTOTPSecret(userSession.Username)
|
||||||
} else {
|
if err != nil {
|
||||||
ctx.ReplyOK()
|
ctx.Error(fmt.Errorf("Unable to load TOTP secret: %s", err), mfaValidationFailedMessage)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
ctx.ReplyOK()
|
isValid := totpVerifier.Verify(bodyJSON.Token, secret)
|
||||||
|
|
||||||
|
if !isValid {
|
||||||
|
ctx.Error(fmt.Errorf("Wrong passcode during TOTP validation for user %s", userSession.Username), mfaValidationFailedMessage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userSession.AuthenticationLevel = authentication.TwoFactor
|
||||||
|
err = ctx.SaveSession(userSession)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(fmt.Errorf("Unable to update the authentication level with TOTP: %s", err), mfaValidationFailedMessage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
HandleAuthResponse(ctx, bodyJSON.TargetURL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,127 @@
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/authelia/authelia/internal/mocks"
|
||||||
|
"github.com/authelia/authelia/internal/session"
|
||||||
|
"github.com/golang/mock/gomock"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
"github.com/tstranex/u2f"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HandlerSignTOTPSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
|
||||||
|
mock *mocks.MockAutheliaCtx
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *HandlerSignTOTPSuite) SetupTest() {
|
||||||
|
s.mock = mocks.NewMockAutheliaCtx(s.T())
|
||||||
|
userSession := s.mock.Ctx.GetSession()
|
||||||
|
userSession.Username = "john"
|
||||||
|
userSession.U2FChallenge = &u2f.Challenge{}
|
||||||
|
userSession.U2FRegistration = &session.U2FRegistration{}
|
||||||
|
s.mock.Ctx.SaveSession(userSession)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *HandlerSignTOTPSuite) TearDownTest() {
|
||||||
|
s.mock.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *HandlerSignTOTPSuite) TestShouldRedirectUserToDefaultURL() {
|
||||||
|
verifier := NewMockTOTPVerifier(s.mock.Ctrl)
|
||||||
|
|
||||||
|
s.mock.StorageProviderMock.EXPECT().
|
||||||
|
LoadTOTPSecret(gomock.Any()).
|
||||||
|
Return("secret", nil)
|
||||||
|
|
||||||
|
verifier.EXPECT().
|
||||||
|
Verify(gomock.Eq("abc"), gomock.Eq("secret")).
|
||||||
|
Return(true)
|
||||||
|
|
||||||
|
s.mock.Ctx.Configuration.DefaultRedirectionURL = "http://redirection.local"
|
||||||
|
|
||||||
|
bodyBytes, err := json.Marshal(signTOTPRequestBody{
|
||||||
|
Token: "abc",
|
||||||
|
})
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.mock.Ctx.Request.SetBody(bodyBytes)
|
||||||
|
|
||||||
|
SecondFactorTOTPPost(verifier)(s.mock.Ctx)
|
||||||
|
s.mock.Assert200OK(s.T(), redirectResponse{
|
||||||
|
Redirect: "http://redirection.local",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *HandlerSignTOTPSuite) TestShouldNotReturnRedirectURL() {
|
||||||
|
verifier := NewMockTOTPVerifier(s.mock.Ctrl)
|
||||||
|
|
||||||
|
s.mock.StorageProviderMock.EXPECT().
|
||||||
|
LoadTOTPSecret(gomock.Any()).
|
||||||
|
Return("secret", nil)
|
||||||
|
|
||||||
|
verifier.EXPECT().
|
||||||
|
Verify(gomock.Eq("abc"), gomock.Eq("secret")).
|
||||||
|
Return(true)
|
||||||
|
|
||||||
|
bodyBytes, err := json.Marshal(signTOTPRequestBody{
|
||||||
|
Token: "abc",
|
||||||
|
})
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.mock.Ctx.Request.SetBody(bodyBytes)
|
||||||
|
|
||||||
|
SecondFactorTOTPPost(verifier)(s.mock.Ctx)
|
||||||
|
s.mock.Assert200OK(s.T(), nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *HandlerSignTOTPSuite) TestShouldRedirectUserToSafeTargetURL() {
|
||||||
|
verifier := NewMockTOTPVerifier(s.mock.Ctrl)
|
||||||
|
|
||||||
|
s.mock.StorageProviderMock.EXPECT().
|
||||||
|
LoadTOTPSecret(gomock.Any()).
|
||||||
|
Return("secret", nil)
|
||||||
|
|
||||||
|
verifier.EXPECT().
|
||||||
|
Verify(gomock.Eq("abc"), gomock.Eq("secret")).
|
||||||
|
Return(true)
|
||||||
|
|
||||||
|
bodyBytes, err := json.Marshal(signTOTPRequestBody{
|
||||||
|
Token: "abc",
|
||||||
|
TargetURL: "https://mydomain.local",
|
||||||
|
})
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.mock.Ctx.Request.SetBody(bodyBytes)
|
||||||
|
|
||||||
|
SecondFactorTOTPPost(verifier)(s.mock.Ctx)
|
||||||
|
s.mock.Assert200OK(s.T(), redirectResponse{
|
||||||
|
Redirect: "https://mydomain.local",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *HandlerSignTOTPSuite) TestShouldNotRedirectToUnsafeURL() {
|
||||||
|
verifier := NewMockTOTPVerifier(s.mock.Ctrl)
|
||||||
|
|
||||||
|
s.mock.StorageProviderMock.EXPECT().
|
||||||
|
LoadTOTPSecret(gomock.Any()).
|
||||||
|
Return("secret", nil)
|
||||||
|
|
||||||
|
verifier.EXPECT().
|
||||||
|
Verify(gomock.Eq("abc"), gomock.Eq("secret")).
|
||||||
|
Return(true)
|
||||||
|
|
||||||
|
bodyBytes, err := json.Marshal(signTOTPRequestBody{
|
||||||
|
Token: "abc",
|
||||||
|
TargetURL: "http://mydomain.local",
|
||||||
|
})
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.mock.Ctx.Request.SetBody(bodyBytes)
|
||||||
|
|
||||||
|
SecondFactorTOTPPost(verifier)(s.mock.Ctx)
|
||||||
|
s.mock.Assert200OK(s.T(), nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunHandlerSignTOTPSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(HandlerSignTOTPSuite))
|
||||||
|
}
|
|
@ -1,74 +1,53 @@
|
||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/elliptic"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
|
||||||
|
|
||||||
"github.com/authelia/authelia/internal/authentication"
|
"github.com/authelia/authelia/internal/authentication"
|
||||||
"github.com/authelia/authelia/internal/middlewares"
|
"github.com/authelia/authelia/internal/middlewares"
|
||||||
"github.com/tstranex/u2f"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// SecondFactorU2FSignPost handler for completing a signing request.
|
// SecondFactorU2FSignPost handler for completing a signing request.
|
||||||
func SecondFactorU2FSignPost(ctx *middlewares.AutheliaCtx) {
|
func SecondFactorU2FSignPost(u2fVerifier U2FVerifier) middlewares.RequestHandler {
|
||||||
var requestBody signU2FRequestBody
|
return func(ctx *middlewares.AutheliaCtx) {
|
||||||
err := ctx.ParseBody(&requestBody)
|
var requestBody signU2FRequestBody
|
||||||
|
err := ctx.ParseBody(&requestBody)
|
||||||
if err != nil {
|
|
||||||
ctx.Error(err, mfaValidationFailedMessage)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
userSession := ctx.GetSession()
|
|
||||||
if userSession.U2FChallenge == nil {
|
|
||||||
ctx.Error(fmt.Errorf("U2F signing has not been initiated yet (no challenge)"), mfaValidationFailedMessage)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if userSession.U2FRegistration == nil {
|
|
||||||
ctx.Error(fmt.Errorf("U2F signing has not been initiated yet (no registration)"), mfaValidationFailedMessage)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var registration u2f.Registration
|
|
||||||
registration.KeyHandle = userSession.U2FRegistration.KeyHandle
|
|
||||||
x, y := elliptic.Unmarshal(elliptic.P256(), userSession.U2FRegistration.PublicKey)
|
|
||||||
registration.PubKey.Curve = elliptic.P256()
|
|
||||||
registration.PubKey.X = x
|
|
||||||
registration.PubKey.Y = y
|
|
||||||
|
|
||||||
// TODO(c.michaud): store the counter to help detecting cloned U2F keys.
|
|
||||||
_, err = registration.Authenticate(
|
|
||||||
requestBody.SignResponse, *userSession.U2FChallenge, 0)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
ctx.Error(err, mfaValidationFailedMessage)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
userSession.AuthenticationLevel = authentication.TwoFactor
|
|
||||||
err = ctx.SaveSession(userSession)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
ctx.Error(fmt.Errorf("Unable to update authentication level with U2F: %s", err), mfaValidationFailedMessage)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if requestBody.TargetURL != "" {
|
|
||||||
targetURL, err := url.ParseRequestURI(requestBody.TargetURL)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Error(fmt.Errorf("Unable to parse target URL with U2F: %s", err), mfaValidationFailedMessage)
|
ctx.Error(err, mfaValidationFailedMessage)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if targetURL != nil && isRedirectionSafe(*targetURL, ctx.Configuration.Session.Domain) {
|
userSession := ctx.GetSession()
|
||||||
ctx.SetJSONBody(redirectResponse{Redirect: requestBody.TargetURL})
|
if userSession.U2FChallenge == nil {
|
||||||
} else {
|
ctx.Error(fmt.Errorf("U2F signing has not been initiated yet (no challenge)"), mfaValidationFailedMessage)
|
||||||
ctx.ReplyOK()
|
return
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
ctx.ReplyOK()
|
if userSession.U2FRegistration == nil {
|
||||||
|
ctx.Error(fmt.Errorf("U2F signing has not been initiated yet (no registration)"), mfaValidationFailedMessage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = u2fVerifier.Verify(
|
||||||
|
userSession.U2FRegistration.KeyHandle,
|
||||||
|
userSession.U2FRegistration.PublicKey,
|
||||||
|
requestBody.SignResponse,
|
||||||
|
*userSession.U2FChallenge)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(err, mfaValidationFailedMessage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userSession.AuthenticationLevel = authentication.TwoFactor
|
||||||
|
err = ctx.SaveSession(userSession)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(fmt.Errorf("Unable to update authentication level with U2F: %s", err), mfaValidationFailedMessage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
HandleAuthResponse(ctx, requestBody.TargetURL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,111 @@
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/authelia/authelia/internal/mocks"
|
||||||
|
"github.com/authelia/authelia/internal/session"
|
||||||
|
"github.com/golang/mock/gomock"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
"github.com/tstranex/u2f"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HandlerSignU2FStep2Suite struct {
|
||||||
|
suite.Suite
|
||||||
|
|
||||||
|
mock *mocks.MockAutheliaCtx
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *HandlerSignU2FStep2Suite) SetupTest() {
|
||||||
|
s.mock = mocks.NewMockAutheliaCtx(s.T())
|
||||||
|
userSession := s.mock.Ctx.GetSession()
|
||||||
|
userSession.Username = "john"
|
||||||
|
userSession.U2FChallenge = &u2f.Challenge{}
|
||||||
|
userSession.U2FRegistration = &session.U2FRegistration{}
|
||||||
|
s.mock.Ctx.SaveSession(userSession)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *HandlerSignU2FStep2Suite) TearDownTest() {
|
||||||
|
s.mock.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *HandlerSignU2FStep2Suite) TestShouldRedirectUserToDefaultURL() {
|
||||||
|
u2fVerifier := NewMockU2FVerifier(s.mock.Ctrl)
|
||||||
|
|
||||||
|
u2fVerifier.EXPECT().
|
||||||
|
Verify(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||||||
|
Return(nil)
|
||||||
|
|
||||||
|
s.mock.Ctx.Configuration.DefaultRedirectionURL = "http://redirection.local"
|
||||||
|
|
||||||
|
bodyBytes, err := json.Marshal(signU2FRequestBody{
|
||||||
|
SignResponse: u2f.SignResponse{},
|
||||||
|
})
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.mock.Ctx.Request.SetBody(bodyBytes)
|
||||||
|
|
||||||
|
SecondFactorU2FSignPost(u2fVerifier)(s.mock.Ctx)
|
||||||
|
s.mock.Assert200OK(s.T(), redirectResponse{
|
||||||
|
Redirect: "http://redirection.local",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *HandlerSignU2FStep2Suite) TestShouldNotReturnRedirectURL() {
|
||||||
|
u2fVerifier := NewMockU2FVerifier(s.mock.Ctrl)
|
||||||
|
|
||||||
|
u2fVerifier.EXPECT().
|
||||||
|
Verify(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||||||
|
Return(nil)
|
||||||
|
|
||||||
|
bodyBytes, err := json.Marshal(signU2FRequestBody{
|
||||||
|
SignResponse: u2f.SignResponse{},
|
||||||
|
})
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.mock.Ctx.Request.SetBody(bodyBytes)
|
||||||
|
|
||||||
|
SecondFactorU2FSignPost(u2fVerifier)(s.mock.Ctx)
|
||||||
|
s.mock.Assert200OK(s.T(), nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *HandlerSignU2FStep2Suite) TestShouldRedirectUserToSafeTargetURL() {
|
||||||
|
u2fVerifier := NewMockU2FVerifier(s.mock.Ctrl)
|
||||||
|
|
||||||
|
u2fVerifier.EXPECT().
|
||||||
|
Verify(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||||||
|
Return(nil)
|
||||||
|
|
||||||
|
bodyBytes, err := json.Marshal(signU2FRequestBody{
|
||||||
|
SignResponse: u2f.SignResponse{},
|
||||||
|
TargetURL: "https://mydomain.local",
|
||||||
|
})
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.mock.Ctx.Request.SetBody(bodyBytes)
|
||||||
|
|
||||||
|
SecondFactorU2FSignPost(u2fVerifier)(s.mock.Ctx)
|
||||||
|
s.mock.Assert200OK(s.T(), redirectResponse{
|
||||||
|
Redirect: "https://mydomain.local",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *HandlerSignU2FStep2Suite) TestShouldNotRedirectToUnsafeURL() {
|
||||||
|
u2fVerifier := NewMockU2FVerifier(s.mock.Ctrl)
|
||||||
|
|
||||||
|
u2fVerifier.EXPECT().
|
||||||
|
Verify(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||||||
|
Return(nil)
|
||||||
|
|
||||||
|
bodyBytes, err := json.Marshal(signU2FRequestBody{
|
||||||
|
SignResponse: u2f.SignResponse{},
|
||||||
|
TargetURL: "http://mydomain.local",
|
||||||
|
})
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.mock.Ctx.Request.SetBody(bodyBytes)
|
||||||
|
|
||||||
|
SecondFactorU2FSignPost(u2fVerifier)(s.mock.Ctx)
|
||||||
|
s.mock.Assert200OK(s.T(), nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunHandlerSignU2FStep2Suite(t *testing.T) {
|
||||||
|
suite.Run(t, new(HandlerSignU2FStep2Suite))
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/authelia/authelia/internal/middlewares"
|
||||||
|
"github.com/authelia/authelia/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func HandleAuthResponse(ctx *middlewares.AutheliaCtx, targetURI string) {
|
||||||
|
if targetURI != "" {
|
||||||
|
targetURL, err := url.ParseRequestURI(targetURI)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(fmt.Errorf("Unable to parse target URL: %s", err), mfaValidationFailedMessage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if targetURL != nil && utils.IsRedirectionSafe(*targetURL, ctx.Configuration.Session.Domain) {
|
||||||
|
ctx.SetJSONBody(redirectResponse{Redirect: targetURI})
|
||||||
|
} else {
|
||||||
|
ctx.ReplyOK()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if ctx.Configuration.DefaultRedirectionURL != "" {
|
||||||
|
ctx.SetJSONBody(redirectResponse{Redirect: ctx.Configuration.DefaultRedirectionURL})
|
||||||
|
} else {
|
||||||
|
ctx.ReplyOK()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/pquerna/otp/totp"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TOTPVerifier interface {
|
||||||
|
Verify(token, secret string) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type TOTPVerifierImpl struct{}
|
||||||
|
|
||||||
|
func (tv *TOTPVerifierImpl) Verify(token, secret string) bool {
|
||||||
|
return totp.Validate(token, secret)
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
// Code generated by MockGen. DO NOT EDIT.
|
||||||
|
// Source: internal/handlers/totp.go
|
||||||
|
|
||||||
|
// Package handlers is a generated GoMock package.
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
gomock "github.com/golang/mock/gomock"
|
||||||
|
reflect "reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockTOTPVerifier is a mock of TOTPVerifier interface
|
||||||
|
type MockTOTPVerifier struct {
|
||||||
|
ctrl *gomock.Controller
|
||||||
|
recorder *MockTOTPVerifierMockRecorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockTOTPVerifierMockRecorder is the mock recorder for MockTOTPVerifier
|
||||||
|
type MockTOTPVerifierMockRecorder struct {
|
||||||
|
mock *MockTOTPVerifier
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMockTOTPVerifier creates a new mock instance
|
||||||
|
func NewMockTOTPVerifier(ctrl *gomock.Controller) *MockTOTPVerifier {
|
||||||
|
mock := &MockTOTPVerifier{ctrl: ctrl}
|
||||||
|
mock.recorder = &MockTOTPVerifierMockRecorder{mock}
|
||||||
|
return mock
|
||||||
|
}
|
||||||
|
|
||||||
|
// EXPECT returns an object that allows the caller to indicate expected use
|
||||||
|
func (m *MockTOTPVerifier) EXPECT() *MockTOTPVerifierMockRecorder {
|
||||||
|
return m.recorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify mocks base method
|
||||||
|
func (m *MockTOTPVerifier) Verify(token, secret string) bool {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "Verify", token, secret)
|
||||||
|
ret0, _ := ret[0].(bool)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify indicates an expected call of Verify
|
||||||
|
func (mr *MockTOTPVerifierMockRecorder) Verify(token, secret interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Verify", reflect.TypeOf((*MockTOTPVerifier)(nil).Verify), token, secret)
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/elliptic"
|
||||||
|
|
||||||
|
"github.com/tstranex/u2f"
|
||||||
|
)
|
||||||
|
|
||||||
|
type U2FVerifier interface {
|
||||||
|
Verify(keyHandle []byte, publicKey []byte, signResponse u2f.SignResponse, challenge u2f.Challenge) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type U2FVerifierImpl struct{}
|
||||||
|
|
||||||
|
func (uv *U2FVerifierImpl) Verify(keyHandle []byte, publicKey []byte,
|
||||||
|
signResponse u2f.SignResponse, challenge u2f.Challenge) error {
|
||||||
|
var registration u2f.Registration
|
||||||
|
registration.KeyHandle = keyHandle
|
||||||
|
x, y := elliptic.Unmarshal(elliptic.P256(), publicKey)
|
||||||
|
registration.PubKey.Curve = elliptic.P256()
|
||||||
|
registration.PubKey.X = x
|
||||||
|
registration.PubKey.Y = y
|
||||||
|
|
||||||
|
// TODO(c.michaud): store the counter to help detecting cloned U2F keys.
|
||||||
|
_, err := registration.Authenticate(
|
||||||
|
signResponse, challenge, 0)
|
||||||
|
return err
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
// Code generated by MockGen. DO NOT EDIT.
|
||||||
|
// Source: internal/handlers/u2f.go
|
||||||
|
|
||||||
|
// Package handlers is a generated GoMock package.
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
gomock "github.com/golang/mock/gomock"
|
||||||
|
u2f "github.com/tstranex/u2f"
|
||||||
|
reflect "reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockU2FVerifier is a mock of U2FVerifier interface
|
||||||
|
type MockU2FVerifier struct {
|
||||||
|
ctrl *gomock.Controller
|
||||||
|
recorder *MockU2FVerifierMockRecorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockU2FVerifierMockRecorder is the mock recorder for MockU2FVerifier
|
||||||
|
type MockU2FVerifierMockRecorder struct {
|
||||||
|
mock *MockU2FVerifier
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMockU2FVerifier creates a new mock instance
|
||||||
|
func NewMockU2FVerifier(ctrl *gomock.Controller) *MockU2FVerifier {
|
||||||
|
mock := &MockU2FVerifier{ctrl: ctrl}
|
||||||
|
mock.recorder = &MockU2FVerifierMockRecorder{mock}
|
||||||
|
return mock
|
||||||
|
}
|
||||||
|
|
||||||
|
// EXPECT returns an object that allows the caller to indicate expected use
|
||||||
|
func (m *MockU2FVerifier) EXPECT() *MockU2FVerifierMockRecorder {
|
||||||
|
return m.recorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify mocks base method
|
||||||
|
func (m *MockU2FVerifier) Verify(keyHandle, publicKey []byte, signResponse u2f.SignResponse, challenge u2f.Challenge) error {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "Verify", keyHandle, publicKey, signResponse, challenge)
|
||||||
|
ret0, _ := ret[0].(error)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify indicates an expected call of Verify
|
||||||
|
func (mr *MockU2FVerifierMockRecorder) Verify(keyHandle, publicKey, signResponse, challenge interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Verify", reflect.TypeOf((*MockU2FVerifier)(nil).Verify), keyHandle, publicKey, signResponse, challenge)
|
||||||
|
}
|
|
@ -93,7 +93,7 @@ type IdentityVerificationFinishBody struct {
|
||||||
// OKResponse model of a status OK response
|
// OKResponse model of a status OK response
|
||||||
type OKResponse struct {
|
type OKResponse struct {
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Data interface{} `json:"data"`
|
Data interface{} `json:"data,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErrorResponse model of an error response
|
// ErrorResponse model of an error response
|
||||||
|
|
|
@ -61,7 +61,7 @@ func StartServer(configuration schema.Configuration, providers middlewares.Provi
|
||||||
router.POST("/api/secondfactor/totp/identity/finish", autheliaMiddleware(
|
router.POST("/api/secondfactor/totp/identity/finish", autheliaMiddleware(
|
||||||
middlewares.RequireFirstFactor(handlers.SecondFactorTOTPIdentityFinish)))
|
middlewares.RequireFirstFactor(handlers.SecondFactorTOTPIdentityFinish)))
|
||||||
router.POST("/api/secondfactor/totp", autheliaMiddleware(
|
router.POST("/api/secondfactor/totp", autheliaMiddleware(
|
||||||
middlewares.RequireFirstFactor(handlers.SecondFactorTOTPPost)))
|
middlewares.RequireFirstFactor(handlers.SecondFactorTOTPPost(&handlers.TOTPVerifierImpl{}))))
|
||||||
|
|
||||||
// U2F related endpoints
|
// U2F related endpoints
|
||||||
router.POST("/api/secondfactor/u2f/identity/start", autheliaMiddleware(
|
router.POST("/api/secondfactor/u2f/identity/start", autheliaMiddleware(
|
||||||
|
@ -74,8 +74,9 @@ func StartServer(configuration schema.Configuration, providers middlewares.Provi
|
||||||
|
|
||||||
router.POST("/api/secondfactor/u2f/sign_request", autheliaMiddleware(
|
router.POST("/api/secondfactor/u2f/sign_request", autheliaMiddleware(
|
||||||
middlewares.RequireFirstFactor(handlers.SecondFactorU2FSignGet)))
|
middlewares.RequireFirstFactor(handlers.SecondFactorU2FSignGet)))
|
||||||
|
|
||||||
router.POST("/api/secondfactor/u2f/sign", autheliaMiddleware(
|
router.POST("/api/secondfactor/u2f/sign", autheliaMiddleware(
|
||||||
middlewares.RequireFirstFactor(handlers.SecondFactorU2FSignPost)))
|
middlewares.RequireFirstFactor(handlers.SecondFactorU2FSignPost(&handlers.U2FVerifierImpl{}))))
|
||||||
|
|
||||||
// Configure DUO api endpoint only if configuration exists
|
// Configure DUO api endpoint only if configuration exists
|
||||||
if configuration.DuoAPI != nil {
|
if configuration.DuoAPI != nil {
|
||||||
|
|
|
@ -6,6 +6,8 @@ port: 9091
|
||||||
|
|
||||||
logs_level: trace
|
logs_level: trace
|
||||||
|
|
||||||
|
default_redirection_url: https://home.example.com:8080/
|
||||||
|
|
||||||
jwt_secret: very_important_secret
|
jwt_secret: very_important_secret
|
||||||
|
|
||||||
authentication_backend:
|
authentication_backend:
|
||||||
|
|
|
@ -12,18 +12,6 @@ logs_level: debug
|
||||||
|
|
||||||
jwt_secret: unsecure_secret
|
jwt_secret: unsecure_secret
|
||||||
|
|
||||||
# Default redirection URL
|
|
||||||
#
|
|
||||||
# If user tries to authenticate without any referer, Authelia
|
|
||||||
# does not know where to redirect the user to at the end of the
|
|
||||||
# authentication process.
|
|
||||||
# This parameter allows you to specify the default redirection
|
|
||||||
# URL Authelia will use in such a case.
|
|
||||||
#
|
|
||||||
# Note: this parameter is optional. If not provided, user won't
|
|
||||||
# be redirected upon successful authentication.
|
|
||||||
default_redirection_url: https://home.example.com:8080/
|
|
||||||
|
|
||||||
# TOTP Issuer Name
|
# TOTP Issuer Name
|
||||||
#
|
#
|
||||||
# This will be the issuer name displayed in Google Authenticator
|
# This will be the issuer name displayed in Google Authenticator
|
||||||
|
|
|
@ -6,8 +6,6 @@ port: 9091
|
||||||
|
|
||||||
logs_level: debug
|
logs_level: debug
|
||||||
|
|
||||||
default_redirection_url: https://home.example.com:8080/
|
|
||||||
|
|
||||||
authentication_backend:
|
authentication_backend:
|
||||||
file:
|
file:
|
||||||
path: /var/lib/authelia/users.yml
|
path: /var/lib/authelia/users.yml
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
package suites
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DefaultRedirectionURLScenario struct {
|
||||||
|
*SeleniumSuite
|
||||||
|
|
||||||
|
secret string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDefaultRedirectionURLScenario() *DefaultRedirectionURLScenario {
|
||||||
|
return &DefaultRedirectionURLScenario{
|
||||||
|
SeleniumSuite: new(SeleniumSuite),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (drus *DefaultRedirectionURLScenario) SetupSuite() {
|
||||||
|
wds, err := StartWebDriver()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
drus.WebDriverSession = wds
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
targetURL := fmt.Sprintf("%s/secret.html", AdminBaseURL)
|
||||||
|
drus.secret = drus.doRegisterAndLogin2FA(ctx, drus.T(), "john", "password", false, targetURL)
|
||||||
|
drus.verifySecretAuthorized(ctx, drus.T())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (drus *DefaultRedirectionURLScenario) TearDownSuite() {
|
||||||
|
err := drus.WebDriverSession.Stop()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (drus *DefaultRedirectionURLScenario) SetupTest() {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
drus.doLogout(ctx, drus.T())
|
||||||
|
drus.doVisit(drus.T(), HomeBaseURL)
|
||||||
|
drus.verifyIsHome(ctx, drus.T())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (drus *DefaultRedirectionURLScenario) TestUserIsRedirectedToDefaultURL() {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
drus.doLoginTwoFactor(ctx, drus.T(), "john", "password", false, drus.secret, "")
|
||||||
|
drus.verifyURLIs(ctx, drus.T(), HomeBaseURL+"/")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShouldRunDefaultRedirectionURLScenario(t *testing.T) {
|
||||||
|
suite.Run(t, NewDefaultRedirectionURLScenario())
|
||||||
|
}
|
|
@ -36,14 +36,14 @@ func (s *DuoPushWebDriverSuite) TearDownSuite() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *DuoPushWebDriverSuite) SetupTest() {
|
func (s *DuoPushWebDriverSuite) SetupTest() {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
s.doLogout(ctx, s.T())
|
s.doLogout(ctx, s.T())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *DuoPushWebDriverSuite) TearDownTest() {
|
func (s *DuoPushWebDriverSuite) TearDownTest() {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
s.doChangeMethod(ctx, s.T(), "one-time-password")
|
s.doChangeMethod(ctx, s.T(), "one-time-password")
|
||||||
|
@ -72,6 +72,48 @@ func (s *DuoPushWebDriverSuite) TestShouldFailAuthentication() {
|
||||||
s.WaitElementLocatedByClassName(ctx, s.T(), "failure-icon")
|
s.WaitElementLocatedByClassName(ctx, s.T(), "failure-icon")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DuoPushDefaultRedirectionSuite struct {
|
||||||
|
*SeleniumSuite
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDuoPushDefaultRedirectionSuite() *DuoPushDefaultRedirectionSuite {
|
||||||
|
return &DuoPushDefaultRedirectionSuite{SeleniumSuite: new(SeleniumSuite)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DuoPushDefaultRedirectionSuite) SetupSuite() {
|
||||||
|
wds, err := StartWebDriver()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.WebDriverSession = wds
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DuoPushDefaultRedirectionSuite) TearDownSuite() {
|
||||||
|
err := s.WebDriverSession.Stop()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DuoPushDefaultRedirectionSuite) SetupTest() {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
s.doLogout(ctx, s.T())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DuoPushDefaultRedirectionSuite) TestUserIsRedirectedToDefaultURL() {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
s.doLoginOneFactor(ctx, s.T(), "john", "password", false, "")
|
||||||
|
s.doChangeMethod(ctx, s.T(), "push-notification")
|
||||||
|
s.verifyURLIs(ctx, s.T(), HomeBaseURL+"/")
|
||||||
|
}
|
||||||
|
|
||||||
type DuoPushSuite struct {
|
type DuoPushSuite struct {
|
||||||
suite.Suite
|
suite.Suite
|
||||||
}
|
}
|
||||||
|
@ -84,6 +126,10 @@ func (s *DuoPushSuite) TestDuoPushWebDriverSuite() {
|
||||||
suite.Run(s.T(), NewDuoPushWebDriverSuite())
|
suite.Run(s.T(), NewDuoPushWebDriverSuite())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *DuoPushSuite) TestDuoPushRedirectionURLSuite() {
|
||||||
|
suite.Run(s.T(), NewDuoPushDefaultRedirectionSuite())
|
||||||
|
}
|
||||||
|
|
||||||
func (s *DuoPushSuite) TestAvailableMethodsScenario() {
|
func (s *DuoPushSuite) TestAvailableMethodsScenario() {
|
||||||
suite.Run(s.T(), NewAvailableMethodsScenario([]string{
|
suite.Run(s.T(), NewAvailableMethodsScenario([]string{
|
||||||
"ONE-TIME PASSWORD",
|
"ONE-TIME PASSWORD",
|
||||||
|
|
|
@ -14,6 +14,10 @@ func NewShortTimeoutsSuite() *ShortTimeoutsSuite {
|
||||||
return &ShortTimeoutsSuite{SeleniumSuite: new(SeleniumSuite)}
|
return &ShortTimeoutsSuite{SeleniumSuite: new(SeleniumSuite)}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *ShortTimeoutsSuite) TestDefaultRedirectionURLScenario() {
|
||||||
|
suite.Run(s.T(), NewDefaultRedirectionURLScenario())
|
||||||
|
}
|
||||||
|
|
||||||
func (s *ShortTimeoutsSuite) TestInactivityScenario() {
|
func (s *ShortTimeoutsSuite) TestInactivityScenario() {
|
||||||
suite.Run(s.T(), NewInactivityScenario())
|
suite.Run(s.T(), NewInactivityScenario())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
package handlers
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func isRedirectionSafe(url url.URL, protectedDomain string) bool {
|
func IsRedirectionSafe(url url.URL, protectedDomain string) bool {
|
||||||
if url.Scheme != "https" {
|
if url.Scheme != "https" {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package handlers
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/url"
|
"net/url"
|
||||||
|
@ -9,7 +9,7 @@ import (
|
||||||
|
|
||||||
func isURLSafe(requestURI string, domain string) bool {
|
func isURLSafe(requestURI string, domain string) bool {
|
||||||
url, _ := url.ParseRequestURI(requestURI)
|
url, _ := url.ParseRequestURI(requestURI)
|
||||||
return isRedirectionSafe(*url, domain)
|
return IsRedirectionSafe(*url, domain)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestShouldReturnFalseOnBadScheme(t *testing.T) {
|
func TestShouldReturnFalseOnBadScheme(t *testing.T) {
|
Loading…
Reference in New Issue