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

Preserve exact original redirect URL in OAuth client #1281

Merged
merged 1 commit into from
Jun 8, 2023

Conversation

mjcheetham
Copy link
Collaborator

The OAuth 2.0 spec requires that redirect URLs be matched exactly if specified, including matching trailing slashes.

Since the .NET Uri type's .ToString() method will append a trailing slash to the end of path-less URLs (e.g., "http://foo" => "http://foo/") we need to use the .OriginalString property instead.

Shoring up this area in anticipation for changes to support multiple GitHub redirect URLs with #594

The OAuth 2.0 spec requires that redirect URLs be matched _exactly_ if
specified, including matching trailing slashes.

Since the .NET `Uri` type's `.ToString()` method will append a trailing
slash to the end of path-less URLs (e.g., "http://foo" => "http://foo/")
we need to use the `.OriginalString` property instead.
@mjcheetham mjcheetham added the auth:oauth Specific to OAuth2 authentication label Jun 6, 2023
@mjcheetham mjcheetham requested a review from ldennington June 6, 2023 23:22
Copy link
Contributor

@ldennington ldennington left a comment

Choose a reason for hiding this comment

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

LGTM! Just had a couple minor questions.

Comment on lines +80 to +93
IOAuth2WebBrowser browser = new TestOAuth2WebBrowser(httpHandler);

var redirectUri = new Uri(expectedRedirectUrl);

var trace2 = new NullTrace2();
OAuth2Client client = new OAuth2Client(
new HttpClient(httpHandler),
endpoints,
TestClientId,
trace2,
redirectUri,
TestClientSecret);

await client.GetAuthorizationCodeAsync(new[] { "unused" }, browser, null, CancellationToken.None);
Copy link
Contributor

Choose a reason for hiding this comment

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

We don't seem to be testing any outcomes here. Are we just validating that this works with these redirect URLs?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Look a little higher up at line 73:

server.AuthorizationEndpointInvoked += (_, request) =>
{
    IDictionary<string, string> actualParams = request.RequestUri.GetQueryParameters();
    Assert.True(actualParams.TryGetValue(OAuth2Constants.RedirectUriParameter, out string actualRedirectUri));
    Assert.Equal(expectedRedirectUrl, actualRedirectUri);
};

We are setting an event handler on the AuthorizationEndpointInvoked event, and performing the validation of the redirect_uri in there.

Copy link
Contributor

Choose a reason for hiding this comment

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

Got it! Thanks for clarifying.

{
return true;
}

// For localhost we ignore the port number
if (redirectUri.IsLoopback && uri.IsLoopback)
// For loopback URLs _only_ we ignore the port number
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm curious - why do we ignore ports for loopback urls?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is to mimic real-world OAuth2 authorisation servers, which typically treat loopback addresses (localhost, 127.0.0.1, ::1) as special.

Redirect URLs are normally supposed to match exactly, including port number. This is the location where the authorisation server will redirect the user agent to, including the authorisation code in the query params for the client to consume.

For public client applications (ones that run on an end-user device), we need to get the authorisation code back to the client application from a user agent somehow. You can achieve this in two ways:

  1. Using an application-specific URL like (myapp://oauth) that is registered with the operating system, or
  2. Using a local loopback URL, and have the client application open a port listening for that HTTP request.

For option 2, you can either explicitly define a port number to use for the redirect, or leave it blank. Leaving it blank, the client will pick any random free port before it makes the initial authorisation request. Since the port may not be known ahead of time, the authorization server cannot restrict ahead of time.

Why not just pick an explicit port? Well.. which one should we use? 😜
Obviously one of the ephemeral ports (49152–65535), but there's of course a non-zero (although small) chance we pick a port that's in use by something else on the system...

return cmp.Equals(redirectUri.Scheme, uri.Scheme) &&
cmp.Equals(redirectUri.Authority, uri.Authority) &&
cmp.Equals(redirectUri.Host, uri.Host) &&
Copy link
Contributor

Choose a reason for hiding this comment

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

Was this incorrect before?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes. I wanted to compare just the 'domain' part, not ports (as the comment said). But had Authority and Host backwards! 🤦

@mjcheetham mjcheetham merged commit 4e8674a into git-ecosystem:main Jun 8, 2023
@mjcheetham mjcheetham deleted the oauth-redirect-exact branch June 8, 2023 16:53
@mjcheetham mjcheetham mentioned this pull request Jun 26, 2023
mjcheetham added a commit that referenced this pull request Jun 26, 2023
**Changes:**

- Use in-proc methods for getting OS version number (#1240, #1264)
- Update System.CommandLine (#1265)
- Suppress GUI from command-line argument (#1267)
- Add github (login|logout|list) commands (#1267)
- cURL Cookie file support (#1251)
- Update target framework on Mac/Linux to .NET 7 (#1274, #1282)
- Replace JSON.NET with System.Text.Json (#1274)
- Preserve exact redirect URI formatting in OAuth requests (#1281)
- Use IP localhost redirect for GitHub (#1286)
- Use WWW-Authenticate headers from Git for Azure Repos authority
(#1288)
- Better GitHub Enterprise Managed User (EMU) account support (#1190)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
auth:oauth Specific to OAuth2 authentication
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants