fix: user is now redirected when authenticated (#2082)
* fix(handlers,web): user is now redirected when authenticated Fix: #1788 * remove dead code and fix ci issues * fix infinite loop in frontend * fix issue with integration tests * handle bot recommendation * fix integration test & add dot to comment * fix last integration test * Update api/openapi.yml Co-authored-by: James Elliott <james-d-elliott@users.noreply.github.com> * Update web/src/services/SafeRedirection.ts Co-authored-by: Amir Zarrinkafsh <nightah@me.com> * Update web/src/services/SafeRedirection.ts Co-authored-by: Amir Zarrinkafsh <nightah@me.com> * Update api/openapi.yml * Update openapi.yml * refactor: valid -> safe * refactor: adjust merge conflicts * Apply suggestions from code review Co-authored-by: James Elliott <james-d-elliott@users.noreply.github.com> * fix: adjust test return messaging Co-authored-by: James Elliott <james-d-elliott@users.noreply.github.com> Co-authored-by: Amir Zarrinkafsh <nightah@me.com>pull/2223/head^2
parent
03274c171e
commit
bc983ce9f5
|
@ -166,13 +166,6 @@ paths:
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: Successful Operation
|
description: Successful Operation
|
||||||
headers:
|
|
||||||
Set-Cookie:
|
|
||||||
style: simple
|
|
||||||
explode: false
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
example: authelia_session=kTTCSLupEUirZVfLeZTijezewFQnNOgs; Path=/
|
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
|
@ -181,6 +174,30 @@ paths:
|
||||||
description: Unauthorized
|
description: Unauthorized
|
||||||
security:
|
security:
|
||||||
- authelia_auth: []
|
- authelia_auth: []
|
||||||
|
/api/checks/safe-redirection:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- Authentication
|
||||||
|
summary: Check whether URI is safe to redirect to.
|
||||||
|
description: >
|
||||||
|
End users usually needs to be redirected to a target website after authentication. This endpoint aims to check
|
||||||
|
if target URL is safe to redirect to. This prevents open redirect attacks.
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/handlers.checkURIWithinDomainRequestBody'
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Successful Operation
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/handlers.checkURIWithinDomainResponseBody'
|
||||||
|
"401":
|
||||||
|
description: Unauthorized
|
||||||
|
security:
|
||||||
|
- authelia_auth: []
|
||||||
/api/logout:
|
/api/logout:
|
||||||
post:
|
post:
|
||||||
tags:
|
tags:
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/authelia/authelia/internal/authentication"
|
||||||
|
"github.com/authelia/authelia/internal/middlewares"
|
||||||
|
"github.com/authelia/authelia/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CheckSafeRedirection handler checking whether the redirection to a given URL provided in body is safe.
|
||||||
|
func CheckSafeRedirection(ctx *middlewares.AutheliaCtx) {
|
||||||
|
userSession := ctx.GetSession()
|
||||||
|
|
||||||
|
if userSession.AuthenticationLevel == authentication.NotAuthenticated {
|
||||||
|
ctx.ReplyUnauthorized()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var reqBody checkURIWithinDomainRequestBody
|
||||||
|
|
||||||
|
err := ctx.ParseBody(&reqBody)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(fmt.Errorf("Unable to parse request body: %w", err), messageOperationFailed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
safe, err := utils.IsRedirectionURISafe(reqBody.URI, ctx.Configuration.Session.Domain)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(fmt.Errorf("Unable to determine if uri %s is safe to redirect to: %w", reqBody.URI, err), messageOperationFailed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ctx.SetJSONBody(checkURIWithinDomainResponseBody{
|
||||||
|
OK: safe,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(fmt.Errorf("Unable to create response body: %w", err), messageOperationFailed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/authelia/authelia/internal/authentication"
|
||||||
|
"github.com/authelia/authelia/internal/mocks"
|
||||||
|
"github.com/authelia/authelia/internal/session"
|
||||||
|
)
|
||||||
|
|
||||||
|
var exampleDotComDomain = "example.com"
|
||||||
|
|
||||||
|
func TestCheckSafeRedirection_ForbiddenCall(t *testing.T) {
|
||||||
|
mock := mocks.NewMockAutheliaCtxWithUserSession(t, session.UserSession{
|
||||||
|
Username: "john",
|
||||||
|
AuthenticationLevel: authentication.NotAuthenticated,
|
||||||
|
})
|
||||||
|
defer mock.Close()
|
||||||
|
mock.Ctx.Configuration.Session.Domain = exampleDotComDomain
|
||||||
|
|
||||||
|
mock.SetRequestBody(t, checkURIWithinDomainRequestBody{
|
||||||
|
URI: "http://myapp.example.com",
|
||||||
|
})
|
||||||
|
|
||||||
|
CheckSafeRedirection(mock.Ctx)
|
||||||
|
assert.Equal(t, 401, mock.Ctx.Response.StatusCode())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckSafeRedirection_UnsafeRedirection(t *testing.T) {
|
||||||
|
mock := mocks.NewMockAutheliaCtxWithUserSession(t, session.UserSession{
|
||||||
|
Username: "john",
|
||||||
|
AuthenticationLevel: authentication.OneFactor,
|
||||||
|
})
|
||||||
|
defer mock.Close()
|
||||||
|
mock.Ctx.Configuration.Session.Domain = exampleDotComDomain
|
||||||
|
|
||||||
|
mock.SetRequestBody(t, checkURIWithinDomainRequestBody{
|
||||||
|
URI: "http://myapp.com",
|
||||||
|
})
|
||||||
|
|
||||||
|
CheckSafeRedirection(mock.Ctx)
|
||||||
|
mock.Assert200OK(t, checkURIWithinDomainResponseBody{
|
||||||
|
OK: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckSafeRedirection_SafeRedirection(t *testing.T) {
|
||||||
|
mock := mocks.NewMockAutheliaCtxWithUserSession(t, session.UserSession{
|
||||||
|
Username: "john",
|
||||||
|
AuthenticationLevel: authentication.OneFactor,
|
||||||
|
})
|
||||||
|
defer mock.Close()
|
||||||
|
mock.Ctx.Configuration.Session.Domain = exampleDotComDomain
|
||||||
|
|
||||||
|
mock.SetRequestBody(t, checkURIWithinDomainRequestBody{
|
||||||
|
URI: "https://myapp.example.com",
|
||||||
|
})
|
||||||
|
|
||||||
|
CheckSafeRedirection(mock.Ctx)
|
||||||
|
mock.Assert200OK(t, checkURIWithinDomainResponseBody{
|
||||||
|
OK: true,
|
||||||
|
})
|
||||||
|
}
|
|
@ -101,10 +101,8 @@ func Handle1FAResponse(ctx *middlewares.AutheliaCtx, targetURI, requestMethod st
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.Logger.Debugf("Redirection URL %s is safe", targetURI)
|
ctx.Logger.Debugf("Redirection URL %s is safe", targetURI)
|
||||||
|
err = ctx.SetJSONBody(redirectResponse{Redirect: targetURI})
|
||||||
|
|
||||||
response := redirectResponse{Redirect: targetURI}
|
|
||||||
|
|
||||||
err = ctx.SetJSONBody(response)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Logger.Errorf("Unable to set redirection URL in body: %s", err)
|
ctx.Logger.Errorf("Unable to set redirection URL in body: %s", err)
|
||||||
}
|
}
|
||||||
|
@ -125,15 +123,17 @@ func Handle2FAResponse(ctx *middlewares.AutheliaCtx, targetURI string) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
targetURL, err := url.ParseRequestURI(targetURI)
|
safe, err := utils.IsRedirectionURISafe(targetURI, ctx.Configuration.Session.Domain)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Error(fmt.Errorf("Unable to parse target URL: %s", err), messageMFAValidationFailed)
|
ctx.Error(fmt.Errorf("Unable to check target URL: %s", err), messageMFAValidationFailed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if targetURL != nil && utils.IsRedirectionSafe(*targetURL, ctx.Configuration.Session.Domain) {
|
if safe {
|
||||||
|
ctx.Logger.Debugf("Redirection URL %s is safe", targetURI)
|
||||||
err := ctx.SetJSONBody(redirectResponse{Redirect: targetURI})
|
err := ctx.SetJSONBody(redirectResponse{Redirect: targetURI})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Logger.Errorf("Unable to set redirection URL in body: %s", err)
|
ctx.Logger.Errorf("Unable to set redirection URL in body: %s", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,6 +53,16 @@ type firstFactorRequestBody struct {
|
||||||
// TODO(c.michaud): add required validation once the above PR is merged.
|
// TODO(c.michaud): add required validation once the above PR is merged.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// checkURIWithinDomainRequestBody represents the JSON body received by the endpoint checking if an URI is within
|
||||||
|
// the configured domain.
|
||||||
|
type checkURIWithinDomainRequestBody struct {
|
||||||
|
URI string `json:"uri"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type checkURIWithinDomainResponseBody struct {
|
||||||
|
OK bool `json:"ok"`
|
||||||
|
}
|
||||||
|
|
||||||
// redirectResponse represent the response sent by the first factor endpoint
|
// redirectResponse represent the response sent by the first factor endpoint
|
||||||
// when a redirection URL has been provided.
|
// when a redirection URL has been provided.
|
||||||
type redirectResponse struct {
|
type redirectResponse struct {
|
||||||
|
|
|
@ -127,12 +127,28 @@ func NewMockAutheliaCtx(t *testing.T) *MockAutheliaCtx {
|
||||||
return mockAuthelia
|
return mockAuthelia
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewMockAutheliaCtxWithUserSession create an instance of AutheliaCtx mock with predefined user session.
|
||||||
|
func NewMockAutheliaCtxWithUserSession(t *testing.T, userSession session.UserSession) *MockAutheliaCtx {
|
||||||
|
mock := NewMockAutheliaCtx(t)
|
||||||
|
err := mock.Ctx.SaveSession(userSession)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return mock
|
||||||
|
}
|
||||||
|
|
||||||
// Close close the mock.
|
// Close close the mock.
|
||||||
func (m *MockAutheliaCtx) Close() {
|
func (m *MockAutheliaCtx) Close() {
|
||||||
m.Hook.Reset()
|
m.Hook.Reset()
|
||||||
m.Ctrl.Finish()
|
m.Ctrl.Finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetRequestBody set the request body from a struct with json tags.
|
||||||
|
func (m *MockAutheliaCtx) SetRequestBody(t *testing.T, body interface{}) {
|
||||||
|
bodyBytes, err := json.Marshal(body)
|
||||||
|
require.NoError(t, err)
|
||||||
|
m.Ctx.Request.SetBody(bodyBytes)
|
||||||
|
}
|
||||||
|
|
||||||
// Assert401KO assert an error response from the service.
|
// Assert401KO assert an error response from the service.
|
||||||
func (m *MockAutheliaCtx) Assert401KO(t *testing.T, message string) {
|
func (m *MockAutheliaCtx) Assert401KO(t *testing.T, message string) {
|
||||||
assert.Equal(t, 401, m.Ctx.Response.StatusCode())
|
assert.Equal(t, 401, m.Ctx.Response.StatusCode())
|
||||||
|
|
|
@ -64,6 +64,8 @@ func registerRoutes(configuration schema.Configuration, providers middlewares.Pr
|
||||||
r.GET("/api/verify", autheliaMiddleware(handlers.VerifyGet(configuration.AuthenticationBackend)))
|
r.GET("/api/verify", autheliaMiddleware(handlers.VerifyGet(configuration.AuthenticationBackend)))
|
||||||
r.HEAD("/api/verify", autheliaMiddleware(handlers.VerifyGet(configuration.AuthenticationBackend)))
|
r.HEAD("/api/verify", autheliaMiddleware(handlers.VerifyGet(configuration.AuthenticationBackend)))
|
||||||
|
|
||||||
|
r.POST("/api/checks/safe-redirection", autheliaMiddleware(handlers.CheckSafeRedirection))
|
||||||
|
|
||||||
r.POST("/api/firstfactor", autheliaMiddleware(handlers.FirstFactorPost(1000, true)))
|
r.POST("/api/firstfactor", autheliaMiddleware(handlers.FirstFactorPost(1000, true)))
|
||||||
r.POST("/api/logout", autheliaMiddleware(handlers.LogoutPost))
|
r.POST("/api/logout", autheliaMiddleware(handlers.LogoutPost))
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ package suites
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
@ -69,12 +70,35 @@ func (s *OneFactorOnlyWebSuite) TestShouldDisplayAuthenticatedView() {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
s.doLoginOneFactor(ctx, s.T(), "john", "password", false, "http://unsafe.local")
|
s.doLoginOneFactor(ctx, s.T(), "john", "password", false, "")
|
||||||
s.verifyURLIs(ctx, s.T(), HomeBaseURL+"/")
|
s.verifyURLIs(ctx, s.T(), HomeBaseURL+"/")
|
||||||
s.doVisit(s.T(), GetLoginBaseURL())
|
s.doVisit(s.T(), GetLoginBaseURL())
|
||||||
s.verifyIsAuthenticatedPage(ctx, s.T())
|
s.verifyIsAuthenticatedPage(ctx, s.T())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *OneFactorOnlyWebSuite) TestShouldRedirectAlreadyAuthenticatedUser() {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
s.doLoginOneFactor(ctx, s.T(), "john", "password", false, "")
|
||||||
|
s.verifyURLIs(ctx, s.T(), HomeBaseURL+"/")
|
||||||
|
|
||||||
|
s.doVisit(s.T(), fmt.Sprintf("%s?rd=https://singlefactor.example.com:8080/secret.html", GetLoginBaseURL()))
|
||||||
|
s.verifyURLIs(ctx, s.T(), "https://singlefactor.example.com:8080/secret.html")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *OneFactorOnlyWebSuite) TestShouldNotRedirectAlreadyAuthenticatedUserToUnsafeURL() {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
s.doLoginOneFactor(ctx, s.T(), "john", "password", false, "")
|
||||||
|
s.verifyURLIs(ctx, s.T(), HomeBaseURL+"/")
|
||||||
|
|
||||||
|
// Visit the login page and wait for redirection to 2FA page with success icon displayed.
|
||||||
|
s.doVisit(s.T(), fmt.Sprintf("%s?rd=https://secure.example.local:8080", GetLoginBaseURL()))
|
||||||
|
s.verifyNotificationDisplayed(ctx, s.T(), "Redirection was determined to be unsafe and aborted. Ensure the redirection URL is correct.")
|
||||||
|
}
|
||||||
|
|
||||||
func (s *OneFactorOnlySuite) TestWeb() {
|
func (s *OneFactorOnlySuite) TestWeb() {
|
||||||
suite.Run(s.T(), NewOneFactorOnlyWebSuite())
|
suite.Run(s.T(), NewOneFactorOnlyWebSuite())
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,6 +67,36 @@ func (s *StandaloneWebDriverSuite) TestShouldLetUserKnowHeIsAlreadyAuthenticated
|
||||||
s.verifyIsAuthenticatedPage(ctx, s.T())
|
s.verifyIsAuthenticatedPage(ctx, s.T())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *StandaloneWebDriverSuite) TestShouldRedirectAlreadyAuthenticatedUser() {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
_ = s.doRegisterAndLogin2FA(ctx, s.T(), "john", "password", false, "")
|
||||||
|
|
||||||
|
// Visit home page to change context.
|
||||||
|
s.doVisit(s.T(), HomeBaseURL)
|
||||||
|
s.verifyIsHome(ctx, s.T())
|
||||||
|
|
||||||
|
// Visit the login page and wait for redirection to 2FA page with success icon displayed.
|
||||||
|
s.doVisit(s.T(), fmt.Sprintf("%s?rd=https://secure.example.com:8080", GetLoginBaseURL()))
|
||||||
|
s.verifyURLIs(ctx, s.T(), "https://secure.example.com:8080/")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StandaloneWebDriverSuite) TestShouldNotRedirectAlreadyAuthenticatedUserToUnsafeURL() {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
_ = s.doRegisterAndLogin2FA(ctx, s.T(), "john", "password", false, "")
|
||||||
|
|
||||||
|
// Visit home page to change context.
|
||||||
|
s.doVisit(s.T(), HomeBaseURL)
|
||||||
|
s.verifyIsHome(ctx, s.T())
|
||||||
|
|
||||||
|
// Visit the login page and wait for redirection to 2FA page with success icon displayed.
|
||||||
|
s.doVisit(s.T(), fmt.Sprintf("%s?rd=https://secure.example.local:8080", GetLoginBaseURL()))
|
||||||
|
s.verifyNotificationDisplayed(ctx, s.T(), "Redirection was determined to be unsafe and aborted. Ensure the redirection URL is correct.")
|
||||||
|
}
|
||||||
|
|
||||||
func (s *StandaloneWebDriverSuite) TestShouldCheckUserIsAskedToRegisterDevice() {
|
func (s *StandaloneWebDriverSuite) TestShouldCheckUserIsAskedToRegisterDevice() {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
package utils
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// IsRedirectionSafe determines if a redirection URL is secured.
|
// IsRedirectionSafe determines whether the URL is safe to be redirected to.
|
||||||
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
|
||||||
|
@ -17,3 +18,14 @@ func IsRedirectionSafe(url url.URL, protectedDomain string) bool {
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsRedirectionURISafe determines whether the URI is safe to be redirected to.
|
||||||
|
func IsRedirectionURISafe(uri, protectedDomain string) (bool, error) {
|
||||||
|
targetURL, err := url.ParseRequestURI(uri)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("Unable to parse redirection URI %s: %w", uri, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return targetURL != nil && IsRedirectionSafe(*targetURL, protectedDomain), nil
|
||||||
|
}
|
||||||
|
|
|
@ -12,14 +12,31 @@ func isURLSafe(requestURI string, domain string) bool { //nolint:unparam
|
||||||
return IsRedirectionSafe(*url, domain)
|
return IsRedirectionSafe(*url, domain)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestShouldReturnFalseOnBadScheme(t *testing.T) {
|
func TestIsRedirectionSafe_ShouldReturnFalseOnBadScheme(t *testing.T) {
|
||||||
assert.False(t, isURLSafe("http://secure.example.com", "example.com"))
|
assert.False(t, isURLSafe("http://secure.example.com", "example.com"))
|
||||||
assert.False(t, isURLSafe("ftp://secure.example.com", "example.com"))
|
assert.False(t, isURLSafe("ftp://secure.example.com", "example.com"))
|
||||||
assert.True(t, isURLSafe("https://secure.example.com", "example.com"))
|
assert.True(t, isURLSafe("https://secure.example.com", "example.com"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestShouldReturnFalseOnBadDomain(t *testing.T) {
|
func TestIsRedirectionSafe_ShouldReturnFalseOnBadDomain(t *testing.T) {
|
||||||
assert.False(t, isURLSafe("https://secure.example.com.c", "example.com"))
|
assert.False(t, isURLSafe("https://secure.example.com.c", "example.com"))
|
||||||
assert.False(t, isURLSafe("https://secure.example.comc", "example.com"))
|
assert.False(t, isURLSafe("https://secure.example.comc", "example.com"))
|
||||||
assert.False(t, isURLSafe("https://secure.example.co", "example.com"))
|
assert.False(t, isURLSafe("https://secure.example.co", "example.com"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIsRedirectionURISafe_CannotParseURI(t *testing.T) {
|
||||||
|
_, err := IsRedirectionURISafe("http//invalid", "example.com")
|
||||||
|
assert.EqualError(t, err, "Unable to parse redirection URI http//invalid: parse \"http//invalid\": invalid URI for request")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsRedirectionURISafe_InvalidRedirectionURI(t *testing.T) {
|
||||||
|
valid, err := IsRedirectionURISafe("http://myurl.com/myresource", "example.com")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.False(t, valid)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsRedirectionURISafe_ValidRedirectionURI(t *testing.T) {
|
||||||
|
valid, err := IsRedirectionURISafe("http://myurl.example.com/myresource", "example.com")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.False(t, valid)
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
|
import { useCallback } from "react";
|
||||||
|
|
||||||
export function useRedirector() {
|
export function useRedirector() {
|
||||||
return (url: string) => {
|
return useCallback((url: string) => {
|
||||||
window.location.href = url;
|
window.location.href = url;
|
||||||
};
|
}, []);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import "@utils/AssetPath";
|
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
|
|
||||||
|
import "@utils/AssetPath";
|
||||||
import "@root/index.css";
|
import "@root/index.css";
|
||||||
import App from "@root/App";
|
import App from "@root/App";
|
||||||
import * as serviceWorker from "@root/serviceWorker";
|
import * as serviceWorker from "@root/serviceWorker";
|
||||||
|
|
|
@ -25,6 +25,7 @@ export const InitiateResetPasswordPath = basePath + "/api/reset-password/identit
|
||||||
export const CompleteResetPasswordPath = basePath + "/api/reset-password/identity/finish";
|
export const CompleteResetPasswordPath = basePath + "/api/reset-password/identity/finish";
|
||||||
// Do the password reset during completion.
|
// Do the password reset during completion.
|
||||||
export const ResetPasswordPath = basePath + "/api/reset-password";
|
export const ResetPasswordPath = basePath + "/api/reset-password";
|
||||||
|
export const ChecksSafeRedirectionPath = basePath + "/api/checks/safe-redirection";
|
||||||
|
|
||||||
export const LogoutPath = basePath + "/api/logout";
|
export const LogoutPath = basePath + "/api/logout";
|
||||||
export const StatePath = basePath + "/api/state";
|
export const StatePath = basePath + "/api/state";
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { ChecksSafeRedirectionPath } from "@services/Api";
|
||||||
|
import { PostWithOptionalResponse } from "@services/Client";
|
||||||
|
|
||||||
|
interface SafeRedirectionResponse {
|
||||||
|
ok: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkSafeRedirection(uri: string) {
|
||||||
|
return PostWithOptionalResponse<SafeRedirectionResponse>(ChecksSafeRedirectionPath, { uri });
|
||||||
|
}
|
|
@ -56,6 +56,7 @@ const RegisterOneTimePassword = function () {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
completeRegistrationProcess();
|
completeRegistrationProcess();
|
||||||
}, [completeRegistrationProcess]);
|
}, [completeRegistrationProcess]);
|
||||||
|
|
||||||
function SecretButton(text: string | undefined, action: string, icon: IconDefinition) {
|
function SecretButton(text: string | undefined, action: string, icon: IconDefinition) {
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import React, { useEffect, Fragment, ReactNode, useState, useCallback } from "react";
|
import React, { Fragment, ReactNode, useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
import { Switch, Route, Redirect, useHistory, useLocation } from "react-router";
|
import { Redirect, Route, Switch, useHistory, useLocation } from "react-router";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
AuthenticatedRoute,
|
||||||
FirstFactorRoute,
|
FirstFactorRoute,
|
||||||
|
SecondFactorPushRoute,
|
||||||
SecondFactorRoute,
|
SecondFactorRoute,
|
||||||
SecondFactorTOTPRoute,
|
SecondFactorTOTPRoute,
|
||||||
SecondFactorPushRoute,
|
|
||||||
SecondFactorU2FRoute,
|
SecondFactorU2FRoute,
|
||||||
AuthenticatedRoute,
|
|
||||||
} from "@constants/Routes";
|
} from "@constants/Routes";
|
||||||
import { useConfiguration } from "@hooks/Configuration";
|
import { useConfiguration } from "@hooks/Configuration";
|
||||||
import { useNotifications } from "@hooks/NotificationsContext";
|
import { useNotifications } from "@hooks/NotificationsContext";
|
||||||
|
@ -18,6 +18,7 @@ import { useRequestMethod } from "@hooks/RequestMethod";
|
||||||
import { useAutheliaState } from "@hooks/State";
|
import { useAutheliaState } from "@hooks/State";
|
||||||
import { useUserPreferences as userUserInfo } from "@hooks/UserInfo";
|
import { useUserPreferences as userUserInfo } from "@hooks/UserInfo";
|
||||||
import { SecondFactorMethod } from "@models/Methods";
|
import { SecondFactorMethod } from "@models/Methods";
|
||||||
|
import { checkSafeRedirection } from "@services/SafeRedirection";
|
||||||
import { AuthenticationLevel } from "@services/State";
|
import { AuthenticationLevel } from "@services/State";
|
||||||
import LoadingPage from "@views/LoadingPage/LoadingPage";
|
import LoadingPage from "@views/LoadingPage/LoadingPage";
|
||||||
import AuthenticatedView from "@views/LoginPortal/AuthenticatedView/AuthenticatedView";
|
import AuthenticatedView from "@views/LoginPortal/AuthenticatedView/AuthenticatedView";
|
||||||
|
@ -29,6 +30,9 @@ export interface Props {
|
||||||
resetPassword: boolean;
|
resetPassword: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const RedirectionErrorMessage =
|
||||||
|
"Redirection was determined to be unsafe and aborted. Ensure the redirection URL is correct.";
|
||||||
|
|
||||||
const LoginPortal = function (props: Props) {
|
const LoginPortal = function (props: Props) {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
@ -87,7 +91,31 @@ const LoginPortal = function (props: Props) {
|
||||||
|
|
||||||
// Redirect to the correct stage if not enough authenticated
|
// Redirect to the correct stage if not enough authenticated
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (state) {
|
(async function () {
|
||||||
|
if (!state) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
redirectionURL &&
|
||||||
|
((configuration &&
|
||||||
|
!configuration.second_factor_enabled &&
|
||||||
|
state.authentication_level >= AuthenticationLevel.OneFactor) ||
|
||||||
|
state.authentication_level === AuthenticationLevel.TwoFactor)
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const res = await checkSafeRedirection(redirectionURL);
|
||||||
|
if (res && res.ok) {
|
||||||
|
redirector(redirectionURL);
|
||||||
|
} else {
|
||||||
|
createErrorNotification(RedirectionErrorMessage);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
createErrorNotification(RedirectionErrorMessage);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const redirectionSuffix = redirectionURL
|
const redirectionSuffix = redirectionURL
|
||||||
? `?rd=${encodeURIComponent(redirectionURL)}${requestMethod ? `&rm=${requestMethod}` : ""}`
|
? `?rd=${encodeURIComponent(redirectionURL)}${requestMethod ? `&rm=${requestMethod}` : ""}`
|
||||||
: "";
|
: "";
|
||||||
|
@ -108,8 +136,18 @@ const LoginPortal = function (props: Props) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
})();
|
||||||
}, [state, redirectionURL, requestMethod, redirect, userInfo, setFirstFactorDisabled, configuration]);
|
}, [
|
||||||
|
state,
|
||||||
|
redirectionURL,
|
||||||
|
requestMethod,
|
||||||
|
redirect,
|
||||||
|
userInfo,
|
||||||
|
setFirstFactorDisabled,
|
||||||
|
configuration,
|
||||||
|
createErrorNotification,
|
||||||
|
redirector,
|
||||||
|
]);
|
||||||
|
|
||||||
const handleAuthSuccess = async (redirectionURL: string | undefined) => {
|
const handleAuthSuccess = async (redirectionURL: string | undefined) => {
|
||||||
if (redirectionURL) {
|
if (redirectionURL) {
|
||||||
|
|
|
@ -38,7 +38,7 @@ const OneTimePasswordMethod = function (props: Props) {
|
||||||
/* eslint-enable react-hooks/exhaustive-deps */
|
/* eslint-enable react-hooks/exhaustive-deps */
|
||||||
|
|
||||||
const signInFunc = useCallback(async () => {
|
const signInFunc = useCallback(async () => {
|
||||||
if (props.authenticationLevel === AuthenticationLevel.TwoFactor) {
|
if (!props.registered || props.authenticationLevel === AuthenticationLevel.TwoFactor) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,7 +59,14 @@ const OneTimePasswordMethod = function (props: Props) {
|
||||||
setState(State.Failure);
|
setState(State.Failure);
|
||||||
}
|
}
|
||||||
setPasscode("");
|
setPasscode("");
|
||||||
}, [passcode, onSignInErrorCallback, onSignInSuccessCallback, redirectionURL, props.authenticationLevel]);
|
}, [
|
||||||
|
passcode,
|
||||||
|
onSignInErrorCallback,
|
||||||
|
onSignInSuccessCallback,
|
||||||
|
redirectionURL,
|
||||||
|
props.authenticationLevel,
|
||||||
|
props.registered,
|
||||||
|
]);
|
||||||
|
|
||||||
// Set successful state if user is already authenticated.
|
// Set successful state if user is already authenticated.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -47,7 +47,7 @@ const SecurityKeyMethod = function (props: Props) {
|
||||||
|
|
||||||
const doInitiateSignIn = useCallback(async () => {
|
const doInitiateSignIn = useCallback(async () => {
|
||||||
// If user is already authenticated, we don't initiate sign in process.
|
// If user is already authenticated, we don't initiate sign in process.
|
||||||
if (!props.registered || props.authenticationLevel >= AuthenticationLevel.TwoFactor) {
|
if (!props.registered || props.authenticationLevel === AuthenticationLevel.TwoFactor) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue