diff --git a/docs/oidc-client-ts.api.md b/docs/oidc-client-ts.api.md index 614d587e..d247e2a2 100644 --- a/docs/oidc-client-ts.api.md +++ b/docs/oidc-client-ts.api.md @@ -108,7 +108,7 @@ export type ExtraHeader = string | (() => string); export type ExtraSigninRequestArgs = Pick; // @public (undocumented) -export type ExtraSignoutRequestArgs = Pick; +export type ExtraSignoutRequestArgs = Pick; // Warning: (ae-forgotten-export) The symbol "Mandatory" needs to be exported by the entry point index.d.ts // @@ -336,7 +336,7 @@ export class OidcClient { // (undocumented) createSigninRequest({ state, request, request_uri, request_type, id_token_hint, login_hint, skipUserInfo, nonce, url_state, response_type, scope, redirect_uri, prompt, display, max_age, ui_locales, acr_values, resource, response_mode, extraQueryParams, extraTokenParams, dpopJkt, omitScopeWhenRequesting, }: CreateSigninRequestArgs): Promise; // (undocumented) - createSignoutRequest({ state, id_token_hint, client_id, request_type, post_logout_redirect_uri, extraQueryParams, }?: CreateSignoutRequestArgs): Promise; + createSignoutRequest({ state, id_token_hint, client_id, request_type, url_state, post_logout_redirect_uri, extraQueryParams, }?: CreateSignoutRequestArgs): Promise; // (undocumented) getDpopProof(dpopStore: DPoPStore, nonce?: string): Promise; // (undocumented) @@ -855,7 +855,7 @@ export type SignoutRedirectArgs = RedirectParams & ExtraSignoutRequestArgs; // @public (undocumented) export class SignoutRequest { - constructor({ url, state_data, id_token_hint, post_logout_redirect_uri, extraQueryParams, request_type, client_id, }: SignoutRequestArgs); + constructor({ url, state_data, id_token_hint, post_logout_redirect_uri, extraQueryParams, request_type, client_id, url_state, }: SignoutRequestArgs); // (undocumented) readonly state?: State; // (undocumented) @@ -877,6 +877,8 @@ export interface SignoutRequestArgs { state_data?: unknown; // (undocumented) url: string; + // (undocumented) + url_state?: string; } // @public (undocumented) @@ -890,6 +892,8 @@ export class SignoutResponse { error_uri: string | null; // (undocumented) readonly state: string | null; + // (undocumented) + url_state?: string; userState: unknown; } diff --git a/src/OidcClient.test.ts b/src/OidcClient.test.ts index 6af0e15a..14b0acba 100644 --- a/src/OidcClient.test.ts +++ b/src/OidcClient.test.ts @@ -1,7 +1,7 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. -import { CryptoUtils, JwtUtils } from "./utils"; +import { CryptoUtils, JwtUtils, URL_STATE_DELIMITER } from "./utils"; import type { ErrorResponse } from "./errors"; import type { JwtClaims } from "./Claims"; import { OidcClient } from "./OidcClient"; @@ -838,6 +838,7 @@ describe("OidcClient", () => { state: "foo", post_logout_redirect_uri: "bar", id_token_hint: "baz", + url_state: "qux", }); // assert @@ -847,6 +848,7 @@ describe("OidcClient", () => { expect(url).toContain("http://sts/signout"); expect(url).toContain("post_logout_redirect_uri=bar"); expect(url).toContain("id_token_hint=baz"); + expect(url).toContain(encodeURIComponent(URL_STATE_DELIMITER + "qux")); }); it("should pass params to SignoutRequest w/o id_token_hint and client_id", async () => { diff --git a/src/OidcClient.ts b/src/OidcClient.ts index ae12edb7..801a9bbc 100644 --- a/src/OidcClient.ts +++ b/src/OidcClient.ts @@ -331,6 +331,7 @@ export class OidcClient { id_token_hint, client_id, request_type, + url_state, post_logout_redirect_uri = this.settings.post_logout_redirect_uri, extraQueryParams = this.settings.extraQueryParams, }: CreateSignoutRequestArgs = {}): Promise { @@ -358,6 +359,7 @@ export class OidcClient { state_data: state, extraQueryParams, request_type, + url_state, }); // house cleaning diff --git a/src/SignoutRequest.test.ts b/src/SignoutRequest.test.ts index 3386d551..1d8dbc92 100644 --- a/src/SignoutRequest.test.ts +++ b/src/SignoutRequest.test.ts @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. import { SignoutRequest, type SignoutRequestArgs } from "./SignoutRequest"; +import { URL_STATE_DELIMITER } from "./utils"; describe("SignoutRequest", () => { @@ -70,6 +71,19 @@ describe("SignoutRequest", () => { expect(subject.url).toContain("state=" + subject.state!.id); }); + it("should include state if post_logout_redirect_uri and url_state provided even if state_data is empty", () => { + // arrange + delete settings.state_data; + settings.url_state = "foo"; + + // act + subject = new SignoutRequest(settings); + + // assert + expect(subject.state).toBeDefined(); + expect(subject.url).toContain("state=" + subject.state!.id + encodeURIComponent(URL_STATE_DELIMITER + "foo")); + }); + it("should not include state if no post_logout_redirect_uri provided", () => { // arrange delete settings.post_logout_redirect_uri; @@ -80,6 +94,29 @@ describe("SignoutRequest", () => { // assert expect(subject.url).not.toContain("state="); }); + + it("should include url_state", async () => { + // arrange + settings.url_state = "foo"; + + // act + subject = new SignoutRequest(settings); + + // assert + expect(subject.url).toContain("state=" + subject.state!.id + encodeURIComponent(URL_STATE_DELIMITER + "foo")); + }); + + it("should not include state or url_state if no post_logout_redirect_uri provided", () => { + // arrange + delete settings.post_logout_redirect_uri; + settings.url_state = "foo"; + + // act + subject = new SignoutRequest(settings); + + // assert + expect(subject.url).not.toContain("state="); + }); it("should include id_token_hint, post_logout_redirect_uri, and state", () => { // assert diff --git a/src/SignoutRequest.ts b/src/SignoutRequest.ts index a310f7dc..2f8795fe 100644 --- a/src/SignoutRequest.ts +++ b/src/SignoutRequest.ts @@ -1,7 +1,7 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. -import { Logger } from "./utils"; +import { Logger, URL_STATE_DELIMITER } from "./utils"; import { State } from "./State"; /** @@ -22,6 +22,7 @@ export interface SignoutRequestArgs { request_type?: string; /** custom "state", which can be used by a caller to have "data" round tripped */ state_data?: unknown; + url_state?: string; } /** @@ -35,7 +36,7 @@ export class SignoutRequest { public constructor({ url, - state_data, id_token_hint, post_logout_redirect_uri, extraQueryParams, request_type, client_id, + state_data, id_token_hint, post_logout_redirect_uri, extraQueryParams, request_type, client_id, url_state, }: SignoutRequestArgs) { if (!url) { this._logger.error("ctor: No url passed"); @@ -53,10 +54,15 @@ export class SignoutRequest { if (post_logout_redirect_uri) { parsedUrl.searchParams.append("post_logout_redirect_uri", post_logout_redirect_uri); - if (state_data) { - this.state = new State({ data: state_data, request_type }); + // Add state if either data needs to be stored, or url_state set for an intermediate proxy + if (state_data || url_state) { + this.state = new State({ data: state_data, request_type, url_state }); - parsedUrl.searchParams.append("state", this.state.id); + let stateParam = this.state.id; + if (url_state) { + stateParam = `${stateParam}${URL_STATE_DELIMITER}${url_state}`; + } + parsedUrl.searchParams.append("state", stateParam); } } diff --git a/src/SignoutResponse.test.ts b/src/SignoutResponse.test.ts index 4c0585e5..791a3110 100644 --- a/src/SignoutResponse.test.ts +++ b/src/SignoutResponse.test.ts @@ -38,5 +38,14 @@ describe("SignoutResponse", () => { // assert expect(subject.state).toEqual("foo"); }); + + it("should read url_state", () => { + // act + const subject = new SignoutResponse(new URLSearchParams("state=foo;bar")); + + // assert + expect(subject.state).toEqual("foo"); + expect(subject.url_state).toEqual("bar"); + }); }); }); diff --git a/src/SignoutResponse.ts b/src/SignoutResponse.ts index d355734b..9646e0ce 100644 --- a/src/SignoutResponse.ts +++ b/src/SignoutResponse.ts @@ -1,6 +1,8 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. +import { URL_STATE_DELIMITER } from "./utils"; + /** * @public * @see https://openid.net/specs/openid-connect-core-1_0.html#AuthError @@ -18,9 +20,17 @@ export class SignoutResponse { /** custom state data set during the initial signin request */ public userState: unknown; + public url_state?: string; public constructor(params: URLSearchParams) { this.state = params.get("state"); + if (this.state) { + const splitState = decodeURIComponent(this.state).split(URL_STATE_DELIMITER); + this.state = splitState[0]; + if (splitState.length > 1) { + this.url_state = splitState.slice(1).join(URL_STATE_DELIMITER); + } + } this.error = params.get("error"); this.error_description = params.get("error_description"); diff --git a/src/UserManager.test.ts b/src/UserManager.test.ts index c78745c1..15cd6cf0 100644 --- a/src/UserManager.test.ts +++ b/src/UserManager.test.ts @@ -1001,6 +1001,7 @@ describe("UserManager", () => { extraQueryParams: { q : "q" }, state: "state", post_logout_redirect_uri: "http://app/extra_callback", + url_state: "foo", }; // act diff --git a/src/UserManager.ts b/src/UserManager.ts index 84ee1eba..180def74 100644 --- a/src/UserManager.ts +++ b/src/UserManager.ts @@ -25,7 +25,7 @@ export type ExtraSigninRequestArgs = Pick; +export type ExtraSignoutRequestArgs = Pick; /** * @public