-
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?
Conversation
|
Thanks for the PR @twobiers, could I ask you to do a rebase? I have made some significant changes, but the general logic should be intact. |
c232659 to
357d19f
Compare
|
@jonkoops Of course you can ask^^ |
|
|
||
| if (parsed && parsed.oauthParams) { | ||
| if (this.flow === 'standard' || this.flow === 'hybrid') { | ||
| if ((parsed.oauthParams.code || parsed.oauthParams.error) && parsed.oauthParams.state) { |
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)
jonkoops
left a comment
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.
This PR generally looks good to me, I just want to make sure this does not introduce any unexpected behavior. I am worried the client application could be using some of the parameters in supportedParams as their own state. For example:
http://myapp.com/?error=someerror
In this case, will initializing Keycloak JS remove this parameter from the URL? That might break expectations of the client. This is likely why newUrl was guarded before on the required paramaters being present (e.g. code, access_token, error).
|
@jonkoops That is a very valid concern I didn't have on my mind. Keycloak-js will only replace the location when This will be the case when at least one oauth parameter from I think we could guard against this by only replacing the window location if it is the desired redirectUri from the authentication request. What do you think? |
Could you elaborate a little on how this would work? I am trying to imagine all the various scenarios, feel free to push a commit with some implementation if that is easier. |
|
See 1f632f5 This will only strip parameters if on a configured redirect uri. Either globally or from a previous login request. So in the case where an authorization flow triggered externally (like registering the user over API, passing redirect URI through E-Mail) the parameters will get removed if the redirect URI matches the one configured in keycloak-js globally. This is the only valid solution I could come up with, without taking the risk of removing random unrelated parameters. |
|
Just did a little bit of testing, and this bug already seems to exist, although this PR does intensify it. For example, if I use the following URL: If we now initialize Keycloak JS, it will remove the |
|
@twobiers let's try your solution and do some testing. Can you push your commit? |
|
@jonkoops I have pushed everything I have. Are you missing something? I would like to bring some unit tests to this branch aswell. Not very familiar with the test setup and if it's testable nice, but I would like to give it a shot. |
I am, and I was looking at an old diff, so that explains. It should be pretty easy to add some tests to validate this behavior, check out the instructions in the |
|
Looks like the changes as they are now will cause parameters not to be removed from the URL after signing in successfully. We'll have to come up with a different solution. |
|
I am going to raise some PR(s) to clean up the existing logic around parsing callback URLs, I'll post here when I have something. |
I see. I've added a (probably too broad to keep) test suite to validate the desired outcome, and the functionality works like I expected it, but it looks like the change breaks other assumption. |
|
@jonkoops I think I found an option that is compatible with the previous behavior and removes parameters in a reasonable approach. The condition to parameter removal is now either a valid callback from a previous login attempt OR the user agent is located on an explicitly configured redirect URI.
|
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.
Pull Request Overview
This PR improves the reliability of parsing and clearing OAuth callback parameters from window.location and adds end-to-end tests to verify that behavior across flows and response modes.
- Refactor
#processInitto only replace URL when on a known redirect URI or after a valid callback - Simplify callback parameter parsing logic and tighten state lookup
- Add Playwright tests to confirm removal/preservation of parameters for standard, implicit, and hybrid flows
Reviewed Changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| test/tests/init.spec.ts | Adds parameterized tests to verify removal or retention of auth response parameters |
| lib/keycloak.js | Refines URL replacement in #processInit, callback validation, and parsing logic |
Comments suppressed due to low confidence (2)
test/tests/init.spec.ts:38
- [nitpick] The helper name
addRandomParamsis ambiguous—consider renaming it to something likeaddAuthResponseParamsto clarify that it appends OAuth callback parameters.
const addRandomParams = (url: Readonly<URL>, params: string[], mode: string) => {
test/tests/init.spec.ts:37
- Only fragment mode is tested for implicit and hybrid flows. If query-mode is supported for these flows, add test cases for
[implicit / query]and[hybrid / query]to improve coverage.
].forEach(({ flow, responseMode, params }) => {
| await page.evaluate(() => { | ||
| return new Promise((resolve) => setTimeout(resolve, 0)); |
Copilot
AI
Jul 11, 2025
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.
[nitpick] Using a zero-delay setTimeout for waiting is brittle; consider using page.waitForURL() or listening for a history event to get a more deterministic synchronization point.
| 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
d371a06 to
2422d7c
Compare
Signed-off-by: twobiers <[email protected]>
Co-authored-by: Jon Koops <[email protected]> Signed-off-by: Tobi <[email protected]>
Signed-off-by: twobiers <[email protected]>
Signed-off-by: twobiers <[email protected]>
Signed-off-by: twobiers <[email protected]>
Signed-off-by: twobiers <[email protected]>
Signed-off-by: twobiers <[email protected]>
Signed-off-by: twobiers <[email protected]>
Signed-off-by: twobiers <[email protected]>
Signed-off-by: twobiers <[email protected]>
Signed-off-by: twobiers <[email protected]>
Signed-off-by: twobiers <[email protected]>
2422d7c to
1e8354e
Compare
Rebased and tested again. Now Line 1015 in 7a67a97
And here it is the redirect URI used for the Authorization Request. Line 1024 in 7a67a97
There is an overlap between these two, and they share semantics, however for the sake of this PR it means I had to introduce an additional check |
This PR partially addresses #85 by parsing the redirect params more reliable and remove them from the window location.
With this change in place the
stateparameter is not required to properly parse the redirect params and remove them from the window location.The state is still required for a successful authorization flow, so if the state is not present, keycloak-js will remove the parameters but doesn't treat it as valid.
That would fix the issue in our particular context where the WAF is blocking the second login attempt due to double encoding