[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
Clément Michaud 2020-02-01 13:54:50 +01:00 committed by GitHub
parent 05592cbe2d
commit ea9b408b70
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 690 additions and 152 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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