Skip to content
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

fix: Check OAuth2 redirect URL for matching callback URL and authorization code in query parameters #2148

Open
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

dakshin-k
Copy link

Fixes #2147

Description

In an Authorization code flow, there may be multiple intermediate redirects before reaching the final one which matches the callback URL and has a code in the query params.

We should wait until we see a redirect URI that matches both the conditions. This fixes the issue where, when a redirect contains code as a query param but is not the final one (i.e., is not to the callback URL) an error is thrown saying the callback URL is invalid.

Contribution Checklist:

  • The pull request only addresses one issue or adds one feature.
  • The pull request does not introduce any breaking changes
  • I have added screenshots or gifs to help explain the change if applicable.
  • I have read the contribution guidelines.
  • Create an issue and link to the pull request.

… code in query parameters

In an Authorization code flow, there may be multiple intermediate redirects before reaching the final one which matches the callback URL and has a code in the query params.

We should wait until we see a redirect URI that matches both the conditions. This fixes the issue where, when a redirect contains `code` as a query param but is not the final one (i.e., is not to the callback URL) an error is thrown saying the callback URL is invalid.

Fixes usebruno#2147
@dakshin-k dakshin-k changed the title Check OAuth2 redirect URL for matching callback URL and authorization code in query parameters fix: Check OAuth2 redirect URL for matching callback URL and authorization code in query parameters Apr 21, 2024
@helloanoop
Copy link
Contributor

@lohit-1 Can you review this?

if (!url || !finalUrl.includes(callbackUrl)) {
reject(new Error('Invalid Callback Url'));
// check if the redirect is to the callback URL and if it contains an authorization code
if (url && url.includes(callbackUrl)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed a case where this check may not be sufficient (which is the problem in this version as it was in the original):
My keycloak server has a redirect uri set to exactly: https://example.com?param=value. As keycloak expects me to use callback url exactly as configured, I do so in bruno, but after I provide correct credentials, I get redirected to https://example.com/?param=value&code=..., whis does not pass the check url.includes(callbackUrl) (extra / breaks this).
Perhaps the string comparison using includes should be done on URL.href's rather than plain string values, to have a level of normalization?

let strurl1 = 'https://example.com?par=val'
let strurl2 = 'https://example.com/?par=val'
strurl1 == strurl2 // ==>false
new URL(strurl1) == new URL(strurl2); // ==> false
new URL(strurl1).href == new URL(strurl2).href; // ==> true
new URL(strurl1).href // => https://example.com/?par=val 
new URL(strurl2).href // => https://example.com/?par=val 

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@helloanoop @lohit-1 Do we have any unit tests on this behaviour? I was not able to find any when creating the PR but it seems like there are several edge cases here so I would like to add some if they are not there

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @dakshin-k, there aren't any unit tests in place for this behaviour currently.
Please feel free to add them. Thanks

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lohit-1 Have added some test cases, please review.

I couldn't write a test for the entire component, including mocking all the window events, so I just extracted that check into a separate function and wrote tests for it.

Comment on lines 4 to 6
const matchesCallbackUrl = (url, callbackUrl) => {
return url ? new URL(url).host == new URL(callbackUrl).host : false;
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope: my callback url is pointing to the same server that is auth url. It will pass this check and fail on missing 'code' even before the first redirect.
Screenshot from 2024-04-22 23-48-42

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh man, there are lots of edge cases here 😅

Comparing the href as you suggested earlier will not work as the href includes any query params in the URL, i.e. while it solves the trailing / problem it breaks for the below case:

callbackUrl: https://callback.url/endpoint
testUrl       : https://callback.url/endpoint?code=abcd

// testUrl.href = https://callback.url/endpoint?code=abcd

This is the reason I compared against hostname instead of href. I currently don't have a better idea to implement this, will think about it and come back later.

Ideas are welcome if I'm missing something 😇

Copy link
Contributor

@lohit-1 lohit-1 Apr 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function could be something similar to this @dakshin-k

const matchesCallbackUrl = (url, callbackUrl) => {
  let { origin: urlOrigin, pathname: urlPathname } = new URL(url);
  let { origin: callbackUrlOrigin, pathname: callbackUrlPathname } = new URL(callbackUrl);

  urlPathname = urlPathname?.at(-1) == '/' ? urlPathname?.slice(0, -1) : urlPathname;
  callbackUrlPathname = callbackUrlPathname?.at(-1) == '/' ? callbackUrlPathname?.slice(0, -1) : callbackUrlPathname;

  const urlWithoutQueryParams = `${urlOrigin}${urlPathname}`;
  const callbackUrlWithoutQueryParams = `${callbackUrlOrigin}${callbackUrlPathname}`;

  return url ? urlWithoutQueryParams == callbackUrlWithoutQueryParams : false;
};

Edit:
Not recommended, seems like too much of an overkill
The below solution should be enough

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't this suffice?
testUrl.href.startswith(callbackUrl.href)

The spec mandates the code param is appended the the query component, so it should be at the end. Also luckily the fragment component (the url part after #) is not allowed, so it does not have to be handled.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, that worked! I have pushed the change, would someone mind testing it out once?

I faced a weird problem testing locally, not sure if I introduced a bug or found an existing one 😅

I'm able to generate auth tokens successfully now, but I can't make any requests - When I click the send request button, it just shows me the response from my OAuth server with the access token, it doesn't actually make a request using that token or display that response:

Screenshot 2024-04-23 at 9 59 51 PM

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't mind that. That's actually #1999

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah okay, thanks!

So I guess that's it on this PR? Let me know if there's anything further to do!

@P-de-Jong
Copy link

I have verified that the changes in this PR allow for using Auth0 with Bruno. Would appreciate for this to get merged and released so I can roll out Bruno in our company :)

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

BUG: OAuth2 Authorization Code Flow fails for intermediate redirects with an auth code
5 participants