rewrite the auth middleware
The old approach of the authentication middlewares had the problem that when an authenticator could not authenticate a request it would still send it to the next handler, in case that the next one can authenticate it. But if no authenticator could successfully authenticate the request, it would still be handled, which leads to unauthorized access.
This commit is contained in:
@@ -25,80 +25,29 @@ type OIDCProvider interface {
|
||||
UserInfo(ctx context.Context, ts oauth2.TokenSource) (*gOidc.UserInfo, error)
|
||||
}
|
||||
|
||||
// OIDCAuth provides a middleware to check access secured by a static token.
|
||||
func OIDCAuth(optionSetters ...Option) func(next http.Handler) http.Handler {
|
||||
options := newOptions(optionSetters...)
|
||||
tokenCache := osync.NewCache(options.UserinfoCacheSize)
|
||||
type OIDCAuthenticator struct {
|
||||
Logger log.Logger
|
||||
HTTPClient *http.Client
|
||||
OIDCIss string
|
||||
TokenCache *osync.Cache
|
||||
TokenCacheTTL time.Duration
|
||||
ProviderFunc func() (OIDCProvider, error)
|
||||
AccessTokenVerifyMethod string
|
||||
JWKSOptions config.JWKS
|
||||
|
||||
h := oidcAuth{
|
||||
logger: options.Logger,
|
||||
providerFunc: options.OIDCProviderFunc,
|
||||
httpClient: options.HTTPClient,
|
||||
oidcIss: options.OIDCIss,
|
||||
tokenCache: &tokenCache,
|
||||
tokenCacheTTL: options.UserinfoCacheTTL,
|
||||
accessTokenVerifyMethod: options.AccessTokenVerifyMethod,
|
||||
jwksOptions: options.JWKS,
|
||||
jwksLock: &sync.Mutex{},
|
||||
providerLock: &sync.Mutex{},
|
||||
}
|
||||
providerLock *sync.Mutex
|
||||
provider OIDCProvider
|
||||
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
// there is no bearer token on the request,
|
||||
if !h.shouldServe(req) {
|
||||
// oidc supported but token not present, add header and handover to the next middleware.
|
||||
userAgentAuthenticateLockIn(w, req, options.CredentialsByUserAgent, "bearer")
|
||||
next.ServeHTTP(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
if h.getProvider() == nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Force init of jwks keyfunc if needed (contacts the .well-known and jwks endpoints on first call)
|
||||
if h.accessTokenVerifyMethod == config.AccessTokenVerificationJWT && h.getKeyfunc() == nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
token := strings.TrimPrefix(req.Header.Get("Authorization"), "Bearer ")
|
||||
|
||||
claims, status := h.getClaims(token, req)
|
||||
if status != 0 {
|
||||
w.WriteHeader(status)
|
||||
return
|
||||
}
|
||||
|
||||
// inject claims to the request context for the account_resolver middleware.
|
||||
next.ServeHTTP(w, req.WithContext(oidc.NewContext(req.Context(), claims)))
|
||||
})
|
||||
}
|
||||
jwksLock *sync.Mutex
|
||||
JWKS *keyfunc.JWKS
|
||||
}
|
||||
|
||||
type oidcAuth struct {
|
||||
logger log.Logger
|
||||
provider OIDCProvider
|
||||
providerLock *sync.Mutex
|
||||
jwksOptions config.JWKS
|
||||
jwks *keyfunc.JWKS
|
||||
jwksLock *sync.Mutex
|
||||
providerFunc func() (OIDCProvider, error)
|
||||
httpClient *http.Client
|
||||
oidcIss string
|
||||
tokenCache *osync.Cache
|
||||
tokenCacheTTL time.Duration
|
||||
accessTokenVerifyMethod string
|
||||
}
|
||||
|
||||
func (m oidcAuth) getClaims(token string, req *http.Request) (claims map[string]interface{}, status int) {
|
||||
hit := m.tokenCache.Load(token)
|
||||
func (m OIDCAuthenticator) getClaims(token string, req *http.Request) (claims map[string]interface{}, status int) {
|
||||
hit := m.TokenCache.Load(token)
|
||||
if hit == nil {
|
||||
aClaims, err := m.verifyAccessToken(token)
|
||||
if err != nil {
|
||||
m.logger.Error().Err(err).Msg("Failed to verify access token")
|
||||
m.Logger.Error().Err(err).Msg("Failed to verify access token")
|
||||
status = http.StatusUnauthorized
|
||||
return
|
||||
}
|
||||
@@ -108,25 +57,25 @@ func (m oidcAuth) getClaims(token string, req *http.Request) (claims map[string]
|
||||
}
|
||||
|
||||
userInfo, err := m.getProvider().UserInfo(
|
||||
context.WithValue(req.Context(), oauth2.HTTPClient, m.httpClient),
|
||||
context.WithValue(req.Context(), oauth2.HTTPClient, m.HTTPClient),
|
||||
oauth2.StaticTokenSource(oauth2Token),
|
||||
)
|
||||
if err != nil {
|
||||
m.logger.Error().Err(err).Msg("Failed to get userinfo")
|
||||
m.Logger.Error().Err(err).Msg("Failed to get userinfo")
|
||||
status = http.StatusUnauthorized
|
||||
return
|
||||
}
|
||||
|
||||
if err := userInfo.Claims(&claims); err != nil {
|
||||
m.logger.Error().Err(err).Interface("userinfo", userInfo).Msg("failed to unmarshal userinfo claims")
|
||||
m.Logger.Error().Err(err).Interface("userinfo", userInfo).Msg("failed to unmarshal userinfo claims")
|
||||
status = http.StatusInternalServerError
|
||||
return
|
||||
}
|
||||
|
||||
expiration := m.extractExpiration(aClaims)
|
||||
m.tokenCache.Store(token, claims, expiration)
|
||||
m.TokenCache.Store(token, claims, expiration)
|
||||
|
||||
m.logger.Debug().Interface("claims", claims).Interface("userInfo", userInfo).Time("expiration", expiration.UTC()).Msg("unmarshalled and cached userinfo")
|
||||
m.Logger.Debug().Interface("claims", claims).Interface("userInfo", userInfo).Time("expiration", expiration.UTC()).Msg("unmarshalled and cached userinfo")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -135,25 +84,25 @@ func (m oidcAuth) getClaims(token string, req *http.Request) (claims map[string]
|
||||
status = http.StatusInternalServerError
|
||||
return
|
||||
}
|
||||
m.logger.Debug().Interface("claims", claims).Msg("cache hit for userinfo")
|
||||
m.Logger.Debug().Interface("claims", claims).Msg("cache hit for userinfo")
|
||||
return
|
||||
}
|
||||
|
||||
func (m oidcAuth) verifyAccessToken(token string) (jwt.RegisteredClaims, error) {
|
||||
switch m.accessTokenVerifyMethod {
|
||||
func (m OIDCAuthenticator) verifyAccessToken(token string) (jwt.RegisteredClaims, error) {
|
||||
switch m.AccessTokenVerifyMethod {
|
||||
case config.AccessTokenVerificationJWT:
|
||||
return m.verifyAccessTokenJWT(token)
|
||||
case config.AccessTokenVerificationNone:
|
||||
m.logger.Debug().Msg("Access Token verification disabled")
|
||||
m.Logger.Debug().Msg("Access Token verification disabled")
|
||||
return jwt.RegisteredClaims{}, nil
|
||||
default:
|
||||
m.logger.Error().Str("access_token_verify_method", m.accessTokenVerifyMethod).Msg("Unknown Access Token verification setting")
|
||||
m.Logger.Error().Str("access_token_verify_method", m.AccessTokenVerifyMethod).Msg("Unknown Access Token verification setting")
|
||||
return jwt.RegisteredClaims{}, errors.New("Unknown Access Token Verification method")
|
||||
}
|
||||
}
|
||||
|
||||
// verifyAccessTokenJWT tries to parse and verify the access token as a JWT.
|
||||
func (m oidcAuth) verifyAccessTokenJWT(token string) (jwt.RegisteredClaims, error) {
|
||||
func (m OIDCAuthenticator) verifyAccessTokenJWT(token string) (jwt.RegisteredClaims, error) {
|
||||
var claims jwt.RegisteredClaims
|
||||
jwks := m.getKeyfunc()
|
||||
if jwks == nil {
|
||||
@@ -161,13 +110,13 @@ func (m oidcAuth) verifyAccessTokenJWT(token string) (jwt.RegisteredClaims, erro
|
||||
}
|
||||
|
||||
_, err := jwt.ParseWithClaims(token, &claims, jwks.Keyfunc)
|
||||
m.logger.Debug().Interface("access token", &claims).Msg("parsed access token")
|
||||
m.Logger.Debug().Interface("access token", &claims).Msg("parsed access token")
|
||||
if err != nil {
|
||||
m.logger.Info().Err(err).Msg("Failed to parse/verify the access token.")
|
||||
m.Logger.Info().Err(err).Msg("Failed to parse/verify the access token.")
|
||||
return claims, err
|
||||
}
|
||||
|
||||
if !claims.VerifyIssuer(m.oidcIss, true) {
|
||||
if !claims.VerifyIssuer(m.OIDCIss, true) {
|
||||
vErr := jwt.ValidationError{}
|
||||
vErr.Inner = jwt.ErrTokenInvalidIssuer
|
||||
vErr.Errors |= jwt.ValidationErrorIssuer
|
||||
@@ -180,19 +129,19 @@ func (m oidcAuth) verifyAccessTokenJWT(token string) (jwt.RegisteredClaims, erro
|
||||
// extractExpiration tries to extract the expriration time from the access token
|
||||
// If the access token does not have an exp claim it will fallback to the configured
|
||||
// default expiration
|
||||
func (m oidcAuth) extractExpiration(aClaims jwt.RegisteredClaims) time.Time {
|
||||
defaultExpiration := time.Now().Add(m.tokenCacheTTL)
|
||||
func (m OIDCAuthenticator) extractExpiration(aClaims jwt.RegisteredClaims) time.Time {
|
||||
defaultExpiration := time.Now().Add(m.TokenCacheTTL)
|
||||
if aClaims.ExpiresAt != nil {
|
||||
m.logger.Debug().Str("exp", aClaims.ExpiresAt.String()).Msg("Expiration Time from access_token")
|
||||
m.Logger.Debug().Str("exp", aClaims.ExpiresAt.String()).Msg("Expiration Time from access_token")
|
||||
return aClaims.ExpiresAt.Time
|
||||
}
|
||||
return defaultExpiration
|
||||
}
|
||||
|
||||
func (m oidcAuth) shouldServe(req *http.Request) bool {
|
||||
func (m OIDCAuthenticator) shouldServe(req *http.Request) bool {
|
||||
header := req.Header.Get("Authorization")
|
||||
|
||||
if m.oidcIss == "" {
|
||||
if m.OIDCIss == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -211,58 +160,58 @@ type jwksJSON struct {
|
||||
JWKSURL string `json:"jwks_uri"`
|
||||
}
|
||||
|
||||
func (m *oidcAuth) getKeyfunc() *keyfunc.JWKS {
|
||||
func (m *OIDCAuthenticator) getKeyfunc() *keyfunc.JWKS {
|
||||
m.jwksLock.Lock()
|
||||
defer m.jwksLock.Unlock()
|
||||
if m.jwks == nil {
|
||||
wellKnown := strings.TrimSuffix(m.oidcIss, "/") + "/.well-known/openid-configuration"
|
||||
if m.JWKS == nil {
|
||||
wellKnown := strings.TrimSuffix(m.OIDCIss, "/") + "/.well-known/openid-configuration"
|
||||
|
||||
resp, err := m.httpClient.Get(wellKnown)
|
||||
resp, err := m.HTTPClient.Get(wellKnown)
|
||||
if err != nil {
|
||||
m.logger.Error().Err(err).Msg("Failed to set request for .well-known/openid-configuration")
|
||||
m.Logger.Error().Err(err).Msg("Failed to set request for .well-known/openid-configuration")
|
||||
return nil
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
m.logger.Error().Err(err).Msg("unable to read discovery response body")
|
||||
m.Logger.Error().Err(err).Msg("unable to read discovery response body")
|
||||
return nil
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
m.logger.Error().Str("status", resp.Status).Str("body", string(body)).Msg("error requesting openid-configuration")
|
||||
m.Logger.Error().Str("status", resp.Status).Str("body", string(body)).Msg("error requesting openid-configuration")
|
||||
return nil
|
||||
}
|
||||
|
||||
var j jwksJSON
|
||||
err = json.Unmarshal(body, &j)
|
||||
if err != nil {
|
||||
m.logger.Error().Err(err).Msg("failed to decode provider openid-configuration")
|
||||
m.Logger.Error().Err(err).Msg("failed to decode provider openid-configuration")
|
||||
return nil
|
||||
}
|
||||
m.logger.Debug().Str("jwks", j.JWKSURL).Msg("discovered jwks endpoint")
|
||||
m.Logger.Debug().Str("jwks", j.JWKSURL).Msg("discovered jwks endpoint")
|
||||
options := keyfunc.Options{
|
||||
Client: m.httpClient,
|
||||
Client: m.HTTPClient,
|
||||
RefreshErrorHandler: func(err error) {
|
||||
m.logger.Error().Err(err).Msg("There was an error with the jwt.Keyfunc")
|
||||
m.Logger.Error().Err(err).Msg("There was an error with the jwt.Keyfunc")
|
||||
},
|
||||
RefreshInterval: time.Minute * time.Duration(m.jwksOptions.RefreshInterval),
|
||||
RefreshRateLimit: time.Second * time.Duration(m.jwksOptions.RefreshRateLimit),
|
||||
RefreshTimeout: time.Second * time.Duration(m.jwksOptions.RefreshTimeout),
|
||||
RefreshUnknownKID: m.jwksOptions.RefreshUnknownKID,
|
||||
RefreshInterval: time.Minute * time.Duration(m.JWKSOptions.RefreshInterval),
|
||||
RefreshRateLimit: time.Second * time.Duration(m.JWKSOptions.RefreshRateLimit),
|
||||
RefreshTimeout: time.Second * time.Duration(m.JWKSOptions.RefreshTimeout),
|
||||
RefreshUnknownKID: m.JWKSOptions.RefreshUnknownKID,
|
||||
}
|
||||
m.jwks, err = keyfunc.Get(j.JWKSURL, options)
|
||||
m.JWKS, err = keyfunc.Get(j.JWKSURL, options)
|
||||
if err != nil {
|
||||
m.jwks = nil
|
||||
m.logger.Error().Err(err).Msg("Failed to create JWKS from resource at the given URL.")
|
||||
m.JWKS = nil
|
||||
m.Logger.Error().Err(err).Msg("Failed to create JWKS from resource at the given URL.")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return m.jwks
|
||||
return m.JWKS
|
||||
}
|
||||
|
||||
func (m *oidcAuth) getProvider() OIDCProvider {
|
||||
func (m *OIDCAuthenticator) getProvider() OIDCProvider {
|
||||
m.providerLock.Lock()
|
||||
defer m.providerLock.Unlock()
|
||||
if m.provider == nil {
|
||||
@@ -271,9 +220,9 @@ func (m *oidcAuth) getProvider() OIDCProvider {
|
||||
// provider needs to be cached as when it is created
|
||||
// it will fetch the keys from the issuer using the .well-known
|
||||
// endpoint
|
||||
provider, err := m.providerFunc()
|
||||
provider, err := m.ProviderFunc()
|
||||
if err != nil {
|
||||
m.logger.Error().Err(err).Msg("could not initialize oidcAuth provider")
|
||||
m.Logger.Error().Err(err).Msg("could not initialize oidcAuth provider")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -281,3 +230,32 @@ func (m *oidcAuth) getProvider() OIDCProvider {
|
||||
}
|
||||
return m.provider
|
||||
}
|
||||
|
||||
func (m OIDCAuthenticator) Authenticate(r *http.Request) (*http.Request, bool) {
|
||||
// there is no bearer token on the request,
|
||||
if !m.shouldServe(r) {
|
||||
// // oidc supported but token not present, add header and handover to the next middleware.
|
||||
// userAgentAuthenticateLockIn(w, r, options.CredentialsByUserAgent, "bearer")
|
||||
// next.ServeHTTP(w, r)
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if m.getProvider() == nil {
|
||||
// w.WriteHeader(http.StatusInternalServerError)
|
||||
return nil, false
|
||||
}
|
||||
// Force init of jwks keyfunc if needed (contacts the .well-known and jwks endpoints on first call)
|
||||
if m.AccessTokenVerifyMethod == config.AccessTokenVerificationJWT && m.getKeyfunc() == nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
token := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
|
||||
|
||||
claims, status := m.getClaims(token, r)
|
||||
if status != 0 {
|
||||
// w.WriteHeader(status)
|
||||
// TODO log
|
||||
return nil, false
|
||||
}
|
||||
return r.WithContext(oidc.NewContext(r.Context(), claims)), true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user