diff --git a/src/grants/abstract/abstract_authorized.grant.ts b/src/grants/abstract/abstract_authorized.grant.ts index 21013293..c9a51790 100644 --- a/src/grants/abstract/abstract_authorized.grant.ts +++ b/src/grants/abstract/abstract_authorized.grant.ts @@ -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( @@ -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"); } diff --git a/src/utils/urls.ts b/src/utils/urls.ts new file mode 100644 index 00000000..ea8e84a6 --- /dev/null +++ b/src/utils/urls.ts @@ -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; + } +} diff --git a/test/e2e/grants/auth_code.grant.spec.ts b/test/e2e/grants/auth_code.grant.spec.ts index 91d7ceff..a74b4f09 100644 --- a/test/e2e/grants/auth_code.grant.spec.ts +++ b/test/e2e/grants/auth_code.grant.spec.ts @@ -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 () => {