diff --git a/idptoken/introspector.go b/idptoken/introspector.go index b202093..3ac7278 100644 --- a/idptoken/introspector.go +++ b/idptoken/introspector.go @@ -362,14 +362,9 @@ func (i *Introspector) makeIntrospectFuncForToken(ctx context.Context, token str if jwtHeaderBytes, err = i.jwtParser.DecodeSegment(token[:jwtHeaderEndIdx]); err != nil { return i.makeStaticIntrospectFuncOrError(fmt.Errorf("decode JWT header: %w", err)) } - headerDecoder := json.NewDecoder(bytes.NewReader(jwtHeaderBytes)) - headerDecoder.UseNumber() - jwtHeader := make(map[string]interface{}) - if err = headerDecoder.Decode(&jwtHeader); err != nil { - return i.makeStaticIntrospectFuncOrError(fmt.Errorf("unmarshal JWT header: %w", err)) - } - if typ, ok := jwtHeader["typ"].(string); !ok || !strings.EqualFold(typ, idputil.JWTTypeAccessToken) { - return i.makeStaticIntrospectFuncOrError(fmt.Errorf("token type is not %s", idputil.JWTTypeAccessToken)) + jwtHeader, err := parserJWTHeader(jwtHeaderBytes) + if err != nil { + return i.makeStaticIntrospectFuncOrError(fmt.Errorf("parse JWT header: %w", err)) } if !checkIntrospectionRequiredByJWTHeader(jwtHeader) { return nil, ErrTokenIntrospectionNotNeeded @@ -550,6 +545,24 @@ func makeBearerToken(token string) string { return "Bearer " + token } +func parserJWTHeader(b []byte) (map[string]interface{}, error) { + headerDecoder := json.NewDecoder(bytes.NewReader(b)) + headerDecoder.UseNumber() + jwtHeader := make(map[string]interface{}) + if err := headerDecoder.Decode(&jwtHeader); err != nil { + return nil, err + } + typVal, exists := jwtHeader["typ"] + if !exists { + return nil, errors.New(`"typ" field is missing`) + } + if typ, ok := typVal.(string); ok && + (strings.EqualFold(typ, idputil.JWTTypeAccessToken) || strings.EqualFold(typ, idputil.JWTTypeAppAccessToken)) { + return jwtHeader, nil + } + return nil, fmt.Errorf(`"typ" field is not %q or %q`, idputil.JWTTypeAccessToken, idputil.JWTTypeAppAccessToken) +} + // checkIntrospectionRequiredByJWTHeader checks if introspection is required by JWT header. // Introspection is required by default. func checkIntrospectionRequiredByJWTHeader(jwtHeader map[string]interface{}) bool { diff --git a/idptoken/introspector_test.go b/idptoken/introspector_test.go index 8025a33..1cc8439 100644 --- a/idptoken/introspector_test.go +++ b/idptoken/introspector_test.go @@ -8,6 +8,7 @@ package idptoken_test import ( "context" + "fmt" "net/url" gotesting "testing" "time" @@ -86,6 +87,11 @@ func TestIntrospector_IntrospectToken(t *gotesting.T) { validJWT := idptest.MustMakeTokenStringSignedWithTestKey(jwt.Claims{RegisteredClaims: validJWTClaims}) httpServerIntrospector.SetResultForToken(validJWT, idptoken.IntrospectionResult{Active: true, TokenType: idputil.TokenTypeBearer, Claims: jwt.Claims{RegisteredClaims: validJWTClaims, Scope: validJWTScope}}) + validJWTWithAppTyp := idptest.MustMakeTokenStringWithHeader(jwt.Claims{ + RegisteredClaims: validJWTClaims, + }, idptest.TestKeyID, idptest.GetTestRSAPrivateKey(), map[string]interface{}{"typ": idputil.JWTTypeAppAccessToken}) + httpServerIntrospector.SetResultForToken(validJWTWithAppTyp, idptoken.IntrospectionResult{Active: true, + TokenType: idputil.TokenTypeBearer, Claims: jwt.Claims{RegisteredClaims: validJWTClaims, Scope: validJWTScope}}) // Opaque token opaqueToken := "opaque-token-" + uuid.NewString() @@ -136,6 +142,17 @@ func TestIntrospector_IntrospectToken(t *gotesting.T) { require.ErrorContains(t, err, "decode JWT header") }, }, + { + name: `error, dynamic introspection endpoint, invalid "typ" field in JWT header`, + tokenToIntrospect: idptest.MustMakeTokenStringWithHeader(jwt.Claims{ + RegisteredClaims: validJWTClaims, + }, idptest.TestKeyID, idptest.GetTestRSAPrivateKey(), map[string]interface{}{"typ": "invalid"}), + checkError: func(t *gotesting.T, err error) { + require.ErrorIs(t, err, idptoken.ErrTokenNotIntrospectable) + require.ErrorContains(t, err, fmt.Sprintf( + `"typ" field is not %q or %q`, idputil.JWTTypeAccessToken, idputil.JWTTypeAppAccessToken)) + }, + }, { name: "error, dynamic introspection endpoint, issuer is not trusted", tokenToIntrospect: idptest.MustMakeTokenStringSignedWithTestKey(jwt.Claims{ @@ -207,6 +224,16 @@ func TestIntrospector_IntrospectToken(t *gotesting.T) { }, expectedHTTPSrvCalled: true, }, + { + name: `ok, dynamic introspection endpoint, introspected token is JWT, "typ" is "application/at+jwt"`, + tokenToIntrospect: validJWTWithAppTyp, + expectedResult: idptoken.IntrospectionResult{ + Active: true, + TokenType: idputil.TokenTypeBearer, + Claims: jwt.Claims{RegisteredClaims: validJWTClaims, Scope: validJWTScope}, + }, + expectedHTTPSrvCalled: true, + }, { name: "ok, static http introspection endpoint, introspected token is opaque", introspectorOpts: idptoken.IntrospectorOpts{ diff --git a/internal/idputil/idp_util.go b/internal/idputil/idp_util.go index e1dd772..89ccd5e 100644 --- a/internal/idputil/idp_util.go +++ b/internal/idputil/idp_util.go @@ -21,6 +21,8 @@ const GrantTypeJWTBearer = "urn:ietf:params:oauth:grant-type:jwt-bearer" //nolin const JWTTypeAccessToken = "at+jwt" +const JWTTypeAppAccessToken = "application/at+jwt" + const TokenTypeBearer = "Bearer" const (