-
-
Notifications
You must be signed in to change notification settings - Fork 368
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add(handler): token exchange rfc8693 in impersonation mode
- Loading branch information
1 parent
e570564
commit 9f78a51
Showing
7 changed files
with
808 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,268 @@ | ||
// Copyright © 2022 Ory Corp | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package rfc8693 | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"fmt" | ||
"time" | ||
|
||
"github.com/ory/fosite" | ||
"github.com/ory/fosite/compose" | ||
"github.com/ory/fosite/handler/oauth2" | ||
"github.com/ory/fosite/token/jwt" | ||
"github.com/ory/x/errorsx" | ||
) | ||
|
||
// #nosec G101 | ||
const ( | ||
tokenTypeIDToken = "urn:ietf:params:oauth:token-type:id_token" | ||
tokenTypeAT = "urn:ietf:params:oauth:token-type:access_token" | ||
) | ||
|
||
func TokenExchangeGrantFactory(config *compose.CommonStrategy, storage, strategy interface{}) interface{} { | ||
return nil | ||
} | ||
|
||
type Handler struct { | ||
Storage RFC8693Storage | ||
Strategy ClientAuthenticationStrategy | ||
ScopeStrategy fosite.ScopeStrategy | ||
AudienceMatchingStrategy fosite.AudienceMatchingStrategy | ||
RefreshTokenStrategy oauth2.RefreshTokenStrategy | ||
RefreshTokenStorage oauth2.RefreshTokenStorage | ||
fosite.RefreshTokenScopesProvider | ||
|
||
*oauth2.HandleHelper | ||
} | ||
|
||
type tokenExchangeParams struct { | ||
subjectToken string | ||
subjectTokenType string | ||
} | ||
|
||
func parseRequestParameter(requester fosite.AccessRequester) (*tokenExchangeParams, error) { | ||
form := requester.GetRequestForm() | ||
|
||
// From https://tools.ietf.org/html/rfc8693#section-2.1: | ||
// | ||
// subject_token | ||
// REQUIRED. A security token that represents the identity of the | ||
// party on behalf of whom the request is being made. Typically, the | ||
// subject of this token will be the subject of the security token | ||
// issued in response to the request. | ||
subjectToken := form.Get("subject_token") | ||
if subjectToken == "" { | ||
return nil, errorsx.WithStack(fosite.ErrInvalidRequest.WithHintf("subject_token is missing")) | ||
} | ||
|
||
// From https://tools.ietf.org/html/rfc8693#section-2.1: | ||
// | ||
// subject_token_type | ||
// REQUIRED. An identifier, that indicates the type of the | ||
// security token in the "subject_token" parameter. | ||
subjectTokenType := form.Get("subject_token_type") | ||
switch subjectTokenType { | ||
case tokenTypeIDToken, tokenTypeAT: | ||
default: | ||
return nil, errorsx.WithStack(fosite.ErrInvalidRequest.WithHintf("unsupported or missing subject_token_type %s", subjectTokenType)) | ||
} | ||
|
||
// From https://tools.ietf.org/html/rfc8693#section-2.1: | ||
// | ||
// requested_token_type | ||
// OPTIONAL. An identifier, for the type of the requested security token. | ||
// If the requested type is unspecified, | ||
// the issued token type is at the discretion of the authorization server and | ||
// may be dictated by knowledge of the requirements of the service or | ||
// resource indicated by the resource or audience parameter. | ||
requestedTokenType := form.Get("requested_token_type") | ||
switch requestedTokenType { | ||
case tokenTypeAT, "": | ||
default: | ||
return nil, errorsx.WithStack(fosite.ErrInvalidRequest.WithHintf("unsupported requested_token_type %s", requestedTokenType)) | ||
} | ||
|
||
// From https://tools.ietf.org/html/rfc8693#section-2.1: | ||
// | ||
// actor_token | ||
// OPTIONAL . A security token that represents the identity of the acting party. | ||
// Typically, this will be the party that is authorized to use the requested security | ||
// token and act on behalf of the subject. | ||
actorToken := form.Get("actor_token") | ||
if actorToken != "" { | ||
return nil, errorsx.WithStack(fosite.ErrInvalidRequest.WithHintf("'actor_token' was provided but delegation is currently not supported.")) | ||
} | ||
|
||
// From https://tools.ietf.org/html/rfc8693#section-2.1: | ||
// | ||
// actor_token_type | ||
// An identifier, as described in Section 3, that indicates the type of the security token | ||
// in the actor_token parameter. This is REQUIRED when the actor_token parameter is present | ||
// in the request but MUST NOT be included otherwise. | ||
actorTokenType := form.Get("actor_token_type") | ||
if actorTokenType != "" { | ||
return nil, errorsx.WithStack(fosite.ErrInvalidRequest.WithHintf("'actor_token_type' was provided but delegation is currently not supported.")) | ||
} | ||
|
||
return &tokenExchangeParams{ | ||
subjectToken: subjectToken, | ||
subjectTokenType: subjectTokenType, | ||
}, nil | ||
} | ||
|
||
func (c *Handler) HandleTokenEndpointRequest(ctx context.Context, requester fosite.AccessRequester) error { | ||
if !c.CanHandleTokenEndpointRequest(requester) { | ||
return errorsx.WithStack(fosite.ErrUnknownRequest) | ||
} | ||
|
||
client := requester.GetClient() | ||
if client.GetID() == "" { | ||
return errorsx.WithStack(fosite.ErrUnauthorizedClient.WithHint("unauthenticated client")) | ||
} | ||
|
||
// Check whether client is allowed to use token exchange. | ||
if !client.GetGrantTypes().Has(string(fosite.GrantTypeTokenExchange)) { | ||
return errorsx.WithStack(fosite.ErrUnauthorizedClient.WithHint("the client is not allowed to use token-exchange")) | ||
} | ||
|
||
// Get request parameter related token exchange. | ||
params, err := parseRequestParameter(requester) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
// Check and grant scope. | ||
for _, scope := range requester.GetRequestedScopes() { | ||
if !c.ScopeStrategy(client.GetScopes(), scope) { | ||
return errorsx.WithStack(fosite.ErrInvalidScope.WithHintf("The OAuth 2.0 Client is not allowed to request scope '%s'.", scope)) | ||
} | ||
requester.GrantScope(scope) | ||
} | ||
|
||
// Check and grant audience. | ||
if err := c.AudienceMatchingStrategy(client.GetAudience(), requester.GetRequestedAudience()); err != nil { | ||
return errorsx.WithStack(fosite.ErrInvalidRequest.WithHintf("audience not match: %v", err)) | ||
} | ||
for _, audience := range requester.GetRequestedAudience() { | ||
requester.GrantAudience(audience) | ||
} | ||
|
||
// Verify subject token. | ||
switch params.subjectTokenType { | ||
case tokenTypeIDToken: | ||
claims := jwt.MapClaims{} | ||
if _, err := jwt.ParseWithClaims(params.subjectToken, claims, c.keyFunc(ctx)); err != nil { | ||
return errorsx.WithStack(fosite.ErrInvalidRequest.WithHintf("failed to verify JWT: %v", err)) | ||
} | ||
subject, err := c.Storage.GetImpersonateSubject(ctx, claims, requester) | ||
if err != nil { | ||
return errorsx.WithStack(fosite.ErrInvalidRequest.WithHintf("not allowed to token exchange by jwt: %v", err)) | ||
} | ||
requester.SetSession(&fosite.DefaultSession{ | ||
Subject: subject, | ||
}) | ||
requester.GetSession().SetExpiresAt(fosite.AccessToken, time.Now().UTC().Add(c.Config.GetAccessTokenLifespan(ctx))) | ||
return nil | ||
case tokenTypeAT: | ||
or, err := c.verifyAccessTokenAsSubjectToken(ctx, client.GetID(), params) | ||
if err != nil { | ||
return errorsx.WithStack(fosite.ErrInvalidRequest.WithHintf("not allowed to token exchange by at: %v", err)) | ||
} | ||
requester.SetSession(or.GetSession().Clone()) | ||
// When the subject_type is AT, the expiration time is same with subject_token. | ||
// Therefore, we don't need to set the expiresAt. | ||
return nil | ||
default: | ||
return errorsx.WithStack(fosite.ErrInvalidRequest.WithHintf("unsupported subject_type %s", params.subjectTokenType)) | ||
} | ||
} | ||
|
||
func (c *Handler) PopulateTokenEndpointResponse(ctx context.Context, requester fosite.AccessRequester, responder fosite.AccessResponder) error { | ||
if !c.CanHandleTokenEndpointRequest(requester) { | ||
return errorsx.WithStack(fosite.ErrUnknownRequest) | ||
} | ||
|
||
if !requester.GetClient().GetGrantTypes().Has(string(fosite.GrantTypeTokenExchange)) { | ||
return errorsx.WithStack(fosite.ErrUnauthorizedClient.WithHintf("The OAuth 2.0 Client is not allowed to use authorization grant '%s'.", fosite.GrantTypeTokenExchange)) | ||
} | ||
|
||
atLifespan := fosite.GetEffectiveLifespan(requester.GetClient(), fosite.GrantTypeTokenExchange, fosite.AccessToken, c.Config.GetAccessTokenLifespan(ctx)) | ||
|
||
if err := c.IssueAccessToken(ctx, atLifespan, requester, responder); err != nil { | ||
return err | ||
} | ||
|
||
if canIssueRefreshToken(ctx, c, requester) { | ||
fmt.Println(requester) | ||
refresh, refreshSignature, err := c.RefreshTokenStrategy.GenerateRefreshToken(ctx, requester) | ||
if err != nil { | ||
return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) | ||
} | ||
if err := c.RefreshTokenStorage.CreateRefreshTokenSession(ctx, refreshSignature, requester); err != nil { | ||
return errorsx.WithStack(fosite.ErrServerError.WithDebug(err.Error())) | ||
} | ||
|
||
responder.SetExtra("refresh_token", refresh) | ||
} | ||
return nil | ||
} | ||
|
||
func canIssueRefreshToken(ctx context.Context, c *Handler, requester fosite.Requester) bool { | ||
scope := c.GetRefreshTokenScopes(ctx) | ||
// Require one of the refresh token scopes, if set. | ||
if len(scope) > 0 && !requester.GetGrantedScopes().HasOneOf(scope...) { | ||
return false | ||
} | ||
// Do not issue a refresh token to clients that cannot use the refresh token grant type. | ||
if !requester.GetClient().GetGrantTypes().Has("refresh_token") { | ||
return false | ||
} | ||
return true | ||
} | ||
|
||
func (c *Handler) CanSkipClientAuth(requester fosite.AccessRequester) bool { | ||
return c.Strategy.CanSkipClientAuth(requester) | ||
} | ||
|
||
func (c *Handler) keyFunc(ctx context.Context) jwt.Keyfunc { | ||
return jwt.Keyfunc(func(t *jwt.Token) (interface{}, error) { | ||
kid, ok := t.Header["kid"].(string) | ||
if !ok { | ||
return nil, errors.New("invalid kid") | ||
} | ||
iss, ok := t.Claims["iss"].(string) | ||
if !ok { | ||
return nil, errors.New("invalid iss") | ||
} | ||
return c.Storage.GetIDTokenPublicKey(ctx, iss, kid) | ||
}) | ||
} | ||
|
||
func (c *Handler) verifyAccessTokenAsSubjectToken(ctx context.Context, clientID string, params *tokenExchangeParams) (fosite.Requester, error) { | ||
sig := c.HandleHelper.AccessTokenStrategy.AccessTokenSignature(ctx, params.subjectToken) | ||
or, err := c.HandleHelper.AccessTokenStorage.GetAccessTokenSession(ctx, sig, nil) | ||
if err != nil { | ||
return nil, errorsx.WithStack(fosite.ErrRequestUnauthorized.WithWrap(err).WithDebug(err.Error())) | ||
} else if err := c.AccessTokenStrategy.ValidateAccessToken(ctx, or, params.subjectToken); err != nil { | ||
return nil, err | ||
} | ||
|
||
allowClientIDs, err := c.Storage.GetAllowedClientIDs(ctx, clientID) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
for _, cid := range allowClientIDs { | ||
if or.GetClient().GetID() == cid { | ||
return or, nil | ||
} | ||
} | ||
return nil, fmt.Errorf("this access_token is not allowed to use token exchange based on AT: original_client:%s, request_client:%s ", or.GetClient().GetID(), clientID) | ||
} | ||
|
||
func (c *Handler) CanHandleTokenEndpointRequest(requester fosite.AccessRequester) bool { | ||
return requester.GetGrantTypes().ExactOne(string(fosite.GrantTypeTokenExchange)) | ||
} |
Oops, something went wrong.