Skip to content

Commit

Permalink
feat(metrics): record openid connect histogram (#5769)
Browse files Browse the repository at this point in the history
This adds a histogram vector on a per-endpoint basis for OpenID Connect 1.0 which records the elapsed seconds by endpoint by code.

Signed-off-by: James Elliott <james-d-elliott@users.noreply.github.com>
  • Loading branch information
james-d-elliott committed Aug 4, 2023
1 parent 80771ca commit bd04624
Show file tree
Hide file tree
Showing 4 changed files with 57 additions and 16 deletions.
1 change: 1 addition & 0 deletions internal/metrics/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type Provider interface {
// Recorder of metrics.
type Recorder interface {
RecordRequest(statusCode, requestMethod string, elapsed time.Duration)
RecordRequestOpenIDConnect(endpoint, statusCode string, elapsed time.Duration)
RecordAuthz(statusCode string)
RecordAuthenticationDuration(success bool, elapsed time.Duration)
}
16 changes: 16 additions & 0 deletions internal/metrics/prometheus.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ func NewPrometheus() (provider *Prometheus) {
type Prometheus struct {
authnDuration *prometheus.HistogramVec
reqDuration *prometheus.HistogramVec
reqDurationOIDC *prometheus.HistogramVec
reqCounter *prometheus.CounterVec
authzCounter *prometheus.CounterVec
authnCounter *prometheus.CounterVec
Expand All @@ -33,6 +34,11 @@ func (r *Prometheus) RecordRequest(statusCode, requestMethod string, elapsed tim
r.reqDuration.WithLabelValues(statusCode).Observe(elapsed.Seconds())
}

// RecordRequestOpenIDConnect takes the statusCode string, requestMethod string, and the elapsed time.Duration to record the request and request duration metrics.
func (r *Prometheus) RecordRequestOpenIDConnect(endpoint, statusCode string, elapsed time.Duration) {
r.reqDurationOIDC.WithLabelValues(endpoint, statusCode).Observe(elapsed.Seconds())
}

// RecordAuthz takes the statusCode string to record the verify endpoint request metrics.
func (r *Prometheus) RecordAuthz(statusCode string) {
r.authzCounter.WithLabelValues(statusCode).Inc()
Expand Down Expand Up @@ -74,6 +80,16 @@ func (r *Prometheus) register() {
[]string{"code"},
)

r.reqDurationOIDC = promauto.NewHistogramVec(
prometheus.HistogramOpts{
Subsystem: "authelia",
Name: "request_duration_openid_connect",
Help: "The time a HTTP request takes to process in seconds for the OpenID Connect 1.0 endpoints.",
Buckets: []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10, 15, 20, 30, 40, 50, 60},
},
[]string{"endpoint", "code"},
)

r.reqCounter = promauto.NewCounterVec(
prometheus.CounterOpts{
Subsystem: "authelia",
Expand Down
22 changes: 22 additions & 0 deletions internal/middlewares/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package middlewares

import (
"strconv"
"strings"
"time"

"github.com/valyala/fasthttp"
Expand Down Expand Up @@ -29,6 +30,27 @@ func NewMetricsRequest(metrics metrics.Recorder) (middleware Basic) {
}
}

// NewMetricsRequestOpenIDConnect returns a middleware if provided with a metrics.Recorder, otherwise it returns nil.
func NewMetricsRequestOpenIDConnect(metrics metrics.Recorder, endpoint string) (middleware Basic) {
if metrics == nil {
return nil
}

endpoint = strings.ReplaceAll(endpoint, "-", "_")

return func(next fasthttp.RequestHandler) (handler fasthttp.RequestHandler) {
return func(ctx *fasthttp.RequestCtx) {
started := time.Now()

next(ctx)

statusCode := strconv.Itoa(ctx.Response.StatusCode())

metrics.RecordRequestOpenIDConnect(endpoint, statusCode, time.Since(started))
}
}
}

// NewMetricsAuthzRequest returns a middleware if provided with a metrics.Recorder, otherwise it returns nil.
func NewMetricsAuthzRequest(metrics metrics.Recorder) (middleware Basic) {
if metrics == nil {
Expand Down
34 changes: 18 additions & 16 deletions internal/server/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -304,32 +304,34 @@ func handleRouter(config *schema.Configuration, providers middlewares.Providers)
allowedOrigins := utils.StringSliceFromURLs(config.IdentityProviders.OIDC.CORS.AllowedOrigins)

r.OPTIONS(oidc.EndpointPathWellKnownOpenIDConfiguration, policyCORSPublicGET.HandleOPTIONS)
r.GET(oidc.EndpointPathWellKnownOpenIDConfiguration, policyCORSPublicGET.Middleware(bridgeOIDC(handlers.OpenIDConnectConfigurationWellKnownGET)))
r.GET(oidc.EndpointPathWellKnownOpenIDConfiguration, middlewares.Wrap(middlewares.NewMetricsRequestOpenIDConnect(providers.Metrics, "openid_configuration"), policyCORSPublicGET.Middleware(bridgeOIDC(handlers.OpenIDConnectConfigurationWellKnownGET))))

r.OPTIONS(oidc.EndpointPathWellKnownOAuthAuthorizationServer, policyCORSPublicGET.HandleOPTIONS)
r.GET(oidc.EndpointPathWellKnownOAuthAuthorizationServer, policyCORSPublicGET.Middleware(bridgeOIDC(handlers.OAuthAuthorizationServerWellKnownGET)))
r.GET(oidc.EndpointPathWellKnownOAuthAuthorizationServer, middlewares.Wrap(middlewares.NewMetricsRequestOpenIDConnect(providers.Metrics, "oauth_configuration"), policyCORSPublicGET.Middleware(bridgeOIDC(handlers.OAuthAuthorizationServerWellKnownGET))))

r.OPTIONS(oidc.EndpointPathJWKs, policyCORSPublicGET.HandleOPTIONS)
r.GET(oidc.EndpointPathJWKs, policyCORSPublicGET.Middleware(middlewareAPI(handlers.JSONWebKeySetGET)))
r.GET(oidc.EndpointPathJWKs, middlewares.Wrap(middlewares.NewMetricsRequestOpenIDConnect(providers.Metrics, "jwks"), policyCORSPublicGET.Middleware(middlewareAPI(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(bridgeOIDC(handlers.JSONWebKeySetGET)))
r.GET("/api/oidc/jwks", middlewares.Wrap(middlewares.NewMetricsRequestOpenIDConnect(providers.Metrics, "jwks"), policyCORSPublicGET.Middleware(bridgeOIDC(handlers.JSONWebKeySetGET))))

policyCORSAuthorization := middlewares.NewCORSPolicyBuilder().
WithAllowedMethods(fasthttp.MethodOptions, fasthttp.MethodGet, fasthttp.MethodPost).
WithAllowedOrigins(allowedOrigins...).
WithEnabled(utils.IsStringInSlice(oidc.EndpointAuthorization, config.IdentityProviders.OIDC.CORS.Endpoints)).
Build()

authorization := middlewares.Wrap(middlewares.NewMetricsRequestOpenIDConnect(providers.Metrics, oidc.EndpointAuthorization), policyCORSAuthorization.Middleware(bridgeOIDC(middlewares.NewHTTPToAutheliaHandlerAdaptor(handlers.OpenIDConnectAuthorization))))

r.OPTIONS(oidc.EndpointPathAuthorization, policyCORSAuthorization.HandleOnlyOPTIONS)
r.GET(oidc.EndpointPathAuthorization, policyCORSAuthorization.Middleware(bridgeOIDC(middlewares.NewHTTPToAutheliaHandlerAdaptor(handlers.OpenIDConnectAuthorization))))
r.POST(oidc.EndpointPathAuthorization, policyCORSAuthorization.Middleware(bridgeOIDC(middlewares.NewHTTPToAutheliaHandlerAdaptor(handlers.OpenIDConnectAuthorization))))
r.GET(oidc.EndpointPathAuthorization, authorization)
r.POST(oidc.EndpointPathAuthorization, authorization)

// TODO (james-d-elliott): Remove in GA. This is a legacy endpoint.
r.OPTIONS("/api/oidc/authorize", policyCORSAuthorization.HandleOnlyOPTIONS)
r.GET("/api/oidc/authorize", policyCORSAuthorization.Middleware(bridgeOIDC(middlewares.NewHTTPToAutheliaHandlerAdaptor(handlers.OpenIDConnectAuthorization))))
r.POST("/api/oidc/authorize", policyCORSAuthorization.Middleware(bridgeOIDC(middlewares.NewHTTPToAutheliaHandlerAdaptor(handlers.OpenIDConnectAuthorization))))
r.GET("/api/oidc/authorize", authorization)
r.POST("/api/oidc/authorize", authorization)

policyCORSPAR := middlewares.NewCORSPolicyBuilder().
WithAllowedMethods(fasthttp.MethodOptions, fasthttp.MethodPost).
Expand All @@ -338,7 +340,7 @@ func handleRouter(config *schema.Configuration, providers middlewares.Providers)
Build()

r.OPTIONS(oidc.EndpointPathPushedAuthorizationRequest, policyCORSPAR.HandleOnlyOPTIONS)
r.POST(oidc.EndpointPathPushedAuthorizationRequest, policyCORSPAR.Middleware(bridgeOIDC(middlewares.NewHTTPToAutheliaHandlerAdaptor(handlers.OpenIDConnectPushedAuthorizationRequest))))
r.POST(oidc.EndpointPathPushedAuthorizationRequest, middlewares.Wrap(middlewares.NewMetricsRequestOpenIDConnect(providers.Metrics, oidc.EndpointPushedAuthorizationRequest), policyCORSPAR.Middleware(bridgeOIDC(middlewares.NewHTTPToAutheliaHandlerAdaptor(handlers.OpenIDConnectPushedAuthorizationRequest)))))

policyCORSToken := middlewares.NewCORSPolicyBuilder().
WithAllowCredentials(true).
Expand All @@ -348,7 +350,7 @@ func handleRouter(config *schema.Configuration, providers middlewares.Providers)
Build()

r.OPTIONS(oidc.EndpointPathToken, policyCORSToken.HandleOPTIONS)
r.POST(oidc.EndpointPathToken, policyCORSToken.Middleware(bridgeOIDC(middlewares.NewHTTPToAutheliaHandlerAdaptor(handlers.OpenIDConnectTokenPOST))))
r.POST(oidc.EndpointPathToken, middlewares.Wrap(middlewares.NewMetricsRequestOpenIDConnect(providers.Metrics, oidc.EndpointToken), policyCORSToken.Middleware(bridgeOIDC(middlewares.NewHTTPToAutheliaHandlerAdaptor(handlers.OpenIDConnectTokenPOST)))))

policyCORSUserinfo := middlewares.NewCORSPolicyBuilder().
WithAllowCredentials(true).
Expand All @@ -358,8 +360,8 @@ func handleRouter(config *schema.Configuration, providers middlewares.Providers)
Build()

r.OPTIONS(oidc.EndpointPathUserinfo, policyCORSUserinfo.HandleOPTIONS)
r.GET(oidc.EndpointPathUserinfo, policyCORSUserinfo.Middleware(bridgeOIDC(middlewares.NewHTTPToAutheliaHandlerAdaptor(handlers.OpenIDConnectUserinfo))))
r.POST(oidc.EndpointPathUserinfo, policyCORSUserinfo.Middleware(bridgeOIDC(middlewares.NewHTTPToAutheliaHandlerAdaptor(handlers.OpenIDConnectUserinfo))))
r.GET(oidc.EndpointPathUserinfo, middlewares.Wrap(middlewares.NewMetricsRequestOpenIDConnect(providers.Metrics, oidc.EndpointUserinfo), policyCORSUserinfo.Middleware(bridgeOIDC(middlewares.NewHTTPToAutheliaHandlerAdaptor(handlers.OpenIDConnectUserinfo)))))
r.POST(oidc.EndpointPathUserinfo, middlewares.Wrap(middlewares.NewMetricsRequestOpenIDConnect(providers.Metrics, oidc.EndpointUserinfo), policyCORSUserinfo.Middleware(bridgeOIDC(middlewares.NewHTTPToAutheliaHandlerAdaptor(handlers.OpenIDConnectUserinfo)))))

policyCORSIntrospection := middlewares.NewCORSPolicyBuilder().
WithAllowCredentials(true).
Expand All @@ -369,11 +371,11 @@ func handleRouter(config *schema.Configuration, providers middlewares.Providers)
Build()

r.OPTIONS(oidc.EndpointPathIntrospection, policyCORSIntrospection.HandleOPTIONS)
r.POST(oidc.EndpointPathIntrospection, policyCORSIntrospection.Middleware(bridgeOIDC(middlewares.NewHTTPToAutheliaHandlerAdaptor(handlers.OAuthIntrospectionPOST))))
r.POST(oidc.EndpointPathIntrospection, middlewares.Wrap(middlewares.NewMetricsRequestOpenIDConnect(providers.Metrics, oidc.EndpointIntrospection), policyCORSIntrospection.Middleware(bridgeOIDC(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(bridgeOIDC(middlewares.NewHTTPToAutheliaHandlerAdaptor(handlers.OAuthIntrospectionPOST))))
r.POST("/api/oidc/introspect", middlewares.Wrap(middlewares.NewMetricsRequestOpenIDConnect(providers.Metrics, oidc.EndpointIntrospection), policyCORSIntrospection.Middleware(bridgeOIDC(middlewares.NewHTTPToAutheliaHandlerAdaptor(handlers.OAuthIntrospectionPOST)))))

policyCORSRevocation := middlewares.NewCORSPolicyBuilder().
WithAllowCredentials(true).
Expand All @@ -383,11 +385,11 @@ func handleRouter(config *schema.Configuration, providers middlewares.Providers)
Build()

r.OPTIONS(oidc.EndpointPathRevocation, policyCORSRevocation.HandleOPTIONS)
r.POST(oidc.EndpointPathRevocation, policyCORSRevocation.Middleware(bridgeOIDC(middlewares.NewHTTPToAutheliaHandlerAdaptor(handlers.OAuthRevocationPOST))))
r.POST(oidc.EndpointPathRevocation, middlewares.Wrap(middlewares.NewMetricsRequestOpenIDConnect(providers.Metrics, oidc.EndpointRevocation), policyCORSRevocation.Middleware(bridgeOIDC(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(bridgeOIDC(middlewares.NewHTTPToAutheliaHandlerAdaptor(handlers.OAuthRevocationPOST))))
r.POST("/api/oidc/revoke", middlewares.Wrap(middlewares.NewMetricsRequestOpenIDConnect(providers.Metrics, oidc.EndpointRevocation), policyCORSRevocation.Middleware(bridgeOIDC(middlewares.NewHTTPToAutheliaHandlerAdaptor(handlers.OAuthRevocationPOST)))))
}

r.HandleMethodNotAllowed = true
Expand Down

0 comments on commit bd04624

Please sign in to comment.