2023-06-23 19:19:56 +00:00
package handlers
import (
"encoding/json"
"fmt"
"net"
"net/url"
2023-06-24 12:08:18 +00:00
"strings"
2023-06-23 19:19:56 +00:00
"github.com/authelia/authelia/v4/internal/authorization"
"github.com/authelia/authelia/v4/internal/configuration/schema"
"github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/session"
"github.com/authelia/authelia/v4/internal/utils"
core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
autha "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3"
envoy_type "github.com/envoyproxy/go-control-plane/envoy/type/v3"
"github.com/gogo/googleapis/google/rpc"
"github.com/valyala/fasthttp"
rpcstatus "google.golang.org/genproto/googleapis/rpc/status"
"google.golang.org/grpc/codes"
healthpb "google.golang.org/grpc/health/grpc_health_v1"
"google.golang.org/grpc/status"
"context"
)
type AuthzGRCP struct {
Config * schema . Configuration
Providers middlewares . Providers
AuthStrategies [ ] AuthnStrategy
Authz Authz
}
func NewAuthzGRCP ( config * schema . Configuration , providers middlewares . Providers ) * AuthzGRCP {
// Determine the refresh interval
authBuilder := NewAuthzBuilder ( ) . WithConfig ( config )
// Only the following strategies are supported. These are hardcoded at the moment and won't be taken from the configuration
2023-06-24 12:08:18 +00:00
strategies := [ ] AuthnStrategy { NewHeaderProxyAuthorizationAuthnStrategy ( ) /* NewHeaderAuthorizationAuthnStrategy(), */ , NewCookieSessionAuthnStrategy ( authBuilder . config . RefreshInterval ) }
2023-06-23 19:19:56 +00:00
return & AuthzGRCP {
Config : config ,
Providers : providers ,
AuthStrategies : strategies ,
Authz : Authz { strategies : strategies } ,
}
}
type GRCPRequestData struct {
RemoteHost string
Domain string
Method string
Protocol string
Path string
AutheliaURL string
}
func ( data * GRCPRequestData ) String ( ) string {
return fmt . Sprintf (
` Extracted data from headers :
Remote Host = % s
Domain = % s
Method = % s
Protocol = % s
Path = % s
AutheliaURL = % s
` , data . RemoteHost , data . Domain , data . Method , data . Protocol , data . Path , data . AutheliaURL )
}
// GetRequestURI returns a request URI from the provided Data inside this struct.
// It returns an error if values are missing
func ( data * GRCPRequestData ) GetRequestURI ( ) ( * url . URL , error ) {
if len ( data . Protocol ) == 0 || len ( data . Domain ) == 0 || len ( data . Path ) == 0 || len ( data . Method ) == 0 {
return nil , fmt . Errorf ( "missing required values for URI. One of 'Protocol', 'Domain', 'Path' or 'Method' was not found" )
}
return url . ParseRequestURI ( fmt . Sprintf ( "%s://%s%s" , data . Protocol , data . Domain , data . Path ) )
}
// GetAutheliaURI returns a URI for the authelia protal from the provided Data inside this struct.
func ( data * GRCPRequestData ) GetAutheliaURI ( provider * session . Session ) ( * url . URL , error ) {
if len ( data . AutheliaURL ) == 0 {
return nil , fmt . Errorf ( "missing required 'AutheliaURL' for request" )
}
// Parse the URL
uri , err := url . ParseRequestURI ( data . AutheliaURL )
if err != nil {
return nil , err
}
// Validate URL
if ! utils . HasURIDomainSuffix ( uri , provider . Config . Domain ) {
return nil , fmt . Errorf ( "authelia url '%s' is not valid for detected domain '%s' as the url does not have the domain as a suffix" , data . AutheliaURL , provider . Config . Domain )
}
return uri , nil
}
// HandleGRPC is the authentication handler for envoy proxies via the gRPC protocoll.
// It is oriented on handler_authz.go
func ( authz * AuthzGRCP ) Check ( goCtx context . Context , req * autha . CheckRequest ) ( * autha . CheckResponse , error ) {
request , data := authz . GetHttpCtxFromGRPC ( req )
ctx := middlewares . NewAutheliaCtx ( request , * authz . Config , authz . Providers )
// Log the parsed data and the inbound headers
ctx . Logger . Debug ( data )
b , err := json . MarshalIndent ( req . Attributes . Request . Http . Headers , "" , " " )
if err == nil {
ctx . Logger . Trace ( "Inbound Headers: " )
ctx . Logger . Trace ( ( string ( b ) ) )
}
// Get request URI
uri , err := data . GetRequestURI ( )
if err != nil {
ctx . Logger . WithError ( err ) . Error ( "Error getting Target URL and Request Method" )
return authz . ErrAuthResponse ( envoy_type . StatusCode_BadRequest , "Bad authentication request" ) , nil
}
// Get authentication object
object := authorization . NewObject ( uri , data . Method )
if ! utils . IsURISecure ( object . URL ) {
// Check for redirect
if ! ctx . Configuration . Server . DisableAutoHttpsRedirect {
// Redirect to https
return authz . AuthResponseHeader ( envoy_type . StatusCode_PermanentRedirect , "" , [ ] * core . HeaderValueOption {
{
Header : & core . HeaderValue {
Key : "Location" ,
Value : fmt . Sprintf ( "https://%s%s" , data . Domain , data . Path ) ,
} ,
} ,
} ) , nil
}
ctx . Logger . Errorf ( "Target URL '%s' has an insecure scheme '%s', only the 'https' and 'wss' schemes are supported so session cookies can be transmitted securely" , object . URL . String ( ) , object . URL . Scheme )
return authz . ErrAuthResponse ( envoy_type . StatusCode_BadRequest , "Bad protocol for authentication request" ) , nil
}
// Get provider
provider , err := ctx . GetSessionProviderByTargetURL ( object . URL )
if err != nil {
ctx . Logger . WithError ( err ) . WithField ( "target_url" , object . URL . String ( ) ) . Error ( "Target URL does not appear to have a relevant session cookies configuration" )
return authz . ErrAuthResponse ( envoy_type . StatusCode_BadRequest , "Bad domain for authentication request" ) , nil
}
// Get authelia url
if len ( data . AutheliaURL ) == 0 {
ctx . Logger . Info ( "Received no authelia URL from headers. Using the default of https://auth.rpjosh.de" )
data . AutheliaURL = "https://auth.rpjosh.de"
}
autheliaURI , err := data . GetAutheliaURI ( provider )
if err != nil {
ctx . Logger . WithError ( err ) . WithField ( "target_url" , object . URL . String ( ) ) . Error ( "Error occurred trying to determine the external Authelia URL for Target URL" )
return authz . ErrAuthResponse ( envoy_type . StatusCode_BadRequest , "Bad authentication request" ) , nil
}
ctx . Logger . Trace ( "Using authelia URL " + autheliaURI . String ( ) )
var (
authn Authn
strategy AuthnStrategy
)
// Get strategie
if authn , strategy , err = authz . Authz . authn ( ctx , provider ) ; err != nil {
authn . Object = object
ctx . Logger . WithError ( err ) . Error ( "Error occurred while attempting to authenticate a request" )
switch strategy {
case nil :
ctx . Logger . Trace ( "Received no strategy to use" )
return authz . ErrAuthResponse ( envoy_type . StatusCode_Unauthorized , "Unauthorized" ) , nil
default :
// Because the response is modified directly, we have to catch this and rewrite this function
if s , ok := strategy . ( * HeaderAuthnStrategy ) ; ok {
ctx . Logger . Trace ( "Rewriting HeaderAuthnStrategy" )
ctx . Logger . Debugf ( "Responding %d %s" , s . authn , autheliaURI )
headers := make ( [ ] * core . HeaderValueOption , 0 )
if s . headerAuthenticate != nil {
headers = append ( headers , & core . HeaderValueOption {
Header : & core . HeaderValue {
Key : string ( s . headerAuthenticate ) ,
Value : string ( headerValueAuthenticateBasic ) ,
} ,
} )
}
return authz . AuthResponseHeader ( envoy_type . StatusCode ( s . statusAuthenticate ) , "" , headers ) , nil
} else if _ , ok := strategy . ( * CookieSessionAuthnStrategy ) ; ok {
ctx . Logger . Trace ( "Rewriting CookieSessionAuthnStrategy" )
}
ctx . Logger . Error ( "Received unsupported auth strategy for gRPC server" )
// strategy.HandleUnauthorized(ctx, &authn, authz.Authz.getRedirectionURL(&object, autheliaURI))
}
return authz . ErrAuthResponse ( envoy_type . StatusCode_Unauthorized , "Unauthorized" ) , nil
}
authn . Object = object
authn . Method = friendlyMethod ( authn . Object . Method )
ruleHasSubject , required := ctx . Providers . Authorizer . GetRequiredLevel (
authorization . Subject {
Username : authn . Details . Username ,
Groups : authn . Details . Groups ,
IP : net . ParseIP ( data . RemoteHost ) ,
} ,
object ,
)
switch isAuthzResult ( authn . Level , required , ruleHasSubject ) {
case AuthzResultForbidden :
ctx . Logger . Infof ( "Access to '%s' is forbidden to user '%s'" , object . URL . String ( ) , authn . Username )
return authz . ErrAuthResponse ( envoy_type . StatusCode_Forbidden , "Forbidden" ) , nil
case AuthzResultUnauthorized :
if strategy != nil {
ctx . Logger . Error ( "Handling not supported handler" )
//strategy.HandleUnauthorized(ctx, &authn, authz.Authz.getRedirectionURL(&object, autheliaURI))
} else {
ctx . Logger . Debugf ( "Redirecting user" )
return authz . HandleUnauthorizedRedirect ( ctx , & authn , authz . Authz . getRedirectionURL ( & object , autheliaURI ) ) , nil
}
case AuthzResultAuthorized :
ctx . Logger . Debugf ( "Authorized request" )
return authz . HandleAuthorized ( ctx , & authn ) , nil
//authz.Authz.handleAuthorized(ctx, &authn)
}
ctx . Logger . Info ( "Handling default redirection" )
return authz . HandleUnauthorizedRedirect ( ctx , & authn , authz . Authz . getRedirectionURL ( & object , autheliaURI ) ) , nil
}
// GetHttpCtxFromGRPC is an Adapter that converts common fields between a grpc request
// a "normal" http request.
// It also returns the
func ( authz * AuthzGRCP ) GetHttpCtxFromGRPC ( req * autha . CheckRequest ) ( * fasthttp . RequestCtx , * GRCPRequestData ) {
// Extract headers
headers := req . Attributes . Request . Http . Headers
// Parse the header data
data := & GRCPRequestData {
RemoteHost : headers [ "x-forwarded-for" ] ,
Domain : headers [ ":authority" ] ,
Method : headers [ ":method" ] ,
Protocol : headers [ "x-forwarded-proto" ] ,
Path : headers [ ":path" ] ,
AutheliaURL : headers [ "" ] ,
}
// Build fasthttp request with common types
rtc := & fasthttp . RequestCtx { }
// General headers
rtc . Request . Header . Set ( fasthttp . HeaderXForwardedFor , data . RemoteHost )
// Needed for NewHeaderProxyAuthorizationAuthnStrategy and NewHeaderAuthorizationAuthnStrategy
2023-06-24 12:08:18 +00:00
authz . setHeaderIfSet ( fasthttp . HeaderAuthorization , rtc , & headers )
authz . setHeaderIfSet ( fasthttp . HeaderProxyAuthorization , rtc , & headers )
authz . setHeaderIfSet ( fasthttp . HeaderWWWAuthenticate , rtc , & headers )
authz . setHeaderIfSet ( fasthttp . HeaderProxyAuthenticate , rtc , & headers )
2023-06-23 19:19:56 +00:00
// Needed for CookieSesseionauthnStrategy
rtc . Request . Header . Set ( "cookie" , headers [ "cookie" ] )
return rtc , data
}
2023-06-24 12:08:18 +00:00
// setHeaderIfSet sets the header in the given fastHttp request if the header from the envoy authentication
// request was also set
func ( authz * AuthzGRCP ) setHeaderIfSet ( headerKeyFast string , rtc * fasthttp . RequestCtx , envoyHeaders * map [ string ] string ) {
// Envoys provided header keys are always lower case
envoyHeaderKey := strings . ToLower ( headerKeyFast )
if val , isSet := ( * envoyHeaders ) [ envoyHeaderKey ] ; isSet {
rtc . Request . Header . Set ( headerKeyFast , val )
}
}
2023-06-23 19:19:56 +00:00
// ErrAuthResponse returns an authentication error for envoy with the given status code
// and the given text body
func ( authz * AuthzGRCP ) ErrAuthResponse ( statuscode envoy_type . StatusCode , body string ) * autha . CheckResponse {
return & autha . CheckResponse {
Status : & rpcstatus . Status {
Code : int32 ( rpc . UNAUTHENTICATED ) ,
} ,
HttpResponse : & autha . CheckResponse_DeniedResponse {
DeniedResponse : & autha . DeniedHttpResponse {
Status : & envoy_type . HttpStatus {
Code : statuscode ,
} ,
Body : body ,
} ,
} ,
}
}
// ErrAuthResponse returns an authentication error for envoy with the given status code
// and the given text body
func ( authz * AuthzGRCP ) AuthResponseHeader ( statuscode envoy_type . StatusCode , body string , headers [ ] * core . HeaderValueOption ) * autha . CheckResponse {
return & autha . CheckResponse {
Status : & rpcstatus . Status {
Code : int32 ( rpc . UNAUTHENTICATED ) ,
} ,
HttpResponse : & autha . CheckResponse_DeniedResponse {
DeniedResponse : & autha . DeniedHttpResponse {
Status : & envoy_type . HttpStatus {
Code : statuscode ,
} ,
Body : body ,
Headers : headers ,
} ,
} ,
}
}
func ( authz * AuthzGRCP ) HandleAuthorized ( ctx * middlewares . AutheliaCtx , authn * Authn ) * autha . CheckResponse {
return & autha . CheckResponse {
Status : & rpcstatus . Status {
Code : int32 ( rpc . OK ) ,
} ,
HttpResponse : & autha . CheckResponse_OkResponse {
OkResponse : & autha . OkHttpResponse {
Headers : [ ] * core . HeaderValueOption {
{
Header : & core . HeaderValue {
Key : "Auth-Handler" ,
Value : getAuthType ( authn . Type ) ,
} ,
} ,
{
Header : & core . HeaderValue {
Key : "Auth-Username" ,
Value : authn . Username ,
} ,
} ,
{
Header : & core . HeaderValue {
Key : "Auth-Username-Display" ,
Value : authn . Details . DisplayName ,
} ,
} ,
{
Header : & core . HeaderValue {
Key : "Auth-Realm" ,
Value : authn . Object . Domain ,
} ,
} ,
} ,
} ,
} ,
}
}
func ( authz * AuthzGRCP ) HandleUnauthorizedRedirect ( ctx * middlewares . AutheliaCtx , authn * Authn , redirectionURL * url . URL ) * autha . CheckResponse {
return & autha . CheckResponse {
Status : & rpcstatus . Status {
Code : int32 ( rpc . UNAUTHENTICATED ) ,
} ,
HttpResponse : & autha . CheckResponse_DeniedResponse {
DeniedResponse : & autha . DeniedHttpResponse {
Status : & envoy_type . HttpStatus {
Code : 302 ,
} ,
Headers : [ ] * core . HeaderValueOption {
{
Header : & core . HeaderValue {
Key : "Location" ,
Value : redirectionURL . String ( ) ,
} ,
} ,
} ,
} ,
} ,
}
}
func getAuthType ( t AuthnType ) string {
switch t {
case AuthnTypeNone :
return "none"
case AuthnTypeCookie :
return "cookie"
case AuthnTypeProxyAuthorization :
return "proxy"
case AuthnTypeAuthorization :
return "header"
default :
return "unknown"
}
}
// HealthEndpoint implements a health function to check if the appliction is
// still alive
type HealthEndpoint struct { }
func ( s * HealthEndpoint ) Check ( ctx context . Context , in * healthpb . HealthCheckRequest ) ( * healthpb . HealthCheckResponse , error ) {
return & healthpb . HealthCheckResponse { Status : healthpb . HealthCheckResponse_SERVING } , nil
}
func ( s * HealthEndpoint ) Watch ( in * healthpb . HealthCheckRequest , srv healthpb . Health_WatchServer ) error {
return status . Error ( codes . Unimplemented , "Watch is not implemented" )
}