-
Notifications
You must be signed in to change notification settings - Fork 35
Parse callback URL reliable #86
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
1584476
8e4741f
5507e9b
c34d6e8
561c9cf
7b7f284
0eac71e
931690c
d0df222
f07c58a
859498a
1e8354e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,8 +1,106 @@ | ||||||||||||
| import { expect } from '@playwright/test' | ||||||||||||
| import { createTestBed, test } from '../support/testbed.ts' | ||||||||||||
| import type { KeycloakFlow, KeycloakResponseMode } from '../../lib/keycloak.js'; | ||||||||||||
|
|
||||||||||||
| test('throws when initializing multiple times', async ({ page, appUrl, authServerUrl }) => { | ||||||||||||
| const { executor } = await createTestBed(page, { appUrl, authServerUrl }) | ||||||||||||
| await executor.initializeAdapter() | ||||||||||||
| await expect(executor.initializeAdapter()).rejects.toThrow("A 'Keycloak' instance can only be initialized once.") | ||||||||||||
| }) | ||||||||||||
| }); | ||||||||||||
|
|
||||||||||||
| const standardParams = ['code', 'state', 'session_state', 'kc_action_status', 'kc_action', 'iss']; | ||||||||||||
| const implicitParams = ['access_token', 'token_type', 'id_token', 'state', 'session_state', 'expires_in', 'kc_action_status', 'kc_action', 'iss']; | ||||||||||||
| const hybridParams = ['access_token', 'token_type', 'id_token', 'code', 'state', 'session_state', 'expires_in', 'kc_action_status', 'kc_action', 'iss']; | ||||||||||||
| const errorParams = ['error', 'error_description', 'error_uri']; | ||||||||||||
|
|
||||||||||||
| [ | ||||||||||||
| { | ||||||||||||
| flow: 'standard', | ||||||||||||
| responseMode: 'fragment', | ||||||||||||
| params: [...standardParams, ...errorParams] | ||||||||||||
| }, | ||||||||||||
| { | ||||||||||||
| flow: 'standard', | ||||||||||||
| responseMode: 'query', | ||||||||||||
| params: [...standardParams, ...errorParams] | ||||||||||||
| }, | ||||||||||||
| { | ||||||||||||
| flow: 'implicit', | ||||||||||||
| responseMode: 'fragment', | ||||||||||||
| params: [...implicitParams, ...errorParams] | ||||||||||||
| }, | ||||||||||||
| { | ||||||||||||
| flow: 'hybrid', | ||||||||||||
| responseMode: 'fragment', | ||||||||||||
| params: [...hybridParams, ...errorParams] | ||||||||||||
| }, | ||||||||||||
| ].forEach(({ flow, responseMode, params }) => { | ||||||||||||
| const addRandomParams = (url: Readonly<URL>, params: string[], mode: string) => { | ||||||||||||
| const newUrl = new URL(url); | ||||||||||||
| for (const param of params) { | ||||||||||||
| if (mode === 'query') { | ||||||||||||
| newUrl.searchParams.set(param, `test-${param}`); | ||||||||||||
| } else { | ||||||||||||
| newUrl.hash = `${newUrl.hash ? newUrl.hash + '&' : ''}${param}=test-${param}`; | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
| return newUrl; | ||||||||||||
| }; | ||||||||||||
|
|
||||||||||||
| test(`[${responseMode} / ${flow}] should remove authorization response parameters from redirect URL`, async ({ page, appUrl, authServerUrl }) => { | ||||||||||||
| const { executor } = await createTestBed(page, { appUrl, authServerUrl }); | ||||||||||||
| const redirect = addRandomParams(appUrl, params, responseMode); | ||||||||||||
|
|
||||||||||||
| await page.goto(redirect.toString()); | ||||||||||||
| await executor.initializeAdapter({ | ||||||||||||
| responseMode: responseMode as KeycloakResponseMode, | ||||||||||||
| flow: flow as KeycloakFlow, | ||||||||||||
| redirectUri: appUrl.toString() | ||||||||||||
| }); | ||||||||||||
| // Wait for the adapter to process the redirect and clean up the URL | ||||||||||||
| await page.evaluate(() => { | ||||||||||||
| return new Promise((resolve) => setTimeout(resolve, 0)); | ||||||||||||
|
Comment on lines
+61
to
+62
|
||||||||||||
| await page.evaluate(() => { | |
| return new Promise((resolve) => setTimeout(resolve, 0)); | |
| await page.waitForURL((url) => { | |
| const cleanedUrl = new URL(url); | |
| return !params.some(param => responseMode === 'query' ? cleanedUrl.searchParams.has(param) : cleanedUrl.hash.includes(`${param}=`)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My idea here was to wait for all (async) JS to be executed before running assertions. I didn't take a closer look at executor.initializeAdapter yet. Maybe this call is not needed.
waitForUrl will slow down test execution in error cases
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not quite sure why we had this check here before, perhaps for security reasons, or to prevent accidental redirects. However, I agree, there is no reason to keep this code around, we validate the callbacks later on either way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Upon closer inspection, this is almost certainly to prevent accidentally stripping some parameters by accident, see #86 (review)