Skip to content

Commit

Permalink
feat: extend token exchange response (#567)
Browse files Browse the repository at this point in the history
* feat: extend token exchange response

This change adds fields to the token exchange and token claims types.

The `act` claim has been added to describe the actor in case of impersonation or delegation. An actor can be nested in case an obtained token is used as actor token to obtain impersonation or delegation. This allows creating a chain of actors. See [RFC 8693, section 4.1](https://www.rfc-editor.org/rfc/rfc8693#name-act-actor-claim).

The `id_token` field has been added to the Token Exchange response  so an ID Token can be returned along with an access token. This is not specified in RFC 8693, but it allows us be consistent with OpenID responses when the scope `openid` is set, while the requested token type may remain access token.

* allow jwt profile for token exchange client

* add invalid target error
  • Loading branch information
muhlemmer committed Mar 13, 2024
1 parent 1532a5c commit ad79802
Show file tree
Hide file tree
Showing 4 changed files with 68 additions and 20 deletions.
13 changes: 13 additions & 0 deletions pkg/client/tokenexchange/tokenexchange.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import (
"context"
"errors"
"net/http"
"time"

"github.com/go-jose/go-jose/v3"
"github.com/zitadel/oidc/v3/pkg/client"
httphelper "github.com/zitadel/oidc/v3/pkg/http"
"github.com/zitadel/oidc/v3/pkg/oidc"
Expand Down Expand Up @@ -33,6 +35,17 @@ func NewTokenExchangerClientCredentials(ctx context.Context, issuer, clientID, c
return newOAuthTokenExchange(ctx, issuer, authorizer, options...)
}

func NewTokenExchangerJWTProfile(ctx context.Context, issuer, clientID string, signer jose.Signer, options ...func(source *OAuthTokenExchange)) (TokenExchanger, error) {
authorizer := func() (any, error) {
assertion, err := client.SignedJWTProfileAssertion(clientID, []string{issuer}, time.Hour, signer)
if err != nil {
return nil, err
}
return client.ClientAssertionFormAuthorization(assertion), nil
}
return newOAuthTokenExchange(ctx, issuer, authorizer, options...)
}

func newOAuthTokenExchange(ctx context.Context, issuer string, authorizer func() (any, error), options ...func(source *OAuthTokenExchange)) (*OAuthTokenExchange, error) {
te := &OAuthTokenExchange{
httpClient: httphelper.DefaultHTTPClient,
Expand Down
13 changes: 13 additions & 0 deletions pkg/oidc/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ const (
SlowDown errorType = "slow_down"
AccessDenied errorType = "access_denied"
ExpiredToken errorType = "expired_token"

// InvalidTarget error is returned by Token Exchange if
// the requested target or audience is invalid.
// [RFC 8693, Section 2.2.2: Error Response](https://www.rfc-editor.org/rfc/rfc8693#section-2.2.2)
InvalidTarget errorType = "invalid_target"
)

var (
Expand Down Expand Up @@ -112,6 +117,14 @@ var (
Description: "The \"device_code\" has expired.",
}
}

// Token exchange error
ErrInvalidTarget = func() *Error {
return &Error{
ErrorType: InvalidTarget,
Description: "The requested audience or target is invalid.",
}
}
)

type Error struct {
Expand Down
53 changes: 40 additions & 13 deletions pkg/oidc/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,19 +34,20 @@ type Tokens[C IDClaims] struct {
// TokenClaims implements the Claims interface,
// and can be used to extend larger claim types by embedding.
type TokenClaims struct {
Issuer string `json:"iss,omitempty"`
Subject string `json:"sub,omitempty"`
Audience Audience `json:"aud,omitempty"`
Expiration Time `json:"exp,omitempty"`
IssuedAt Time `json:"iat,omitempty"`
AuthTime Time `json:"auth_time,omitempty"`
NotBefore Time `json:"nbf,omitempty"`
Nonce string `json:"nonce,omitempty"`
AuthenticationContextClassReference string `json:"acr,omitempty"`
AuthenticationMethodsReferences []string `json:"amr,omitempty"`
AuthorizedParty string `json:"azp,omitempty"`
ClientID string `json:"client_id,omitempty"`
JWTID string `json:"jti,omitempty"`
Issuer string `json:"iss,omitempty"`
Subject string `json:"sub,omitempty"`
Audience Audience `json:"aud,omitempty"`
Expiration Time `json:"exp,omitempty"`
IssuedAt Time `json:"iat,omitempty"`
AuthTime Time `json:"auth_time,omitempty"`
NotBefore Time `json:"nbf,omitempty"`
Nonce string `json:"nonce,omitempty"`
AuthenticationContextClassReference string `json:"acr,omitempty"`
AuthenticationMethodsReferences []string `json:"amr,omitempty"`
AuthorizedParty string `json:"azp,omitempty"`
ClientID string `json:"client_id,omitempty"`
JWTID string `json:"jti,omitempty"`
Actor *ActorClaims `json:"act,omitempty"`

// Additional information set by this framework
SignatureAlg jose.SignatureAlgorithm `json:"-"`
Expand Down Expand Up @@ -204,6 +205,28 @@ func (i *IDTokenClaims) UnmarshalJSON(data []byte) error {
return unmarshalJSONMulti(data, (*itcAlias)(i), &i.Claims)
}

// ActorClaims provides the `act` claims used for impersonation or delegation Token Exchange.
//
// An actor can be nested in case an obtained token is used as actor token to obtain impersonation or delegation.
// This allows creating a chain of actors.
// See [RFC 8693, section 4.1](https://www.rfc-editor.org/rfc/rfc8693#name-act-actor-claim).
type ActorClaims struct {
Actor *ActorClaims `json:"act,omitempty"`
Issuer string `json:"iss,omitempty"`
Subject string `json:"sub,omitempty"`
Claims map[string]any `json:"-"`
}

type acAlias ActorClaims

func (c *ActorClaims) MarshalJSON() ([]byte, error) {
return mergeAndMarshalClaims((*acAlias)(c), c.Claims)
}

func (c *ActorClaims) UnmarshalJSON(data []byte) error {
return unmarshalJSONMulti(data, (*acAlias)(c), &c.Claims)
}

type AccessTokenResponse struct {
AccessToken string `json:"access_token,omitempty" schema:"access_token,omitempty"`
TokenType string `json:"token_type,omitempty" schema:"token_type,omitempty"`
Expand Down Expand Up @@ -352,4 +375,8 @@ type TokenExchangeResponse struct {
ExpiresIn uint64 `json:"expires_in,omitempty"`
Scopes SpaceDelimitedArray `json:"scope,omitempty"`
RefreshToken string `json:"refresh_token,omitempty"`

// IDToken field allows returning an additional ID token
// if the requested_token_type was Access Token and scope contained openid.
IDToken string `json:"id_token,omitempty"`
}
9 changes: 2 additions & 7 deletions pkg/oidc/token_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package oidc
import (
"encoding/json"
"fmt"
"slices"
"time"

jose "github.com/go-jose/go-jose/v3"
Expand Down Expand Up @@ -57,13 +58,7 @@ var AllTokenTypes = []TokenType{
type TokenType string

func (t TokenType) IsSupported() bool {
for _, tt := range AllTokenTypes {
if t == tt {
return true
}
}

return false
return slices.Contains(AllTokenTypes, t)
}

type TokenRequest interface {
Expand Down

0 comments on commit ad79802

Please sign in to comment.