From 7c8319db4b0b22add008c27f065cdde151fc6425 Mon Sep 17 00:00:00 2001 From: Trevor Foster Date: Tue, 2 Jul 2024 13:35:01 -0400 Subject: [PATCH] feat(token_hook): pass associated oauth client data to token hook --- client/client.go | 8 ++++ consent/handler.go | 8 ++-- consent/helper.go | 10 +---- contrib/quickstart/5-min/hydra.yml | 2 + ...esh_token_hook_if_configured-hook=new.json | 43 +++++++++++++++++++ ...esh_token_hook_if_configured-hook=new.json | 43 +++++++++++++++++++ ...esh_token_hook_if_configured-hook=new.json | 43 +++++++++++++++++++ ...esh_token_hook_if_configured-hook=new.json | 43 +++++++++++++++++++ ...esh_token_hook_if_configured-hook=new.json | 43 +++++++++++++++++++ ...esh_token_hook_if_configured-hook=new.json | 43 +++++++++++++++++++ oauth2/oauth2_auth_code_test.go | 21 ++++++--- oauth2/token_hook.go | 12 +++++- 12 files changed, 299 insertions(+), 20 deletions(-) diff --git a/client/client.go b/client/client.go index 52ee86b558d..31a6a68900a 100644 --- a/client/client.go +++ b/client/client.go @@ -580,3 +580,11 @@ type IDer interface{ GetID() string } func CookieSuffix(client IDer) string { return strconv.Itoa(int(murmur3.Sum32([]byte(client.GetID())))) } + +func GetSanitizedCopy(c *Client) *Client { + cc := new(Client) + // Remove the hashed secret here + *cc = *c + cc.Secret = "" + return cc +} diff --git a/consent/handler.go b/consent/handler.go index 3ae98aa23c2..3a7d1815497 100644 --- a/consent/handler.go +++ b/consent/handler.go @@ -9,6 +9,8 @@ import ( "net/url" "time" + "github.com/ory/hydra/v2/client" + "github.com/ory/hydra/v2/flow" "github.com/ory/hydra/v2/oauth2/flowctx" "github.com/ory/hydra/v2/x/events" @@ -212,7 +214,7 @@ func (h *Handler) listOAuth2ConsentSessions(w http.ResponseWriter, r *http.Reque var a []flow.OAuth2ConsentSession for _, session := range s { - session.ConsentRequest.Client = sanitizeClient(session.ConsentRequest.Client) + session.ConsentRequest.Client = client.GetSanitizedCopy(session.ConsentRequest.Client) a = append(a, flow.OAuth2ConsentSession(session)) } @@ -372,7 +374,7 @@ func (h *Handler) getOAuth2LoginRequest(w http.ResponseWriter, r *http.Request, request.RequestedAudience = []string{} } - request.Client = sanitizeClient(request.Client) + request.Client = client.GetSanitizedCopy(request.Client) h.r.Writer().Write(w, r, request) } @@ -679,7 +681,7 @@ func (h *Handler) getOAuth2ConsentRequest(w http.ResponseWriter, r *http.Request request.RequestedAudience = []string{} } - request.Client = sanitizeClient(request.Client) + request.Client = client.GetSanitizedCopy(request.Client) h.r.Writer().Write(w, r, request) } diff --git a/consent/helper.go b/consent/helper.go index 362f2952284..00ac5d0a3a0 100644 --- a/consent/helper.go +++ b/consent/helper.go @@ -10,15 +10,7 @@ import ( ) func sanitizeClientFromRequest(ar fosite.AuthorizeRequester) *client.Client { - return sanitizeClient(ar.GetClient().(*client.Client)) -} - -func sanitizeClient(c *client.Client) *client.Client { - cc := new(client.Client) - // Remove the hashed secret here - *cc = *c - cc.Secret = "" - return cc + return client.GetSanitizedCopy(ar.GetClient().(*client.Client)) } func matchScopes(scopeStrategy fosite.ScopeStrategy, previousConsent []flow.AcceptOAuth2ConsentRequest, requestedScope []string) *flow.AcceptOAuth2ConsentRequest { diff --git a/contrib/quickstart/5-min/hydra.yml b/contrib/quickstart/5-min/hydra.yml index 8d69cc1d243..d97e1fb874c 100644 --- a/contrib/quickstart/5-min/hydra.yml +++ b/contrib/quickstart/5-min/hydra.yml @@ -20,3 +20,5 @@ oidc: - public pairwise: salt: youReallyNeedToChangeThis +oauth2: + token_hook: http://localhost:8080 diff --git a/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=jwt-case=0-description=should_pass_request_if_strategy_passes-should_call_refresh_token_hook_if_configured-hook=new.json b/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=jwt-case=0-description=should_pass_request_if_strategy_passes-should_call_refresh_token_hook_if_configured-hook=new.json index 3748c3744f1..449ad8f05fd 100644 --- a/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=jwt-case=0-description=should_pass_request_if_strategy_passes-should_call_refresh_token_hook_if_configured-hook=new.json +++ b/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=jwt-case=0-description=should_pass_request_if_strategy_passes-should_call_refresh_token_hook_if_configured-hook=new.json @@ -35,6 +35,49 @@ }, "request": { "client_id": "app-client", + "client": { + "client_id": "app-client", + "client_name": "", + "grant_types": [ + "implicit", + "refresh_token", + "authorization_code", + "password", + "client_credentials" + ], + "response_types": [ + "id_token", + "code", + "token" + ], + "scope": "hydra.* offline openid", + "audience": [], + "owner": "", + "policy_uri": "", + "allowed_cors_origins": [], + "tos_uri": "", + "client_uri": "", + "logo_uri": "", + "contacts": [], + "client_secret_expires_at": 0, + "subject_type": "", + "jwks": {}, + "metadata": { + "some-meta-key": "some-meta-value" + }, + "skip_consent": false, + "skip_logout_consent": false, + "authorization_code_grant_access_token_lifespan": null, + "authorization_code_grant_id_token_lifespan": null, + "authorization_code_grant_refresh_token_lifespan": null, + "client_credentials_grant_access_token_lifespan": null, + "implicit_grant_access_token_lifespan": null, + "implicit_grant_id_token_lifespan": null, + "jwt_bearer_grant_access_token_lifespan": null, + "refresh_token_grant_id_token_lifespan": null, + "refresh_token_grant_access_token_lifespan": null, + "refresh_token_grant_refresh_token_lifespan": null + }, "granted_scopes": [ "offline", "openid", diff --git a/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=jwt-case=2-description=should_pass_because_prompt=none_and_max_age_is_less_than_auth_time-should_call_refresh_token_hook_if_configured-hook=new.json b/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=jwt-case=2-description=should_pass_because_prompt=none_and_max_age_is_less_than_auth_time-should_call_refresh_token_hook_if_configured-hook=new.json index 3748c3744f1..449ad8f05fd 100644 --- a/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=jwt-case=2-description=should_pass_because_prompt=none_and_max_age_is_less_than_auth_time-should_call_refresh_token_hook_if_configured-hook=new.json +++ b/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=jwt-case=2-description=should_pass_because_prompt=none_and_max_age_is_less_than_auth_time-should_call_refresh_token_hook_if_configured-hook=new.json @@ -35,6 +35,49 @@ }, "request": { "client_id": "app-client", + "client": { + "client_id": "app-client", + "client_name": "", + "grant_types": [ + "implicit", + "refresh_token", + "authorization_code", + "password", + "client_credentials" + ], + "response_types": [ + "id_token", + "code", + "token" + ], + "scope": "hydra.* offline openid", + "audience": [], + "owner": "", + "policy_uri": "", + "allowed_cors_origins": [], + "tos_uri": "", + "client_uri": "", + "logo_uri": "", + "contacts": [], + "client_secret_expires_at": 0, + "subject_type": "", + "jwks": {}, + "metadata": { + "some-meta-key": "some-meta-value" + }, + "skip_consent": false, + "skip_logout_consent": false, + "authorization_code_grant_access_token_lifespan": null, + "authorization_code_grant_id_token_lifespan": null, + "authorization_code_grant_refresh_token_lifespan": null, + "client_credentials_grant_access_token_lifespan": null, + "implicit_grant_access_token_lifespan": null, + "implicit_grant_id_token_lifespan": null, + "jwt_bearer_grant_access_token_lifespan": null, + "refresh_token_grant_id_token_lifespan": null, + "refresh_token_grant_access_token_lifespan": null, + "refresh_token_grant_refresh_token_lifespan": null + }, "granted_scopes": [ "offline", "openid", diff --git a/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=jwt-case=5-description=should_pass_with_prompt=login_when_authentication_time_is_recent-should_call_refresh_token_hook_if_configured-hook=new.json b/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=jwt-case=5-description=should_pass_with_prompt=login_when_authentication_time_is_recent-should_call_refresh_token_hook_if_configured-hook=new.json index 3748c3744f1..449ad8f05fd 100644 --- a/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=jwt-case=5-description=should_pass_with_prompt=login_when_authentication_time_is_recent-should_call_refresh_token_hook_if_configured-hook=new.json +++ b/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=jwt-case=5-description=should_pass_with_prompt=login_when_authentication_time_is_recent-should_call_refresh_token_hook_if_configured-hook=new.json @@ -35,6 +35,49 @@ }, "request": { "client_id": "app-client", + "client": { + "client_id": "app-client", + "client_name": "", + "grant_types": [ + "implicit", + "refresh_token", + "authorization_code", + "password", + "client_credentials" + ], + "response_types": [ + "id_token", + "code", + "token" + ], + "scope": "hydra.* offline openid", + "audience": [], + "owner": "", + "policy_uri": "", + "allowed_cors_origins": [], + "tos_uri": "", + "client_uri": "", + "logo_uri": "", + "contacts": [], + "client_secret_expires_at": 0, + "subject_type": "", + "jwks": {}, + "metadata": { + "some-meta-key": "some-meta-value" + }, + "skip_consent": false, + "skip_logout_consent": false, + "authorization_code_grant_access_token_lifespan": null, + "authorization_code_grant_id_token_lifespan": null, + "authorization_code_grant_refresh_token_lifespan": null, + "client_credentials_grant_access_token_lifespan": null, + "implicit_grant_access_token_lifespan": null, + "implicit_grant_id_token_lifespan": null, + "jwt_bearer_grant_access_token_lifespan": null, + "refresh_token_grant_id_token_lifespan": null, + "refresh_token_grant_access_token_lifespan": null, + "refresh_token_grant_refresh_token_lifespan": null + }, "granted_scopes": [ "offline", "openid", diff --git a/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=opaque-case=0-description=should_pass_request_if_strategy_passes-should_call_refresh_token_hook_if_configured-hook=new.json b/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=opaque-case=0-description=should_pass_request_if_strategy_passes-should_call_refresh_token_hook_if_configured-hook=new.json index 3748c3744f1..449ad8f05fd 100644 --- a/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=opaque-case=0-description=should_pass_request_if_strategy_passes-should_call_refresh_token_hook_if_configured-hook=new.json +++ b/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=opaque-case=0-description=should_pass_request_if_strategy_passes-should_call_refresh_token_hook_if_configured-hook=new.json @@ -35,6 +35,49 @@ }, "request": { "client_id": "app-client", + "client": { + "client_id": "app-client", + "client_name": "", + "grant_types": [ + "implicit", + "refresh_token", + "authorization_code", + "password", + "client_credentials" + ], + "response_types": [ + "id_token", + "code", + "token" + ], + "scope": "hydra.* offline openid", + "audience": [], + "owner": "", + "policy_uri": "", + "allowed_cors_origins": [], + "tos_uri": "", + "client_uri": "", + "logo_uri": "", + "contacts": [], + "client_secret_expires_at": 0, + "subject_type": "", + "jwks": {}, + "metadata": { + "some-meta-key": "some-meta-value" + }, + "skip_consent": false, + "skip_logout_consent": false, + "authorization_code_grant_access_token_lifespan": null, + "authorization_code_grant_id_token_lifespan": null, + "authorization_code_grant_refresh_token_lifespan": null, + "client_credentials_grant_access_token_lifespan": null, + "implicit_grant_access_token_lifespan": null, + "implicit_grant_id_token_lifespan": null, + "jwt_bearer_grant_access_token_lifespan": null, + "refresh_token_grant_id_token_lifespan": null, + "refresh_token_grant_access_token_lifespan": null, + "refresh_token_grant_refresh_token_lifespan": null + }, "granted_scopes": [ "offline", "openid", diff --git a/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=opaque-case=2-description=should_pass_because_prompt=none_and_max_age_is_less_than_auth_time-should_call_refresh_token_hook_if_configured-hook=new.json b/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=opaque-case=2-description=should_pass_because_prompt=none_and_max_age_is_less_than_auth_time-should_call_refresh_token_hook_if_configured-hook=new.json index 3748c3744f1..449ad8f05fd 100644 --- a/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=opaque-case=2-description=should_pass_because_prompt=none_and_max_age_is_less_than_auth_time-should_call_refresh_token_hook_if_configured-hook=new.json +++ b/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=opaque-case=2-description=should_pass_because_prompt=none_and_max_age_is_less_than_auth_time-should_call_refresh_token_hook_if_configured-hook=new.json @@ -35,6 +35,49 @@ }, "request": { "client_id": "app-client", + "client": { + "client_id": "app-client", + "client_name": "", + "grant_types": [ + "implicit", + "refresh_token", + "authorization_code", + "password", + "client_credentials" + ], + "response_types": [ + "id_token", + "code", + "token" + ], + "scope": "hydra.* offline openid", + "audience": [], + "owner": "", + "policy_uri": "", + "allowed_cors_origins": [], + "tos_uri": "", + "client_uri": "", + "logo_uri": "", + "contacts": [], + "client_secret_expires_at": 0, + "subject_type": "", + "jwks": {}, + "metadata": { + "some-meta-key": "some-meta-value" + }, + "skip_consent": false, + "skip_logout_consent": false, + "authorization_code_grant_access_token_lifespan": null, + "authorization_code_grant_id_token_lifespan": null, + "authorization_code_grant_refresh_token_lifespan": null, + "client_credentials_grant_access_token_lifespan": null, + "implicit_grant_access_token_lifespan": null, + "implicit_grant_id_token_lifespan": null, + "jwt_bearer_grant_access_token_lifespan": null, + "refresh_token_grant_id_token_lifespan": null, + "refresh_token_grant_access_token_lifespan": null, + "refresh_token_grant_refresh_token_lifespan": null + }, "granted_scopes": [ "offline", "openid", diff --git a/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=opaque-case=5-description=should_pass_with_prompt=login_when_authentication_time_is_recent-should_call_refresh_token_hook_if_configured-hook=new.json b/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=opaque-case=5-description=should_pass_with_prompt=login_when_authentication_time_is_recent-should_call_refresh_token_hook_if_configured-hook=new.json index 3748c3744f1..449ad8f05fd 100644 --- a/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=opaque-case=5-description=should_pass_with_prompt=login_when_authentication_time_is_recent-should_call_refresh_token_hook_if_configured-hook=new.json +++ b/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=opaque-case=5-description=should_pass_with_prompt=login_when_authentication_time_is_recent-should_call_refresh_token_hook_if_configured-hook=new.json @@ -35,6 +35,49 @@ }, "request": { "client_id": "app-client", + "client": { + "client_id": "app-client", + "client_name": "", + "grant_types": [ + "implicit", + "refresh_token", + "authorization_code", + "password", + "client_credentials" + ], + "response_types": [ + "id_token", + "code", + "token" + ], + "scope": "hydra.* offline openid", + "audience": [], + "owner": "", + "policy_uri": "", + "allowed_cors_origins": [], + "tos_uri": "", + "client_uri": "", + "logo_uri": "", + "contacts": [], + "client_secret_expires_at": 0, + "subject_type": "", + "jwks": {}, + "metadata": { + "some-meta-key": "some-meta-value" + }, + "skip_consent": false, + "skip_logout_consent": false, + "authorization_code_grant_access_token_lifespan": null, + "authorization_code_grant_id_token_lifespan": null, + "authorization_code_grant_refresh_token_lifespan": null, + "client_credentials_grant_access_token_lifespan": null, + "implicit_grant_access_token_lifespan": null, + "implicit_grant_id_token_lifespan": null, + "jwt_bearer_grant_access_token_lifespan": null, + "refresh_token_grant_id_token_lifespan": null, + "refresh_token_grant_access_token_lifespan": null, + "refresh_token_grant_refresh_token_lifespan": null + }, "granted_scopes": [ "offline", "openid", diff --git a/oauth2/oauth2_auth_code_test.go b/oauth2/oauth2_auth_code_test.go index 9ea6e8f6a10..c1a0f8731e7 100644 --- a/oauth2/oauth2_auth_code_test.go +++ b/oauth2/oauth2_auth_code_test.go @@ -20,6 +20,8 @@ import ( "testing" "time" + "github.com/ory/x/sqlxx" + "github.com/go-jose/go-jose/v3" "github.com/golang-jwt/jwt/v5" "github.com/julienschmidt/httprouter" @@ -1329,12 +1331,14 @@ func TestAuthCodeWithMockStrategy(t *testing.T) { var mutex sync.Mutex require.NoError(t, reg.ClientManager().CreateClient(context.TODO(), &client.Client{ - ID: "app-client", - Secret: "secret", - RedirectURIs: []string{ts.URL + "/callback"}, - ResponseTypes: []string{"id_token", "code", "token"}, - GrantTypes: []string{"implicit", "refresh_token", "authorization_code", "password", "client_credentials"}, - Scope: "hydra.* offline openid", + ID: "app-client", + Secret: "secret", + RedirectURIs: []string{ts.URL + "/callback"}, + ResponseTypes: []string{"id_token", "code", "token"}, + GrantTypes: []string{"implicit", "refresh_token", "authorization_code", "password", "client_credentials"}, + Scope: "hydra.* offline openid", + Metadata: sqlxx.JSONRawMessage(`{"some-meta-key":"some-meta-value"}`), + SkipLogoutConsent: sqlxx.NullBool{Bool: false, Valid: true}, })) oauthConfig := &oauth2.Config{ @@ -1640,6 +1644,11 @@ func TestAuthCodeWithMockStrategy(t *testing.T) { "session.id_token.id_token_claims.exp", "session.id_token.id_token_claims.rat", "session.id_token.id_token_claims.auth_time", + "request.client.nid", + "request.client.redirect_uris", + "request.client.client_secret", + "request.client.created_at", + "request.client.updated_at", } if hookType == "legacy" { diff --git a/oauth2/token_hook.go b/oauth2/token_hook.go index d32cadd7e4d..981d40f45dd 100644 --- a/oauth2/token_hook.go +++ b/oauth2/token_hook.go @@ -9,9 +9,10 @@ import ( "encoding/json" "net/http" + "github.com/hashicorp/go-retryablehttp" "github.com/pkg/errors" - "github.com/hashicorp/go-retryablehttp" + "github.com/ory/hydra/v2/client" "github.com/ory/hydra/v2/flow" "github.com/ory/hydra/v2/x" @@ -29,7 +30,8 @@ type AccessRequestHook func(ctx context.Context, requester fosite.AccessRequeste // swagger:ignore type Request struct { // ClientID is the identifier of the OAuth 2.0 client. - ClientID string `json:"client_id"` + ClientID string `json:"client_id"` + Client *client.Client `json:"client"` // GrantedScopes is the list of scopes granted to the OAuth 2.0 client. GrantedScopes []string `json:"granted_scopes"` // GrantedAudience is the list of audiences granted to the OAuth 2.0 client. @@ -166,8 +168,14 @@ func TokenHook(reg interface { return nil } + var oauthClient *client.Client + if hydraClient, ok := requester.GetClient().(*client.Client); ok { + oauthClient = client.GetSanitizedCopy(hydraClient) + } + request := Request{ ClientID: requester.GetClient().GetID(), + Client: oauthClient, GrantedScopes: requester.GetGrantedScopes(), GrantedAudience: requester.GetGrantedAudience(), GrantTypes: requester.GetGrantTypes(),