[FEATURE] Allow Authelia to listen on a specified path (#1027)

* [FEATURE] Allow Authelia to listen on a specified path

* Fix linting and add a couple typescript types

* Template index.html to support base_url

* Update docs and configuration template

* Access base path from body attribute.

* Update CSP

* Fix unit test
Also remove check for body as this will never get triggered, react itself is loaded inside the body so this has to always be successful.

* Template index.html with ${PUBLIC_URL}

* Define PUBLIC_URL in .env(s)

* Add docs clarification

Co-authored-by: Amir Zarrinkafsh <nightah@me.com>
Co-authored-by: Clement Michaud <clement.michaud34@gmail.com>
This commit is contained in:
James Elliott 2020-05-21 12:20:55 +10:00 committed by GitHub
parent 469daedd36
commit fcd0b5e46a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 196 additions and 90 deletions

View File

@ -16,6 +16,8 @@ server:
read_buffer_size: 4096 read_buffer_size: 4096
# Write buffer size configures the http server's maximum outgoing response size in bytes. # Write buffer size configures the http server's maximum outgoing response size in bytes.
write_buffer_size: 4096 write_buffer_size: 4096
# Set the single level path Authelia listens on, must be alphanumeric chars and should not contain any slashes.
path: ""
# Level of verbosity for logs: info, debug, trace # Level of verbosity for logs: info, debug, trace
log_level: debug log_level: debug

View File

@ -20,6 +20,8 @@ server:
read_buffer_size: 4096 read_buffer_size: 4096
# Write buffer size configures the http server's maximum outgoing response size in bytes. # Write buffer size configures the http server's maximum outgoing response size in bytes.
write_buffer_size: 4096 write_buffer_size: 4096
# Set the single level path Authelia listens on, must be alphanumeric chars and should not contain any slashes.
path: ""
``` ```
### Buffer Sizes ### Buffer Sizes
@ -27,3 +29,23 @@ server:
The read and write buffer sizes generally should be the same. This is because when Authelia verifies The read and write buffer sizes generally should be the same. This is because when Authelia verifies
if the user is authorized to visit a URL, it also sends back nearly the same size response if the user is authorized to visit a URL, it also sends back nearly the same size response
(write_buffer_size) as the request (read_buffer_size). (write_buffer_size) as the request (read_buffer_size).
### Path
Authelia by default is served from the root `/` location, either via its own domain or subdomain.
Example: https://auth.example.com/, https://example.com/
```yaml
server:
path: ""
```
Modifying this setting will allow you to serve Authelia out from a specified base path. Please note
that currently only a single level path is supported meaning slashes are not allowed, and only
alphanumeric characters are supported.
Example: https://auth.example.com/authelia/, https://example.com/authelia/
```yaml
server:
path: authelia
```

View File

@ -2,8 +2,9 @@ package schema
// ServerConfiguration represents the configuration of the http server. // ServerConfiguration represents the configuration of the http server.
type ServerConfiguration struct { type ServerConfiguration struct {
ReadBufferSize int `mapstructure:"read_buffer_size"` Path string `mapstructure:"path"`
WriteBufferSize int `mapstructure:"write_buffer_size"` ReadBufferSize int `mapstructure:"read_buffer_size"`
WriteBufferSize int `mapstructure:"write_buffer_size"`
} }
// DefaultServerConfiguration represents the default values of the ServerConfiguration. // DefaultServerConfiguration represents the default values of the ServerConfiguration.

View File

@ -14,6 +14,7 @@ var validKeys = []string{
// Server Keys. // Server Keys.
"server.read_buffer_size", "server.read_buffer_size",
"server.write_buffer_size", "server.write_buffer_size",
"server.path",
// TOTP Keys. // TOTP Keys.
"totp.issuer", "totp.issuer",

View File

@ -2,8 +2,11 @@ package validator
import ( import (
"fmt" "fmt"
"path"
"strings"
"github.com/authelia/authelia/internal/configuration/schema" "github.com/authelia/authelia/internal/configuration/schema"
"github.com/authelia/authelia/internal/utils"
) )
var defaultReadBufferSize = 4096 var defaultReadBufferSize = 4096
@ -11,6 +14,16 @@ var defaultWriteBufferSize = 4096
// ValidateServer checks a server configuration is correct. // ValidateServer checks a server configuration is correct.
func ValidateServer(configuration *schema.ServerConfiguration, validator *schema.StructValidator) { func ValidateServer(configuration *schema.ServerConfiguration, validator *schema.StructValidator) {
switch {
case strings.Contains(configuration.Path, "/"):
validator.Push(fmt.Errorf("server path must not contain any forward slashes"))
case !utils.IsStringAlphaNumeric(configuration.Path):
validator.Push(fmt.Errorf("server path must only be alpha numeric characters"))
case configuration.Path == "": // Don't do anything if it's blank.
default:
configuration.Path = path.Clean("/" + configuration.Path)
}
if configuration.ReadBufferSize == 0 { if configuration.ReadBufferSize == 0 {
configuration.ReadBufferSize = defaultReadBufferSize configuration.ReadBufferSize = defaultReadBufferSize
} else if configuration.ReadBufferSize < 0 { } else if configuration.ReadBufferSize < 0 {

View File

@ -12,9 +12,7 @@ import (
func TestShouldSetDefaultConfig(t *testing.T) { func TestShouldSetDefaultConfig(t *testing.T) {
validator := schema.NewStructValidator() validator := schema.NewStructValidator()
config := schema.ServerConfiguration{} config := schema.ServerConfiguration{}
ValidateServer(&config, validator) ValidateServer(&config, validator)
require.Len(t, validator.Errors(), 0) require.Len(t, validator.Errors(), 0)
assert.Equal(t, defaultReadBufferSize, config.ReadBufferSize) assert.Equal(t, defaultReadBufferSize, config.ReadBufferSize)
assert.Equal(t, defaultWriteBufferSize, config.WriteBufferSize) assert.Equal(t, defaultWriteBufferSize, config.WriteBufferSize)
@ -26,10 +24,28 @@ func TestShouldRaiseOnNegativeValues(t *testing.T) {
ReadBufferSize: -1, ReadBufferSize: -1,
WriteBufferSize: -1, WriteBufferSize: -1,
} }
ValidateServer(&config, validator) ValidateServer(&config, validator)
require.Len(t, validator.Errors(), 2) require.Len(t, validator.Errors(), 2)
assert.EqualError(t, validator.Errors()[0], "server read buffer size must be above 0") assert.EqualError(t, validator.Errors()[0], "server read buffer size must be above 0")
assert.EqualError(t, validator.Errors()[1], "server write buffer size must be above 0") assert.EqualError(t, validator.Errors()[1], "server write buffer size must be above 0")
} }
func TestShouldRaiseOnNonAlphanumericCharsInPath(t *testing.T) {
validator := schema.NewStructValidator()
config := schema.ServerConfiguration{
Path: "app le",
}
ValidateServer(&config, validator)
require.Len(t, validator.Errors(), 1)
assert.Error(t, validator.Errors()[0], "server path must only be alpha numeric characters")
}
func TestShouldRaiseOnForwardSlashInPath(t *testing.T) {
validator := schema.NewStructValidator()
config := schema.ServerConfiguration{
Path: "app/le",
}
ValidateServer(&config, validator)
assert.Len(t, validator.Errors(), 1)
assert.Error(t, validator.Errors()[0], "server path must not contain any forward slashes")
}

View File

@ -0,0 +1,22 @@
package middlewares
import (
"bytes"
"github.com/valyala/fasthttp"
)
// StripPathMiddleware strips the first level of a path.
func StripPathMiddleware(next fasthttp.RequestHandler) fasthttp.RequestHandler {
return func(ctx *fasthttp.RequestCtx) {
uri := ctx.Request.RequestURI()
n := bytes.IndexByte(uri[1:], '/')
if n >= 0 {
uri = uri[n+1:]
ctx.Request.SetRequestURI(string(uri))
}
next(ctx)
}
}

View File

@ -2,8 +2,8 @@ package server
import ( import (
"fmt" "fmt"
"html/template"
"io/ioutil" "io/ioutil"
"text/template"
"github.com/valyala/fasthttp" "github.com/valyala/fasthttp"
@ -16,7 +16,7 @@ var alphaNumericRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUV
// ServeIndex serve the index.html file with nonce generated for supporting // ServeIndex serve the index.html file with nonce generated for supporting
// restrictive CSP while using material-ui from the embedded virtual filesystem. // restrictive CSP while using material-ui from the embedded virtual filesystem.
//go:generate broccoli -src ../../public_html -o public_html //go:generate broccoli -src ../../public_html -o public_html
func ServeIndex(publicDir string) fasthttp.RequestHandler { func ServeIndex(publicDir, base string) fasthttp.RequestHandler {
f, err := br.Open(publicDir + "/index.html") f, err := br.Open(publicDir + "/index.html")
if err != nil { if err != nil {
logging.Logger().Fatalf("Unable to open index.html: %v", err) logging.Logger().Fatalf("Unable to open index.html: %v", err)
@ -36,9 +36,9 @@ func ServeIndex(publicDir string) fasthttp.RequestHandler {
nonce := utils.RandomString(32, alphaNumericRunes) nonce := utils.RandomString(32, alphaNumericRunes)
ctx.SetContentType("text/html; charset=utf-8") ctx.SetContentType("text/html; charset=utf-8")
ctx.Response.Header.Add("Content-Security-Policy", fmt.Sprintf("default-src 'self'; style-src 'self' 'nonce-%s'", nonce)) ctx.Response.Header.Add("Content-Security-Policy", fmt.Sprintf("default-src 'self'; object-src 'none'; require-trusted-types-for 'script'; style-src 'self' 'nonce-%s'", nonce))
err := tmpl.Execute(ctx.Response.BodyWriter(), struct{ CSPNonce string }{CSPNonce: nonce}) err := tmpl.Execute(ctx.Response.BodyWriter(), struct{ CSPNonce, Base string }{CSPNonce: nonce, Base: base})
if err != nil { if err != nil {
ctx.Error("An error occurred", 503) ctx.Error("An error occurred", 503)
logging.Logger().Errorf("Unable to execute template: %v", err) logging.Logger().Errorf("Unable to execute template: %v", err)

View File

@ -23,74 +23,74 @@ func StartServer(configuration schema.Configuration, providers middlewares.Provi
autheliaMiddleware := middlewares.AutheliaMiddleware(configuration, providers) autheliaMiddleware := middlewares.AutheliaMiddleware(configuration, providers)
embeddedAssets := "/public_html" embeddedAssets := "/public_html"
rootFiles := []string{"favicon.ico", "manifest.json", "robots.txt"} rootFiles := []string{"favicon.ico", "manifest.json", "robots.txt"}
// TODO: Remove in v4.18.0. // TODO: Remove in v4.18.0.
if os.Getenv("PUBLIC_DIR") != "" { if os.Getenv("PUBLIC_DIR") != "" {
logging.Logger().Warn("PUBLIC_DIR environment variable has been deprecated, assets are now embedded.") logging.Logger().Warn("PUBLIC_DIR environment variable has been deprecated, assets are now embedded.")
} }
router := router.New() r := router.New()
r.GET("/", ServeIndex(embeddedAssets, configuration.Server.Path))
router.GET("/", ServeIndex(embeddedAssets))
for _, f := range rootFiles { for _, f := range rootFiles {
router.GET("/"+f, fasthttpadaptor.NewFastHTTPHandler(br.Serve(embeddedAssets))) r.GET("/"+f, fasthttpadaptor.NewFastHTTPHandler(br.Serve(embeddedAssets)))
} }
router.GET("/static/{filepath:*}", fasthttpadaptor.NewFastHTTPHandler(br.Serve(embeddedAssets))) r.GET("/static/{filepath:*}", fasthttpadaptor.NewFastHTTPHandler(br.Serve(embeddedAssets)))
router.GET("/api/state", autheliaMiddleware(handlers.StateGet)) r.GET("/api/state", autheliaMiddleware(handlers.StateGet))
router.GET("/api/configuration", autheliaMiddleware(handlers.ConfigurationGet)) r.GET("/api/configuration", autheliaMiddleware(handlers.ConfigurationGet))
router.GET("/api/configuration/extended", autheliaMiddleware( r.GET("/api/configuration/extended", autheliaMiddleware(
middlewares.RequireFirstFactor(handlers.ExtendedConfigurationGet))) middlewares.RequireFirstFactor(handlers.ExtendedConfigurationGet)))
router.GET("/api/verify", autheliaMiddleware(handlers.VerifyGet(configuration.AuthenticationBackend))) r.GET("/api/verify", autheliaMiddleware(handlers.VerifyGet(configuration.AuthenticationBackend)))
router.HEAD("/api/verify", autheliaMiddleware(handlers.VerifyGet(configuration.AuthenticationBackend))) r.HEAD("/api/verify", autheliaMiddleware(handlers.VerifyGet(configuration.AuthenticationBackend)))
router.POST("/api/firstfactor", autheliaMiddleware(handlers.FirstFactorPost(1000, true))) r.POST("/api/firstfactor", autheliaMiddleware(handlers.FirstFactorPost(1000, true)))
router.POST("/api/logout", autheliaMiddleware(handlers.LogoutPost)) r.POST("/api/logout", autheliaMiddleware(handlers.LogoutPost))
// Only register endpoints if forgot password is not disabled. // Only register endpoints if forgot password is not disabled.
if !configuration.AuthenticationBackend.DisableResetPassword { if !configuration.AuthenticationBackend.DisableResetPassword {
// Password reset related endpoints. // Password reset related endpoints.
router.POST("/api/reset-password/identity/start", autheliaMiddleware( r.POST("/api/reset-password/identity/start", autheliaMiddleware(
handlers.ResetPasswordIdentityStart)) handlers.ResetPasswordIdentityStart))
router.POST("/api/reset-password/identity/finish", autheliaMiddleware( r.POST("/api/reset-password/identity/finish", autheliaMiddleware(
handlers.ResetPasswordIdentityFinish)) handlers.ResetPasswordIdentityFinish))
router.POST("/api/reset-password", autheliaMiddleware( r.POST("/api/reset-password", autheliaMiddleware(
handlers.ResetPasswordPost)) handlers.ResetPasswordPost))
} }
// Information about the user. // Information about the user.
router.GET("/api/user/info", autheliaMiddleware( r.GET("/api/user/info", autheliaMiddleware(
middlewares.RequireFirstFactor(handlers.UserInfoGet))) middlewares.RequireFirstFactor(handlers.UserInfoGet)))
router.POST("/api/user/info/2fa_method", autheliaMiddleware( r.POST("/api/user/info/2fa_method", autheliaMiddleware(
middlewares.RequireFirstFactor(handlers.MethodPreferencePost))) middlewares.RequireFirstFactor(handlers.MethodPreferencePost)))
// TOTP related endpoints. // TOTP related endpoints.
router.POST("/api/secondfactor/totp/identity/start", autheliaMiddleware( r.POST("/api/secondfactor/totp/identity/start", autheliaMiddleware(
middlewares.RequireFirstFactor(handlers.SecondFactorTOTPIdentityStart))) middlewares.RequireFirstFactor(handlers.SecondFactorTOTPIdentityStart)))
router.POST("/api/secondfactor/totp/identity/finish", autheliaMiddleware( r.POST("/api/secondfactor/totp/identity/finish", autheliaMiddleware(
middlewares.RequireFirstFactor(handlers.SecondFactorTOTPIdentityFinish))) middlewares.RequireFirstFactor(handlers.SecondFactorTOTPIdentityFinish)))
router.POST("/api/secondfactor/totp", autheliaMiddleware( r.POST("/api/secondfactor/totp", autheliaMiddleware(
middlewares.RequireFirstFactor(handlers.SecondFactorTOTPPost(&handlers.TOTPVerifierImpl{ middlewares.RequireFirstFactor(handlers.SecondFactorTOTPPost(&handlers.TOTPVerifierImpl{
Period: uint(configuration.TOTP.Period), Period: uint(configuration.TOTP.Period),
Skew: uint(*configuration.TOTP.Skew), Skew: uint(*configuration.TOTP.Skew),
})))) }))))
// U2F related endpoints. // U2F related endpoints.
router.POST("/api/secondfactor/u2f/identity/start", autheliaMiddleware( r.POST("/api/secondfactor/u2f/identity/start", autheliaMiddleware(
middlewares.RequireFirstFactor(handlers.SecondFactorU2FIdentityStart))) middlewares.RequireFirstFactor(handlers.SecondFactorU2FIdentityStart)))
router.POST("/api/secondfactor/u2f/identity/finish", autheliaMiddleware( r.POST("/api/secondfactor/u2f/identity/finish", autheliaMiddleware(
middlewares.RequireFirstFactor(handlers.SecondFactorU2FIdentityFinish))) middlewares.RequireFirstFactor(handlers.SecondFactorU2FIdentityFinish)))
router.POST("/api/secondfactor/u2f/register", autheliaMiddleware( r.POST("/api/secondfactor/u2f/register", autheliaMiddleware(
middlewares.RequireFirstFactor(handlers.SecondFactorU2FRegister))) middlewares.RequireFirstFactor(handlers.SecondFactorU2FRegister)))
router.POST("/api/secondfactor/u2f/sign_request", autheliaMiddleware( r.POST("/api/secondfactor/u2f/sign_request", autheliaMiddleware(
middlewares.RequireFirstFactor(handlers.SecondFactorU2FSignGet))) middlewares.RequireFirstFactor(handlers.SecondFactorU2FSignGet)))
router.POST("/api/secondfactor/u2f/sign", autheliaMiddleware( r.POST("/api/secondfactor/u2f/sign", autheliaMiddleware(
middlewares.RequireFirstFactor(handlers.SecondFactorU2FSignPost(&handlers.U2FVerifierImpl{})))) middlewares.RequireFirstFactor(handlers.SecondFactorU2FSignPost(&handlers.U2FVerifierImpl{}))))
// Configure DUO api endpoint only if configuration exists. // Configure DUO api endpoint only if configuration exists.
@ -108,21 +108,26 @@ func StartServer(configuration schema.Configuration, providers middlewares.Provi
configuration.DuoAPI.Hostname, "")) configuration.DuoAPI.Hostname, ""))
} }
router.POST("/api/secondfactor/duo", autheliaMiddleware( r.POST("/api/secondfactor/duo", autheliaMiddleware(
middlewares.RequireFirstFactor(handlers.SecondFactorDuoPost(duoAPI)))) middlewares.RequireFirstFactor(handlers.SecondFactorDuoPost(duoAPI))))
} }
// If trace is set, enable pprofhandler and expvarhandler. // If trace is set, enable pprofhandler and expvarhandler.
if configuration.LogLevel == "trace" { if configuration.LogLevel == "trace" {
router.GET("/debug/pprof/{name?}", pprofhandler.PprofHandler) r.GET("/debug/pprof/{name?}", pprofhandler.PprofHandler)
router.GET("/debug/vars", expvarhandler.ExpvarHandler) r.GET("/debug/vars", expvarhandler.ExpvarHandler)
} }
router.NotFound = ServeIndex(embeddedAssets) r.NotFound = ServeIndex(embeddedAssets, configuration.Server.Path)
handler := middlewares.LogRequestMiddleware(r.Handler)
if configuration.Server.Path != "" {
handler = middlewares.StripPathMiddleware(handler)
}
server := &fasthttp.Server{ server := &fasthttp.Server{
ErrorHandler: autheliaErrorHandler, ErrorHandler: autheliaErrorHandler,
Handler: middlewares.LogRequestMiddleware(router.Handler), Handler: handler,
NoDefaultServerHeader: true, NoDefaultServerHeader: true,
ReadBufferSize: configuration.Server.ReadBufferSize, ReadBufferSize: configuration.Server.ReadBufferSize,
WriteBufferSize: configuration.Server.WriteBufferSize, WriteBufferSize: configuration.Server.WriteBufferSize,

View File

@ -3,8 +3,20 @@ package utils
import ( import (
"math/rand" "math/rand"
"time" "time"
"unicode"
) )
// IsStringAlphaNumeric returns false if any rune in the string is not alpha-numeric.
func IsStringAlphaNumeric(input string) bool {
for _, r := range input {
if !unicode.IsLetter(r) && !unicode.IsNumber(r) {
return false
}
}
return true
}
// IsStringInSlice checks if a single string is in an array of strings. // IsStringInSlice checks if a single string is in an array of strings.
func IsStringInSlice(a string, list []string) (inSlice bool) { func IsStringInSlice(a string, list []string) (inSlice bool) {
for _, b := range list { for _, b := range list {

View File

@ -1,2 +1,2 @@
HOST=authelia-frontend
HOST=authelia-frontend PUBLIC_URL=""

1
web/.env.production Normal file
View File

@ -0,0 +1 @@
PUBLIC_URL={{.Base}}

View File

@ -1,22 +1,19 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head>
<meta property="csp-nonce" content="{{.CSPNonce}}" />
<meta charset="utf-8" /> <head>
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> <meta property="csp-nonce" content="{{.CSPNonce}}" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta charset="utf-8" />
<meta name="theme-color" content="#000000" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta <meta name="theme-color" content="#000000" />
name="description" <meta name="description" content="Authelia login portal for your apps" />
content="Authelia login portal for your apps" <!--
/>
<!--
manifest.json provides metadata used when your web app is installed on a manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/ user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
--> -->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!-- <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<!--
Notice the use of %PUBLIC_URL% in the tags above. Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build. It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML. Only files inside the `public` folder can be referenced from the HTML.
@ -25,12 +22,13 @@
work correctly both with client-side routing and a non-root public URL. work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`. Learn how to configure a non-root public URL by running `npm run build`.
--> -->
<title>Login - Authelia</title> <title>Login - Authelia</title>
</head> </head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript> <body data-basepath="%PUBLIC_URL%">
<div id="root"></div> <noscript>You need to enable JavaScript to run this app.</noscript>
<!-- <div id="root"></div>
<!--
This HTML file is a template. This HTML file is a template.
If you open it directly in the browser, you will see an empty page. If you open it directly in the browser, you will see an empty page.
@ -40,5 +38,6 @@
To begin the development, run `npm start` or `yarn start`. To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`. To create a production bundle, use `npm run build` or `yarn build`.
--> -->
</body> </body>
</html>
</html>

View File

@ -19,7 +19,8 @@ import NotificationBar from './components/NotificationBar';
import SignOut from './views/LoginPortal/SignOut/SignOut'; import SignOut from './views/LoginPortal/SignOut/SignOut';
import { useConfiguration } from './hooks/Configuration'; import { useConfiguration } from './hooks/Configuration';
import '@fortawesome/fontawesome-svg-core/styles.css' import '@fortawesome/fontawesome-svg-core/styles.css'
import {config as faConfig} from '@fortawesome/fontawesome-svg-core'; import { config as faConfig } from '@fortawesome/fontawesome-svg-core';
import { useBasePath } from './hooks/BasePath';
faConfig.autoAddCss = false; faConfig.autoAddCss = false;
@ -37,7 +38,7 @@ const App: React.FC = () => {
return ( return (
<NotificationsContext.Provider value={{ notification, setNotification }} > <NotificationsContext.Provider value={{ notification, setNotification }} >
<Router> <Router basename={useBasePath()}>
<NotificationBar onClose={() => setNotification(null)} /> <NotificationBar onClose={() => setNotification(null)} />
<Switch> <Switch>
<Route path={ResetPasswordStep1Route} exact> <Route path={ResetPasswordStep1Route} exact>
@ -61,7 +62,7 @@ const App: React.FC = () => {
resetPassword={configuration?.reset_password === true} /> resetPassword={configuration?.reset_password === true} />
</Route> </Route>
<Route path="/"> <Route path="/">
<Redirect to={FirstFactorRoute}></Redirect> <Redirect to={FirstFactorRoute} />
</Route> </Route>
</Switch> </Switch>
</Router> </Router>

View File

@ -2,4 +2,4 @@
export const GoogleAuthenticator = { export const GoogleAuthenticator = {
googlePlay: "https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en_us", googlePlay: "https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en_us",
appleStore: "https://apps.apple.com/us/app/google-authenticator/id388497605", appleStore: "https://apps.apple.com/us/app/google-authenticator/id388497605",
}; };

View File

@ -0,0 +1,8 @@
export function useBasePath() {
const basePath = document.body.getAttribute("data-basepath");
if (basePath === null) {
throw new Error("No base path detected");
}
return basePath;
}

View File

@ -1,32 +1,34 @@
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
import { useBasePath } from "../hooks/BasePath";
export const FirstFactorPath = "/api/firstfactor"; const basePath = useBasePath();
export const InitiateTOTPRegistrationPath = "/api/secondfactor/totp/identity/start";
export const CompleteTOTPRegistrationPath = "/api/secondfactor/totp/identity/finish";
export const InitiateU2FRegistrationPath = "/api/secondfactor/u2f/identity/start"; export const FirstFactorPath = basePath + "/api/firstfactor";
export const CompleteU2FRegistrationStep1Path = "/api/secondfactor/u2f/identity/finish"; export const InitiateTOTPRegistrationPath = basePath + "/api/secondfactor/totp/identity/start";
export const CompleteU2FRegistrationStep2Path = "/api/secondfactor/u2f/register"; export const CompleteTOTPRegistrationPath = basePath + "/api/secondfactor/totp/identity/finish";
export const InitiateU2FSignInPath = "/api/secondfactor/u2f/sign_request"; export const InitiateU2FRegistrationPath = basePath + "/api/secondfactor/u2f/identity/start";
export const CompleteU2FSignInPath = "/api/secondfactor/u2f/sign"; export const CompleteU2FRegistrationStep1Path = basePath + "/api/secondfactor/u2f/identity/finish";
export const CompleteU2FRegistrationStep2Path = basePath + "/api/secondfactor/u2f/register";
export const CompletePushNotificationSignInPath = "/api/secondfactor/duo" export const InitiateU2FSignInPath = basePath + "/api/secondfactor/u2f/sign_request";
export const CompleteTOTPSignInPath = "/api/secondfactor/totp" export const CompleteU2FSignInPath = basePath + "/api/secondfactor/u2f/sign";
export const InitiateResetPasswordPath = "/api/reset-password/identity/start"; export const CompletePushNotificationSignInPath = basePath + "/api/secondfactor/duo"
export const CompleteResetPasswordPath = "/api/reset-password/identity/finish"; export const CompleteTOTPSignInPath = basePath + "/api/secondfactor/totp"
export const InitiateResetPasswordPath = basePath + "/api/reset-password/identity/start";
export const CompleteResetPasswordPath = basePath + "/api/reset-password/identity/finish";
// Do the password reset during completion. // Do the password reset during completion.
export const ResetPasswordPath = "/api/reset-password" export const ResetPasswordPath = basePath + "/api/reset-password"
export const LogoutPath = "/api/logout"; export const LogoutPath = basePath + "/api/logout";
export const StatePath = "/api/state"; export const StatePath = basePath + "/api/state";
export const UserInfoPath = "/api/user/info"; export const UserInfoPath = basePath + "/api/user/info";
export const UserInfo2FAMethodPath = "/api/user/info/2fa_method"; export const UserInfo2FAMethodPath = basePath + "/api/user/info/2fa_method";
export const Available2FAMethodsPath = "/api/secondfactor/available";
export const ConfigurationPath = "/api/configuration"; export const ConfigurationPath = basePath + "/api/configuration";
export const ExtendedConfigurationPath = "/api/configuration/extended"; export const ExtendedConfigurationPath = basePath + "/api/configuration/extended";
export interface ErrorResponse { export interface ErrorResponse {
status: "KO"; status: "KO";

View File

@ -18,14 +18,14 @@ export async function Post<T>(path: string, body?: any) {
return res; return res;
} }
export async function Get<T = undefined>(path: string) { export async function Get<T = undefined>(path: string): Promise<T> {
const res = await axios.get<ServiceResponse<T>>(path); const res = await axios.get<ServiceResponse<T>>(path);
if (res.status !== 200 || hasServiceError(res)) { if (res.status !== 200 || hasServiceError(res)) {
throw new Error(`Failed GET from ${path}. Code: ${res.status}.`); throw new Error(`Failed GET from ${path}. Code: ${res.status}.`);
} }
const d = toData(res); const d = toData<T>(res);
if (!d) { if (!d) {
throw new Error("unexpected type of response"); throw new Error("unexpected type of response");
} }

View File

@ -12,6 +12,6 @@ export interface AutheliaState {
authentication_level: AuthenticationLevel authentication_level: AuthenticationLevel
} }
export function getState() { export async function getState(): Promise<AutheliaState> {
return Get<AutheliaState>(StatePath); return Get<AutheliaState>(StatePath);
} }

View File

@ -1,3 +1,4 @@
import { configure } from 'enzyme'; import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16'; import Adapter from 'enzyme-adapter-react-16';
document.body.setAttribute("data-basepath", "");
configure({ adapter: new Adapter() }); configure({ adapter: new Adapter() });