Skip to content

Commit

Permalink
Accept JWT Access Tokens with application/at+jwt "typ" field
Browse files Browse the repository at this point in the history
It follows RFC 9068.
  • Loading branch information
vasayxtx committed Nov 18, 2024
1 parent 42727c0 commit b9fbec6
Show file tree
Hide file tree
Showing 3 changed files with 50 additions and 8 deletions.
29 changes: 21 additions & 8 deletions idptoken/introspector.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
27 changes: 27 additions & 0 deletions idptoken/introspector_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package idptoken_test

import (
"context"
"fmt"
"net/url"
gotesting "testing"
"time"
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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{
Expand Down
2 changes: 2 additions & 0 deletions internal/idputil/idp_util.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down

0 comments on commit b9fbec6

Please sign in to comment.