2022-04-07 07:58:51 +07:00
package server
import (
"net"
2022-04-08 11:13:47 +07:00
"os"
"strconv"
2022-04-07 07:58:51 +07:00
"strings"
2022-04-08 11:13:47 +07:00
"time"
2022-04-07 07:58:51 +07:00
2022-04-08 11:13:47 +07:00
duoapi "github.com/duosecurity/duo_api_golang"
"github.com/fasthttp/router"
2022-04-07 07:58:51 +07:00
"github.com/valyala/fasthttp"
2022-04-08 11:13:47 +07:00
"github.com/valyala/fasthttp/expvarhandler"
"github.com/valyala/fasthttp/pprofhandler"
2022-04-07 07:58:51 +07:00
2022-04-08 11:13:47 +07:00
"github.com/authelia/authelia/v4/internal/configuration/schema"
"github.com/authelia/authelia/v4/internal/duo"
2022-04-07 07:58:51 +07:00
"github.com/authelia/authelia/v4/internal/handlers"
"github.com/authelia/authelia/v4/internal/logging"
2022-04-08 11:13:47 +07:00
"github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/oidc"
"github.com/authelia/authelia/v4/internal/utils"
2022-04-07 07:58:51 +07:00
)
// Replacement for the default error handler in fasthttp.
2022-04-08 11:13:47 +07:00
func handlerError ( ) func ( ctx * fasthttp . RequestCtx , err error ) {
2022-04-07 07:58:51 +07:00
logger := logging . Logger ( )
2022-04-08 11:13:47 +07:00
headerXForwardedFor := [ ] byte ( fasthttp . HeaderXForwardedFor )
getRemoteIP := func ( ctx * fasthttp . RequestCtx ) string {
if hdr := ctx . Request . Header . PeekBytes ( headerXForwardedFor ) ; hdr != nil {
ips := strings . Split ( string ( hdr ) , "," )
if len ( ips ) > 0 {
return strings . Trim ( ips [ 0 ] , " " )
}
}
return ctx . RemoteIP ( ) . String ( )
}
return func ( ctx * fasthttp . RequestCtx , err error ) {
switch e := err . ( type ) {
case * fasthttp . ErrSmallBuffer :
logger . Tracef ( "Request was too large to handle from client %s. Response Code %d." , getRemoteIP ( ctx ) , fasthttp . StatusRequestHeaderFieldsTooLarge )
ctx . Error ( "request header too large" , fasthttp . StatusRequestHeaderFieldsTooLarge )
case * net . OpError :
if e . Timeout ( ) {
logger . Tracef ( "Request timeout occurred while handling from client %s: %s. Response Code %d." , getRemoteIP ( ctx ) , ctx . RequestURI ( ) , fasthttp . StatusRequestTimeout )
ctx . Error ( "request timeout" , fasthttp . StatusRequestTimeout )
} else {
logger . Tracef ( "An unknown error occurred while handling a request from client %s: %s. Response Code %d." , getRemoteIP ( ctx ) , ctx . RequestURI ( ) , fasthttp . StatusBadRequest )
ctx . Error ( "error when parsing request" , fasthttp . StatusBadRequest )
}
default :
logger . Tracef ( "An unknown error occurred while handling a request from client %s: %s. Response Code %d." , getRemoteIP ( ctx ) , ctx . RequestURI ( ) , fasthttp . StatusBadRequest )
2022-04-07 07:58:51 +07:00
ctx . Error ( "error when parsing request" , fasthttp . StatusBadRequest )
}
}
}
func handlerNotFound ( next fasthttp . RequestHandler ) fasthttp . RequestHandler {
return func ( ctx * fasthttp . RequestCtx ) {
path := strings . ToLower ( string ( ctx . Path ( ) ) )
for i := 0 ; i < len ( httpServerDirs ) ; i ++ {
if path == httpServerDirs [ i ] . name || strings . HasPrefix ( path , httpServerDirs [ i ] . prefix ) {
handlers . SetStatusCodeResponse ( ctx , fasthttp . StatusNotFound )
return
}
}
next ( ctx )
}
}
func handlerMethodNotAllowed ( ctx * fasthttp . RequestCtx ) {
handlers . SetStatusCodeResponse ( ctx , fasthttp . StatusMethodNotAllowed )
}
2022-04-08 11:13:47 +07:00
func getHandler ( config schema . Configuration , providers middlewares . Providers ) fasthttp . RequestHandler {
rememberMe := strconv . FormatBool ( config . Session . RememberMeDuration != schema . RememberMeDisabled )
resetPassword := strconv . FormatBool ( ! config . AuthenticationBackend . DisableResetPassword )
resetPasswordCustomURL := config . AuthenticationBackend . PasswordReset . CustomURL . String ( )
duoSelfEnrollment := f
if config . DuoAPI != nil {
duoSelfEnrollment = strconv . FormatBool ( config . DuoAPI . EnableSelfEnrollment )
}
https := config . Server . TLS . Key != "" && config . Server . TLS . Certificate != ""
serveIndexHandler := ServeTemplatedFile ( embeddedAssets , indexFile , config . Server . AssetPath , duoSelfEnrollment , rememberMe , resetPassword , resetPasswordCustomURL , config . Session . Name , config . Theme , https )
serveSwaggerHandler := ServeTemplatedFile ( swaggerAssets , indexFile , config . Server . AssetPath , duoSelfEnrollment , rememberMe , resetPassword , resetPasswordCustomURL , config . Session . Name , config . Theme , https )
serveSwaggerAPIHandler := ServeTemplatedFile ( swaggerAssets , apiFile , config . Server . AssetPath , duoSelfEnrollment , rememberMe , resetPassword , resetPasswordCustomURL , config . Session . Name , config . Theme , https )
handlerPublicHTML := newPublicHTMLEmbeddedHandler ( )
handlerLocales := newLocalesEmbeddedHandler ( )
middleware := middlewares . AutheliaMiddleware ( config , providers )
policyCORSPublicGET := middlewares . NewCORSPolicyBuilder ( ) .
WithAllowedMethods ( "OPTIONS" , "GET" ) .
WithAllowedOrigins ( "*" ) .
Build ( )
r := router . New ( )
// Static Assets.
r . GET ( "/" , middleware ( serveIndexHandler ) )
for _ , f := range rootFiles {
r . GET ( "/" + f , handlerPublicHTML )
}
r . GET ( "/favicon.ico" , middlewares . AssetOverrideMiddleware ( config . Server . AssetPath , 0 , handlerPublicHTML ) )
r . GET ( "/static/media/logo.png" , middlewares . AssetOverrideMiddleware ( config . Server . AssetPath , 2 , handlerPublicHTML ) )
r . GET ( "/static/{filepath:*}" , handlerPublicHTML )
// Locales.
r . GET ( "/locales/{language:[a-z]{1,3}}-{variant:[a-z0-9-]+}/{namespace:[a-z]+}.json" , middlewares . AssetOverrideMiddleware ( config . Server . AssetPath , 0 , handlerLocales ) )
r . GET ( "/locales/{language:[a-z]{1,3}}/{namespace:[a-z]+}.json" , middlewares . AssetOverrideMiddleware ( config . Server . AssetPath , 0 , handlerLocales ) )
// Swagger.
r . GET ( "/api/" , middleware ( serveSwaggerHandler ) )
r . OPTIONS ( "/api/" , policyCORSPublicGET . HandleOPTIONS )
r . GET ( "/api/" + apiFile , policyCORSPublicGET . Middleware ( middleware ( serveSwaggerAPIHandler ) ) )
r . OPTIONS ( "/api/" + apiFile , policyCORSPublicGET . HandleOPTIONS )
for _ , file := range swaggerFiles {
r . GET ( "/api/" + file , handlerPublicHTML )
}
r . GET ( "/api/health" , middleware ( handlers . HealthGET ) )
r . GET ( "/api/state" , middleware ( handlers . StateGET ) )
r . GET ( "/api/configuration" , middleware ( middlewares . Require1FA ( handlers . ConfigurationGET ) ) )
r . GET ( "/api/configuration/password-policy" , middleware ( handlers . PasswordPolicyConfigurationGet ) )
r . GET ( "/api/verify" , middleware ( handlers . VerifyGET ( config . AuthenticationBackend ) ) )
r . HEAD ( "/api/verify" , middleware ( handlers . VerifyGET ( config . AuthenticationBackend ) ) )
r . POST ( "/api/checks/safe-redirection" , middleware ( handlers . CheckSafeRedirectionPOST ) )
delayFunc := middlewares . TimingAttackDelay ( 10 , 250 , 85 , time . Second )
r . POST ( "/api/firstfactor" , middleware ( handlers . FirstFactorPOST ( delayFunc ) ) )
r . POST ( "/api/logout" , middleware ( handlers . LogoutPOST ) )
// Only register endpoints if forgot password is not disabled.
if ! config . AuthenticationBackend . DisableResetPassword &&
config . AuthenticationBackend . PasswordReset . CustomURL . String ( ) == "" {
// Password reset related endpoints.
r . POST ( "/api/reset-password/identity/start" , middleware ( handlers . ResetPasswordIdentityStart ) )
r . POST ( "/api/reset-password/identity/finish" , middleware ( handlers . ResetPasswordIdentityFinish ) )
r . POST ( "/api/reset-password" , middleware ( handlers . ResetPasswordPOST ) )
}
// Information about the user.
r . GET ( "/api/user/info" , middleware ( middlewares . Require1FA ( handlers . UserInfoGET ) ) )
r . POST ( "/api/user/info" , middleware ( middlewares . Require1FA ( handlers . UserInfoPOST ) ) )
r . POST ( "/api/user/info/2fa_method" , middleware ( middlewares . Require1FA ( handlers . MethodPreferencePOST ) ) )
if ! config . TOTP . Disable {
// TOTP related endpoints.
r . GET ( "/api/user/info/totp" , middleware ( middlewares . Require1FA ( handlers . UserTOTPInfoGET ) ) )
r . POST ( "/api/secondfactor/totp/identity/start" , middleware ( middlewares . Require1FA ( handlers . TOTPIdentityStart ) ) )
r . POST ( "/api/secondfactor/totp/identity/finish" , middleware ( middlewares . Require1FA ( handlers . TOTPIdentityFinish ) ) )
r . POST ( "/api/secondfactor/totp" , middleware ( middlewares . Require1FA ( handlers . TimeBasedOneTimePasswordPOST ) ) )
}
if ! config . Webauthn . Disable {
// Webauthn Endpoints.
r . POST ( "/api/secondfactor/webauthn/identity/start" , middleware ( middlewares . Require1FA ( handlers . WebauthnIdentityStart ) ) )
r . POST ( "/api/secondfactor/webauthn/identity/finish" , middleware ( middlewares . Require1FA ( handlers . WebauthnIdentityFinish ) ) )
r . POST ( "/api/secondfactor/webauthn/attestation" , middleware ( middlewares . Require1FA ( handlers . WebauthnAttestationPOST ) ) )
r . GET ( "/api/secondfactor/webauthn/assertion" , middleware ( middlewares . Require1FA ( handlers . WebauthnAssertionGET ) ) )
r . POST ( "/api/secondfactor/webauthn/assertion" , middleware ( middlewares . Require1FA ( handlers . WebauthnAssertionPOST ) ) )
}
// Configure DUO api endpoint only if configuration exists.
if config . DuoAPI != nil {
var duoAPI duo . API
if os . Getenv ( "ENVIRONMENT" ) == dev {
duoAPI = duo . NewDuoAPI ( duoapi . NewDuoApi (
config . DuoAPI . IntegrationKey ,
config . DuoAPI . SecretKey ,
config . DuoAPI . Hostname , "" , duoapi . SetInsecure ( ) ) )
} else {
duoAPI = duo . NewDuoAPI ( duoapi . NewDuoApi (
config . DuoAPI . IntegrationKey ,
config . DuoAPI . SecretKey ,
config . DuoAPI . Hostname , "" ) )
}
r . GET ( "/api/secondfactor/duo_devices" , middleware ( middlewares . Require1FA ( handlers . DuoDevicesGET ( duoAPI ) ) ) )
r . POST ( "/api/secondfactor/duo" , middleware ( middlewares . Require1FA ( handlers . DuoPOST ( duoAPI ) ) ) )
r . POST ( "/api/secondfactor/duo_device" , middleware ( middlewares . Require1FA ( handlers . DuoDevicePOST ) ) )
}
if config . Server . EnablePprof {
r . GET ( "/debug/pprof/{name?}" , pprofhandler . PprofHandler )
}
if config . Server . EnableExpvars {
r . GET ( "/debug/vars" , expvarhandler . ExpvarHandler )
}
if providers . OpenIDConnect . Fosite != nil {
r . GET ( "/api/oidc/consent" , middleware ( handlers . OpenIDConnectConsentGET ) )
r . POST ( "/api/oidc/consent" , middleware ( handlers . OpenIDConnectConsentPOST ) )
allowedOrigins := utils . StringSliceFromURLs ( config . IdentityProviders . OIDC . CORS . AllowedOrigins )
r . OPTIONS ( oidc . WellKnownOpenIDConfigurationPath , policyCORSPublicGET . HandleOPTIONS )
r . GET ( oidc . WellKnownOpenIDConfigurationPath , policyCORSPublicGET . Middleware ( middleware ( handlers . OpenIDConnectConfigurationWellKnownGET ) ) )
r . OPTIONS ( oidc . WellKnownOAuthAuthorizationServerPath , policyCORSPublicGET . HandleOPTIONS )
r . GET ( oidc . WellKnownOAuthAuthorizationServerPath , policyCORSPublicGET . Middleware ( middleware ( handlers . OAuthAuthorizationServerWellKnownGET ) ) )
r . OPTIONS ( oidc . JWKsPath , policyCORSPublicGET . HandleOPTIONS )
r . GET ( oidc . JWKsPath , policyCORSPublicGET . Middleware ( middleware ( handlers . JSONWebKeySetGET ) ) )
// TODO (james-d-elliott): Remove in GA. This is a legacy implementation of the above endpoint.
r . OPTIONS ( "/api/oidc/jwks" , policyCORSPublicGET . HandleOPTIONS )
r . GET ( "/api/oidc/jwks" , policyCORSPublicGET . Middleware ( middleware ( handlers . JSONWebKeySetGET ) ) )
policyCORSAuthorization := middlewares . NewCORSPolicyBuilder ( ) .
WithAllowedMethods ( "OPTIONS" , "GET" ) .
WithAllowedOrigins ( allowedOrigins ... ) .
WithEnabled ( utils . IsStringInSlice ( oidc . AuthorizationEndpoint , config . IdentityProviders . OIDC . CORS . Endpoints ) ) .
Build ( )
r . OPTIONS ( oidc . AuthorizationPath , policyCORSAuthorization . HandleOnlyOPTIONS )
r . GET ( oidc . AuthorizationPath , middleware ( middlewares . NewHTTPToAutheliaHandlerAdaptor ( handlers . OpenIDConnectAuthorizationGET ) ) )
// TODO (james-d-elliott): Remove in GA. This is a legacy endpoint.
r . OPTIONS ( "/api/oidc/authorize" , policyCORSAuthorization . HandleOnlyOPTIONS )
r . GET ( "/api/oidc/authorize" , middleware ( middlewares . NewHTTPToAutheliaHandlerAdaptor ( handlers . OpenIDConnectAuthorizationGET ) ) )
policyCORSToken := middlewares . NewCORSPolicyBuilder ( ) .
WithAllowCredentials ( true ) .
WithAllowedMethods ( "OPTIONS" , "POST" ) .
WithAllowedOrigins ( allowedOrigins ... ) .
WithEnabled ( utils . IsStringInSlice ( oidc . TokenEndpoint , config . IdentityProviders . OIDC . CORS . Endpoints ) ) .
Build ( )
r . OPTIONS ( oidc . TokenPath , policyCORSToken . HandleOPTIONS )
r . POST ( oidc . TokenPath , policyCORSToken . Middleware ( middleware ( middlewares . NewHTTPToAutheliaHandlerAdaptor ( handlers . OpenIDConnectTokenPOST ) ) ) )
policyCORSUserinfo := middlewares . NewCORSPolicyBuilder ( ) .
WithAllowCredentials ( true ) .
WithAllowedMethods ( "OPTIONS" , "GET" , "POST" ) .
WithAllowedOrigins ( allowedOrigins ... ) .
WithEnabled ( utils . IsStringInSlice ( oidc . UserinfoEndpoint , config . IdentityProviders . OIDC . CORS . Endpoints ) ) .
Build ( )
r . OPTIONS ( oidc . UserinfoPath , policyCORSUserinfo . HandleOPTIONS )
r . GET ( oidc . UserinfoPath , policyCORSUserinfo . Middleware ( middleware ( middlewares . NewHTTPToAutheliaHandlerAdaptor ( handlers . OpenIDConnectUserinfo ) ) ) )
r . POST ( oidc . UserinfoPath , policyCORSUserinfo . Middleware ( middleware ( middlewares . NewHTTPToAutheliaHandlerAdaptor ( handlers . OpenIDConnectUserinfo ) ) ) )
policyCORSIntrospection := middlewares . NewCORSPolicyBuilder ( ) .
WithAllowCredentials ( true ) .
WithAllowedMethods ( "OPTIONS" , "POST" ) .
WithAllowedOrigins ( allowedOrigins ... ) .
WithEnabled ( utils . IsStringInSlice ( oidc . IntrospectionEndpoint , config . IdentityProviders . OIDC . CORS . Endpoints ) ) .
Build ( )
r . OPTIONS ( oidc . IntrospectionPath , policyCORSIntrospection . HandleOPTIONS )
r . POST ( oidc . IntrospectionPath , policyCORSIntrospection . Middleware ( middleware ( middlewares . NewHTTPToAutheliaHandlerAdaptor ( handlers . OAuthIntrospectionPOST ) ) ) )
// TODO (james-d-elliott): Remove in GA. This is a legacy implementation of the above endpoint.
r . OPTIONS ( "/api/oidc/introspect" , policyCORSIntrospection . HandleOPTIONS )
r . POST ( "/api/oidc/introspect" , policyCORSIntrospection . Middleware ( middleware ( middlewares . NewHTTPToAutheliaHandlerAdaptor ( handlers . OAuthIntrospectionPOST ) ) ) )
policyCORSRevocation := middlewares . NewCORSPolicyBuilder ( ) .
WithAllowCredentials ( true ) .
WithAllowedMethods ( "OPTIONS" , "POST" ) .
WithAllowedOrigins ( allowedOrigins ... ) .
WithEnabled ( utils . IsStringInSlice ( oidc . RevocationEndpoint , config . IdentityProviders . OIDC . CORS . Endpoints ) ) .
Build ( )
r . OPTIONS ( oidc . RevocationPath , policyCORSRevocation . HandleOPTIONS )
r . POST ( oidc . RevocationPath , policyCORSRevocation . Middleware ( middleware ( middlewares . NewHTTPToAutheliaHandlerAdaptor ( handlers . OAuthRevocationPOST ) ) ) )
// TODO (james-d-elliott): Remove in GA. This is a legacy implementation of the above endpoint.
r . OPTIONS ( "/api/oidc/revoke" , policyCORSRevocation . HandleOPTIONS )
r . POST ( "/api/oidc/revoke" , policyCORSRevocation . Middleware ( middleware ( middlewares . NewHTTPToAutheliaHandlerAdaptor ( handlers . OAuthRevocationPOST ) ) ) )
}
r . NotFound = handlerNotFound ( middleware ( serveIndexHandler ) )
r . HandleMethodNotAllowed = true
r . MethodNotAllowed = handlerMethodNotAllowed
handler := middlewares . LogRequestMiddleware ( r . Handler )
if config . Server . Path != "" {
handler = middlewares . StripPathMiddleware ( config . Server . Path , handler )
}
return handler
}