diff --git a/src/client/auth.test.ts b/src/client/auth.test.ts index 48be870bc..1b9fb0712 100644 --- a/src/client/auth.test.ts +++ b/src/client/auth.test.ts @@ -366,6 +366,31 @@ describe("OAuth Authorization", () => { expect(authorizationUrl.searchParams.has("scope")).toBe(false); }); + it("includes state parameter when provided", async () => { + const { authorizationUrl } = await startAuthorization( + "https://auth.example.com", + { + clientInformation: validClientInfo, + redirectUrl: "http://localhost:3000/callback", + state: "foobar", + } + ); + + expect(authorizationUrl.searchParams.get("state")).toBe("foobar"); + }); + + it("excludes state parameter when not provided", async () => { + const { authorizationUrl } = await startAuthorization( + "https://auth.example.com", + { + clientInformation: validClientInfo, + redirectUrl: "http://localhost:3000/callback", + } + ); + + expect(authorizationUrl.searchParams.has("state")).toBe(false); + }); + it("uses metadata authorization_endpoint when provided", async () => { const { authorizationUrl } = await startAuthorization( "https://auth.example.com", diff --git a/src/client/auth.ts b/src/client/auth.ts index 16f0a5505..7a91eb256 100644 --- a/src/client/auth.ts +++ b/src/client/auth.ts @@ -21,6 +21,11 @@ export interface OAuthClientProvider { */ get clientMetadata(): OAuthClientMetadata; + /** + * Returns a OAuth2 state parameter. + */ + state?(): string | Promise; + /** * Loads information about this OAuth client, as registered already with the * server, or returns `undefined` if the client is not registered with the @@ -162,10 +167,13 @@ export async function auth( } } + const state = provider.state ? await provider.state() : undefined; + // Start new authorization flow const { authorizationUrl, codeVerifier } = await startAuthorization(authorizationServerUrl, { metadata, clientInformation, + state, redirectUrl: provider.redirectUrl, scope: scope || provider.clientMetadata.scope, }); @@ -301,11 +309,13 @@ export async function startAuthorization( clientInformation, redirectUrl, scope, + state, }: { metadata?: OAuthMetadata; clientInformation: OAuthClientInformation; redirectUrl: string | URL; scope?: string; + state?: string; }, ): Promise<{ authorizationUrl: URL; codeVerifier: string }> { const responseType = "code"; @@ -347,6 +357,10 @@ export async function startAuthorization( ); authorizationUrl.searchParams.set("redirect_uri", String(redirectUrl)); + if (state) { + authorizationUrl.searchParams.set("state", state); + } + if (scope) { authorizationUrl.searchParams.set("scope", scope); }