diff --git a/config.template.yml b/config.template.yml index b1d6cda6b..2fa3825e9 100644 --- a/config.template.yml +++ b/config.template.yml @@ -7,7 +7,7 @@ ## Certificates directory specifies where Authelia will load trusted certificates (public portion) from in addition to ## the system certificates store. ## They should be in base64 format, and have one of the following extensions: *.cer, *.crt, *.pem. -# certificates_directory: /config/certificates +# certificates_directory: /config/certificates/ ## The theme to display: light, dark, grey, auto. theme: light @@ -40,6 +40,10 @@ server: ## Must be alphanumeric chars and should not contain any slashes. path: "" + ## Set the path on disk to Authelia assets. + ## Useful to allow overriding of specific static assets. + # asset_path: /config/assets/ + ## Buffers usually should be configured to be the same value. ## Explanation at https://www.authelia.com/docs/configuration/server.html ## Read buffer size adjusts the server's max incoming request size in bytes. diff --git a/docs/configuration/server.md b/docs/configuration/server.md index 1ccae5f6d..ba8cc8d5d 100644 --- a/docs/configuration/server.md +++ b/docs/configuration/server.md @@ -87,6 +87,33 @@ server: path: authelia ``` +### asset_path +
+type: string +{: .label .label-config .label-purple } +default: "" +{: .label .label-config .label-blue } +required: no +{: .label .label-config .label-green } +
+ +Authelia by default serves all static assets from an embedded filesystem in the Go binary. + +Modifying this setting will allow you to override and serve specific assets for Authelia from a specified path. +All files that can be overridden are documented below and must be placed in the `asset_path` with a flat file structure. + +Example: +```console +/config/assets/ +├── favicon.ico +└── logo.png +``` + +|Asset |File name| +|:-----:|:---------------:| +|Favicon|favicon.ico | +|Logo |logo.png | + ### read_buffer_size
type: integer @@ -189,3 +216,8 @@ The path to the public certificate for TLS connections. Must be in DER base64/PE 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 as the request. However you're able to tune these individually depending on your needs. + +### Asset Overrides + +If replacing the Logo for your Authelia portal it is recommended to upload a transparent PNG of your desired logo. +Authelia will automatically resize the logo to an appropriate size to present in the frontend. \ No newline at end of file diff --git a/internal/configuration/config.template.yml b/internal/configuration/config.template.yml index b1d6cda6b..2fa3825e9 100644 --- a/internal/configuration/config.template.yml +++ b/internal/configuration/config.template.yml @@ -7,7 +7,7 @@ ## Certificates directory specifies where Authelia will load trusted certificates (public portion) from in addition to ## the system certificates store. ## They should be in base64 format, and have one of the following extensions: *.cer, *.crt, *.pem. -# certificates_directory: /config/certificates +# certificates_directory: /config/certificates/ ## The theme to display: light, dark, grey, auto. theme: light @@ -40,6 +40,10 @@ server: ## Must be alphanumeric chars and should not contain any slashes. path: "" + ## Set the path on disk to Authelia assets. + ## Useful to allow overriding of specific static assets. + # asset_path: /config/assets/ + ## Buffers usually should be configured to be the same value. ## Explanation at https://www.authelia.com/docs/configuration/server.html ## Read buffer size adjusts the server's max incoming request size in bytes. diff --git a/internal/configuration/schema/server.go b/internal/configuration/schema/server.go index d07165cb0..b05c448f8 100644 --- a/internal/configuration/schema/server.go +++ b/internal/configuration/schema/server.go @@ -5,6 +5,7 @@ type ServerConfiguration struct { Host string `koanf:"host"` Port int `koanf:"port"` Path string `koanf:"path"` + AssetPath string `koanf:"asset_path"` ReadBufferSize int `koanf:"read_buffer_size"` WriteBufferSize int `koanf:"write_buffer_size"` EnablePprof bool `koanf:"enable_endpoint_pprof"` diff --git a/internal/configuration/validator/const.go b/internal/configuration/validator/const.go index b6339ca33..b56819e1c 100644 --- a/internal/configuration/validator/const.go +++ b/internal/configuration/validator/const.go @@ -147,6 +147,7 @@ var ValidKeys = []string{ "server.read_buffer_size", "server.write_buffer_size", "server.path", + "server.asset_path", "server.enable_pprof", "server.enable_expvars", "server.disable_healthcheck", diff --git a/internal/middlewares/asset_override.go b/internal/middlewares/asset_override.go new file mode 100644 index 000000000..649ed965b --- /dev/null +++ b/internal/middlewares/asset_override.go @@ -0,0 +1,29 @@ +package middlewares + +import ( + "os" + "strings" + + "github.com/valyala/fasthttp" + + "github.com/authelia/authelia/v4/internal/utils" +) + +// AssetOverrideMiddleware allows overriding and serving of specific embedded assets from disk. +func AssetOverrideMiddleware(assetPath string, next fasthttp.RequestHandler) fasthttp.RequestHandler { + return func(ctx *fasthttp.RequestCtx) { + uri := string(ctx.RequestURI()) + file := uri[strings.LastIndex(uri, "/")+1:] + + if assetPath != "" && utils.IsStringInSlice(file, validOverrideAssets) { + _, err := os.Stat(assetPath + file) + if err != nil { + next(ctx) + } else { + fasthttp.FSHandler(assetPath, strings.Count(uri, "/")-1)(ctx) + } + } else { + next(ctx) + } + } +} diff --git a/internal/middlewares/const.go b/internal/middlewares/const.go index 1d404a708..27c157eb9 100644 --- a/internal/middlewares/const.go +++ b/internal/middlewares/const.go @@ -29,3 +29,4 @@ const ( ) var protoHostSeparator = []byte("://") +var validOverrideAssets = []string{"favicon.ico", "logo.png"} diff --git a/internal/server/const.go b/internal/server/const.go index 9251aa95e..a01473383 100644 --- a/internal/server/const.go +++ b/internal/server/const.go @@ -1,9 +1,14 @@ package server -const embeddedAssets = "public_html/" -const swaggerAssets = embeddedAssets + "api/" -const apiFile = "openapi.yml" -const indexFile = "index.html" +const ( + embeddedAssets = "public_html/" + swaggerAssets = embeddedAssets + "api/" + apiFile = "openapi.yml" + indexFile = "index.html" + logoFile = "logo.png" +) + +var rootFiles = []string{"favicon.ico", "manifest.json", "robots.txt"} const dev = "dev" diff --git a/internal/server/server.go b/internal/server/server.go index 907ecb0ce..1f99bfef2 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -32,13 +32,12 @@ func registerRoutes(configuration schema.Configuration, providers middlewares.Pr embeddedPath, _ := fs.Sub(assets, "public_html") embeddedFS := fasthttpadaptor.NewFastHTTPHandler(http.FileServer(http.FS(embeddedPath))) - rootFiles := []string{"favicon.ico", "manifest.json", "robots.txt"} https := configuration.Server.TLS.Key != "" && configuration.Server.TLS.Certificate != "" - serveIndexHandler := ServeTemplatedFile(embeddedAssets, indexFile, rememberMe, resetPassword, configuration.Session.Name, configuration.Theme, https) - serveSwaggerHandler := ServeTemplatedFile(swaggerAssets, indexFile, rememberMe, resetPassword, configuration.Session.Name, configuration.Theme, https) - serveSwaggerAPIHandler := ServeTemplatedFile(swaggerAssets, apiFile, rememberMe, resetPassword, configuration.Session.Name, configuration.Theme, https) + serveIndexHandler := ServeTemplatedFile(embeddedAssets, indexFile, configuration.Server.AssetPath, rememberMe, resetPassword, configuration.Session.Name, configuration.Theme, https) + serveSwaggerHandler := ServeTemplatedFile(swaggerAssets, indexFile, configuration.Server.AssetPath, rememberMe, resetPassword, configuration.Session.Name, configuration.Theme, https) + serveSwaggerAPIHandler := ServeTemplatedFile(swaggerAssets, apiFile, configuration.Server.AssetPath, rememberMe, resetPassword, configuration.Session.Name, configuration.Theme, https) r := router.New() r.GET("/", serveIndexHandler) @@ -48,10 +47,10 @@ func registerRoutes(configuration schema.Configuration, providers middlewares.Pr r.GET("/api/"+apiFile, serveSwaggerAPIHandler) for _, f := range rootFiles { - r.GET("/"+f, embeddedFS) + r.GET("/"+f, middlewares.AssetOverrideMiddleware(configuration.Server.AssetPath, embeddedFS)) } - r.GET("/static/{filepath:*}", embeddedFS) + r.GET("/static/{filepath:*}", middlewares.AssetOverrideMiddleware(configuration.Server.AssetPath, embeddedFS)) r.ANY("/api/{filepath:*}", embeddedFS) r.GET("/api/health", autheliaMiddleware(handlers.HealthGet)) diff --git a/internal/server/template.go b/internal/server/template.go index a6e658452..6b6a39163 100644 --- a/internal/server/template.go +++ b/internal/server/template.go @@ -16,7 +16,7 @@ import ( // ServeTemplatedFile serves a templated version of a specified file, // this is utilised to pass information between the backend and frontend // and generate a nonce to support a restrictive CSP while using material-ui. -func ServeTemplatedFile(publicDir, file, rememberMe, resetPassword, session, theme string, https bool) fasthttp.RequestHandler { +func ServeTemplatedFile(publicDir, file, assetPath, rememberMe, resetPassword, session, theme string, https bool) fasthttp.RequestHandler { logger := logging.Logger() f, err := assets.Open(publicDir + file) @@ -40,6 +40,14 @@ func ServeTemplatedFile(publicDir, file, rememberMe, resetPassword, session, the base = baseURL.(string) } + logoOverride := "false" + + if assetPath != "" { + if _, err := os.Stat(assetPath + logoFile); err == nil { + logoOverride = "true" + } + } + var scheme = "https" if !https { @@ -71,7 +79,7 @@ func ServeTemplatedFile(publicDir, file, rememberMe, resetPassword, session, the ctx.Response.Header.Add("Content-Security-Policy", fmt.Sprintf("default-src 'self' ; object-src 'none'; style-src 'self' 'nonce-%s'", nonce)) } - err := tmpl.Execute(ctx.Response.BodyWriter(), struct{ Base, BaseURL, CSPNonce, RememberMe, ResetPassword, Session, Theme string }{Base: base, BaseURL: baseURL, CSPNonce: nonce, RememberMe: rememberMe, ResetPassword: resetPassword, Session: session, Theme: theme}) + err := tmpl.Execute(ctx.Response.BodyWriter(), struct{ Base, BaseURL, CSPNonce, LogoOverride, RememberMe, ResetPassword, Session, Theme string }{Base: base, BaseURL: baseURL, CSPNonce: nonce, LogoOverride: logoOverride, RememberMe: rememberMe, ResetPassword: resetPassword, Session: session, Theme: theme}) if err != nil { ctx.Error("an error occurred", 503) logger.Errorf("Unable to execute template: %v", err) diff --git a/internal/suites/Traefik/configuration.yml b/internal/suites/Traefik/configuration.yml index 046c181dc..d76301826 100644 --- a/internal/suites/Traefik/configuration.yml +++ b/internal/suites/Traefik/configuration.yml @@ -7,6 +7,7 @@ jwt_secret: unsecure_secret server: port: 9091 + asset_path: /config/assets/ tls: certificate: /config/ssl/cert.pem key: /config/ssl/key.pem diff --git a/internal/suites/Traefik2/configuration.yml b/internal/suites/Traefik2/configuration.yml index c2427fdc7..6390464aa 100644 --- a/internal/suites/Traefik2/configuration.yml +++ b/internal/suites/Traefik2/configuration.yml @@ -7,6 +7,7 @@ jwt_secret: unsecure_secret server: port: 9091 + asset_path: /config/assets/ tls: certificate: /config/ssl/cert.pem key: /config/ssl/key.pem @@ -46,6 +47,9 @@ access_control: - domain: "singlefactor.example.com" policy: one_factor +ntp: + version: 3 + notifier: smtp: host: smtp diff --git a/internal/suites/Traefik2/docker-compose.yml b/internal/suites/Traefik2/docker-compose.yml index 3d2c16e79..4cdf56953 100644 --- a/internal/suites/Traefik2/docker-compose.yml +++ b/internal/suites/Traefik2/docker-compose.yml @@ -5,5 +5,7 @@ services: volumes: - './Traefik2/configuration.yml:/config/configuration.yml:ro' - './Traefik2/users.yml:/config/users.yml' + - './Traefik2/favicon.ico:/config/assets/favicon.ico' + - './Traefik2/logo.png:/config/assets/logo.png' - './common/ssl:/config/ssl:ro' ... diff --git a/internal/suites/Traefik2/favicon.ico b/internal/suites/Traefik2/favicon.ico new file mode 100644 index 000000000..46d3d0e2a Binary files /dev/null and b/internal/suites/Traefik2/favicon.ico differ diff --git a/internal/suites/Traefik2/logo.png b/internal/suites/Traefik2/logo.png new file mode 100644 index 000000000..0b2e35e47 Binary files /dev/null and b/internal/suites/Traefik2/logo.png differ diff --git a/web/.env.development b/web/.env.development index 80deba03d..7e7d77ed4 100644 --- a/web/.env.development +++ b/web/.env.development @@ -1,4 +1,5 @@ VITE_HMR_PORT=8080 +VITE_LOGO_OVERRIDE=false VITE_PUBLIC_URL="" VITE_REMEMBER_ME=true VITE_RESET_PASSWORD=true diff --git a/web/.env.production b/web/.env.production index b9208b2f2..9c94810ec 100644 --- a/web/.env.production +++ b/web/.env.production @@ -1,3 +1,4 @@ +VITE_LOGO_OVERRIDE={{.LogoOverride}} VITE_PUBLIC_URL={{.Base}} VITE_REMEMBER_ME={{.RememberMe}} VITE_RESET_PASSWORD={{.ResetPassword}} diff --git a/web/index.html b/web/index.html index b51a1f864..e6858e07d 100644 --- a/web/index.html +++ b/web/index.html @@ -13,7 +13,7 @@ Login - Authelia - +
diff --git a/web/package.json b/web/package.json index 7f52c6f5a..2606e0228 100644 --- a/web/package.json +++ b/web/package.json @@ -56,7 +56,7 @@ "sourcemap": true } ], - "^.+\\.(css|svg)$": "jest-transform-stub" + "^.+\\.(css|png|svg)$": "jest-transform-stub" }, "transformIgnorePatterns": [ "[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs|cjs|ts|tsx)$" diff --git a/web/src/layouts/LoginLayout.tsx b/web/src/layouts/LoginLayout.tsx index 376880ab1..a47e98793 100644 --- a/web/src/layouts/LoginLayout.tsx +++ b/web/src/layouts/LoginLayout.tsx @@ -4,6 +4,7 @@ import { Grid, makeStyles, Container, Typography, Link } from "@material-ui/core import { grey } from "@material-ui/core/colors"; import { ReactComponent as UserSvg } from "@assets/images/user.svg"; +import { getLogoOverride } from "@utils/Configuration"; export interface Props { id?: string; @@ -14,12 +15,17 @@ export interface Props { const LoginLayout = function (props: Props) { const style = useStyles(); + const logo = getLogoOverride() ? ( + Logo + ) : ( + + ); return ( - + {logo} {props.title ? ( diff --git a/web/src/utils/Configuration.ts b/web/src/utils/Configuration.ts index a84dc308a..73aab612e 100644 --- a/web/src/utils/Configuration.ts +++ b/web/src/utils/Configuration.ts @@ -7,6 +7,10 @@ export function getEmbeddedVariable(variableName: string) { return value; } +export function getLogoOverride() { + return getEmbeddedVariable("logooverride") === "true"; +} + export function getRememberMe() { return getEmbeddedVariable("rememberme") === "true"; }