Skip to content

Commit 7f342c5

Browse files
committed
feat(token_hook): filter out sensitive params instead of only allowing certain params
feat(token_hook): pass client metadata in hook payload
1 parent d19e847 commit 7f342c5

11 files changed

+88
-7
lines changed

oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=jwt-case=0-description=should_pass_request_if_strategy_passes-should_call_refresh_token_hook_if_configured-hook=new.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
},
3636
"request": {
3737
"client_id": "app-client",
38+
"client_metadata": {},
3839
"granted_scopes": [
3940
"offline",
4041
"openid",

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
},
3636
"request": {
3737
"client_id": "app-client",
38+
"client_metadata": {},
3839
"granted_scopes": [
3940
"offline",
4041
"openid",

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
},
3636
"request": {
3737
"client_id": "app-client",
38+
"client_metadata": {},
3839
"granted_scopes": [
3940
"offline",
4041
"openid",

oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=opaque-case=0-description=should_pass_request_if_strategy_passes-should_call_refresh_token_hook_if_configured-hook=new.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
},
3636
"request": {
3737
"client_id": "app-client",
38+
"client_metadata": {},
3839
"granted_scopes": [
3940
"offline",
4041
"openid",

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
},
3636
"request": {
3737
"client_id": "app-client",
38+
"client_metadata": {},
3839
"granted_scopes": [
3940
"offline",
4041
"openid",

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
},
3636
"request": {
3737
"client_id": "app-client",
38+
"client_metadata": {},
3839
"granted_scopes": [
3940
"offline",
4041
"openid",

oauth2/handler.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ const (
6363
DeleteTokensPath = "/oauth2/tokens" // #nosec G101
6464
)
6565

66+
func GrantTypesSupported() []string {
67+
return []string{"authorization_code", "implicit", "client_credentials", "refresh_token"}
68+
}
69+
6670
type Handler struct {
6771
r InternalRegistry
6872
c *config.DefaultProvider
@@ -496,7 +500,7 @@ func (h *Handler) discoverOidcConfiguration(w http.ResponseWriter, r *http.Reque
496500
IDTokenSigningAlgValuesSupported: []string{key.Algorithm},
497501
IDTokenSignedResponseAlg: []string{key.Algorithm},
498502
UserinfoSignedResponseAlg: []string{key.Algorithm},
499-
GrantTypesSupported: []string{"authorization_code", "implicit", "client_credentials", "refresh_token"},
503+
GrantTypesSupported: GrantTypesSupported(),
500504
ResponseModesSupported: []string{"query", "fragment"},
501505
UserinfoSigningAlgValuesSupported: []string{"none", key.Algorithm},
502506
RequestParameterSupported: true,

oauth2/oauth2_auth_code_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1347,6 +1347,7 @@ func TestAuthCodeWithMockStrategy(t *testing.T) {
13471347
RedirectURL: ts.URL + "/callback",
13481348
Scopes: []string{"hydra.*", "offline", "openid"},
13491349
}
1350+
clientMetadata := json.RawMessage("{}")
13501351

13511352
var code string
13521353
for k, tc := range []struct {
@@ -1665,6 +1666,7 @@ func TestAuthCodeWithMockStrategy(t *testing.T) {
16651666
require.Equal(t, hookReq.Session.ClientID, oauthConfig.ClientID)
16661667
require.NotEmpty(t, hookReq.Request)
16671668
require.Equal(t, hookReq.Request.ClientID, oauthConfig.ClientID)
1669+
require.Equal(t, hookReq.Request.ClientMetadata, clientMetadata)
16681670
require.ElementsMatch(t, hookReq.Request.GrantedScopes, expectedGrantedScopes)
16691671
require.ElementsMatch(t, hookReq.Request.GrantedAudience, []string{})
16701672
require.Equal(t, hookReq.Request.Payload, map[string][]string{"grant_type": {"refresh_token"}})

oauth2/oauth2_client_credentials_test.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -264,14 +264,15 @@ func TestClientCredentials(t *testing.T) {
264264
var hookReq hydraoauth2.TokenHookRequest
265265
require.NoError(t, json.NewDecoder(r.Body).Decode(&hookReq))
266266
require.NotEmpty(t, hookReq.Session)
267-
require.Equal(t, hookReq.Session.Extra, map[string]interface{}{})
267+
require.Equal(t, map[string]interface{}{}, hookReq.Session.Extra)
268268
require.NotEmpty(t, hookReq.Request)
269269
require.ElementsMatch(t, hookReq.Request.GrantedScopes, expectedGrantedScopes)
270270
require.ElementsMatch(t, hookReq.Request.GrantedAudience, expectedGrantedAudience)
271-
require.Equal(t, hookReq.Request.Payload, map[string][]string{
271+
require.Equal(t, map[string][]string{
272+
"audience": {"https://api.ory.sh/"},
272273
"grant_type": {"client_credentials"},
273274
"scope": {"foobar"},
274-
})
275+
}, hookReq.Request.Payload)
275276

276277
claims := map[string]interface{}{
277278
"hooked": true,

oauth2/token_hook.go

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,15 @@ import (
77
"bytes"
88
"context"
99
"encoding/json"
10+
"fmt"
11+
"maps"
1012
"net/http"
13+
"slices"
1114

15+
"github.com/hashicorp/go-retryablehttp"
1216
"github.com/pkg/errors"
1317

14-
"github.com/hashicorp/go-retryablehttp"
18+
"github.com/ory/hydra/v2/client"
1519

1620
"github.com/ory/hydra/v2/flow"
1721
"github.com/ory/hydra/v2/x"
@@ -29,7 +33,8 @@ type AccessRequestHook func(ctx context.Context, requester fosite.AccessRequeste
2933
// swagger:ignore
3034
type Request struct {
3135
// ClientID is the identifier of the OAuth 2.0 client.
32-
ClientID string `json:"client_id"`
36+
ClientID string `json:"client_id"`
37+
ClientMetadata json.RawMessage `json:"client_metadata"`
3338
// GrantedScopes is the list of scopes granted to the OAuth 2.0 client.
3439
GrantedScopes []string `json:"granted_scopes"`
3540
// GrantedAudience is the list of audiences granted to the OAuth 2.0 client.
@@ -150,6 +155,31 @@ func executeHookAndUpdateSession(ctx context.Context, reg x.HTTPClientProvider,
150155
return nil
151156
}
152157

158+
func sensitiveAccessRequestParametersForGrantTypes(grantTypes ...string) []string {
159+
var parameters []string
160+
161+
for i := range grantTypes {
162+
switch fosite.GrantType(grantTypes[i]) {
163+
case fosite.GrantTypeClientCredentials:
164+
parameters = append(parameters, "client_secret")
165+
case fosite.GrantTypeAuthorizationCode:
166+
parameters = append(parameters, "code", "code_verifier")
167+
case fosite.GrantTypeRefreshToken:
168+
parameters = append(parameters, "refresh_token")
169+
case fosite.GrantTypePassword:
170+
parameters = append(parameters, "password")
171+
case fosite.GrantTypeImplicit:
172+
// Implicit grant doesn't utilize the token endpoint.
173+
case fosite.GrantTypeJWTBearer:
174+
// Assertion isn't considered sensitive.
175+
default:
176+
panic(fmt.Sprintf("Can't execute hook for grant type %s without sensitive parameters configured. This is a hydra bug.", grantTypes[i]))
177+
}
178+
}
179+
180+
return parameters
181+
}
182+
153183
// TokenHook is an AccessRequestHook called for all grant types.
154184
func TokenHook(reg interface {
155185
config.Provider
@@ -166,12 +196,28 @@ func TokenHook(reg interface {
166196
return nil
167197
}
168198

199+
var clientMeta json.RawMessage
200+
if hydraClient, ok := requester.GetClient().(*client.Client); ok {
201+
clientMeta = json.RawMessage(hydraClient.Metadata)
202+
} else {
203+
clientMeta = []byte("null")
204+
}
205+
206+
// Always include client authentication parameters, and any sensitive parameters for the requested grant types.
207+
sensitiveParameters := append([]string{"client_secret", "client_assertion"}, sensitiveAccessRequestParametersForGrantTypes(requester.GetGrantTypes()...)...)
208+
209+
payload := maps.Clone(requester.GetRequestForm())
210+
// Delete all sensitive parameters from the access request form before sending it to the hook.
211+
maps.DeleteFunc(payload, func(s string, _ []string) bool {
212+
return slices.Contains(sensitiveParameters, s)
213+
})
169214
request := Request{
170215
ClientID: requester.GetClient().GetID(),
216+
ClientMetadata: clientMeta,
171217
GrantedScopes: requester.GetGrantedScopes(),
172218
GrantedAudience: requester.GetGrantedAudience(),
173219
GrantTypes: requester.GetGrantTypes(),
174-
Payload: requester.Sanitize([]string{"assertion"}).GetRequestForm(),
220+
Payload: payload,
175221
}
176222

177223
reqBody := TokenHookRequest{

oauth2/token_hook_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Copyright © 2024 Ory Corp
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package oauth2
5+
6+
import (
7+
"fmt"
8+
"testing"
9+
10+
"github.com/stretchr/testify/assert"
11+
)
12+
13+
func Test_sensitiveAccessRequestParametersForGrantTypes_is_exhaustive(t *testing.T) {
14+
for _, grantType := range GrantTypesSupported() {
15+
grantType := grantType
16+
t.Run(fmt.Sprintf("grant type %s handled", grantType), func(t *testing.T) {
17+
assert.NotPanics(t, func() {
18+
_ = sensitiveAccessRequestParametersForGrantTypes(grantType)
19+
})
20+
})
21+
}
22+
}

0 commit comments

Comments
 (0)