mirror of https://github.com/ory/hydra
204 lines
9.1 KiB
Go
204 lines
9.1 KiB
Go
// Copyright © 2025 Ory Corp
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package oauth2_test
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/go-jose/go-jose/v3"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/tidwall/gjson"
|
|
"golang.org/x/oauth2"
|
|
|
|
hydra "github.com/ory/hydra-client-go/v2"
|
|
"github.com/ory/hydra/v2/client"
|
|
"github.com/ory/hydra/v2/driver"
|
|
"github.com/ory/hydra/v2/driver/config"
|
|
"github.com/ory/hydra/v2/internal/testhelpers"
|
|
"github.com/ory/hydra/v2/jwk"
|
|
"github.com/ory/hydra/v2/x"
|
|
"github.com/ory/x/configx"
|
|
"github.com/ory/x/pointerx"
|
|
"github.com/ory/x/snapshotx"
|
|
"github.com/ory/x/uuidx"
|
|
)
|
|
|
|
func TestAuthCodeFlowE2E(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
reg := testhelpers.NewRegistryMemory(t, driver.WithConfigOptions(configx.WithValues(map[string]any{
|
|
config.KeyAccessTokenStrategy: "opaque",
|
|
config.KeyRefreshTokenHook: "",
|
|
config.KeyLoginURL: x.LoginURL,
|
|
config.KeyConsentURL: x.ConsentURL,
|
|
config.KeyAccessTokenLifespan: 10 * time.Minute, // allow to debug
|
|
config.KeyRefreshTokenLifespan: 20 * time.Minute, // allow to debug
|
|
config.KeyScopeStrategy: "exact",
|
|
config.KeyIssuerURL: "https://hydra.ory",
|
|
})))
|
|
|
|
jwk.EnsureAsymmetricKeypairExists(t, reg, string(jose.ES256), x.OpenIDConnectKeyName)
|
|
jwk.EnsureAsymmetricKeypairExists(t, reg, string(jose.ES256), x.OAuth2JWTKeyName)
|
|
|
|
publicTS, adminTS := testhelpers.NewConfigurableOAuth2Server(t.Context(), t, reg)
|
|
publicClient := hydra.NewAPIClient(hydra.NewConfiguration())
|
|
publicClient.GetConfig().Servers = hydra.ServerConfigurations{{URL: publicTS.URL}}
|
|
adminClient := hydra.NewAPIClient(hydra.NewConfiguration())
|
|
adminClient.GetConfig().Servers = hydra.ServerConfigurations{{URL: adminTS.URL}}
|
|
|
|
t.Run("auth code flow", func(t *testing.T) {
|
|
t.Run("rejects invalid audience", func(t *testing.T) {
|
|
cl := x.NewEmptyJarClient(t)
|
|
cl.CheckRedirect = func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }
|
|
_, conf := newOAuth2Client(t, reg, x.ClientCallbackURL)
|
|
loc := x.GetExpectRedirect(t, cl, conf.AuthCodeURL(uuidx.NewV4().String(), oauth2.SetAuthURLParam("audience", "invalid-audience")))
|
|
require.Equal(t, x.ClientCallbackURL, fmt.Sprintf("%s://%s%s", loc.Scheme, loc.Host, loc.Path))
|
|
assert.Equal(t, "invalid_request", loc.Query().Get("error"))
|
|
assert.Contains(t, loc.Query().Get("error_description"), "Requested audience 'invalid-audience' has not been whitelisted by the OAuth 2.0 Client.")
|
|
})
|
|
|
|
for _, accessTokenStrategy := range []string{"opaque", "jwt"} {
|
|
t.Run("strategy="+accessTokenStrategy, func(t *testing.T) {
|
|
cl, conf := newOAuth2Client(t, reg, x.ClientCallbackURL, func(c *client.Client) {
|
|
c.AccessTokenStrategy = accessTokenStrategy
|
|
c.Audience = []string{"audience-1", "audience-2"}
|
|
c.ID = "64f78bf1-f388-4eeb-9fee-e7207226c6be-" + accessTokenStrategy
|
|
})
|
|
sub := "c6a8ee1c-e0c4-404c-bba7-6a5b8702a2e9"
|
|
|
|
t.Run("access and id tokens with extra claims", func(t *testing.T) {
|
|
token := x.PerformAuthCodeFlow(t.Context(), t, nil, conf, adminClient, func(t *testing.T, req *hydra.OAuth2LoginRequest) hydra.AcceptOAuth2LoginRequest {
|
|
snapshotx.SnapshotT(t, req,
|
|
snapshotx.ExceptPaths("challenge", "client.created_at", "client.updated_at", "session_id", "request_url"),
|
|
snapshotx.WithName("login_request"))
|
|
return hydra.AcceptOAuth2LoginRequest{
|
|
Amr: []string{"amr1", "amr2"},
|
|
Acr: pointerx.Ptr("acr-value"),
|
|
Subject: sub,
|
|
}
|
|
}, func(t *testing.T, req *hydra.OAuth2ConsentRequest) hydra.AcceptOAuth2ConsentRequest {
|
|
snapshotx.SnapshotT(t, req,
|
|
snapshotx.ExceptPaths("challenge", "client.created_at", "client.updated_at", "consent_request_id", "login_challenge", "login_session_id", "request_url"),
|
|
snapshotx.WithName("consent_request"))
|
|
return hydra.AcceptOAuth2ConsentRequest{
|
|
GrantScope: []string{"openid"},
|
|
Session: &hydra.AcceptOAuth2ConsentRequestSession{
|
|
AccessToken: map[string]any{"key_access": "extra access token value"},
|
|
IdToken: map[string]any{"key_id": "extra id token value"},
|
|
},
|
|
}
|
|
})
|
|
|
|
// check access token
|
|
introspected := testhelpers.IntrospectToken(t, token.AccessToken, adminTS)
|
|
require.True(t, introspected.Get("active").Bool())
|
|
testhelpers.AssertAccessToken(t, introspected, sub, cl.ID)
|
|
assert.Equal(t, "extra access token value", introspected.Get("ext.key_access").Str)
|
|
|
|
if accessTokenStrategy == "jwt" {
|
|
dec := gjson.ParseBytes(testhelpers.InsecureDecodeJWT(t, token.AccessToken))
|
|
testhelpers.AssertAccessToken(t, dec, sub, cl.ID)
|
|
assert.Equal(t, "extra access token value", dec.Get("ext.key_access").Str)
|
|
} else {
|
|
assert.Len(t, strings.Split(token.AccessToken, "."), 2)
|
|
}
|
|
|
|
idToken := testhelpers.DecodeIDToken(t, token)
|
|
testhelpers.AssertIDToken(t, idToken, sub, cl.ID)
|
|
assert.Equal(t, "extra id token value", idToken.Get("key_id").Str)
|
|
assert.JSONEq(t, `["amr1", "amr2"]`, idToken.Get("amr").Raw)
|
|
assert.Equal(t, "acr-value", idToken.Get("acr").Str)
|
|
})
|
|
|
|
t.Run("refreshed access and id tokens with extra claims", func(t *testing.T) {
|
|
token := x.PerformAuthCodeFlow(t.Context(), t, nil, conf, adminClient, func(*testing.T, *hydra.OAuth2LoginRequest) hydra.AcceptOAuth2LoginRequest {
|
|
return hydra.AcceptOAuth2LoginRequest{Subject: sub}
|
|
}, func(*testing.T, *hydra.OAuth2ConsentRequest) hydra.AcceptOAuth2ConsentRequest {
|
|
return hydra.AcceptOAuth2ConsentRequest{
|
|
GrantScope: []string{"openid", "offline"},
|
|
Session: &hydra.AcceptOAuth2ConsentRequestSession{
|
|
AccessToken: map[string]any{"key_access": "extra access token value"},
|
|
IdToken: map[string]any{"key_id": "extra id token value"},
|
|
},
|
|
}
|
|
})
|
|
|
|
token.Expiry = time.Now().Add(-time.Hour)
|
|
refreshed, err := conf.TokenSource(t.Context(), token).Token()
|
|
require.NoError(t, err)
|
|
require.NotEqual(t, token.AccessToken, refreshed.AccessToken)
|
|
require.NotEqual(t, token.RefreshToken, refreshed.RefreshToken)
|
|
require.NotEqual(t, token.Extra("id_token"), refreshed.Extra("id_token"))
|
|
|
|
// check access token
|
|
introspected := testhelpers.IntrospectToken(t, refreshed.AccessToken, adminTS)
|
|
require.True(t, introspected.Get("active").Bool())
|
|
testhelpers.AssertAccessToken(t, introspected, sub, cl.ID)
|
|
assert.Equal(t, "extra access token value", introspected.Get("ext.key_access").Str)
|
|
|
|
if accessTokenStrategy == "jwt" {
|
|
dec := gjson.ParseBytes(testhelpers.InsecureDecodeJWT(t, refreshed.AccessToken))
|
|
testhelpers.AssertAccessToken(t, dec, sub, cl.ID)
|
|
assert.Equal(t, "extra access token value", dec.Get("ext.key_access").Str)
|
|
} else {
|
|
assert.Len(t, strings.Split(refreshed.AccessToken, "."), 2)
|
|
}
|
|
|
|
// check id token
|
|
idToken := testhelpers.DecodeIDToken(t, refreshed)
|
|
testhelpers.AssertIDToken(t, idToken, sub, cl.ID)
|
|
assert.Equal(t, "extra id token value", idToken.Get("key_id").Str)
|
|
|
|
t.Run("original tokens are invalidated", func(t *testing.T) {
|
|
introspected := testhelpers.IntrospectToken(t, token.AccessToken, adminTS)
|
|
assert.False(t, introspected.Get("active").Bool(), introspected.Raw)
|
|
introspected = testhelpers.IntrospectToken(t, token.RefreshToken, adminTS)
|
|
assert.False(t, introspected.Get("active").Bool(), introspected.Raw)
|
|
})
|
|
})
|
|
|
|
t.Run("audience is forwarded to access token", func(t *testing.T) {
|
|
token := x.PerformAuthCodeFlow(t.Context(), t, nil, conf, adminClient, func(t *testing.T, req *hydra.OAuth2LoginRequest) hydra.AcceptOAuth2LoginRequest {
|
|
assert.EqualValues(t, cl.Audience, req.RequestedAccessTokenAudience)
|
|
return hydra.AcceptOAuth2LoginRequest{Subject: sub}
|
|
}, func(t *testing.T, req *hydra.OAuth2ConsentRequest) hydra.AcceptOAuth2ConsentRequest {
|
|
assert.EqualValues(t, cl.Audience, req.RequestedAccessTokenAudience)
|
|
return hydra.AcceptOAuth2ConsentRequest{
|
|
GrantScope: []string{"openid"},
|
|
GrantAccessTokenAudience: req.RequestedAccessTokenAudience,
|
|
}
|
|
}, oauth2.SetAuthURLParam("audience", strings.Join(cl.Audience, " ")))
|
|
|
|
expectedAud, err := json.Marshal(cl.Audience)
|
|
require.NoError(t, err)
|
|
|
|
introspected := testhelpers.IntrospectToken(t, token.AccessToken, adminTS)
|
|
require.True(t, introspected.Get("active").Bool())
|
|
testhelpers.AssertAccessToken(t, introspected, sub, cl.ID)
|
|
assert.JSONEq(t, string(expectedAud), introspected.Get("aud").Raw)
|
|
|
|
if accessTokenStrategy == "jwt" {
|
|
decoded := gjson.ParseBytes(testhelpers.InsecureDecodeJWT(t, token.AccessToken))
|
|
testhelpers.AssertAccessToken(t, decoded, sub, cl.ID)
|
|
assert.JSONEq(t, string(expectedAud), decoded.Get("aud").Raw)
|
|
} else {
|
|
assert.Len(t, strings.Split(token.AccessToken, "."), 2)
|
|
}
|
|
|
|
idToken := testhelpers.DecodeIDToken(t, token)
|
|
testhelpers.AssertIDToken(t, idToken, sub, cl.ID)
|
|
require.Len(t, idToken.Get("aud").Array(), 1)
|
|
assert.Equal(t, cl.ID, idToken.Get("aud.0").Str)
|
|
})
|
|
})
|
|
}
|
|
})
|
|
}
|