mirror of
https://github.com/pocket-id/pocket-id.git
synced 2025-12-06 05:02:58 +03:00
feat: JWT bearer assertions for client authentication (#566)
Co-authored-by: Kyle Mendell <ksm@ofkm.us> Co-authored-by: Kyle Mendell <kmendell@ofkm.us> Co-authored-by: Elias Schneider <login@eliasschneider.com>
This commit is contained in:
committed by
GitHub
parent
035b2c022b
commit
05bfe00924
@@ -35,6 +35,17 @@ export const oidcClients = {
|
||||
callbackUrl: 'http://immich/auth/callback',
|
||||
secret: 'PYjrE9u4v9GVqXKi52eur0eb2Ci4kc0x'
|
||||
},
|
||||
federated: {
|
||||
id: "c48232ff-ff65-45ed-ae96-7afa8a9b443b",
|
||||
name: 'Federated',
|
||||
callbackUrl: 'http://federated/auth/callback',
|
||||
federatedJWT: {
|
||||
issuer: 'https://external-idp.local',
|
||||
audience: 'api://PocketID',
|
||||
subject: 'c48232ff-ff65-45ed-ae96-7afa8a9b443b',
|
||||
},
|
||||
accessCodes: ['federated']
|
||||
},
|
||||
pingvinShare: {
|
||||
name: 'Pingvin Share',
|
||||
callbackUrl: 'http://pingvin.share/auth/callback',
|
||||
|
||||
14
tests/package-lock.json
generated
14
tests/package-lock.json
generated
@@ -7,6 +7,7 @@
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.52.0",
|
||||
"@types/node": "^22.15.21",
|
||||
"dotenv": "^16.5.0",
|
||||
"jose": "^6.0.11"
|
||||
}
|
||||
},
|
||||
@@ -36,6 +37,19 @@
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "16.5.0",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz",
|
||||
"integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.52.0",
|
||||
"@types/node": "^22.15.21",
|
||||
"jose": "^6.0.11"
|
||||
"jose": "^6.0.11",
|
||||
"dotenv": "^16.5.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +1,31 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
import "dotenv/config";
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
export default defineConfig({
|
||||
outputDir: './.output',
|
||||
timeout: 10000,
|
||||
testDir: './specs',
|
||||
fullyParallel: false,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 1 : 0,
|
||||
workers: 1,
|
||||
reporter: process.env.CI
|
||||
? [['html', { outputFolder: '.report' }], ['github']]
|
||||
: [['line'], ['html', { open: 'never', outputFolder: '.report' }]],
|
||||
use: {
|
||||
baseURL: process.env.APP_URL ?? 'http://localhost:1411',
|
||||
video: 'retain-on-failure',
|
||||
trace: 'on-first-retry'
|
||||
},
|
||||
projects: [
|
||||
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'], storageState: '.auth/user.json' },
|
||||
dependencies: ['setup']
|
||||
}
|
||||
]
|
||||
outputDir: "./.output",
|
||||
timeout: 10000,
|
||||
testDir: "./specs",
|
||||
fullyParallel: false,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 1 : 0,
|
||||
workers: 1,
|
||||
reporter: process.env.CI
|
||||
? [["html", { outputFolder: ".report" }], ["github"]]
|
||||
: [["line"], ["html", { open: "never", outputFolder: ".report" }]],
|
||||
use: {
|
||||
baseURL: process.env.APP_URL ?? "http://localhost:1411",
|
||||
video: "retain-on-failure",
|
||||
trace: "on-first-retry",
|
||||
},
|
||||
projects: [
|
||||
{ name: "setup", testMatch: /.*\.setup\.ts/ },
|
||||
{
|
||||
name: "chromium",
|
||||
use: { ...devices["Desktop Chrome"], storageState: ".auth/user.json" },
|
||||
dependencies: ["setup"],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -4,6 +4,8 @@ import { cleanupBackend } from '../utils/cleanup.util';
|
||||
test.beforeEach(cleanupBackend);
|
||||
|
||||
test.describe('LDAP Integration', () => {
|
||||
test.skip(process.env.SKIP_LDAP_TESTS === "true", 'Skipping LDAP tests due to SKIP_LDAP_TESTS environment variable');
|
||||
|
||||
test('LDAP configuration is working properly', async ({ page }) => {
|
||||
await page.goto('/settings/admin/application-configuration');
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import test, { expect } from "@playwright/test";
|
||||
import { oidcClients, refreshTokens, users } from "../data";
|
||||
import { cleanupBackend } from "../utils/cleanup.util";
|
||||
import { generateIdToken, generateOauthAccessToken } from "../utils/jwt.util";
|
||||
import oidcUtil from "../utils/oidc.util";
|
||||
import * as oidcUtil from "../utils/oidc.util";
|
||||
import passkeyUtil from "../utils/passkey.util";
|
||||
|
||||
test.beforeEach(cleanupBackend);
|
||||
@@ -449,3 +449,40 @@ test("Authorize new client with device authorization with user group not allowed
|
||||
.filter({ hasText: "You're not allowed to access this service." })
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("Federated identity fails with invalid client assertion", async ({
|
||||
page,
|
||||
}) => {
|
||||
const client = oidcClients.federated;
|
||||
|
||||
const res = await oidcUtil.exchangeCode(page, {
|
||||
client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
|
||||
grant_type: 'authorization_code',
|
||||
redirect_uri: client.callbackUrl,
|
||||
code: client.accessCodes[0],
|
||||
client_id: client.id,
|
||||
client_assertion:'not-an-assertion',
|
||||
});
|
||||
|
||||
expect(res?.error).toBe('Invalid client assertion');
|
||||
});
|
||||
|
||||
test("Authorize existing client with federated identity", async ({
|
||||
page,
|
||||
}) => {
|
||||
const client = oidcClients.federated;
|
||||
const clientAssertion = await oidcUtil.getClientAssertion(page, client.federatedJWT);
|
||||
|
||||
const res = await oidcUtil.exchangeCode(page, {
|
||||
client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
|
||||
grant_type: 'authorization_code',
|
||||
redirect_uri: client.callbackUrl,
|
||||
code: client.accessCodes[0],
|
||||
client_id: client.id,
|
||||
client_assertion: clientAssertion,
|
||||
});
|
||||
|
||||
expect(res.access_token).not.toBeNull;
|
||||
expect(res.expires_in).not.toBeNull;
|
||||
expect(res.token_type).toBe('Bearer');
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": "."
|
||||
"baseUrl": ".",
|
||||
"lib": ["ES2022"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import playwrightConfig from "../playwright.config";
|
||||
|
||||
export async function cleanupBackend() {
|
||||
const response = await fetch(
|
||||
playwrightConfig.use!.baseURL + "/api/test/reset",
|
||||
{
|
||||
method: "POST",
|
||||
}
|
||||
);
|
||||
const url = new URL("/api/test/reset", playwrightConfig.use!.baseURL);
|
||||
|
||||
if (process.env.SKIP_LDAP_TESTS === "true") {
|
||||
url.searchParams.append("skip-ldap", "true");
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
async function getUserCode(page: Page, clientId: string, clientSecret: string) {
|
||||
const response = await page.request
|
||||
export async function getUserCode(page: Page, clientId: string, clientSecret: string): Promise<string> {
|
||||
return page.request
|
||||
.post('/api/oidc/device/authorize', {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
@@ -12,11 +12,29 @@ async function getUserCode(page: Page, clientId: string, clientSecret: string) {
|
||||
scope: 'openid profile email'
|
||||
}
|
||||
})
|
||||
.then((r) => r.json());
|
||||
|
||||
return response.user_code;
|
||||
.then((r) => r.json())
|
||||
.then((r) => r.user_code);
|
||||
}
|
||||
|
||||
export default {
|
||||
getUserCode
|
||||
};
|
||||
export async function exchangeCode(page: Page, params: Record<string,string>): Promise<{access_token?: string, token_type?: string, expires_in?: number, error?: string}> {
|
||||
return page.request
|
||||
.post('/api/oidc/token', {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
form: params,
|
||||
})
|
||||
.then((r) => r.json());
|
||||
}
|
||||
|
||||
export async function getClientAssertion(page: Page, data: {issuer: string, audience: string, subject: string}): Promise<string> {
|
||||
return page.request
|
||||
.post('/api/externalidp/sign', {
|
||||
data: {
|
||||
iss: data.issuer,
|
||||
aud: data.audience,
|
||||
sub: data.subject,
|
||||
},
|
||||
})
|
||||
.then((r) => r.text());
|
||||
}
|
||||
Reference in New Issue
Block a user