refactor(server): simplify templating and url derivation (#4547)
This refactors a few areas of the server templating and related functions.pull/4585/head
parent
3de693623e
commit
d13247ce43
|
@ -1,6 +1,13 @@
|
|||
---
|
||||
extends: default
|
||||
|
||||
locale: en_US.UTF-8
|
||||
|
||||
yaml-files:
|
||||
- '*.yaml'
|
||||
- '*.yml'
|
||||
- '.yamllint'
|
||||
|
||||
ignore: |
|
||||
docs/pnpm-lock.yaml
|
||||
internal/configuration/test_resources/config_bad_quoting.yml
|
||||
|
|
|
@ -52,13 +52,7 @@ func OpenIDConnectAuthorization(ctx *middlewares.AutheliaCtx, rw http.ResponseWr
|
|||
return
|
||||
}
|
||||
|
||||
if issuer, err = ctx.IssuerURL(); err != nil {
|
||||
ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' could not be processed: error occurred determining issuer: %+v", requester.GetID(), clientID, err)
|
||||
|
||||
ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, oidc.ErrIssuerCouldNotDerive)
|
||||
|
||||
return
|
||||
}
|
||||
issuer = ctx.RootURL()
|
||||
|
||||
userSession := ctx.GetSession()
|
||||
|
||||
|
|
|
@ -130,12 +130,7 @@ func OpenIDConnectConsentPOST(ctx *middlewares.AutheliaCtx) {
|
|||
query url.Values
|
||||
)
|
||||
|
||||
if redirectURI, err = ctx.IssuerURL(); err != nil {
|
||||
ctx.Logger.Errorf("Failed to parse the consent redirect URL: %+v", err)
|
||||
ctx.SetJSONError(messageOperationFailed)
|
||||
|
||||
return
|
||||
}
|
||||
redirectURI = ctx.RootURL()
|
||||
|
||||
if query, err = url.ParseQuery(consent.Form); err != nil {
|
||||
ctx.Logger.Errorf("Failed to parse the consent form values: %+v", err)
|
||||
|
|
|
@ -20,13 +20,7 @@ func OpenIDConnectConfigurationWellKnownGET(ctx *middlewares.AutheliaCtx) {
|
|||
err error
|
||||
)
|
||||
|
||||
if issuer, err = ctx.IssuerURL(); err != nil {
|
||||
ctx.Logger.Errorf("Error occurred determining OpenID Connect issuer details: %+v", err)
|
||||
|
||||
ctx.ReplyStatusCode(fasthttp.StatusBadRequest)
|
||||
|
||||
return
|
||||
}
|
||||
issuer = ctx.RootURL()
|
||||
|
||||
wellKnown := ctx.Providers.OpenIDConnect.GetOpenIDConnectWellKnownConfiguration(issuer.String())
|
||||
|
||||
|
@ -52,13 +46,7 @@ func OAuthAuthorizationServerWellKnownGET(ctx *middlewares.AutheliaCtx) {
|
|||
err error
|
||||
)
|
||||
|
||||
if issuer, err = ctx.IssuerURL(); err != nil {
|
||||
ctx.Logger.Errorf("Error occurred determining OpenID Connect issuer details: %+v", err)
|
||||
|
||||
ctx.ReplyStatusCode(fasthttp.StatusBadRequest)
|
||||
|
||||
return
|
||||
}
|
||||
issuer = ctx.RootURL()
|
||||
|
||||
wellKnown := ctx.Providers.OpenIDConnect.GetOAuth2WellKnownConfiguration(issuer.String())
|
||||
|
||||
|
|
|
@ -144,11 +144,7 @@ func handleOIDCWorkflowResponseWithTargetURL(ctx *middlewares.AutheliaCtx, targe
|
|||
return
|
||||
}
|
||||
|
||||
if issuerURL, err = ctx.IssuerURL(); err != nil {
|
||||
ctx.Error(fmt.Errorf("unable to get issuer for redirection: %w", err), messageAuthenticationFailed)
|
||||
|
||||
return
|
||||
}
|
||||
issuerURL = ctx.RootURL()
|
||||
|
||||
if targetURL.Host != issuerURL.Host {
|
||||
ctx.Error(fmt.Errorf("unable to redirect to '%s': target host '%s' does not match expected issuer host '%s'", targetURL, targetURL.Host, issuerURL.Host), messageAuthenticationFailed)
|
||||
|
@ -221,11 +217,7 @@ func handleOIDCWorkflowResponseWithID(ctx *middlewares.AutheliaCtx, id string) {
|
|||
form url.Values
|
||||
)
|
||||
|
||||
if targetURL, err = ctx.IssuerURL(); err != nil {
|
||||
ctx.Error(fmt.Errorf("unable to get issuer for redirection: %w", err), messageAuthenticationFailed)
|
||||
|
||||
return
|
||||
}
|
||||
targetURL = ctx.RootURL()
|
||||
|
||||
if form, err = consent.GetForm(); err != nil {
|
||||
ctx.Error(fmt.Errorf("unable to get authorization form values from consent session with challenge id '%s': %w", consent.ChallengeID, err), messageAuthenticationFailed)
|
||||
|
|
|
@ -5,7 +5,6 @@ import (
|
|||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
|
@ -81,7 +80,7 @@ func (ctx *AutheliaCtx) ReplyError(err error, message string) {
|
|||
ctx.Logger.Error(marshalErr)
|
||||
}
|
||||
|
||||
ctx.SetContentTypeBytes(contentTypeApplicationJSON)
|
||||
ctx.SetContentTypeApplicationJSON()
|
||||
ctx.SetBody(b)
|
||||
ctx.Logger.Debug(err)
|
||||
}
|
||||
|
@ -90,7 +89,7 @@ func (ctx *AutheliaCtx) ReplyError(err error, message string) {
|
|||
func (ctx *AutheliaCtx) ReplyStatusCode(statusCode int) {
|
||||
ctx.Response.Reset()
|
||||
ctx.SetStatusCode(statusCode)
|
||||
ctx.SetContentTypeBytes(contentTypeTextPlain)
|
||||
ctx.SetContentTypeTextPlain()
|
||||
ctx.SetBodyString(fmt.Sprintf("%d %s", statusCode, fasthttp.StatusMessage(statusCode)))
|
||||
}
|
||||
|
||||
|
@ -108,7 +107,7 @@ func (ctx *AutheliaCtx) ReplyJSON(data any, statusCode int) (err error) {
|
|||
ctx.SetStatusCode(statusCode)
|
||||
}
|
||||
|
||||
ctx.SetContentTypeBytes(contentTypeApplicationJSON)
|
||||
ctx.SetContentTypeApplicationJSON()
|
||||
ctx.SetBody(body)
|
||||
|
||||
return nil
|
||||
|
@ -145,7 +144,7 @@ func (ctx *AutheliaCtx) XForwardedProto() (proto []byte) {
|
|||
}
|
||||
|
||||
// XForwardedMethod return the content of the X-Forwarded-Method header.
|
||||
func (ctx *AutheliaCtx) XForwardedMethod() (method []byte) {
|
||||
func (ctx *AutheliaCtx) XForwardedMethod() []byte {
|
||||
return ctx.RequestCtx.Request.Header.PeekBytes(headerXForwardedMethod)
|
||||
}
|
||||
|
||||
|
@ -171,79 +170,61 @@ func (ctx *AutheliaCtx) XForwardedURI() (uri []byte) {
|
|||
return uri
|
||||
}
|
||||
|
||||
// XAutheliaURL return the content of the X-Authelia-URL header.
|
||||
func (ctx *AutheliaCtx) XAutheliaURL() (autheliaURL []byte) {
|
||||
// XOriginalURL returns the content of the X-Original-URL header.
|
||||
func (ctx *AutheliaCtx) XOriginalURL() []byte {
|
||||
return ctx.RequestCtx.Request.Header.PeekBytes(headerXOriginalURL)
|
||||
}
|
||||
|
||||
// XOriginalMethod return the content of the X-Original-Method header.
|
||||
func (ctx *AutheliaCtx) XOriginalMethod() []byte {
|
||||
return ctx.RequestCtx.Request.Header.PeekBytes(headerXOriginalMethod)
|
||||
}
|
||||
|
||||
// XAutheliaURL return the content of the X-Authelia-URL header which is used to communicate the location of the
|
||||
// portal when using proxies like Envoy.
|
||||
func (ctx *AutheliaCtx) XAutheliaURL() []byte {
|
||||
return ctx.RequestCtx.Request.Header.PeekBytes(headerXAutheliaURL)
|
||||
}
|
||||
|
||||
// QueryArgRedirect return the content of the rd query argument.
|
||||
func (ctx *AutheliaCtx) QueryArgRedirect() (val []byte) {
|
||||
return ctx.RequestCtx.QueryArgs().PeekBytes(queryArgRedirect)
|
||||
func (ctx *AutheliaCtx) QueryArgRedirect() []byte {
|
||||
return ctx.RequestCtx.QueryArgs().PeekBytes(qryArgRedirect)
|
||||
}
|
||||
|
||||
// BasePath returns the base_url as per the path visited by the client.
|
||||
func (ctx *AutheliaCtx) BasePath() (base string) {
|
||||
func (ctx *AutheliaCtx) BasePath() string {
|
||||
if baseURL := ctx.UserValueBytes(UserValueKeyBaseURL); baseURL != nil {
|
||||
return baseURL.(string)
|
||||
}
|
||||
|
||||
return base
|
||||
return ""
|
||||
}
|
||||
|
||||
// ExternalRootURL gets the X-Forwarded-Proto, X-Forwarded-Host headers and the BasePath and forms them into a URL.
|
||||
func (ctx *AutheliaCtx) ExternalRootURL() (string, error) {
|
||||
protocol := ctx.XForwardedProto()
|
||||
if protocol == nil {
|
||||
return "", errMissingXForwardedProto
|
||||
// BasePathSlash is the same as BasePath but returns a final slash as well.
|
||||
func (ctx *AutheliaCtx) BasePathSlash() string {
|
||||
if baseURL := ctx.UserValueBytes(UserValueKeyBaseURL); baseURL != nil {
|
||||
return baseURL.(string) + strSlash
|
||||
}
|
||||
|
||||
host := ctx.XForwardedHost()
|
||||
if host == nil {
|
||||
return "", errMissingXForwardedHost
|
||||
}
|
||||
|
||||
externalRootURL := fmt.Sprintf("%s://%s", protocol, host)
|
||||
|
||||
if base := ctx.BasePath(); base != "" {
|
||||
externalBaseURL, err := url.ParseRequestURI(externalRootURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
externalBaseURL.Path = path.Join(externalBaseURL.Path, base)
|
||||
|
||||
return externalBaseURL.String(), nil
|
||||
}
|
||||
|
||||
return externalRootURL, nil
|
||||
return strSlash
|
||||
}
|
||||
|
||||
// IssuerURL returns the expected Issuer.
|
||||
func (ctx *AutheliaCtx) IssuerURL() (issuerURL *url.URL, err error) {
|
||||
issuerURL = &url.URL{
|
||||
Scheme: "https",
|
||||
// RootURL returns the Root URL.
|
||||
func (ctx *AutheliaCtx) RootURL() (issuerURL *url.URL) {
|
||||
return &url.URL{
|
||||
Scheme: string(ctx.XForwardedProto()),
|
||||
Host: string(ctx.XForwardedHost()),
|
||||
Path: ctx.BasePath(),
|
||||
}
|
||||
|
||||
if scheme := ctx.XForwardedProto(); scheme != nil {
|
||||
issuerURL.Scheme = string(scheme)
|
||||
}
|
||||
|
||||
if host := ctx.XForwardedHost(); len(host) != 0 {
|
||||
issuerURL.Host = string(host)
|
||||
} else {
|
||||
return nil, errMissingXForwardedHost
|
||||
}
|
||||
|
||||
if base := ctx.BasePath(); base != "" {
|
||||
issuerURL.Path = path.Join(issuerURL.Path, base)
|
||||
}
|
||||
|
||||
return issuerURL, nil
|
||||
}
|
||||
|
||||
// XOriginalURL return the content of the X-Original-URL header.
|
||||
func (ctx *AutheliaCtx) XOriginalURL() []byte {
|
||||
return ctx.RequestCtx.Request.Header.PeekBytes(headerXOriginalURL)
|
||||
// RootURLSlash is the same as RootURL but includes a final slash as well.
|
||||
func (ctx *AutheliaCtx) RootURLSlash() (issuerURL *url.URL) {
|
||||
return &url.URL{
|
||||
Scheme: string(ctx.XForwardedProto()),
|
||||
Host: string(ctx.XForwardedHost()),
|
||||
Path: ctx.BasePathSlash(),
|
||||
}
|
||||
}
|
||||
|
||||
// GetSession return the user session. Any update will be saved in cache.
|
||||
|
@ -264,7 +245,7 @@ func (ctx *AutheliaCtx) SaveSession(userSession session.UserSession) error {
|
|||
|
||||
// ReplyOK is a helper method to reply ok.
|
||||
func (ctx *AutheliaCtx) ReplyOK() {
|
||||
ctx.SetContentTypeBytes(contentTypeApplicationJSON)
|
||||
ctx.SetContentTypeApplicationJSON()
|
||||
ctx.SetBody(okMessageBytes)
|
||||
}
|
||||
|
||||
|
@ -377,7 +358,7 @@ func (ctx *AutheliaCtx) SpecialRedirect(uri string, statusCode int) {
|
|||
statusCode = fasthttp.StatusFound
|
||||
}
|
||||
|
||||
ctx.SetContentTypeBytes(contentTypeTextHTML)
|
||||
ctx.SetContentTypeTextHTML()
|
||||
ctx.SetStatusCode(statusCode)
|
||||
|
||||
u := fasthttp.AcquireURI()
|
||||
|
@ -400,3 +381,18 @@ func (ctx *AutheliaCtx) RecordAuthentication(success, regulated bool, method str
|
|||
|
||||
ctx.Providers.Metrics.RecordAuthentication(success, regulated, method)
|
||||
}
|
||||
|
||||
// SetContentTypeTextPlain efficiently sets the Content-Type header to 'text/plain; charset=utf-8'.
|
||||
func (ctx *AutheliaCtx) SetContentTypeTextPlain() {
|
||||
ctx.SetContentTypeBytes(contentTypeTextPlain)
|
||||
}
|
||||
|
||||
// SetContentTypeTextHTML efficiently sets the Content-Type header to 'text/html; charset=utf-8'.
|
||||
func (ctx *AutheliaCtx) SetContentTypeTextHTML() {
|
||||
ctx.SetContentTypeBytes(contentTypeTextHTML)
|
||||
}
|
||||
|
||||
// SetContentTypeApplicationJSON efficiently sets the Content-Type header to 'application/json; charset=utf-8'.
|
||||
func (ctx *AutheliaCtx) SetContentTypeApplicationJSON() {
|
||||
ctx.SetContentTypeBytes(contentTypeApplicationJSON)
|
||||
}
|
||||
|
|
|
@ -21,7 +21,6 @@ func TestIssuerURL(t *testing.T) {
|
|||
name string
|
||||
proto, host, base string
|
||||
expected string
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "Standard",
|
||||
|
@ -36,7 +35,7 @@ func TestIssuerURL(t *testing.T) {
|
|||
{
|
||||
name: "NoHost",
|
||||
proto: "https", host: "", base: "",
|
||||
err: "Missing header X-Forwarded-Host",
|
||||
expected: "https:",
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -52,21 +51,14 @@ func TestIssuerURL(t *testing.T) {
|
|||
mock.Ctx.SetUserValue("base_url", tc.base)
|
||||
}
|
||||
|
||||
actual, err := mock.Ctx.IssuerURL()
|
||||
actual := mock.Ctx.RootURL()
|
||||
|
||||
switch tc.err {
|
||||
case "":
|
||||
assert.NoError(t, err)
|
||||
require.NotNil(t, actual)
|
||||
require.NotNil(t, actual)
|
||||
|
||||
assert.Equal(t, tc.expected, actual.String())
|
||||
assert.Equal(t, tc.proto, actual.Scheme)
|
||||
assert.Equal(t, tc.host, actual.Host)
|
||||
assert.Equal(t, tc.base, actual.Path)
|
||||
default:
|
||||
assert.EqualError(t, err, tc.err)
|
||||
assert.Nil(t, actual)
|
||||
}
|
||||
assert.Equal(t, tc.expected, actual.String())
|
||||
assert.Equal(t, tc.proto, actual.Scheme)
|
||||
assert.Equal(t, tc.host, actual.Host)
|
||||
assert.Equal(t, tc.base, actual.Path)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ var (
|
|||
|
||||
headerXForwardedURI = []byte("X-Forwarded-URI")
|
||||
headerXOriginalURL = []byte("X-Original-URL")
|
||||
headerXOriginalMethod = []byte("X-Original-Method")
|
||||
headerXForwardedMethod = []byte("X-Forwarded-Method")
|
||||
|
||||
headerVary = []byte(fasthttp.HeaderVary)
|
||||
|
@ -67,13 +68,17 @@ var (
|
|||
const (
|
||||
strProtoHTTPS = "https"
|
||||
strProtoHTTP = "http"
|
||||
strSlash = "/"
|
||||
|
||||
queryArgRedirect = "rd"
|
||||
queryArgToken = "token"
|
||||
)
|
||||
|
||||
var (
|
||||
protoHTTPS = []byte(strProtoHTTPS)
|
||||
protoHTTP = []byte(strProtoHTTP)
|
||||
|
||||
queryArgRedirect = []byte("rd")
|
||||
qryArgRedirect = []byte(queryArgRedirect)
|
||||
|
||||
// UserValueKeyBaseURL is the User Value key where we store the Base URL.
|
||||
UserValueKeyBaseURL = []byte("base_url")
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"net/mail"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
|
@ -51,7 +52,7 @@ func IdentityVerificationStart(args IdentityVerificationStartArgs, delayFunc Tim
|
|||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
|
||||
ss, err := token.SignedString([]byte(ctx.Configuration.JWTSecret))
|
||||
signedToken, err := token.SignedString([]byte(ctx.Configuration.JWTSecret))
|
||||
if err != nil {
|
||||
ctx.Error(err, messageOperationFailed)
|
||||
return
|
||||
|
@ -62,23 +63,23 @@ func IdentityVerificationStart(args IdentityVerificationStartArgs, delayFunc Tim
|
|||
return
|
||||
}
|
||||
|
||||
var (
|
||||
uri string
|
||||
)
|
||||
|
||||
if uri, err = ctx.ExternalRootURL(); err != nil {
|
||||
ctx.Error(err, messageOperationFailed)
|
||||
return
|
||||
}
|
||||
|
||||
disableHTML := false
|
||||
if ctx.Configuration.Notifier.SMTP != nil {
|
||||
disableHTML = ctx.Configuration.Notifier.SMTP.DisableHTMLEmails
|
||||
}
|
||||
|
||||
linkURL := ctx.RootURL()
|
||||
|
||||
query := linkURL.Query()
|
||||
|
||||
query.Set(queryArgToken, signedToken)
|
||||
|
||||
linkURL.Path = path.Join(linkURL.Path, args.TargetEndpoint)
|
||||
linkURL.RawQuery = query.Encode()
|
||||
|
||||
values := templates.EmailIdentityVerificationValues{
|
||||
Title: args.MailTitle,
|
||||
LinkURL: fmt.Sprintf("%s%s?token=%s", uri, args.TargetEndpoint, ss),
|
||||
LinkURL: linkURL.String(),
|
||||
LinkText: args.MailButtonContent,
|
||||
DisplayName: identity.DisplayName,
|
||||
RemoteIP: ctx.RemoteIP().String(),
|
||||
|
|
|
@ -91,24 +91,6 @@ func TestShouldFailSendingAnEmail(t *testing.T) {
|
|||
assert.Equal(t, "no notif", mock.Hook.LastEntry().Message)
|
||||
}
|
||||
|
||||
func TestShouldFailWhenXForwardedHostHeaderIsMissing(t *testing.T) {
|
||||
mock := mocks.NewMockAutheliaCtx(t)
|
||||
defer mock.Close()
|
||||
|
||||
mock.Ctx.Configuration.JWTSecret = testJWTSecret
|
||||
mock.Ctx.Request.Header.Add("X-Forwarded-Proto", "http")
|
||||
|
||||
mock.StorageMock.EXPECT().
|
||||
SaveIdentityVerification(mock.Ctx, gomock.Any()).
|
||||
Return(nil)
|
||||
|
||||
args := newArgs(defaultRetriever)
|
||||
middlewares.IdentityVerificationStart(args, nil)(mock.Ctx)
|
||||
|
||||
assert.Equal(t, 200, mock.Ctx.Response.StatusCode())
|
||||
assert.Equal(t, "Missing header X-Forwarded-Host", mock.Hook.LastEntry().Message)
|
||||
}
|
||||
|
||||
func TestShouldSucceedIdentityVerificationStartProcess(t *testing.T) {
|
||||
mock := mocks.NewMockAutheliaCtx(t)
|
||||
|
||||
|
|
|
@ -11,6 +11,9 @@ const (
|
|||
fileOpenAPI = "openapi.yml"
|
||||
fileIndexHTML = "index.html"
|
||||
fileLogo = "logo.png"
|
||||
|
||||
extHTML = ".html"
|
||||
extJSON = ".json"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -47,6 +50,7 @@ var (
|
|||
)
|
||||
|
||||
const (
|
||||
environment = "ENVIRONMENT"
|
||||
dev = "dev"
|
||||
f = "false"
|
||||
t = "true"
|
||||
|
|
|
@ -3,7 +3,6 @@ package server
|
|||
import (
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -92,21 +91,11 @@ func handleNotFound(next fasthttp.RequestHandler) fasthttp.RequestHandler {
|
|||
}
|
||||
|
||||
func handleRouter(config schema.Configuration, providers middlewares.Providers) fasthttp.RequestHandler {
|
||||
rememberMe := strconv.FormatBool(config.Session.RememberMeDuration != schema.RememberMeDisabled)
|
||||
resetPassword := strconv.FormatBool(!config.AuthenticationBackend.PasswordReset.Disable)
|
||||
optsTemplatedFile := NewTemplatedFileOptions(&config)
|
||||
|
||||
resetPasswordCustomURL := config.AuthenticationBackend.PasswordReset.CustomURL.String()
|
||||
|
||||
duoSelfEnrollment := f
|
||||
if !config.DuoAPI.Disable {
|
||||
duoSelfEnrollment = strconv.FormatBool(config.DuoAPI.EnableSelfEnrollment)
|
||||
}
|
||||
|
||||
https := config.Server.TLS.Key != "" && config.Server.TLS.Certificate != ""
|
||||
|
||||
serveIndexHandler := ServeTemplatedFile(assetsRoot, fileIndexHTML, config.Server.AssetPath, duoSelfEnrollment, rememberMe, resetPassword, resetPasswordCustomURL, config.Session.Name, config.Theme, https)
|
||||
serveSwaggerHandler := ServeTemplatedFile(assetsSwagger, fileIndexHTML, config.Server.AssetPath, duoSelfEnrollment, rememberMe, resetPassword, resetPasswordCustomURL, config.Session.Name, config.Theme, https)
|
||||
serveSwaggerAPIHandler := ServeTemplatedFile(assetsSwagger, fileOpenAPI, config.Server.AssetPath, duoSelfEnrollment, rememberMe, resetPassword, resetPasswordCustomURL, config.Session.Name, config.Theme, https)
|
||||
serveIndexHandler := ServeTemplatedFile(assetsRoot, fileIndexHTML, optsTemplatedFile)
|
||||
serveSwaggerHandler := ServeTemplatedFile(assetsSwagger, fileIndexHTML, optsTemplatedFile)
|
||||
serveSwaggerAPIHandler := ServeTemplatedFile(assetsSwagger, fileOpenAPI, optsTemplatedFile)
|
||||
|
||||
handlerPublicHTML := newPublicHTMLEmbeddedHandler()
|
||||
handlerLocales := newLocalesEmbeddedHandler()
|
||||
|
@ -115,7 +104,7 @@ func handleRouter(config schema.Configuration, providers middlewares.Providers)
|
|||
WithPreMiddlewares(middlewares.SecurityHeaders).Build()
|
||||
|
||||
policyCORSPublicGET := middlewares.NewCORSPolicyBuilder().
|
||||
WithAllowedMethods("OPTIONS", "GET").
|
||||
WithAllowedMethods(fasthttp.MethodOptions, fasthttp.MethodGet).
|
||||
WithAllowedOrigins("*").
|
||||
Build()
|
||||
|
||||
|
|
|
@ -6,11 +6,13 @@ import (
|
|||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
||||
"github.com/authelia/authelia/v4/internal/logging"
|
||||
"github.com/authelia/authelia/v4/internal/middlewares"
|
||||
"github.com/authelia/authelia/v4/internal/utils"
|
||||
|
@ -19,7 +21,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, assetPath, duoSelfEnrollment, rememberMe, resetPassword, resetPasswordCustomURL, session, theme string, https bool) middlewares.RequestHandler {
|
||||
func ServeTemplatedFile(publicDir, file string, opts *TemplatedFileOptions) middlewares.RequestHandler {
|
||||
logger := logging.Logger()
|
||||
|
||||
a, err := assets.Open(path.Join(publicDir, file))
|
||||
|
@ -37,55 +39,40 @@ func ServeTemplatedFile(publicDir, file, assetPath, duoSelfEnrollment, rememberM
|
|||
logger.Fatalf("Unable to parse %s template: %s", file, err)
|
||||
}
|
||||
|
||||
return func(ctx *middlewares.AutheliaCtx) {
|
||||
base := ""
|
||||
if baseURL := ctx.UserValueBytes(middlewares.UserValueKeyBaseURL); baseURL != nil {
|
||||
base = baseURL.(string)
|
||||
}
|
||||
isDevEnvironment := os.Getenv(environment) == dev
|
||||
|
||||
return func(ctx *middlewares.AutheliaCtx) {
|
||||
logoOverride := f
|
||||
|
||||
if assetPath != "" {
|
||||
if _, err := os.Stat(filepath.Join(assetPath, fileLogo)); err == nil {
|
||||
if opts.AssetPath != "" {
|
||||
if _, err = os.Stat(filepath.Join(opts.AssetPath, fileLogo)); err == nil {
|
||||
logoOverride = t
|
||||
}
|
||||
}
|
||||
|
||||
var scheme = schemeHTTPS
|
||||
|
||||
if !https {
|
||||
proto := string(ctx.XForwardedProto())
|
||||
switch proto {
|
||||
case "":
|
||||
break
|
||||
case schemeHTTP, schemeHTTPS:
|
||||
scheme = proto
|
||||
}
|
||||
}
|
||||
|
||||
baseURL := scheme + "://" + string(ctx.XForwardedHost()) + base + "/"
|
||||
nonce := utils.RandomString(32, utils.CharSetAlphaNumeric, true)
|
||||
|
||||
switch extension := filepath.Ext(file); extension {
|
||||
case ".html":
|
||||
ctx.SetContentType("text/html; charset=utf-8")
|
||||
case extHTML:
|
||||
ctx.SetContentTypeTextHTML()
|
||||
case extJSON:
|
||||
ctx.SetContentTypeApplicationJSON()
|
||||
default:
|
||||
ctx.SetContentType("text/plain; charset=utf-8")
|
||||
ctx.SetContentTypeTextPlain()
|
||||
}
|
||||
|
||||
nonce := utils.RandomString(32, utils.CharSetAlphaNumeric, true)
|
||||
|
||||
switch {
|
||||
case publicDir == assetsSwagger:
|
||||
ctx.Response.Header.Add(fasthttp.HeaderContentSecurityPolicy, fmt.Sprintf(tmplCSPSwagger, nonce, nonce))
|
||||
case ctx.Configuration.Server.Headers.CSPTemplate != "":
|
||||
ctx.Response.Header.Add(fasthttp.HeaderContentSecurityPolicy, strings.ReplaceAll(ctx.Configuration.Server.Headers.CSPTemplate, placeholderCSPNonce, nonce))
|
||||
case os.Getenv("ENVIRONMENT") == dev:
|
||||
case isDevEnvironment:
|
||||
ctx.Response.Header.Add(fasthttp.HeaderContentSecurityPolicy, fmt.Sprintf(tmplCSPDevelopment, nonce))
|
||||
default:
|
||||
ctx.Response.Header.Add(fasthttp.HeaderContentSecurityPolicy, fmt.Sprintf(tmplCSPDefault, nonce))
|
||||
}
|
||||
|
||||
err := tmpl.Execute(ctx.Response.BodyWriter(), struct{ Base, BaseURL, CSPNonce, DuoSelfEnrollment, LogoOverride, RememberMe, ResetPassword, ResetPasswordCustomURL, Session, Theme string }{Base: base, BaseURL: baseURL, CSPNonce: nonce, DuoSelfEnrollment: duoSelfEnrollment, LogoOverride: logoOverride, RememberMe: rememberMe, ResetPassword: resetPassword, ResetPasswordCustomURL: resetPasswordCustomURL, Session: session, Theme: theme})
|
||||
if err != nil {
|
||||
if err = tmpl.Execute(ctx.Response.BodyWriter(), opts.CommonData(ctx.BasePath(), ctx.RootURLSlash().String(), nonce, logoOverride)); err != nil {
|
||||
ctx.RequestCtx.Error("an error occurred", 503)
|
||||
logger.Errorf("Unable to execute template: %v", err)
|
||||
|
||||
|
@ -128,3 +115,62 @@ func writeHealthCheckEnv(disabled bool, scheme, host, path string, port int) (er
|
|||
|
||||
return err
|
||||
}
|
||||
|
||||
// NewTemplatedFileOptions returns a new *TemplatedFileOptions.
|
||||
func NewTemplatedFileOptions(config *schema.Configuration) (opts *TemplatedFileOptions) {
|
||||
opts = &TemplatedFileOptions{
|
||||
AssetPath: config.Server.AssetPath,
|
||||
DuoSelfEnrollment: f,
|
||||
RememberMe: strconv.FormatBool(config.Session.RememberMeDuration != schema.RememberMeDisabled),
|
||||
ResetPassword: strconv.FormatBool(!config.AuthenticationBackend.PasswordReset.Disable),
|
||||
ResetPasswordCustomURL: config.AuthenticationBackend.PasswordReset.CustomURL.String(),
|
||||
Theme: config.Theme,
|
||||
}
|
||||
|
||||
if !config.DuoAPI.Disable {
|
||||
opts.DuoSelfEnrollment = strconv.FormatBool(config.DuoAPI.EnableSelfEnrollment)
|
||||
}
|
||||
|
||||
return opts
|
||||
}
|
||||
|
||||
// TemplatedFileOptions is a struct which is used for many templated files.
|
||||
type TemplatedFileOptions struct {
|
||||
AssetPath string
|
||||
DuoSelfEnrollment string
|
||||
RememberMe string
|
||||
ResetPassword string
|
||||
ResetPasswordCustomURL string
|
||||
Session string
|
||||
Theme string
|
||||
}
|
||||
|
||||
// CommonData returns a TemplatedFileCommonData with the dynamic options.
|
||||
func (options *TemplatedFileOptions) CommonData(base, baseURL, nonce, logoOverride string) TemplatedFileCommonData {
|
||||
return TemplatedFileCommonData{
|
||||
Base: base,
|
||||
BaseURL: baseURL,
|
||||
CSPNonce: nonce,
|
||||
LogoOverride: logoOverride,
|
||||
DuoSelfEnrollment: options.DuoSelfEnrollment,
|
||||
RememberMe: options.RememberMe,
|
||||
ResetPassword: options.ResetPassword,
|
||||
ResetPasswordCustomURL: options.ResetPasswordCustomURL,
|
||||
Session: options.Session,
|
||||
Theme: options.Theme,
|
||||
}
|
||||
}
|
||||
|
||||
// TemplatedFileCommonData is a struct which is used for many templated files.
|
||||
type TemplatedFileCommonData struct {
|
||||
Base string
|
||||
BaseURL string
|
||||
CSPNonce string
|
||||
LogoOverride string
|
||||
DuoSelfEnrollment string
|
||||
RememberMe string
|
||||
ResetPassword string
|
||||
ResetPasswordCustomURL string
|
||||
Session string
|
||||
Theme string
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
:8085 {
|
||||
log
|
||||
reverse_proxy authelia-backend:9091 {
|
||||
header_up X-Forwarded-Proto https
|
||||
import tls-transport
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
yaml "gopkg.in/yaml.v3"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/model"
|
||||
"github.com/authelia/authelia/v4/internal/storage"
|
||||
|
|
Loading…
Reference in New Issue