hydra/oauth2/e2e_test.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)
})
})
}
})
}