Skip to content

Commit

Permalink
fix(oidc): missing introspection claims (#7049)
Browse files Browse the repository at this point in the history
This fixes a regression of the claims returned by the introspection endpoint.

Signed-off-by: James Elliott <james-d-elliott@users.noreply.github.com>
  • Loading branch information
james-d-elliott committed Mar 31, 2024
1 parent a224420 commit 2ffd5c5
Show file tree
Hide file tree
Showing 6 changed files with 141 additions and 104 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/authelia/authelia/v4
go 1.21

require (
authelia.com/provider/oauth2 v0.1.4-0.20240331003803-77b086f7aae4
authelia.com/provider/oauth2 v0.1.4
github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2
github.com/authelia/jsonschema v0.1.7
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
authelia.com/provider/oauth2 v0.1.4-0.20240331003803-77b086f7aae4 h1:KPTnfrYzKcBo5nZkLBH6wye9diF48ALFtVeMzu76SjI=
authelia.com/provider/oauth2 v0.1.4-0.20240331003803-77b086f7aae4/go.mod h1:NDritA+Ls5jTa5+t3CSQy58f/QPDofR53IKP8rRHJ8M=
authelia.com/provider/oauth2 v0.1.4 h1:7egKmT2GNRZ5/EImlP1X88WOFwX3fa82Dw3Eq1vQ8NI=
authelia.com/provider/oauth2 v0.1.4/go.mod h1:NDritA+Ls5jTa5+t3CSQy58f/QPDofR53IKP8rRHJ8M=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
Expand Down
97 changes: 1 addition & 96 deletions internal/handlers/handler_oauth_introspection.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,10 @@
package handlers

import (
"encoding/json"
"net/http"
"net/url"
"time"

oauthelia2 "authelia.com/provider/oauth2"
"authelia.com/provider/oauth2/token/jwt"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/valyala/fasthttp"

"github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/oidc"
Expand Down Expand Up @@ -46,94 +40,5 @@ func OAuthIntrospectionPOST(ctx *middlewares.AutheliaCtx, rw http.ResponseWriter

ctx.Logger.Tracef("Introspection Request with id '%s' yielded a %s (active: %t) requested at %s created with request id '%s' on client with id '%s'", requestID, responder.GetTokenUse(), responder.IsActive(), responder.GetAccessRequester().GetRequestedAt().String(), responder.GetAccessRequester().GetID(), responder.GetAccessRequester().GetClient().GetID())

aud, introspection := responder.ToMap()

var (
client oidc.Client
ok bool
)

if client, ok = responder.GetAccessRequester().GetClient().(oidc.Client); !ok {
ctx.Logger.Errorf("Introspection Request with id '%s' failed with error: %s", requestID, oauthelia2.ErrorToDebugRFC6749Error(oauthelia2.ErrInvalidClient.WithDebugf("The client does not implement the correct type as it's a '%T'", responder.GetAccessRequester().GetClient())))

ctx.Providers.OpenIDConnect.WriteIntrospectionError(ctx, rw, oauthelia2.ErrInvalidClient)

return
}

switch alg := client.GetIntrospectionSignedResponseAlg(); alg {
case oidc.SigningAlgNone:
rw.Header().Set(fasthttp.HeaderContentType, "application/json; charset=utf-8")
rw.Header().Set(fasthttp.HeaderCacheControl, "no-store")
rw.Header().Set(fasthttp.HeaderPragma, "no-cache")
rw.WriteHeader(http.StatusOK)

_ = json.NewEncoder(rw).Encode(introspection)
default:
var (
issuer *url.URL
token string
jwk *oidc.JWK
jti uuid.UUID
)

if issuer, err = ctx.IssuerURL(); err != nil {
ctx.Logger.WithError(err).Errorf("Error occurred determining issuer")

ctx.Providers.OpenIDConnect.WriteIntrospectionError(ctx, rw, errors.WithStack(oauthelia2.ErrServerError.WithHint("Failed to lookup required information to perform this request.").WithDebugf("The issuer could not be determined with error %+v.", err)))

return
}

if jwk = ctx.Providers.OpenIDConnect.KeyManager.Get(ctx, client.GetIntrospectionSignedResponseKeyID(), alg); jwk == nil {
ctx.Logger.WithError(err).Errorf("Introspection Request with id '%s' failed to lookup key for key manager due to likely no support for the key algorithm", requestID)

ctx.Providers.OpenIDConnect.WriteIntrospectionError(ctx, rw, errors.WithStack(oauthelia2.ErrServerError.WithHint("Failed to lookup required information to perform this request.").WithDebugf("The JWK matching algorithm '%s' and key id '%s' could not be found.", alg, client.GetIntrospectionSignedResponseKeyID())))

return
}

if jti, err = uuid.NewRandom(); err != nil {
ctx.Logger.WithError(err).Errorf("Introspection Request with id '%s' failed to generate a JTI", requestID)

ctx.Providers.OpenIDConnect.WriteIntrospectionError(ctx, rw, errors.WithStack(oauthelia2.ErrServerError.WithHint("Failed to lookup required information to perform this request.").WithDebugf("The JTI could not be generated for the Introspection JWT response type with error %+v.", err)))

return
}

headers := &jwt.Headers{
Extra: map[string]any{
oidc.JWTHeaderKeyIdentifier: jwk.KeyID(),
oidc.JWTHeaderKeyType: oidc.JWTHeaderTypeValueTokenIntrospectionJWT,
},
}

claims := map[string]any{
oidc.ClaimJWTID: jti.String(),
oidc.ClaimIssuer: issuer.String(),
oidc.ClaimIssuedAt: time.Now().UTC().Unix(),
oidc.ClaimTokenIntrospection: introspection,
}

if aud != nil {
claims[oidc.ClaimAudience] = aud
}

if token, _, err = jwk.Strategy().Generate(ctx, claims, headers); err != nil {
ctx.Logger.WithError(err).Errorf("Introspection Request with id '%s' failed to generate the Introspection JWT response", requestID)

ctx.Providers.OpenIDConnect.WriteIntrospectionError(ctx, rw, errors.WithStack(oauthelia2.ErrServerError.WithHint("Failed to generate the response.").WithDebugf("The Introspection JWT itself could not be generated with error %+v.", err)))

return
}

rw.Header().Set(fasthttp.HeaderContentType, "application/token-introspection+jwt; charset=utf-8")
rw.Header().Set(fasthttp.HeaderCacheControl, "no-store")
rw.Header().Set(fasthttp.HeaderPragma, "no-cache")
rw.WriteHeader(http.StatusOK)

_, _ = rw.Write([]byte(token))
}

ctx.Logger.Debugf("Introspection Request with id '%s' was processed successfully", requestID)
ctx.Providers.OpenIDConnect.WriteIntrospectionResponse(ctx, rw, responder)
}
21 changes: 16 additions & 5 deletions internal/oidc/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,9 @@ type PARConfig struct {

// IssuersConfig holds specific oauthelia2.Configurator information for the issuer.
type IssuersConfig struct {
IDToken string
AccessToken string
IDToken string
AccessToken string
Introspection string

AuthorizationServerIssuerIdentification string
JWTSecuredResponseMode string
Expand Down Expand Up @@ -476,6 +477,16 @@ func (c *Config) GetAuthorizationServerIdentificationIssuer(ctx context.Context)
return c.GetIssuerFallback(ctx, c.Issuers.AuthorizationServerIssuerIdentification)
}

// GetIntrospectionIssuer returns the Introspection issuer.
func (c *Config) GetIntrospectionIssuer(ctx context.Context) (issuer string) {
return c.GetIssuerFallback(ctx, c.Issuers.Introspection)
}

// GetIntrospectionJWTResponseSigner returns jwt.Signer for Introspection JWT Responses.
func (c *Config) GetIntrospectionJWTResponseSigner(ctx context.Context) jwt.Signer {
return c.Signer
}

// GetDisableRefreshTokenValidation returns the disable refresh token validation flag.
func (c *Config) GetDisableRefreshTokenValidation(ctx context.Context) (disable bool) {
return c.DisableRefreshTokenValidation
Expand Down Expand Up @@ -745,15 +756,15 @@ func (c *Config) GetResponseModeParameterHandlers(ctx context.Context) oauthelia
return c.Handlers.ResponseModeParameter
}

func (c *Config) GetRevokeRefreshTokensExplicit(ctx context.Context) bool {
func (c *Config) GetRevokeRefreshTokensExplicit(ctx context.Context) (explicit bool) {
return c.RevokeRefreshTokensExplicit
}

func (c *Config) GetEnforceRevokeFlowRevokeRefreshTokensExplicitClient(ctx context.Context) bool {
func (c *Config) GetEnforceRevokeFlowRevokeRefreshTokensExplicitClient(ctx context.Context) (enforce bool) {
return c.EnforceRevokeFlowRevokeRefreshTokensExplicitClient
}

func (c *Config) GetTokenURL(ctx context.Context) string {
func (c *Config) GetTokenURL(ctx context.Context) (url string) {
return c.getEndpointURL(ctx, EndpointPathToken, c.TokenURL)
}

Expand Down
120 changes: 120 additions & 0 deletions internal/oidc/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -256,3 +256,123 @@ func TestNewConfig(t *testing.T) {

assert.Len(t, config.Handlers.TokenIntrospection, 2)
}

func TestConfig_GetIssuerFuncs(t *testing.T) {
testCases := []struct {
name string
have oidc.IssuersConfig
ctx context.Context
expectIntrospection, expectIDToken, expectAccessToken, expectAS, expectJARM string
}{
{
"ShouldReturnCtxValues",
oidc.IssuersConfig{},
&TestContext{
Context: context.Background(),
IssuerURLFunc: func() (issuerURL *url.URL, err error) {
return &url.URL{Scheme: "https", Host: "example.com", Path: "/issuer"}, nil
},
},
"https://example.com/issuer",
"https://example.com/issuer",
"https://example.com/issuer",
"https://example.com/issuer",
"https://example.com/issuer",
},
{
"ShouldNotReturnDefaultValues",
oidc.IssuersConfig{
IDToken: "https://example.com/id-issuer",
},
&TestContext{
Context: context.Background(),
IssuerURLFunc: func() (issuerURL *url.URL, err error) {
return &url.URL{Scheme: "https", Host: "example.com", Path: "/issuer"}, nil
},
},
"https://example.com/issuer",
"https://example.com/issuer",
"https://example.com/issuer",
"https://example.com/issuer",
"https://example.com/issuer",
},
{
"ShouldReturnDefaultValues",
oidc.IssuersConfig{
IDToken: "https://example.com/id-issuer",
AccessToken: "https://example.com/at-issuer",
Introspection: "https://example.com/i-issuer",
JWTSecuredResponseMode: "https://example.com/jarm-issuer",
AuthorizationServerIssuerIdentification: "https://example.com/as-issuer",
},
context.Background(),
"https://example.com/i-issuer",
"https://example.com/id-issuer",
"https://example.com/at-issuer",
"https://example.com/as-issuer",
"https://example.com/jarm-issuer",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
config := &oidc.Config{
Issuers: tc.have,
}

assert.Equal(t, tc.expectIntrospection, config.GetIntrospectionIssuer(tc.ctx))
assert.Equal(t, tc.expectIDToken, config.GetIDTokenIssuer(tc.ctx))
assert.Equal(t, tc.expectAccessToken, config.GetAccessTokenIssuer(tc.ctx))
assert.Equal(t, tc.expectAS, config.GetAuthorizationServerIdentificationIssuer(tc.ctx))
assert.Equal(t, tc.expectJARM, config.GetJWTSecuredAuthorizeResponseModeIssuer(tc.ctx))
})
}
}

func TestMisc(t *testing.T) {
tctx := &TestContext{
Context: context.Background(),
IssuerURLFunc: func() (issuerURL *url.URL, err error) {
return &url.URL{Scheme: "https", Host: "example.com", Path: "/issuer"}, nil
},
}

config := &oidc.Config{}
assert.Nil(t, config.GetIntrospectionJWTResponseSigner(context.Background()))
assert.Nil(t, config.GetJWTSecuredAuthorizeResponseModeSigner(context.Background()))

secret, err := config.GetGlobalSecret(context.Background())
assert.NoError(t, err)
assert.Nil(t, secret)

secrets, err := config.GetRotatedGlobalSecrets(context.Background())
assert.NoError(t, err)
assert.Nil(t, secrets)

assert.Equal(t, time.Minute*5, config.GetJWTSecuredAuthorizeResponseModeLifespan(context.Background()))

assert.False(t, config.GetRevokeRefreshTokensExplicit(context.Background()))
assert.False(t, config.GetEnforceRevokeFlowRevokeRefreshTokensExplicitClient(context.Background()))
assert.False(t, config.GetClientCredentialsFlowImplicitGrantRequested(context.Background()))
assert.False(t, config.GetEnforceJWTProfileAccessTokens(context.Background()))
config.ClientCredentialsFlowImplicitGrantRequested = true

assert.True(t, config.GetClientCredentialsFlowImplicitGrantRequested(context.Background()))

assert.NotNil(t, config.GetHMACHasher(context.Background()))
assert.NotNil(t, config.GetFormPostResponseWriter(context.Background()))

assert.Equal(t, time.Hour, config.GetVerifiableCredentialsNonceLifespan(context.Background()))
assert.Nil(t, config.GetResponseModeHandlers(context.Background()))
assert.Nil(t, config.GetResponseModeParameterHandlers(context.Background()))
assert.Nil(t, config.GetRFC8628DeviceAuthorizeEndpointHandlers(context.Background()))
assert.Nil(t, config.GetRFC8628UserAuthorizeEndpointHandlers(context.Background()))
assert.Nil(t, config.GetRFC8693TokenTypes(context.Background()))

assert.Equal(t, "", config.GetDefaultRFC8693RequestedTokenType(context.Background()))
assert.Equal(t, time.Minute*10, config.GetRFC8628CodeLifespan(context.Background()))
assert.Equal(t, time.Second*10, config.GetRFC8628TokenPollingInterval(context.Background()))

assert.Equal(t, "https://example.com/issuer/api/oidc/token", config.GetTokenURL(tctx))
assert.Equal(t, "https://example.com/issuer/api/oidc/device-code/user-verification", config.GetRFC8628UserVerificationURL(tctx))
}
1 change: 1 addition & 0 deletions internal/oidc/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -995,4 +995,5 @@ var (
_ oauthelia2.ClientCredentialsFlowRequestedScopeImplicitClient = (*RegisteredClient)(nil)
_ oauthelia2.RequestedAudienceImplicitClient = (*RegisteredClient)(nil)
_ oauthelia2.JWTProfileClient = (*RegisteredClient)(nil)
_ oauthelia2.IntrospectionJWTResponseClient = (*RegisteredClient)(nil)
)

0 comments on commit 2ffd5c5

Please sign in to comment.