Skip to content

Commit

Permalink
Merge pull request #271 from zitadel/jwt-token
Browse files Browse the repository at this point in the history
feat: allow to specify token type of JWT Profile Grant
  • Loading branch information
muhlemmer authored Feb 6, 2023
2 parents df35979 + 2832b76 commit 3e6ea03
Show file tree
Hide file tree
Showing 4 changed files with 64 additions and 13 deletions.
6 changes: 6 additions & 0 deletions pkg/op/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ type OPStorage interface {
ValidateJWTProfileScopes(ctx context.Context, userID string, scopes []string) ([]string, error)
}

// JWTProfileTokenStorage is an additional, optional storage to implement
// implementing it, allows specifying the [AccessTokenType] of the access_token returned form the JWT Profile TokenRequest
type JWTProfileTokenStorage interface {
JWTProfileTokenType(ctx context.Context, request TokenRequest) (AccessTokenType, error)
}

type Storage interface {
AuthStorage
OPStorage
Expand Down
15 changes: 11 additions & 4 deletions pkg/op/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ type TokenRequest interface {
GetScopes() []string
}

type AccessTokenClient interface {
GetID() string
ClockSkew() time.Duration
RestrictAdditionalAccessTokenScopes() func(scopes []string) []string
GrantTypes() []oidc.GrantType
}

func CreateTokenResponse(ctx context.Context, request IDTokenRequest, client Client, creator TokenCreator, createAccessToken bool, code, refreshToken string) (*oidc.AccessTokenResponse, error) {
var accessToken, newRefreshToken string
var validity time.Duration
Expand Down Expand Up @@ -55,15 +62,15 @@ func CreateTokenResponse(ctx context.Context, request IDTokenRequest, client Cli
}, nil
}

func createTokens(ctx context.Context, tokenRequest TokenRequest, storage Storage, refreshToken string, client Client) (id, newRefreshToken string, exp time.Time, err error) {
func createTokens(ctx context.Context, tokenRequest TokenRequest, storage Storage, refreshToken string, client AccessTokenClient) (id, newRefreshToken string, exp time.Time, err error) {
if needsRefreshToken(tokenRequest, client) {
return storage.CreateAccessAndRefreshTokens(ctx, tokenRequest, refreshToken)
}
id, exp, err = storage.CreateAccessToken(ctx, tokenRequest)
return
}

func needsRefreshToken(tokenRequest TokenRequest, client Client) bool {
func needsRefreshToken(tokenRequest TokenRequest, client AccessTokenClient) bool {
switch req := tokenRequest.(type) {
case AuthRequest:
return strings.Contains(req.GetScopes(), oidc.ScopeOfflineAccess) && req.GetResponseType() == oidc.ResponseTypeCode && ValidateGrantType(client, oidc.GrantTypeRefreshToken)
Expand All @@ -74,7 +81,7 @@ func needsRefreshToken(tokenRequest TokenRequest, client Client) bool {
}
}

func CreateAccessToken(ctx context.Context, tokenRequest TokenRequest, accessTokenType AccessTokenType, creator TokenCreator, client Client, refreshToken string) (accessToken, newRefreshToken string, validity time.Duration, err error) {
func CreateAccessToken(ctx context.Context, tokenRequest TokenRequest, accessTokenType AccessTokenType, creator TokenCreator, client AccessTokenClient, refreshToken string) (accessToken, newRefreshToken string, validity time.Duration, err error) {
id, newRefreshToken, exp, err := createTokens(ctx, tokenRequest, creator.Storage(), refreshToken, client)
if err != nil {
return "", "", 0, err
Expand All @@ -96,7 +103,7 @@ func CreateBearerToken(tokenID, subject string, crypto Crypto) (string, error) {
return crypto.Encrypt(tokenID + ":" + subject)
}

func CreateJWT(ctx context.Context, issuer string, tokenRequest TokenRequest, exp time.Time, id string, client Client, storage Storage) (string, error) {
func CreateJWT(ctx context.Context, issuer string, tokenRequest TokenRequest, exp time.Time, id string, client AccessTokenClient, storage Storage) (string, error) {
claims := oidc.NewAccessTokenClaims(issuer, tokenRequest.GetSubject(), tokenRequest.GetAudience(), exp, id, client.GetID(), client.ClockSkew())
if client != nil {
restrictedScopes := client.RestrictAdditionalAccessTokenScopes()(tokenRequest.GetScopes())
Expand Down
52 changes: 45 additions & 7 deletions pkg/op/token_jwt_profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,21 +53,35 @@ func ParseJWTProfileGrantRequest(r *http.Request, decoder httphelper.Decoder) (*
return tokenReq, nil
}

//CreateJWTTokenResponse creates
// CreateJWTTokenResponse creates an access_token response for a JWT Profile Grant request
// by default the access_token is an opaque string, but can be specified by implementing the JWTProfileTokenStorage interface
func CreateJWTTokenResponse(ctx context.Context, tokenRequest TokenRequest, creator TokenCreator) (*oidc.AccessTokenResponse, error) {
id, exp, err := creator.Storage().CreateAccessToken(ctx, tokenRequest)
if err != nil {
return nil, err
// return an opaque token as default to not break current implementations
tokenType := AccessTokenTypeBearer

// the current CreateAccessToken function, esp. CreateJWT requires an implementation of an AccessTokenClient
client := &jwtProfileClient{
id: tokenRequest.GetSubject(),
}

// by implementing the JWTProfileTokenStorage the storage can specify the AccessTokenType to be returned
tokenStorage, ok := creator.Storage().(JWTProfileTokenStorage)
if ok {
var err error
tokenType, err = tokenStorage.JWTProfileTokenType(ctx, tokenRequest)
if err != nil {
return nil, err
}
}
accessToken, err := CreateBearerToken(id, tokenRequest.GetSubject(), creator.Crypto())

accessToken, _, validity, err := CreateAccessToken(ctx, tokenRequest, tokenType, creator, client, "")
if err != nil {
return nil, err
}

return &oidc.AccessTokenResponse{
AccessToken: accessToken,
TokenType: oidc.BearerToken,
ExpiresIn: uint64(exp.Sub(time.Now().UTC()).Seconds()),
ExpiresIn: uint64(validity.Seconds()),
}, nil
}

Expand All @@ -77,3 +91,27 @@ func CreateJWTTokenResponse(ctx context.Context, tokenRequest TokenRequest, crea
func ParseJWTProfileRequest(r *http.Request, decoder httphelper.Decoder) (*oidc.JWTProfileGrantRequest, error) {
return ParseJWTProfileGrantRequest(r, decoder)
}

type jwtProfileClient struct {
id string
}

func (j *jwtProfileClient) GetID() string {
return j.id
}

func (j *jwtProfileClient) ClockSkew() time.Duration {
return 0
}

func (j *jwtProfileClient) RestrictAdditionalAccessTokenScopes() func(scopes []string) []string {
return func(scopes []string) []string {
return scopes
}
}

func (j *jwtProfileClient) GrantTypes() []oidc.GrantType {
return []oidc.GrantType{
oidc.GrantTypeBearer,
}
}
4 changes: 2 additions & 2 deletions pkg/op/token_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,8 @@ func AuthorizePrivateJWTKey(ctx context.Context, clientAssertion string, exchang
return client, nil
}

//ValidateGrantType ensures that the requested grant_type is allowed by the Client
func ValidateGrantType(client Client, grantType oidc.GrantType) bool {
//ValidateGrantType ensures that the requested grant_type is allowed by the client
func ValidateGrantType(client interface{ GrantTypes() []oidc.GrantType }, grantType oidc.GrantType) bool {
if client == nil {
return false
}
Expand Down

0 comments on commit 3e6ea03

Please sign in to comment.