mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-03-27 09:46:36 +00:00
feat: implement token introspection (#405)
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
8d6c1e5c08
commit
7e5d16be9b
@@ -26,12 +26,14 @@ export const oidcClients = {
|
||||
id: '3654a746-35d4-4321-ac61-0bdcff2b4055',
|
||||
name: 'Nextcloud',
|
||||
callbackUrl: 'http://nextcloud/auth/callback',
|
||||
logoutCallbackUrl: 'http://nextcloud/auth/logout/callback'
|
||||
logoutCallbackUrl: 'http://nextcloud/auth/logout/callback',
|
||||
secret: 'w2mUeZISmEvIDMEDvpY0PnxQIpj1m3zY'
|
||||
},
|
||||
immich: {
|
||||
id: '606c7782-f2b1-49e5-8ea9-26eb1b06d018',
|
||||
name: 'Immich',
|
||||
callbackUrl: 'http://immich/auth/callback'
|
||||
callbackUrl: 'http://immich/auth/callback',
|
||||
secret: 'PYjrE9u4v9GVqXKi52eur0eb2Ci4kc0x'
|
||||
},
|
||||
pingvinShare: {
|
||||
name: 'Pingvin Share',
|
||||
@@ -82,3 +84,33 @@ export const refreshTokens = [
|
||||
expired: true
|
||||
}
|
||||
];
|
||||
|
||||
export const idTokens = [
|
||||
{
|
||||
token:
|
||||
'eyJhbGciOiJSUzI1NiIsImtpZCI6Ijh1SER3M002cmY4IiwidHlwIjoiaWQrand0In0.eyJhdWQiOiIzNjU0YTc0Ni0zNWQ0LTQzMjEtYWM2MS0wYmRjZmYyYjQwNTUiLCJlbWFpbCI6InRpbS5jb29rQHRlc3QuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImV4cCI6MTY5MDAwMDAwMSwiZmFtaWx5X25hbWUiOiJUaW0iLCJnaXZlbl9uYW1lIjoiQ29vayIsImlhdCI6MTY5MDAwMDAwMCwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdCIsIm5hbWUiOiJUaW0gQ29vayIsIm5vbmNlIjoib1cxQTFPNzhHUTE1RDczT3NIRXg3V1FLajdacXZITFp1XzM3bWRYSXFBUSIsInN1YiI6IjRiODlkYzItNjJmYi00NmJmLTlmNWYtYzM0ZjRlYWZlOTNlIn0.LHwNnp9WxFc_NbIVsBC41trA-1LUBxTfKwIqfgGP4WC5j39M2Rmc0G4rw7J96tfwyEobwgPFAP0YJ3BqMaZgHT4Zu0rYSenU-yv_CICWiLL4csyeojlqbqDKDiOD3Gsl4_ZUuo8UuN190RGz6HlxmTwxpmceerSFpx6dBtA6chYZfgnUf289DRWIgTsNrXnkohZRa8zWc8bjbw_hj1u7H6Ev9Yu3U2k4K0cHWZLFjQiPWt3JBaWNAldSEn2q7a3Rkyv17_Gx8Nwl5L4ugWKV8M1YkcHbEkYCJKaJCbZi13R89yH1E0EOfHYXK5Z0KqBq47eTYRGRUtFiP-uTlUDQUQ',
|
||||
clientId: oidcClients.nextcloud.id,
|
||||
expired: true
|
||||
},
|
||||
{
|
||||
token:
|
||||
'eyJhbGciOiJSUzI1NiIsImtpZCI6Ijh1SER3M002cmY4IiwidHlwIjoiaWQrand0In0.eyJhdWQiOiIzNjU0YTc0Ni0zNWQ0LTQzMjEtYWM2MS0wYmRjZmYyYjQwNTUiLCJlbWFpbCI6InRpbS5jb29rQHRlc3QuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImV4cCI6MjY5MDAwMDAwMSwiZmFtaWx5X25hbWUiOiJUaW0iLCJnaXZlbl9uYW1lIjoiQ29vayIsImlhdCI6MTY5MDAwMDAwMCwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdCIsIm5hbWUiOiJUaW0gQ29vayIsIm5vbmNlIjoib1cxQTFPNzhHUTE1RDczT3NIRXg3V1FLajdacXZITFp1XzM3bWRYSXFBUSIsInN1YiI6IjRiODlkYzItNjJmYi00NmJmLTlmNWYtYzM0ZjRlYWZlOTNlIn0.yG21sES1HMyQg6GeJtd-6sUJ5a_QBS-hHq3mDTjRoMkL604RxprPvIJ-ypYhzcV5LwlTiD-7jJQ2Z95uUb82aNek55V5Pzq_rcLM5EtHh2bHSegt_1QXcpBzl8mWB1AIZBSRzFDaB1msnkyxGnndJk4VHpUVStvubcldxksH3e9v286x9ak4oTNoaLy4kMi4KAE8WCwrqsYc1iieLOSFTRHjpM9YxWa8X9hGNsikC85NJ0tj1pG9I4QTG62h4ZqJ4-jFWe5dogg_vd9Sk7tA3f9S779XSCG6hpj1V-sxQqLCy9uAmB2URP4N60jamKTn2TCxc1R7xgQ7M9Rc9ty68g',
|
||||
clientId: oidcClients.nextcloud.id,
|
||||
expired: false
|
||||
}
|
||||
];
|
||||
|
||||
export const accessTokens = [
|
||||
{
|
||||
token:
|
||||
'eyJhbGciOiJSUzI1NiIsImtpZCI6Ijh1SER3M002cmY4IiwidHlwIjoiYXQrand0In0.eyJhdWQiOlsiMzY1NGE3NDYtMzVkNC00MzIxLWFjNjEtMGJkY2ZmMmI0MDU1Il0sImV4cCI6MTc0Mzk3MjI4MywiaWF0IjoxNzQzOTY4NjgzLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0Iiwic3ViIjoiZjRiODlkYzItNjJmYi00NmJmLTlmNWYtYzM0ZjRlYWZlOTNlIn0.iwkQR96BKTJugh87_YOrDb7hXSWsv0RJXrqrqxHn3rwhcKNxwGnYAhTiQ12wyi-77-AFkzUlgs9E9pwgVi3_sE37QCVZ3YZzHjbg5crmT1EJ4f8gN8hw5cDqC3ny0R8rhgNzzirpZNe-i7SXzWCIySyEVh7MGFTPqNA-1ZlGh06FuOFRb22GVaHfrDkpE2RhkeZ-ZLlua9pbTcT1T9CihlCrW8JKTUwT2QspCwtnaJGs34iH77sHry31cTYVyOqd5q218tg_N4ky9iV6k7mK6b7uaPsjYHrtpfK1tp-9_MSp6Fqzw6wu_vrvg5WrIWwiREaz_wJj-SjIuBR5TlntdA',
|
||||
clientId: oidcClients.nextcloud.id,
|
||||
expired: true
|
||||
},
|
||||
{
|
||||
token:
|
||||
'eyJhbGciOiJSUzI1NiIsImtpZCI6Ijh1SER3M002cmY4IiwidHlwIjoiYXQrand0In0.eyJhdWQiOlsiMzY1NGE3NDYtMzVkNC00MzIxLWFjNjEtMGJkY2ZmMmI0MDU1Il0sImV4cCI6Mjc0Mzk3MjI4MywiaWF0IjoxNzQzOTY4NjgzLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0Iiwic3ViIjoiZjRiODlkYzItNjJmYi00NmJmLTlmNWYtYzM0ZjRlYWZlOTNlIn0.lZMEohQeOi6oKDsKLKDDRYJIJNedUilvCLCi6XLADcHPtKlFJbPqH8IuQxuzryeIYAnTILsjvTkxkHAeRoQZCXQR7oS5BguGx6MtQYjgj--GpLBQ39r_nz-SEfhKtuMzEzPsN1raxOH8jWbnPM7zHxf5NIz7AHDKtCSWRA3JlE9kgAU7S-RRc6xP_BYVPDB97J6k-xuO5zxcdNTb92j8pZWbPPokv6CGG9CTPNzcrNHf-M98M6GE8SVM-8R2MAbpUCqTkTc_O46GHEexZzif2Wg8K5O-htiSQnwumoXXN08zKHCzCAvSdSa9JRMB-cgP7jsM7I6itUBXWxgvWDK3rA',
|
||||
clientId: oidcClients.nextcloud.id,
|
||||
expired: false
|
||||
}
|
||||
];
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import test, { expect } from '@playwright/test';
|
||||
import { oidcClients, refreshTokens } from './data';
|
||||
import { accessTokens, idTokens, oidcClients, refreshTokens, users } from './data';
|
||||
import { cleanupBackend } from './utils/cleanup.util';
|
||||
import passkeyUtil from './utils/passkey.util';
|
||||
|
||||
@@ -116,10 +116,7 @@ test('End session without id token hint shows confirmation page', async ({ page
|
||||
|
||||
test('End session with id token hint redirects to callback URL', async ({ page }) => {
|
||||
const client = oidcClients.nextcloud;
|
||||
// Note: this token has expired, but it should be accepted by the logout endpoint anyways, per spec
|
||||
const idToken =
|
||||
'eyJhbGciOiJSUzI1NiIsImtpZCI6Ijh1SER3M002cmY4IiwidHlwIjoiSldUIn0.eyJhdWQiOiIzNjU0YTc0Ni0zNWQ0LTQzMjEtYWM2MS0wYmRjZmYyYjQwNTUiLCJlbWFpbCI6InRpbS5jb29rQHRlc3QuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImV4cCI6MTY5MDAwMDAwMSwiZmFtaWx5X25hbWUiOiJUaW0iLCJnaXZlbl9uYW1lIjoiQ29vayIsImlhdCI6MTY5MDAwMDAwMCwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdCIsIm5hbWUiOiJUaW0gQ29vayIsIm5vbmNlIjoib1cxQTFPNzhHUTE1RDczT3NIRXg3V1FLajdacXZITFp1XzM3bWRYSXFBUSIsInN1YiI6IjRiODlkYzItNjJmYi00NmJmLTlmNWYtYzM0ZjRlYWZlOTNlIn0.ruYCyjA2BNjROpmLGPNHrhgUNLnpJMEuncvjDYVuv1dAZwvOPfG-Rn-OseAgJDJbV7wJ0qf6ZmBkGWiifwc_B9h--fgd4Vby9fefj0MiHbSDgQyaU5UmpvJU8OlvM-TueD6ICJL0NeT3DwoW5xpIWaHtt3JqJIdP__Q-lTONL2Zokq50kWm0IO-bIw2QrQviSfHNpv8A5rk1RTzpXCPXYNB-eJbm3oBqYQWzerD9HaNrSvrKA7mKG8Te1mI9aMirPpG9FvcAU-I3lY8ky1hJZDu42jHpVEUdWPAmUZPZafoX8iYtlPfkoklDnHj_cdg4aZBGN5bfjM6xf1Oe_rLDWg';
|
||||
|
||||
const idToken = idTokens.filter((token) => token.expired)[0].token;
|
||||
let redirectedCorrectly = false;
|
||||
await page
|
||||
.goto(
|
||||
@@ -192,3 +189,91 @@ test('Using refresh token invalidates it for future use', async ({ request }) =>
|
||||
});
|
||||
expect(refreshResponse.status()).toBe(400);
|
||||
});
|
||||
|
||||
test.describe('Introspection endpoint', () => {
|
||||
const client = oidcClients.nextcloud;
|
||||
const validAccessToken = accessTokens.filter((token) => !token.expired)[0].token;
|
||||
test('without client_id and client_secret fails', async ({ request }) => {
|
||||
const introspectionResponse = await request.post('/api/oidc/introspect', {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
form: {
|
||||
token: validAccessToken
|
||||
}
|
||||
});
|
||||
|
||||
expect(introspectionResponse.status()).toBe(400);
|
||||
});
|
||||
|
||||
test('with client_id and client_secret succeeds', async ({ request }) => {
|
||||
const introspectionResponse = await request.post('/api/oidc/introspect', {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Authorization: 'Basic ' + Buffer.from(`${client.id}:${client.secret}`).toString('base64')
|
||||
},
|
||||
form: {
|
||||
token: validAccessToken
|
||||
}
|
||||
});
|
||||
|
||||
expect(introspectionResponse.status()).toBe(200);
|
||||
const introspectionBody = await introspectionResponse.json();
|
||||
expect(introspectionBody.active).toBe(true);
|
||||
expect(introspectionBody.token_type).toBe('access_token');
|
||||
expect(introspectionBody.iss).toBe('http://localhost');
|
||||
expect(introspectionBody.sub).toBe(users.tim.id);
|
||||
expect(introspectionBody.aud).toStrictEqual([oidcClients.nextcloud.id]);
|
||||
});
|
||||
|
||||
test('non-expired refresh_token can be verified', async ({ request }) => {
|
||||
const { token } = refreshTokens.filter((token) => !token.expired)[0];
|
||||
|
||||
const introspectionResponse = await request.post('/api/oidc/introspect', {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Authorization: 'Basic ' + Buffer.from(`${client.id}:${client.secret}`).toString('base64')
|
||||
},
|
||||
form: {
|
||||
token: token
|
||||
}
|
||||
});
|
||||
|
||||
expect(introspectionResponse.status()).toBe(200);
|
||||
const introspectionBody = await introspectionResponse.json();
|
||||
expect(introspectionBody.active).toBe(true);
|
||||
expect(introspectionBody.token_type).toBe('refresh_token');
|
||||
});
|
||||
|
||||
test('expired refresh_token can be verified', async ({ request }) => {
|
||||
const { token } = refreshTokens.filter((token) => token.expired)[0];
|
||||
|
||||
const introspectionResponse = await request.post('/api/oidc/introspect', {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Authorization: 'Basic ' + Buffer.from(`${client.id}:${client.secret}`).toString('base64')
|
||||
},
|
||||
form: {
|
||||
token: token
|
||||
}
|
||||
});
|
||||
|
||||
expect(introspectionResponse.status()).toBe(200);
|
||||
const introspectionBody = await introspectionResponse.json();
|
||||
expect(introspectionBody.active).toBe(false);
|
||||
});
|
||||
|
||||
test("expired access_token can't be verified", async ({ request }) => {
|
||||
const expiredAccessToken = accessTokens.filter((token) => token.expired)[0].token;
|
||||
const introspectionResponse = await request.post('/api/oidc/introspect', {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
form: {
|
||||
token: expiredAccessToken
|
||||
}
|
||||
});
|
||||
|
||||
expect(introspectionResponse.status()).toBe(400);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user