mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-07-16 04:03:47 +03:00
feat: add ability to skip consent for client
This commit is contained in:
@@ -18,6 +18,7 @@ type OidcClientDto struct {
|
||||
IsPublic bool `json:"isPublic"`
|
||||
PkceEnabled bool `json:"pkceEnabled"`
|
||||
RequiresPushedAuthorizationRequests bool `json:"requiresPushedAuthorizationRequests"`
|
||||
SkipConsent bool `json:"skipConsent"`
|
||||
Credentials OidcClientCredentialsDto `json:"credentials"`
|
||||
IsGroupRestricted bool `json:"isGroupRestricted"`
|
||||
}
|
||||
@@ -40,6 +41,7 @@ type OidcClientUpdateDto struct {
|
||||
PkceEnabled bool `json:"pkceEnabled"`
|
||||
RequiresReauthentication bool `json:"requiresReauthentication"`
|
||||
RequiresPushedAuthorizationRequests bool `json:"requiresPushedAuthorizationRequests"`
|
||||
SkipConsent bool `json:"skipConsent"`
|
||||
Credentials OidcClientCredentialsDto `json:"credentials"`
|
||||
LaunchURL *string `json:"launchURL" binding:"omitempty,url"`
|
||||
HasLogo bool `json:"hasLogo"`
|
||||
|
||||
@@ -32,6 +32,7 @@ type OidcClient struct {
|
||||
PkceEnabled bool `sortable:"true" filterable:"true"`
|
||||
RequiresReauthentication bool `sortable:"true" filterable:"true"`
|
||||
RequiresPushedAuthorizationRequests bool `sortable:"true" filterable:"true"`
|
||||
SkipConsent bool `sortable:"true" filterable:"true"`
|
||||
Credentials OidcClientCredentials
|
||||
LaunchURL *string
|
||||
IsGroupRestricted bool `sortable:"true" filterable:"true"`
|
||||
|
||||
@@ -57,6 +57,15 @@ func (p promptValues) has(value string) bool {
|
||||
return slices.Contains(p, value)
|
||||
}
|
||||
|
||||
// consentRequired reports whether the user has to be shown the consent screen
|
||||
// A client can be configured to skip consent so trusted first-party apps are not prompted on every new authorization, but an explicit prompt=consent always forces the screen regardless of that setting
|
||||
func consentRequired(hasAlreadyAuthorizedClient, clientSkipsConsent bool, prompt promptValues) bool {
|
||||
if prompt.has("consent") {
|
||||
return true
|
||||
}
|
||||
return !hasAlreadyAuthorizedClient && !clientSkipsConsent
|
||||
}
|
||||
|
||||
// authorizeInput is the authorization request as provided by the handler.
|
||||
type authorizeInput struct {
|
||||
userID string
|
||||
@@ -236,7 +245,7 @@ func (s *authorizationService) resolveRequirements(ctx context.Context, req auth
|
||||
}
|
||||
|
||||
requirements := interactionRequirements{
|
||||
ConsentRequired: !hasAlreadyAuthorizedClient || req.prompt.has("consent"),
|
||||
ConsentRequired: consentRequired(hasAlreadyAuthorizedClient, req.client.SkipConsent, req.prompt),
|
||||
ReauthenticationRequired: req.prompt.has("login") || req.client.RequiresReauthentication || maxAgeReauthenticationRequired,
|
||||
AccountSelectionRequired: req.prompt.has("select_account"),
|
||||
AuthenticationRequired: false,
|
||||
@@ -568,7 +577,7 @@ func (s *authorizationService) interactionRequirementsForUser(ctx context.Contex
|
||||
}
|
||||
|
||||
return interactionRequirements{
|
||||
ConsentRequired: !hasAlreadyAuthorizedClient || prompt.has("consent"),
|
||||
ConsentRequired: consentRequired(hasAlreadyAuthorizedClient, interactionSession.Client.SkipConsent, prompt),
|
||||
ReauthenticationRequired: prompt.has("login") || interactionSession.Client.RequiresReauthentication || maxAgeReauthenticationRequired,
|
||||
AccountSelectionRequired: prompt.has("select_account"),
|
||||
AuthenticationRequired: false,
|
||||
|
||||
@@ -913,3 +913,89 @@ func newTestAuthorizeRequesterWithForm(requestID, clientID string, form url.Valu
|
||||
func stringPointer(value string) *string {
|
||||
return &value
|
||||
}
|
||||
|
||||
func TestConsentRequired(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
hasAlreadyAuthorized bool
|
||||
skipConsent bool
|
||||
prompt string
|
||||
want bool
|
||||
}{
|
||||
{"new client without skip requires consent", false, false, "", true},
|
||||
{"returning client without skip skips consent", true, false, "", false},
|
||||
{"new client with skip skips consent", false, true, "", false},
|
||||
{"returning client with skip skips consent", true, true, "", false},
|
||||
{"prompt=consent forces consent despite skip", false, true, "consent", true},
|
||||
{"prompt=consent forces consent for returning client", true, false, "consent", true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := consentRequired(tt.hasAlreadyAuthorized, tt.skipConsent, newPromptValues(tt.prompt))
|
||||
require.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// A client with SkipConsent must be granted without a consent interaction even on the first authorization, while consent is still recorded so the user can later see and revoke the client
|
||||
func TestAuthorizationServiceSkipConsentGrantsWithoutInteraction(t *testing.T) {
|
||||
db := testutils.NewDatabaseForTest(t)
|
||||
auditLogger := &fakeAuditLogger{}
|
||||
service := newAuthorizationService(db, newInteractionSessionService(db), newClaimsService(db, nil, "", nil), nil, auditLogger)
|
||||
|
||||
const (
|
||||
userID = "test-user"
|
||||
clientID = "test-client"
|
||||
)
|
||||
require.NoError(t, db.Create(&model.User{Base: model.Base{ID: userID}}).Error)
|
||||
require.NoError(t, db.Create(&model.OidcClient{Base: model.Base{ID: clientID}, Name: "Test Client", SkipConsent: true}).Error)
|
||||
|
||||
// No prior UserAuthorizedOidcClient record exists, so a client without skip-consent would require the consent screen here
|
||||
requester := newTestAuthorizeRequester("skip-consent-request", clientID, "")
|
||||
requester.(*fosite.AuthorizeRequest).Client = Client{OidcClient: model.OidcClient{Base: model.Base{ID: clientID}, Name: "Test Client", SkipConsent: true}}
|
||||
|
||||
authorization, err := service.authorize(t.Context(), authorizeInput{
|
||||
userID: userID,
|
||||
authenticationTime: time.Now().UTC(),
|
||||
requester: requester,
|
||||
meta: requestMeta{IPAddress: "203.0.113.1", UserAgent: "test-agent"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.False(t, authorization.RequiresInteraction)
|
||||
require.NotNil(t, authorization.Session)
|
||||
|
||||
var count int64
|
||||
require.NoError(t, db.Model(&model.UserAuthorizedOidcClient{}).Where("user_id = ? AND client_id = ?", userID, clientID).Count(&count).Error)
|
||||
require.Equal(t, int64(1), count)
|
||||
|
||||
require.Equal(t, []model.AuditLogEvent{model.AuditLogEventNewClientAuthorization}, auditLogger.events)
|
||||
}
|
||||
|
||||
// A client with SkipConsent must still show the consent screen when the request explicitly asks for it with prompt=consent
|
||||
func TestAuthorizationServiceSkipConsentHonorsPromptConsent(t *testing.T) {
|
||||
db := testutils.NewDatabaseForTest(t)
|
||||
service := newAuthorizationService(db, newInteractionSessionService(db), newClaimsService(db, nil, "", nil), nil, nil)
|
||||
|
||||
const (
|
||||
userID = "test-user"
|
||||
clientID = "test-client"
|
||||
)
|
||||
require.NoError(t, db.Create(&model.User{Base: model.Base{ID: userID}}).Error)
|
||||
require.NoError(t, db.Create(&model.OidcClient{Base: model.Base{ID: clientID}, Name: "Test Client", SkipConsent: true}).Error)
|
||||
|
||||
requester := newTestAuthorizeRequester("skip-consent-prompt-request", clientID, "consent")
|
||||
requester.(*fosite.AuthorizeRequest).Client = Client{OidcClient: model.OidcClient{Base: model.Base{ID: clientID}, Name: "Test Client", SkipConsent: true}}
|
||||
|
||||
authorization, err := service.authorize(t.Context(), authorizeInput{
|
||||
userID: userID,
|
||||
authenticationTime: time.Now().UTC(),
|
||||
requester: requester,
|
||||
meta: requestMeta{},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.True(t, authorization.RequiresInteraction)
|
||||
|
||||
interactionSession, err := newInteractionSessionService(db).get(t.Context(), authorization.InteractionID)
|
||||
require.NoError(t, err)
|
||||
require.True(t, interactionSession.ConsentRequired)
|
||||
}
|
||||
|
||||
@@ -157,7 +157,8 @@ func (s *deviceService) getDeviceCodeInfo(ctx context.Context, userCode, userID
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
authorizationRequired = !hasAuthorizedClient
|
||||
// The device flow has no per-request prompt parameter, so consent depends only on prior authorization and the client's skip-consent setting
|
||||
authorizationRequired = consentRequired(hasAuthorizedClient, client.SkipConsent, nil)
|
||||
}
|
||||
|
||||
return &dto.DeviceCodeInfoDto{
|
||||
|
||||
@@ -257,6 +257,17 @@ func (s *TestService) SeedDatabase(baseURL string) error {
|
||||
CallbackURLs: model.UrlList{"http://par-client.localhost/auth/callback"},
|
||||
CreatedByID: new(users[0].ID),
|
||||
},
|
||||
{
|
||||
Base: model.Base{
|
||||
ID: "e1f2a3b4-c5d6-7890-abcd-ef0000000002",
|
||||
},
|
||||
Name: "Skip Consent Client",
|
||||
Secret: "$2a$10$9dypwot8nGuCjT6wQWWpJOckZfRprhe2EkwpKizxS/fpVHrOLEJHC", // w2mUeZISmEvIDMEDvpY0PnxQIpj1m3zY
|
||||
CallbackURLs: model.UrlList{"http://skip-consent.localhost/auth/callback"},
|
||||
CreatedByID: new(users[0].ID),
|
||||
// Trusted client that bypasses the consent screen by default
|
||||
SkipConsent: true,
|
||||
},
|
||||
}
|
||||
for _, client := range oidcClients {
|
||||
if err := tx.Create(&client).Error; err != nil {
|
||||
@@ -770,7 +781,7 @@ func (s *TestService) newFositeTokenRequest(session fositeTokenSession) *fosite.
|
||||
RequestedAt: requestedAt,
|
||||
AuthTime: requestedAt,
|
||||
Subject: session.UserID,
|
||||
Issuer: common.EnvConfig.AppURL,
|
||||
Issuer: common.EnvConfig.AppURL,
|
||||
},
|
||||
}
|
||||
oidcSession.SetExpiresAt(session.TokenType, session.ExpiresAt)
|
||||
|
||||
@@ -220,6 +220,7 @@ func updateOIDCClientModelFromDto(client *model.OidcClient, input *dto.OidcClien
|
||||
client.PkceEnabled = input.IsPublic || input.PkceEnabled
|
||||
client.RequiresReauthentication = input.RequiresReauthentication
|
||||
client.RequiresPushedAuthorizationRequests = input.RequiresPushedAuthorizationRequests
|
||||
client.SkipConsent = input.SkipConsent
|
||||
client.LaunchURL = input.LaunchURL
|
||||
client.IsGroupRestricted = input.IsGroupRestricted
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE oidc_clients DROP COLUMN skip_consent;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE oidc_clients ADD COLUMN skip_consent BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
@@ -0,0 +1,5 @@
|
||||
PRAGMA foreign_keys=OFF;
|
||||
BEGIN;
|
||||
ALTER TABLE oidc_clients DROP COLUMN skip_consent;
|
||||
COMMIT;
|
||||
PRAGMA foreign_keys=ON;
|
||||
@@ -0,0 +1,5 @@
|
||||
PRAGMA foreign_keys=OFF;
|
||||
BEGIN;
|
||||
ALTER TABLE oidc_clients ADD COLUMN skip_consent BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
COMMIT;
|
||||
PRAGMA foreign_keys=ON;
|
||||
@@ -182,6 +182,8 @@
|
||||
"smtp_tls_option": "SMTP TLS Option",
|
||||
"email_tls_option": "Email TLS Option",
|
||||
"skip_certificate_verification": "Skip Certificate Verification",
|
||||
"skip_consent": "Skip Consent Screen",
|
||||
"skip_consent_description": "Don't ask users to approve this client and the scopes it requests. Only enable this for trusted first-party clients.",
|
||||
"this_can_be_useful_for_selfsigned_certificates": "This can be useful for self-signed certificates.",
|
||||
"enabled_emails": "Enabled Emails",
|
||||
"email_login_notification": "Email Login Notification",
|
||||
|
||||
@@ -28,6 +28,7 @@ export type OidcClient = OidcClientMetaData & {
|
||||
pkceEnabled: boolean;
|
||||
requiresReauthentication: boolean;
|
||||
requiresPushedAuthorizationRequests: boolean;
|
||||
skipConsent: boolean;
|
||||
credentials?: OidcClientCredentials;
|
||||
launchURL?: string;
|
||||
isGroupRestricted: boolean;
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
requiresReauthentication: existingClient?.requiresReauthentication || false,
|
||||
requiresPushedAuthorizationRequests:
|
||||
existingClient?.requiresPushedAuthorizationRequests || false,
|
||||
skipConsent: existingClient?.skipConsent || false,
|
||||
launchURL: existingClient?.launchURL || '',
|
||||
credentials: {
|
||||
federatedIdentities: existingClient?.credentials?.federatedIdentities || []
|
||||
@@ -77,6 +78,7 @@
|
||||
pkceEnabled: z.boolean(),
|
||||
requiresReauthentication: z.boolean(),
|
||||
requiresPushedAuthorizationRequests: z.boolean(),
|
||||
skipConsent: z.boolean(),
|
||||
launchURL: optionalUrl,
|
||||
logoUrl: optionalUrl,
|
||||
darkLogoUrl: optionalUrl,
|
||||
@@ -226,6 +228,12 @@
|
||||
description={m.requires_users_to_authenticate_again_on_each_authorization()}
|
||||
bind:checked={$inputs.requiresReauthentication.value}
|
||||
/>
|
||||
<SwitchWithLabel
|
||||
id="skip-consent"
|
||||
label={m.skip_consent()}
|
||||
description={m.skip_consent_description()}
|
||||
bind:checked={$inputs.skipConsent.value}
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-7 w-full md:w-1/2">
|
||||
<Tabs.Root value="light-logo">
|
||||
|
||||
@@ -73,6 +73,12 @@ export const oidcClients = {
|
||||
name: 'PAR Test Client',
|
||||
callbackUrl: 'http://par-client.localhost/auth/callback',
|
||||
secret: 'w2mUeZISmEvIDMEDvpY0PnxQIpj1m3zY'
|
||||
},
|
||||
skipConsent: {
|
||||
id: 'e1f2a3b4-c5d6-7890-abcd-ef0000000002',
|
||||
name: 'Skip Consent Client',
|
||||
callbackUrl: 'http://skip-consent.localhost/auth/callback',
|
||||
secret: 'w2mUeZISmEvIDMEDvpY0PnxQIpj1m3zY'
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"provider": "sqlite",
|
||||
"version": 20260607120000,
|
||||
"version": 20260626120000,
|
||||
"tableOrder": ["users", "user_groups", "oidc_clients", "signup_tokens"],
|
||||
"tables": {
|
||||
"api_keys": [
|
||||
@@ -56,7 +56,8 @@
|
||||
"pkce_enabled": false,
|
||||
"requires_pushed_authorization_requests": false,
|
||||
"requires_reauthentication": false,
|
||||
"secret": "$2a$10$9dypwot8nGuCjT6wQWWpJOckZfRprhe2EkwpKizxS/fpVHrOLEJHC"
|
||||
"secret": "$2a$10$9dypwot8nGuCjT6wQWWpJOckZfRprhe2EkwpKizxS/fpVHrOLEJHC",
|
||||
"skip_consent": false
|
||||
},
|
||||
{
|
||||
"callback_urls": "WyJodHRwOi8vaW1taWNoLmxvY2FsaG9zdC9hdXRoL2NhbGxiYWNrIl0=",
|
||||
@@ -74,7 +75,8 @@
|
||||
"pkce_enabled": false,
|
||||
"requires_pushed_authorization_requests": false,
|
||||
"requires_reauthentication": false,
|
||||
"secret": "$2a$10$Ak.FP8riD1ssy2AGGbG.gOpnp/rBpymd74j0nxNMtW0GG1Lb4gzxe"
|
||||
"secret": "$2a$10$Ak.FP8riD1ssy2AGGbG.gOpnp/rBpymd74j0nxNMtW0GG1Lb4gzxe",
|
||||
"skip_consent": false
|
||||
},
|
||||
{
|
||||
"callback_urls": "WyJodHRwOi8vdGFpbHNjYWxlLmxvY2FsaG9zdC9hdXRoL2NhbGxiYWNrIl0=",
|
||||
@@ -92,7 +94,8 @@
|
||||
"pkce_enabled": false,
|
||||
"requires_pushed_authorization_requests": false,
|
||||
"requires_reauthentication": false,
|
||||
"secret": "$2a$10$xcRReBsvkI1XI6FG8xu/pOgzeF00bH5Wy4d/NThwcdi3ZBpVq/B9a"
|
||||
"secret": "$2a$10$xcRReBsvkI1XI6FG8xu/pOgzeF00bH5Wy4d/NThwcdi3ZBpVq/B9a",
|
||||
"skip_consent": false
|
||||
},
|
||||
{
|
||||
"callback_urls": "WyJodHRwOi8vZmVkZXJhdGVkLmxvY2FsaG9zdC9hdXRoL2NhbGxiYWNrIl0=",
|
||||
@@ -110,7 +113,8 @@
|
||||
"pkce_enabled": false,
|
||||
"requires_pushed_authorization_requests": false,
|
||||
"requires_reauthentication": false,
|
||||
"secret": "$2a$10$Ak.FP8riD1ssy2AGGbG.gOpnp/rBpymd74j0nxNMtW0GG1Lb4gzxe"
|
||||
"secret": "$2a$10$Ak.FP8riD1ssy2AGGbG.gOpnp/rBpymd74j0nxNMtW0GG1Lb4gzxe",
|
||||
"skip_consent": false
|
||||
},
|
||||
{
|
||||
"callback_urls": "WyJodHRwOi8vc2NpbWNsaWVudC5sb2NhbGhvc3QvYXV0aC9jYWxsYmFjayJd",
|
||||
@@ -127,7 +131,8 @@
|
||||
"pkce_enabled": false,
|
||||
"requires_pushed_authorization_requests": false,
|
||||
"requires_reauthentication": false,
|
||||
"secret": "$2a$10$h4wfa8gI7zavDAxwzSq1sOwYU4e8DwK1XZ8ZweNnY5KzlJ3Iz.qdK"
|
||||
"secret": "$2a$10$h4wfa8gI7zavDAxwzSq1sOwYU4e8DwK1XZ8ZweNnY5KzlJ3Iz.qdK",
|
||||
"skip_consent": false
|
||||
},
|
||||
{
|
||||
"callback_urls": "WyJodHRwOi8vcGFyLWNsaWVudC5sb2NhbGhvc3QvYXV0aC9jYWxsYmFjayJd",
|
||||
@@ -145,7 +150,27 @@
|
||||
"pkce_enabled": false,
|
||||
"requires_pushed_authorization_requests": false,
|
||||
"requires_reauthentication": false,
|
||||
"secret": "$2a$10$9dypwot8nGuCjT6wQWWpJOckZfRprhe2EkwpKizxS/fpVHrOLEJHC"
|
||||
"secret": "$2a$10$9dypwot8nGuCjT6wQWWpJOckZfRprhe2EkwpKizxS/fpVHrOLEJHC",
|
||||
"skip_consent": false
|
||||
},
|
||||
{
|
||||
"callback_urls": "WyJodHRwOi8vc2tpcC1jb25zZW50LmxvY2FsaG9zdC9hdXRoL2NhbGxiYWNrIl0=",
|
||||
"created_at": "2025-11-25T12:39:02Z",
|
||||
"created_by_id": "f4b89dc2-62fb-46bf-9f5f-c34f4eafe93e",
|
||||
"credentials": "e30=",
|
||||
"dark_image_type": null,
|
||||
"id": "e1f2a3b4-c5d6-7890-abcd-ef0000000002",
|
||||
"image_type": null,
|
||||
"is_group_restricted": false,
|
||||
"is_public": false,
|
||||
"launch_url": null,
|
||||
"logout_callback_urls": "bnVsbA==",
|
||||
"name": "Skip Consent Client",
|
||||
"pkce_enabled": false,
|
||||
"requires_pushed_authorization_requests": false,
|
||||
"requires_reauthentication": false,
|
||||
"secret": "$2a$10$9dypwot8nGuCjT6wQWWpJOckZfRprhe2EkwpKizxS/fpVHrOLEJHC",
|
||||
"skip_consent": true
|
||||
}
|
||||
],
|
||||
"oidc_clients_allowed_user_groups": [
|
||||
|
||||
@@ -11,7 +11,7 @@ test('Dashboard shows all clients in the correct order', async ({ page }) => {
|
||||
|
||||
await page.goto('/settings/apps');
|
||||
|
||||
await expect(page.getByTestId('authorized-oidc-client-card')).toHaveCount(6);
|
||||
await expect(page.getByTestId('authorized-oidc-client-card')).toHaveCount(7);
|
||||
|
||||
// Should be first
|
||||
const card1 = page.getByTestId('authorized-oidc-client-card').first();
|
||||
@@ -32,7 +32,7 @@ test.describe('Dashboard shows only clients where user has access', () => {
|
||||
|
||||
const cards = page.getByTestId('authorized-oidc-client-card');
|
||||
|
||||
await expect(cards).toHaveCount(5);
|
||||
await expect(cards).toHaveCount(6);
|
||||
|
||||
const cardTexts = await cards.allTextContents();
|
||||
expect(cardTexts.some((text) => text.includes(notVisibleClient.name))).toBe(false);
|
||||
@@ -40,7 +40,7 @@ test.describe('Dashboard shows only clients where user has access', () => {
|
||||
test('User can see all clients', async ({ page }) => {
|
||||
await page.goto('/settings/apps');
|
||||
const cards = page.getByTestId('authorized-oidc-client-card');
|
||||
await expect(cards).toHaveCount(6);
|
||||
await expect(cards).toHaveCount(7);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1440,3 +1440,49 @@ test.describe('Pushed Authorization Requests (PAR)', () => {
|
||||
await expect(savedToggle).toBeChecked();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('OIDC skip consent', () => {
|
||||
// This seeded client has skip-consent enabled and is not pre-authorized for the signed-in
|
||||
// user, so the consent screen would normally be shown on first authorization.
|
||||
const client = oidcClients.skipConsent;
|
||||
|
||||
test('skips the consent screen on first authorization', async ({ page }) => {
|
||||
// The flow must go straight through to the callback instead of stopping at the consent screen
|
||||
const urlParams = createUrlParams(client);
|
||||
const callbackUrl = await expectCallbackRedirect(page, client.callbackUrl, () =>
|
||||
page.goto(`/authorize?${urlParams.toString()}`)
|
||||
);
|
||||
|
||||
expect(callbackUrl.searchParams.get('code')).toBeTruthy();
|
||||
expect(callbackUrl.searchParams.get('error')).toBeNull();
|
||||
});
|
||||
|
||||
test('still shows the consent screen when prompt=consent is requested', async ({ page }) => {
|
||||
const urlParams = createUrlParams(client);
|
||||
urlParams.set('prompt', 'consent');
|
||||
await page.goto(`/authorize?${urlParams.toString()}`);
|
||||
|
||||
// prompt=consent overrides the client's skip-consent setting, so the scopes must still be shown
|
||||
await expectScopes(page, ['Email', 'Profile']);
|
||||
|
||||
const callbackUrl = await expectCallbackRedirect(page, client.callbackUrl, () =>
|
||||
page.getByRole('button', { name: 'Sign in' }).click()
|
||||
);
|
||||
expect(callbackUrl.searchParams.get('code')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('Admin UI: skip consent toggle reflects the seeded value and persists', async ({ page }) => {
|
||||
await page.goto(`/settings/admin/oidc-clients/${client.id}`);
|
||||
|
||||
// The seeded client has skip-consent enabled, so the toggle loads checked
|
||||
const toggle = page.getByRole('switch', { name: 'Skip Consent Screen' });
|
||||
await expect(toggle).toBeChecked();
|
||||
|
||||
// Disabling it and saving must persist across a reload
|
||||
await toggle.click();
|
||||
await page.getByRole('button', { name: /save/i }).click();
|
||||
await page.reload();
|
||||
|
||||
await expect(page.getByRole('switch', { name: 'Skip Consent Screen' })).not.toBeChecked();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user