mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-09-20 09:59:03 +02:00
feat: add ability to customize session duration of clients (#1641)
This commit is contained in:
@@ -2,6 +2,11 @@ import test, { expect, Page } from '@playwright/test';
|
||||
import { oidcClients, userGroups } from '../data';
|
||||
import { cleanupBackend } from '../utils/cleanup.util';
|
||||
|
||||
const defaultTokenLifetimes = {
|
||||
accessTokenDurationMinutes: 60,
|
||||
refreshTokenDurationMinutes: 30 * 24 * 60
|
||||
};
|
||||
|
||||
test.beforeEach(async () => await cleanupBackend());
|
||||
|
||||
test.describe('Create OIDC client', () => {
|
||||
@@ -78,7 +83,8 @@ test('Edit OIDC client', async ({ page }) => {
|
||||
await page.locator('[role="tab"][data-value="dark-logo"]').first().click();
|
||||
await page.setInputFiles('#oidc-client-logo-dark', 'resources/images/cloud-logo.png');
|
||||
await page.getByLabel('Client Launch URL').fill(oidcClient.launchURL);
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
const clientForm = page.getByLabel('Name').locator('xpath=ancestor::form');
|
||||
await clientForm.getByRole('button', { name: 'Save' }).click();
|
||||
|
||||
await expect(page.locator('[data-type="success"]')).toHaveText(
|
||||
'OIDC client updated successfully'
|
||||
@@ -110,6 +116,98 @@ test('Displays OIDC client endpoints from discovery configuration', async ({ pag
|
||||
await expect(page.getByText(oidcConfiguration.jwks_uri, { exact: true })).toBeVisible();
|
||||
});
|
||||
|
||||
test('Update OIDC client token lifetimes', async ({ page }) => {
|
||||
await page.goto(`/settings/admin/oidc-clients/${oidcClients.nextcloud.id}`);
|
||||
|
||||
const card = page.getByTestId('token-lifetimes-card');
|
||||
const accessLifetime = card.getByLabel('Access token lifetime', { exact: true });
|
||||
const accessUnit = card.getByLabel('Access token lifetime unit');
|
||||
const refreshLifetime = card.getByLabel('Refresh token inactivity timeout', { exact: true });
|
||||
const refreshUnit = card.getByLabel('Refresh token inactivity timeout unit');
|
||||
|
||||
await expect(accessLifetime).toHaveValue('1');
|
||||
await expect(accessUnit).toHaveText('Hours');
|
||||
await expect(refreshLifetime).toHaveValue('30');
|
||||
await expect(refreshUnit).toHaveText('Days');
|
||||
|
||||
await accessUnit.click();
|
||||
await page.getByRole('option', { name: 'Minutes' }).click();
|
||||
await expect(accessLifetime).toHaveValue('60');
|
||||
await accessLifetime.fill('90');
|
||||
|
||||
await refreshUnit.click();
|
||||
await page.getByRole('option', { name: 'Hours' }).click();
|
||||
await expect(refreshLifetime).toHaveValue('720');
|
||||
await refreshLifetime.fill('336');
|
||||
|
||||
await card.getByRole('button', { name: 'Save' }).click();
|
||||
await expect(page.getByText('OIDC client updated successfully', { exact: true })).toBeVisible();
|
||||
|
||||
await page.reload();
|
||||
await expect(card.getByLabel('Access token lifetime', { exact: true })).toHaveValue('90');
|
||||
await expect(card.getByLabel('Access token lifetime unit')).toHaveText('Minutes');
|
||||
await expect(card.getByLabel('Refresh token inactivity timeout', { exact: true })).toHaveValue(
|
||||
'14'
|
||||
);
|
||||
await expect(card.getByLabel('Refresh token inactivity timeout unit')).toHaveText('Days');
|
||||
|
||||
await card.getByLabel('Access token lifetime', { exact: true }).fill('0');
|
||||
await card.getByRole('button', { name: 'Save' }).click();
|
||||
await expect(card.getByText('Token lifetime must be at least 1 minute.')).toBeVisible();
|
||||
|
||||
await card.getByLabel('Access token lifetime', { exact: true }).fill('525601');
|
||||
await card.getByRole('button', { name: 'Save' }).click();
|
||||
await expect(card.getByText('Token lifetime cannot exceed 365 days.')).toBeVisible();
|
||||
|
||||
await card.getByLabel('Access token lifetime', { exact: true }).fill('1.5');
|
||||
await card.getByRole('button', { name: 'Save' }).click();
|
||||
await expect(card.getByText('Token lifetime must use whole-minute increments.')).toBeVisible();
|
||||
|
||||
await card.getByLabel('Access token lifetime', { exact: true }).fill('60');
|
||||
await card.getByLabel('Refresh token inactivity timeout', { exact: true }).fill('30');
|
||||
await card.getByRole('button', { name: 'Save' }).click();
|
||||
await expect(page.getByText('OIDC client updated successfully', { exact: true })).toBeVisible();
|
||||
});
|
||||
|
||||
test('Update OIDC client federated credentials', async ({ page }) => {
|
||||
const client = oidcClients.nextcloud;
|
||||
await page.goto(`/settings/admin/oidc-clients/${client.id}`);
|
||||
|
||||
const card = page.getByTestId('federated-credentials-card');
|
||||
await card.getByRole('button', { name: 'Create', exact: true }).click();
|
||||
await card.getByLabel('Issuer').fill('https://issuer.example.com');
|
||||
await card.getByLabel('Subject').fill('workload-client');
|
||||
await card.getByLabel('Audience').fill('https://pocket-id.example.com');
|
||||
|
||||
const cardUpdate = page.waitForResponse(
|
||||
(response) =>
|
||||
response.request().method() === 'PUT' &&
|
||||
response.url().endsWith(`/api/oidc/clients/${client.id}`)
|
||||
);
|
||||
await card.getByRole('button', { name: 'Save' }).click();
|
||||
expect((await cardUpdate).ok()).toBeTruthy();
|
||||
|
||||
await page.reload();
|
||||
await expect(card.getByLabel('Issuer')).toHaveValue('https://issuer.example.com');
|
||||
await expect(card.getByLabel('Subject')).toHaveValue('workload-client');
|
||||
await expect(card.getByLabel('Audience')).toHaveValue('https://pocket-id.example.com');
|
||||
|
||||
// Saving the main client form must preserve credentials managed by the separate card
|
||||
const description = page.getByLabel('Description');
|
||||
await description.fill('Updated without replacing federated credentials');
|
||||
const clientForm = description.locator('xpath=ancestor::form');
|
||||
const formUpdate = page.waitForResponse(
|
||||
(response) =>
|
||||
response.request().method() === 'PUT' &&
|
||||
response.url().endsWith(`/api/oidc/clients/${client.id}`)
|
||||
);
|
||||
await clientForm.getByRole('button', { name: 'Save' }).click();
|
||||
expect((await formUpdate).ok()).toBeTruthy();
|
||||
|
||||
await page.reload();
|
||||
await expect(card.getByLabel('Issuer')).toHaveValue('https://issuer.example.com');
|
||||
});
|
||||
|
||||
test('Create new OIDC client secret', async ({ page }) => {
|
||||
const oidcClient = oidcClients.nextcloud;
|
||||
await page.goto(`/settings/admin/oidc-clients/${oidcClient.id}`);
|
||||
@@ -148,6 +246,7 @@ test('Filter OIDC clients by PAR requirement', async ({ page, request }) => {
|
||||
// Enable PAR on the PAR test client
|
||||
await request.put(`/api/oidc/clients/${parClient.id}`, {
|
||||
data: {
|
||||
...defaultTokenLifetimes,
|
||||
name: parClient.name,
|
||||
callbackURLs: [parClient.callbackUrl],
|
||||
logoutCallbackURLs: [],
|
||||
|
||||
@@ -5,6 +5,11 @@ import { generateIdToken } from '../utils/jwt.util';
|
||||
import * as oidcUtil from '../utils/oidc.util';
|
||||
import passkeyUtil from '../utils/passkey.util';
|
||||
|
||||
const defaultTokenLifetimes = {
|
||||
accessTokenDurationMinutes: 60,
|
||||
refreshTokenDurationMinutes: 30 * 24 * 60
|
||||
};
|
||||
|
||||
test.beforeEach(async () => await cleanupBackend());
|
||||
|
||||
async function generateSeededOauthAccessToken(
|
||||
@@ -760,6 +765,7 @@ test('Device authorization flow forces reauthentication when client requires it'
|
||||
const client = oidcClients.nextcloud;
|
||||
await request.put(`/api/oidc/clients/${client.id}`, {
|
||||
data: {
|
||||
...defaultTokenLifetimes,
|
||||
name: client.name,
|
||||
callbackURLs: [client.callbackUrl],
|
||||
logoutCallbackURLs: [client.logoutCallbackUrl],
|
||||
@@ -885,6 +891,7 @@ test('Forces reauthentication when client requires it', async ({ page, request }
|
||||
|
||||
await request.put(`/api/oidc/clients/${oidcClients.nextcloud.id}`, {
|
||||
data: {
|
||||
...defaultTokenLifetimes,
|
||||
name: oidcClients.nextcloud.name,
|
||||
callbackURLs: [oidcClients.nextcloud.callbackUrl],
|
||||
logoutCallbackURLs: [oidcClients.nextcloud.logoutCallbackUrl],
|
||||
@@ -1438,6 +1445,7 @@ test.describe('Pushed Authorization Requests (PAR)', () => {
|
||||
await page.request.put(`/api/oidc/clients/${client.id}`, {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: {
|
||||
...defaultTokenLifetimes,
|
||||
name: client.name,
|
||||
callbackURLs: [client.callbackUrl],
|
||||
logoutCallbackURLs: [],
|
||||
@@ -1481,6 +1489,7 @@ test.describe('Pushed Authorization Requests (PAR)', () => {
|
||||
await request.put(`/api/oidc/clients/${client.id}`, {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: {
|
||||
...defaultTokenLifetimes,
|
||||
name: client.name,
|
||||
callbackURLs: [client.callbackUrl],
|
||||
logoutCallbackURLs: [],
|
||||
@@ -1514,7 +1523,7 @@ test.describe('Pushed Authorization Requests (PAR)', () => {
|
||||
await parToggle.click();
|
||||
}
|
||||
|
||||
await page.getByRole('button', { name: /save/i }).click();
|
||||
await page.getByRole('button', { name: 'Save', exact: true }).first().click();
|
||||
await expect(page.getByText('OIDC client updated successfully', { exact: true })).toBeVisible();
|
||||
await page.reload();
|
||||
|
||||
@@ -1564,7 +1573,8 @@ test.describe('OIDC skip consent', () => {
|
||||
// Disabling it and saving must persist across a reload
|
||||
await toggle.click();
|
||||
await expect(toggle).not.toBeChecked();
|
||||
await page.getByRole('button', { name: /save/i }).click();
|
||||
const clientForm = toggle.locator('xpath=ancestor::form');
|
||||
await clientForm.getByRole('button', { name: 'Save', exact: true }).click();
|
||||
await expect(page.getByText('OIDC client updated successfully', { exact: true })).toBeVisible();
|
||||
await page.reload();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user