Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions consent/export_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright © 2025 Ory Corp
// SPDX-License-Identifier: Apache-2.0

package consent

import (
"context"
"net/http"

"github.com/ory/hydra/v2/flow"
"github.com/ory/hydra/v2/fosite"
)

// VerifyConsentForTest exposes the unexported verifyConsent for testing.
func VerifyConsentForTest(s Strategy, ctx context.Context, r *http.Request, req fosite.AuthorizeRequester, verifier string) (*flow.Flow, error) {
return s.(*defaultStrategy).verifyConsent(ctx, nil, r, req, verifier)
}
54 changes: 54 additions & 0 deletions consent/strategy_client_binding_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright © 2025 Ory Corp
// SPDX-License-Identifier: Apache-2.0

package consent_test

import (
"net/http/httptest"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/ory/hydra/v2/client"
"github.com/ory/hydra/v2/consent"
"github.com/ory/hydra/v2/flow"
"github.com/ory/hydra/v2/fosite"
"github.com/ory/hydra/v2/internal/testhelpers"
)

// TestVerifyConsentUsesParsedClient proves that consent verification binds the
// flow to the parsed request client and not to the raw query parameter. On a
// POST /oauth2/auth the body value resolved by fosite must win over the query
// value, otherwise the two can disagree and a consent granted for one client is
// applied to a request made by another.
func TestVerifyConsentUsesParsedClient(t *testing.T) {
ctx := t.Context()
reg := testhelpers.NewRegistryMemory(t)

f := &flow.Flow{
ID: "client-binding-flow",
NID: reg.Networker().NetworkID(ctx),
Client: &client.Client{ID: "client-a"},
Subject: "subject",
RequestedAt: time.Now().UTC(),
State: flow.FlowStateConsentUnused,
}

verifier, err := f.ToConsentVerifier(ctx, reg)
require.NoError(t, err)

// Query carries the flow's own client id (client-a), but the parsed request
// resolves to a different client (client-b).
r := httptest.NewRequest("POST", "/oauth2/auth?client_id=client-a", nil)
req := fosite.NewAuthorizeRequest()
req.Client = &client.Client{ID: "client-b"}

s := consent.NewStrategy(reg)
_, err = consent.VerifyConsentForTest(s, ctx, r, req, verifier)

require.Error(t, err)
assert.ErrorIs(t, err, fosite.ErrInvalidClient)
assert.Contains(t, fosite.ErrorToRFC6749Error(err).HintField, "does not match")
}
8 changes: 4 additions & 4 deletions consent/strategy_default.go
Original file line number Diff line number Diff line change
Expand Up @@ -622,7 +622,7 @@ func (s *defaultStrategy) forwardConsentRequest(
return errors.WithStack(ErrUserRedirected)
}

func (s *defaultStrategy) verifyConsent(ctx context.Context, _ http.ResponseWriter, r *http.Request, verifier string) (_ *flow.Flow, err error) {
func (s *defaultStrategy) verifyConsent(ctx context.Context, _ http.ResponseWriter, r *http.Request, req fosite.AuthorizeRequester, verifier string) (_ *flow.Flow, err error) {
ctx, span := trace.SpanFromContext(ctx).TracerProvider().Tracer("").Start(ctx, "DefaultStrategy.verifyConsent")
defer otelx.End(span, &err)

Expand All @@ -631,7 +631,7 @@ func (s *defaultStrategy) verifyConsent(ctx context.Context, _ http.ResponseWrit
return nil, errors.WithStack(fosite.ErrAccessDenied.WithHint("The consent verifier has already been used, has not been granted, or is invalid."))
} else if err != nil {
return nil, err
} else if f.Client.GetID() != r.URL.Query().Get("client_id") {
} else if f.Client.GetID() != req.GetClient().GetID() {
return nil, errors.WithStack(fosite.ErrInvalidClient.WithHint("The flow client id does not match the authorize request client id."))
}

Expand Down Expand Up @@ -1164,7 +1164,7 @@ func (s *defaultStrategy) HandleOAuth2AuthorizationRequest(
return f, s.requestConsent(ctx, w, r, req, f)
}

f, err := s.verifyConsent(ctx, w, r, consentVerifier)
f, err := s.verifyConsent(ctx, w, r, req, consentVerifier)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -1241,7 +1241,7 @@ func (s *defaultStrategy) HandleOAuth2DeviceAuthorizationRequest(
return f, s.requestConsent(ctx, w, r, ar, f)
}

return s.verifyConsent(ctx, w, r, consentVerifier)
return s.verifyConsent(ctx, w, r, ar, consentVerifier)
}

func (s *defaultStrategy) ObfuscateSubjectIdentifier(ctx context.Context, cl fosite.Client, subject, forcedIdentifier string) (string, error) {
Expand Down
8 changes: 6 additions & 2 deletions oauth2/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -1391,11 +1391,15 @@ func (h *Handler) updateSessionWithRequest(
session *Session,
) (*Session, error) {
for _, scope := range flow.GrantedScope {
request.GrantScope(scope)
if h.r.Config().GetScopeStrategy(ctx)(request.GetClient().GetScopes(), scope) {
request.GrantScope(scope)
}
}

for _, audience := range flow.GrantedAudience {
request.GrantAudience(audience)
if fosite.DefaultAudienceMatchingStrategy(request.GetClient().GetAudience(), []string{audience}) == nil {
request.GrantAudience(audience)
}
}

openIDKeyID, err := h.r.OpenIDJWTSigner().GetPublicKeyID(ctx)
Expand Down
Loading