Skip to content
Open
42 changes: 39 additions & 3 deletions oauth2/oauth2_auth_code_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ func acceptLoginHandler(t *testing.T, c *client.Client, adminClient *hydra.APICl
assert.EqualValues(t, c.LogoURI, pointerx.Deref(rr.Client.LogoUri))
assert.EqualValues(t, c.RedirectURIs, rr.Client.RedirectUris)
assert.EqualValues(t, r.URL.Query().Get("login_challenge"), rr.Challenge)
assert.EqualValues(t, []string{"hydra", "offline", "openid"}, rr.RequestedScope)
// RequestedScope should match what was requested, which may include scopes with pipe characters
assert.NotEmpty(t, rr.RequestedScope)
assert.Contains(t, rr.RequestUrl, reg.Config().OAuth2AuthURL(ctx).String())

acceptBody := hydra.AcceptOAuth2LoginRequest{
Expand Down Expand Up @@ -125,12 +126,12 @@ func acceptConsentHandler(t *testing.T, c *client.Client, adminClient *hydra.API
assert.EqualValues(t, c.LogoURI, pointerx.Deref(rr.Client.LogoUri))
assert.EqualValues(t, c.RedirectURIs, rr.Client.RedirectUris)
assert.EqualValues(t, subject, pointerx.Deref(rr.Subject))
assert.EqualValues(t, []string{"hydra", "offline", "openid"}, rr.RequestedScope)
assert.NotEmpty(t, rr.RequestedScope)
assert.Contains(t, *rr.RequestUrl, reg.Config().OAuth2AuthURL(r.Context()).String())
assert.Equal(t, map[string]interface{}{"context": "bar"}, rr.Context)

acceptBody := hydra.AcceptOAuth2ConsentRequest{
GrantScope: []string{"hydra", "offline", "openid"},
GrantScope: rr.RequestedScope,
GrantAccessTokenAudience: rr.RequestedAccessTokenAudience,
Remember: pointerx.Ptr(true),
RememberFor: pointerx.Ptr[int64](0),
Expand Down Expand Up @@ -750,6 +751,41 @@ func TestAuthCodeWithDefaultStrategy(t *testing.T) {
assertIDToken(t, token, conf, subject, nonce, time.Now().Add(reg.Config().GetIDTokenLifespan(ctx)))
})

t.Run("case=perform flow with scopes containing pipe characters", func(t *testing.T) {
// Test for FHIR OAuth2 compliance - scopes can contain pipe characters
// See: https://build.fhir.org/ig/HL7/smart-app-launch/scopes-and-launch-context.html
scopeWithPipe := "openid profile patient|read patient|write"
scopeParts := []string{"openid", "profile", "patient|read", "patient|write"}

c, conf := newOAuth2Client(
t,
reg,
testhelpers.NewCallbackURL(t, "callback", testhelpers.HTTPServerNotImplementedHandler),
withScope(scopeWithPipe),
)

testhelpers.NewLoginConsentUI(t, reg.Config(),
acceptLoginHandler(t, c, adminClient, reg, subject, nil),
acceptConsentHandler(t, c, adminClient, reg, subject, nil),
)

code, _ := getAuthorizeCode(t, conf, nil, oauth2.SetAuthURLParam("nonce", nonce))
require.NotEmpty(t, code)

token, err := conf.Exchange(context.Background(), code)
require.NoError(t, err)

// Verify scopes are preserved in token introspection
introspect := testhelpers.IntrospectToken(t, token.AccessToken, adminTS)
assert.True(t, introspect.Get("active").Bool(), "%s", introspect)

// Verify that the scope field contains the correct space-separated scopes with pipe characters preserved
scopes := introspect.Get("scope").String()
assert.NotEmpty(t, scopes)
actualScopes := strings.Split(scopes, " ")
assert.ElementsMatch(t, scopeParts, actualScopes, "Scopes should be preserved as space-separated with pipe characters intact. Expected: %v, got: %v", scopeParts, actualScopes)
})

t.Run("case=respects client token lifespan configuration", func(t *testing.T) {
run := func(t *testing.T, strategy string, c *client.Client, conf *oauth2.Config, expectedLifespans client.Lifespans) {
testhelpers.NewLoginConsentUI(t, reg.Config(),
Expand Down
28 changes: 13 additions & 15 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 8 additions & 8 deletions persistence/sql/persister_device.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,10 @@ func (r *DeviceRequestSQL) toRequest(ctx context.Context, session fosite.Session
RequestedAt: r.RequestedAt,
// ExpiresAt does not need to be populated as we get the expiry time from the session.
Client: c,
RequestedScope: stringsx.Splitx(r.Scopes, "|"),
GrantedScope: stringsx.Splitx(r.GrantedScope, "|"),
RequestedAudience: stringsx.Splitx(r.RequestedAudience, "|"),
GrantedAudience: stringsx.Splitx(r.GrantedAudience, "|"),
RequestedScope: stringsx.Splitx(r.Scopes, " "),
GrantedScope: stringsx.Splitx(r.GrantedScope, " "),
RequestedAudience: stringsx.Splitx(r.RequestedAudience, " "),
GrantedAudience: stringsx.Splitx(r.GrantedAudience, " "),
Form: val,
Session: session,
},
Expand Down Expand Up @@ -140,10 +140,10 @@ func (p *Persister) sqlDeviceSchemaFromRequest(ctx context.Context, deviceCodeSi
RequestedAt: r.GetRequestedAt(),
InternalExpiresAt: sqlxx.NullTime(expiresAt),
Client: r.GetClient().GetID(),
Scopes: strings.Join(r.GetRequestedScopes(), "|"),
GrantedScope: strings.Join(r.GetGrantedScopes(), "|"),
GrantedAudience: strings.Join(r.GetGrantedAudience(), "|"),
RequestedAudience: strings.Join(r.GetRequestedAudience(), "|"),
Scopes: strings.Join(r.GetRequestedScopes(), " "),
GrantedScope: strings.Join(r.GetGrantedScopes(), " "),
GrantedAudience: strings.Join(r.GetGrantedAudience(), " "),
RequestedAudience: strings.Join(r.GetRequestedAudience(), " "),
Form: r.GetRequestForm().Encode(),
Session: session,
Subject: subject,
Expand Down
16 changes: 8 additions & 8 deletions persistence/sql/persister_oauth2.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,10 @@ func (r *OAuth2RequestSQL) toRequest(ctx context.Context, session fosite.Session
RequestedAt: r.RequestedAt,
// ExpiresAt does not need to be populated as we get the expiry time from the session.
Client: c,
RequestedScope: stringsx.Splitx(r.Scopes, "|"),
GrantedScope: stringsx.Splitx(r.GrantedScope, "|"),
RequestedAudience: stringsx.Splitx(r.RequestedAudience, "|"),
GrantedAudience: stringsx.Splitx(r.GrantedAudience, "|"),
RequestedScope: stringsx.Splitx(r.Scopes, " "),
GrantedScope: stringsx.Splitx(r.GrantedScope, " "),
RequestedAudience: stringsx.Splitx(r.RequestedAudience, " "),
GrantedAudience: stringsx.Splitx(r.GrantedAudience, " "),
Form: val,
Session: session,
}, nil
Expand Down Expand Up @@ -283,10 +283,10 @@ func (p *Persister) sqlSchemaFromRequest(ctx context.Context, signature string,
RequestedAt: r.GetRequestedAt(),
InternalExpiresAt: sqlxx.NullTime(expiresAt),
Client: r.GetClient().GetID(),
Scopes: strings.Join(r.GetRequestedScopes(), "|"),
GrantedScope: strings.Join(r.GetGrantedScopes(), "|"),
GrantedAudience: strings.Join(r.GetGrantedAudience(), "|"),
RequestedAudience: strings.Join(r.GetRequestedAudience(), "|"),
Scopes: strings.Join(r.GetRequestedScopes(), " "),
GrantedScope: strings.Join(r.GetGrantedScopes(), " "),
GrantedAudience: strings.Join(r.GetGrantedAudience(), " "),
RequestedAudience: strings.Join(r.GetRequestedAudience(), " "),
Form: r.GetRequestForm().Encode(),
Session: session,
Subject: subject,
Expand Down
Loading