Skip to content

Commit

Permalink
feat: support redirect uri with port
Browse files Browse the repository at this point in the history
resolves #104
  • Loading branch information
jasonraimondi committed Feb 11, 2024
1 parent d7de14f commit aeec5a8
Show file tree
Hide file tree
Showing 3 changed files with 57 additions and 24 deletions.
3 changes: 2 additions & 1 deletion src/grants/abstract/abstract_authorized.grant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { OAuthClient } from "../../entities/client.entity.js";
import { OAuthException } from "../../exceptions/oauth.exception.js";
import { RequestInterface } from "../../requests/request.js";
import { AbstractGrant } from "./abstract.grant.js";
import { urlsAreSameIgnoringPort } from "../../utils/urls.js";

export abstract class AbstractAuthorizedGrant extends AbstractGrant {
protected makeRedirectUrl(
Expand Down Expand Up @@ -56,7 +57,7 @@ export abstract class AbstractAuthorizedGrant extends AbstractGrant {
}

const redirectUriWithoutQuery = redirectUri.split("?")[0];
if (!client.redirectUris.includes(redirectUriWithoutQuery)) {
if (!client.redirectUris.some(uri => urlsAreSameIgnoringPort(redirectUriWithoutQuery, uri))) {
throw OAuthException.invalidClient("Invalid redirect_uri");
}

Expand Down
15 changes: 15 additions & 0 deletions src/utils/urls.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export function urlsAreSameIgnoringPort(url1: string, url2: string) {
try {
const parsedUrl1 = new URL(url1);
const parsedUrl2 = new URL(url2);

// Compare protocol, hostname, and pathname to ensure URLs are the same, ignoring port
return (
parsedUrl1.protocol === parsedUrl2.protocol &&
parsedUrl1.hostname === parsedUrl2.hostname &&
parsedUrl1.pathname === parsedUrl2.pathname
);
} catch (error) {
return false;
}
}
63 changes: 40 additions & 23 deletions test/e2e/grants/auth_code.grant.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,30 +164,47 @@ describe("authorization_code grant", () => {
expect(authorizationRequest.scopes).toStrictEqual([{ name: "scope-1" }]);
});

it("is successful with request redirect uri with querystring", async () => {
client.redirectUris = ["http://example.com"];
inMemoryDatabase.clients[client.id] = client;
request = new OAuthRequest({
query: {
response_type: "code",
client_id: client.id,
redirect_uri: "http://example.com?this_should_work=true&also-this=yeah",
scope: "scope-1",
state: "state-is-a-secret",
code_challenge: codeChallenge, // code verifier plain
code_challenge_method: "S256",
},
// prettier-ignore
[
{
testName: "is successful with redirect uri with querystring",
allowed: ["http://oauth2.example.com"],
received: "http://oauth2.example.com?this_should_work=true&also-this=yeah",
},
{
testName: "is successful with redirect uri with port",
allowed: ["http://oauth2.example.com/callback"],
received: "http://oauth2.example.com:3000/callback",
},
{
testName: "is successful with application style redirect uri",
allowed: ["com.exampleapp.oauth2://callback"],
received: "com.exampleapp.oauth2://callback",
},
{
testName: "is successful with application style redirect uri with port",
allowed: ["com.exampleapp.oauth2://callback"],
received: "com.exampleapp.oauth2://callback:3000",
},
].map(({ testName, allowed, received }) => {
it(testName, async () => {
client.redirectUris = allowed;
inMemoryDatabase.clients[client.id] = client;
request = new OAuthRequest({
query: {
response_type: "code",
client_id: client.id,
redirect_uri: received,
scope: "scope-1",
state: "state-is-a-secret",
code_challenge: codeChallenge, // code verifier plain
code_challenge_method: "S256",
},
});
const authorizationRequest = await grant.validateAuthorizationRequest(request);

expect(authorizationRequest.redirectUri).toBe(received);
});
const authorizationRequest = await grant.validateAuthorizationRequest(request);

expect(authorizationRequest.isAuthorizationApproved).toBe(false);
expect(authorizationRequest.client.id).toBe(client.id);
expect(authorizationRequest.client.name).toBe(client.name);
expect(authorizationRequest.redirectUri).toBe("http://example.com?this_should_work=true&also-this=yeah");
expect(authorizationRequest.state).toBe("state-is-a-secret");
expect(authorizationRequest.codeChallenge).toBe(codeChallenge);
expect(authorizationRequest.codeChallengeMethod).toBe("S256");
expect(authorizationRequest.scopes).toStrictEqual([{ name: "scope-1" }]);
});

it("is successful without using PKCE flow", async () => {
Expand Down

0 comments on commit aeec5a8

Please sign in to comment.