package handlers import ( "encoding/json" "fmt" "net" "net/url" "strings" "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 strategies := []AuthnStrategy{NewHeaderProxyAuthorizationAuthnStrategy() /* NewHeaderAuthorizationAuthnStrategy(), */, NewCookieSessionAuthnStrategy(authBuilder.config.RefreshInterval)} 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 authz.setHeaderIfSet(fasthttp.HeaderAuthorization, rtc, &headers) authz.setHeaderIfSet(fasthttp.HeaderProxyAuthorization, rtc, &headers) authz.setHeaderIfSet(fasthttp.HeaderWWWAuthenticate, rtc, &headers) authz.setHeaderIfSet(fasthttp.HeaderProxyAuthenticate, rtc, &headers) // Needed for CookieSesseionauthnStrategy rtc.Request.Header.Set("cookie", headers["cookie"]) return rtc, data } // 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) } } // 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") }