diff --git a/CHANGELOG.md b/CHANGELOG.md index bc9da0cbcd..61acc5f619 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ The following changes have been implemented but not released yet: implemented. For Solid apps we recommend the use of a [Public Client Identifier Document](https://docs.inrupt.com/developer-tools/javascript/client-libraries/tutorial/authenticate-client/) +- We've also removed support for the iframe-based session renewal. ### Bugfixes @@ -29,28 +30,28 @@ The following changes have been implemented but not released yet: ### Breaking change - the `useEssSession` option is deprecated, and the associated session endpoint -is no longer used. Note that this option defaulted to false, and no public ESS -instance was enabling this endpoint, so unless you were explicitly using this -feature in an ESS instance you were running yourself, this change should not -affect you. If you were using this in a demo app, you may want to clear its local -storage. + is no longer used. Note that this option defaulted to false, and no public ESS + instance was enabling this endpoint, so unless you were explicitly using this + feature in an ESS instance you were running yourself, this change should not + affect you. If you were using this in a demo app, you may want to clear its local + storage. ### Bugfix #### browser -- The refresh flow was broken for browser-based applications using a client identifier, -leading to short session lifetime. Now that this is fixed, the background refresh -will happen normally, and the session will remain active. +- The refresh flow was broken for browser-based applications using a client identifier, + leading to short session lifetime. Now that this is fixed, the background refresh + will happen normally, and the session will remain active. - The incoming redirect sometimes left OAuth parameters on the URL, despite -already having consumed them, this only happened in certain error scenarios, -but now the parameters will always be removed, such that the user doesn't get -stuck at an error. + already having consumed them, this only happened in certain error scenarios, + but now the parameters will always be removed, such that the user doesn't get + stuck at an error. #### node - The client credential flow as implemented by the Community Solid Server Identity -Provider is now supported. See [the documentation](https://github.com/CommunitySolidServer/CommunitySolidServer/blob/main/documentation/client-credentials.md) for more details. + Provider is now supported. See [the documentation](https://github.com/CommunitySolidServer/CommunitySolidServer/blob/main/documentation/client-credentials.md) for more details. ## 1.11.7 - 2022-03-16 @@ -58,8 +59,8 @@ Provider is now supported. See [the documentation](https://github.com/CommunityS - @inrupt/oidc-client-ext: remove ts-jest from package dependencies. - The PKCE verifier is now cleared from storage as soon as it has been used in the -token exchange, regardless of the token type (it used to not be cleared from part -of the storage when getting a DPoP-bound token). + token exchange, regardless of the token type (it used to not be cleared from part + of the storage when getting a DPoP-bound token). ## 1.11.6 - 2022-03-04 @@ -69,7 +70,7 @@ of the storage when getting a DPoP-bound token). - Silent authentication is only attempted once, and no longer retries indefinitely on failure. - Default values are provided for the OIDC Provider supported scopes if not present -in the configuration. This fixes https://github.com/inrupt/solid-client-authn-js/issues/1991. + in the configuration. This fixes https://github.com/inrupt/solid-client-authn-js/issues/1991. ## 1.11.5 - 2022-02-14 @@ -82,9 +83,9 @@ No changes; fixed issue with npm publish. #### node and browser - The HTU field of the DPoP header is now normalized to remove the query parameters. -Thanks to @diegoaraujo for his first contribution to the project! + Thanks to @diegoaraujo for his first contribution to the project! - The Solid-OIDC discovery implemented a deprecated method, and is now updated to -align with the latest [Solid-OIDC specification](https://solid.github.io/solid-oidc/#discovery). + align with the latest [Solid-OIDC specification](https://solid.github.io/solid-oidc/#discovery). ## 1.11.3 - 2021-08-24 @@ -97,17 +98,17 @@ align with the latest [Solid-OIDC specification](https://solid.github.io/solid-o #### node and browser - Passing custom headers to a session's fetch as a Headers object would result in the headers -being overlooked. + being overlooked. - As per [the Solid-OIDC spec](https://solid.github.io/solid-oidc/#webid-scope), the `webid` -scope is now added to token requests. + scope is now added to token requests. - The ID token is no longer kept in storage. #### oidc -- Since `oidc-client` has been deprecated, and won't be maintained anymore, the -OIDC package now depends on a fork, `@inrupt/oidc-client`, so that we can ensure -the dependencies are kept up-to-date. This should be transparent for users of -`@inrupt/solid-client-authn-browser`. +- Since `oidc-client` has been deprecated, and won't be maintained anymore, the + OIDC package now depends on a fork, `@inrupt/oidc-client`, so that we can ensure + the dependencies are kept up-to-date. This should be transparent for users of + `@inrupt/solid-client-authn-browser`. ## 1.11.2 - 2021-08-24 @@ -116,34 +117,34 @@ the dependencies are kept up-to-date. This should be transparent for users of #### oidc - When dynamically registering a Client to a Solid Identity Provider, the subject -type was incorrectly set to `pairwise`, instead of `public`. Only `public` makes -sense in the context of Solid, where subjects (in this case, users) are uniquely -identified by their WebID. This was disregarded by current Solid Identity Providers, -so it should not have affected dependants, but it's technically more correct. + type was incorrectly set to `pairwise`, instead of `public`. Only `public` makes + sense in the context of Solid, where subjects (in this case, users) are uniquely + identified by their WebID. This was disregarded by current Solid Identity Providers, + so it should not have affected dependants, but it's technically more correct. #### node - The `prompt=consent` parameter was missing when redirecting the user to the Solid -Identity Provider authorization endpoint. This prevented working with the Community -Solid Server Identity Provider. + Identity Provider authorization endpoint. This prevented working with the Community + Solid Server Identity Provider. - Proactive refreshing of the token prevented NodeJS from shutting down gracefully. -Logging out now clear the timeout previously set, which resolves the issue. + Logging out now clear the timeout previously set, which resolves the issue. ## 1.11.1 - 2021-08-20 #### oidc - When dynamically registering a Client to a Solid Identity Provider, the subject -type was incorrectly set to `pairwise`, instead of `public`. Only `public` makes -sense in the context of Solid, where subjects (in this case, users) are uniquely -identified by their WebID. This was disregarded by current Solid Identity Providers, -so it should not have affected dependants, but it's technically more correct. + type was incorrectly set to `pairwise`, instead of `public`. Only `public` makes + sense in the context of Solid, where subjects (in this case, users) are uniquely + identified by their WebID. This was disregarded by current Solid Identity Providers, + so it should not have affected dependants, but it's technically more correct. #### node - The `prompt=consent` parameter was missing when redirecting the user to the Solid -Identity Provider authorization endpoint. This prevented working with the Community -Solid Server Identity Provider. + Identity Provider authorization endpoint. This prevented working with the Community + Solid Server Identity Provider. ## 1.11.0 - 2021-08-12 @@ -152,22 +153,22 @@ Solid Server Identity Provider. #### browser - Use refresh tokens to keep the sesion alive: The browser client now requests a -refresh token, and uses it when its access token is about to expire to get a new -access token. This enables keeping a session alive for longer than the lifetime -of a single access token. + refresh token, and uses it when its access token is about to expire to get a new + access token. This enables keeping a session alive for longer than the lifetime + of a single access token. - The `Session` class now exposes an `onError` method, which is a hook where -error-handling callbacks may be registered. + error-handling callbacks may be registered. - The `Session` class now exposes an `onSessionExpiration` method, which is a hook -where a callback may be registered to handle session expiration in the case when -silent authentication fails. + where a callback may be registered to handle session expiration in the case when + silent authentication fails. ### Bugfixes #### node - Trying to log a session in providing dynamically registered client credentials -along with a refresh token was mistaken for a static client login, leading to an -"Invalid client credentials" error. + along with a refresh token was mistaken for a static client login, leading to an + "Invalid client credentials" error. ## 1.10.1 - 2021-08-02 @@ -176,8 +177,8 @@ along with a refresh token was mistaken for a static client login, leading to an #### node - A transitive dependency used submodule exports, which aren't supported yet by -significant parts of the ecosystem, such as Jest. With an internal change, we enabled -using @inrupt/solid-client-authn-node without encountering submodule exports. + significant parts of the ecosystem, such as Jest. With an internal change, we enabled + using @inrupt/solid-client-authn-node without encountering submodule exports. ## 1.10.0 - 2021-07-28 @@ -186,14 +187,14 @@ using @inrupt/solid-client-authn-node without encountering submodule exports. #### node - DPoP-bound refresh tokens are now supported, which allows for an increased protection -against refresh token extraction. + against refresh token extraction. - Client credential grant: for Solid Identity Providers which support it, a client -may statically register, and use the obtained credentials (client ID and secret) -to log in to an Identity Provider. This is convenient in some cases, such as CI -environment. However, it requires offline provider/client interaction, which does -not scale well in the decentralized ecosystem of Solid. As such, it should only be -used in specific cases, where the user is able to statically register their app -to their identity provider (which requires some technical background). + may statically register, and use the obtained credentials (client ID and secret) + to log in to an Identity Provider. This is convenient in some cases, such as CI + environment. However, it requires offline provider/client interaction, which does + not scale well in the decentralized ecosystem of Solid. As such, it should only be + used in specific cases, where the user is able to statically register their app + to their identity provider (which requires some technical background). ### Bugs fixed @@ -210,7 +211,7 @@ to their identity provider (which requires some technical background). - Trying to call `Session.fetch` for a Session that had not yet authenticated would result in the following error: - 'fetch' called on an object that does not implement interface Window. + 'fetch' called on an object that does not implement interface Window. ## 1.9.0 - 2021-06-16 @@ -296,8 +297,8 @@ The following sections document changes that have been released already: #### node - It is now possible to specify a callback when constructing a function in order -to invoke custom code when the refresh token is rotated. This is useful for users -who wish to run authenticated scripts, without implementing a brand new storage. + to invoke custom code when the refresh token is rotated. This is useful for users + who wish to run authenticated scripts, without implementing a brand new storage. ## 1.7.4 - 2021-04-15 @@ -306,14 +307,14 @@ who wish to run authenticated scripts, without implementing a brand new storage. #### node and oidc - The OIDC issuer profile is used to negotiate the preferred signature algorithm -for ID tokens. + for ID tokens. #### node - During client registration, the client explicitly specifies both the 'refresh_token' -and the 'authorization_code' grants as part of its profile, instead of only relying -on scopes to get refresh tokens. Depending on the Identity Provider, the former -behaviour could result in not getting refresh tokens. + and the 'authorization_code' grants as part of its profile, instead of only relying + on scopes to get refresh tokens. Depending on the Identity Provider, the former + behaviour could result in not getting refresh tokens. ## 1.7.3 - 2021-04-09 @@ -333,29 +334,29 @@ behaviour could result in not getting refresh tokens. #### browser and node - When loaded in the same environment (e.g. a full-stack NextJS app), it is no longer -possible that the browser and node code get mixed together, resulting in code being -executed in the wrong environment. + possible that the browser and node code get mixed together, resulting in code being + executed in the wrong environment. ## 1.7.2 - 2021-03-10 #### browser and node - A client WebID can now be provided as part of the `login` options. The library will -check for compliance of the chosen Solid Identity Provider, and go use the provided -client WebID or go through Dynamic Client Registration accordingly. + check for compliance of the chosen Solid Identity Provider, and go use the provided + client WebID or go through Dynamic Client Registration accordingly. ### Bugfixes #### browser -- Attempting to log in with a hash fragment in the redirect URL no longer throws, -the hash fragment is simply discarded. +- Attempting to log in with a hash fragment in the redirect URL no longer throws, + the hash fragment is simply discarded. - The ID token is now validated when asking for DPoP-bound tokens, and not only when asking for a Bearer token. #### node -- The OIDC parameters added to the redirect IRI by the Solid Identity Provider -are no longer included in the redirect IRI provided at the token endpoint. +- The OIDC parameters added to the redirect IRI by the Solid Identity Provider + are no longer included in the redirect IRI provided at the token endpoint. - The provided redirect IRI is now normalized. ## 1.7.1 - 2021-03-04 @@ -372,17 +373,17 @@ are no longer included in the redirect IRI provided at the token endpoint. #### browser -- New option `useEssSession` for `session.handleIncomingRedirect`: Control to -enable and disble the behaviour introduced in 1.4.0. If set to false, the -`/session` endpoint isn't looked up, and cookie-based auth is disabled. The -behaviour is similar when `restorePreviousSession` is true. +- New option `useEssSession` for `session.handleIncomingRedirect`: Control to + enable and disble the behaviour introduced in 1.4.0. If set to false, the + `/session` endpoint isn't looked up, and cookie-based auth is disabled. The + behaviour is similar when `restorePreviousSession` is true. ### Bugfixes #### browser - Some components of the redirect URL are no longer lost after redirect, which -prevents silent authentication from failing. + prevents silent authentication from failing. ## 1.6.1 - 2021-02-26 @@ -417,7 +418,7 @@ prevents silent authentication from failing. #### oidc - `validateIdToken`: A function to check that an ID token has been signed by the -correct issuer, and that it contains some expected values. + correct issuer, and that it contains some expected values. #### browser @@ -425,7 +426,7 @@ correct issuer, and that it contains some expected values. the developer to register an event callback that will be called whenever a session is restored (e.g., due to a browser page refresh). The callback is given a URL parameter, which represents the current URL of the browser - *before* the session restoration (to allow the developer to restore their + _before_ the session restoration (to allow the developer to restore their app's state if needed, e.g., if the app is a Single Page App (SPA) and the developer wishes to restore the users 'current page' to exactly where they were before the refresh). @@ -435,14 +436,14 @@ correct issuer, and that it contains some expected values. #### browser - Refreshing the page no longer logs the session out, no matter what Resource Server -the data is collected from. -- When a session expires, the session is now marked as logged out, and a + the data is collected from. +- When a session expires, the session is now marked as logged out, and a `logout` event is thrown. - The 'client_id' option, if specified as an option when logging in, is now stored in storage, ready to be retrieved again from storage when the login flow redirects back to the client application (previously it was only being stored if DCR was invoked). -- The issuer URL associated with the session is now necessarily the __canonical__ +- The issuer URL associated with the session is now necessarily the **canonical** issuer's URL, instead of potentially including/missing a trailing slash. ## 1.5.1 - 2020-02-03 @@ -453,8 +454,8 @@ the data is collected from. - Deprecated SessionManager - The implicit flow is no longer supported. However, no known Solid Identity issuer -only supports the implicit flow and not the auth code flow, and no user-facing -controls enable choosing one's flow, so this has no user impact. + only supports the implicit flow and not the auth code flow, and no user-facing + controls enable choosing one's flow, so this has no user impact. ### New features @@ -478,11 +479,11 @@ controls enable choosing one's flow, so this has no user impact. #### node - `getSessionFromStorage`: a function to retrieve a session from storage based on -its session ID (for multi-session management). + its session ID (for multi-session management). - `getSessionIdFromStorageAll`: a function to retrieve the session IDs for all stored -sessions. + sessions. - `clearSessionFromStorageAll`: a function to clear all information about all sessions in -storage. + storage. ### Bugfix @@ -490,8 +491,8 @@ storage. #### node -- Building multiple sessions with the default storage re-initialized a new storage -each time. +- Building multiple sessions with the default storage re-initialized a new storage + each time. ## 1.4.2 - 2020-01-19 @@ -508,7 +509,7 @@ each time. #### node - For `solid-client-authn-node`, the `secureStorage` and `insecureStorage` are -deprecated, and replaced by `storage`. + deprecated, and replaced by `storage`. ### Bugfix @@ -523,25 +524,25 @@ deprecated, and replaced by `storage`. ### New features - Updating the browser window will no longer log the user out if their WebID is -hosted on an ESS instance (such as https://pod.inrupt.com). A better, global -solution will be implemented later in order not to break compatibility in the -ecosystem. The current solution is based on a custom `/session` endpoint lookup, -and a Resource Server cookie. + hosted on an ESS instance (such as https://pod.inrupt.com). A better, global + solution will be implemented later in order not to break compatibility in the + ecosystem. The current solution is based on a custom `/session` endpoint lookup, + and a Resource Server cookie. ## 1.3.0 - 2020-01-06 ### New feature - Although still possible, it is now no longer required to manually instantiate -a new `Session` object when using `solid-client-authn-browser`. Instead, you can -directly import `fetch`, `login`, `logout` and `handleIncomingRedirect`, -which will instantiate a new Session implicitly behind the scenes. If you do -need access to this Session, you can do so using the new function -`getDefaultSession`. + a new `Session` object when using `solid-client-authn-browser`. Instead, you can + directly import `fetch`, `login`, `logout` and `handleIncomingRedirect`, + which will instantiate a new Session implicitly behind the scenes. If you do + need access to this Session, you can do so using the new function + `getDefaultSession`. ### Bugfix -- The `session.info.isLoggedIn` property is now set to false on logout. +- The `session.info.isLoggedIn` property is now set to false on logout. ## 1.2.6 - 2020-12-23 @@ -558,22 +559,22 @@ need access to this Session, you can do so using the new function ## 1.2.4 - 2020-12-17 - `ajv` was imported through a dependency instead of being explicitly declared as -a direct dependency of `solid-client-authn-core` + a direct dependency of `solid-client-authn-core` ## 1.2.3 - 2020-12-17 ### Bugfix - The `browser` entry in the `package.json` was incorrect, leading to issues when -bundling the library. + bundling the library. ## 1.2.2 - 2020-12-16 ### Bugfix -- The WebID is now REALLY set on the session when logging in a script. The initial -fix introduced in 1.2.1 did compute the WebID from the identity provider response, -but did not set it properly on the session. +- The WebID is now REALLY set on the session when logging in a script. The initial + fix introduced in 1.2.1 did compute the WebID from the identity provider response, + but did not set it properly on the session. The following sections document changes that have been released already: @@ -582,26 +583,25 @@ The following sections document changes that have been released already: ### Bugfix - Addressed part of issue https://github.com/inrupt/solid-client-authn-js/issues/684, -by providing a `browser` entry in the `package.json` file. The ES modules export will -be adressed in a different PR. + by providing a `browser` entry in the `package.json` file. The ES modules export will + be adressed in a different PR. - The WebID is now set on the session when logging in a script. - When logging in with a refresh token (e.g. for a script), if the provided credentials are incorrect, an error is thrown. - ## 1.2.0 - 2020-12-04 ### New feature - Support for authenticated scripts: It's now possible to provide a script with login -parameters for a refresh token, a client ID and a client secret, which enables it to access -private resources on Pods. This means that it's now easier to write small backend -scripts which can interact with Pods in an automated way (i.e. no human interaction -required). + parameters for a refresh token, a client ID and a client secret, which enables it to access + private resources on Pods. This means that it's now easier to write small backend + scripts which can interact with Pods in an automated way (i.e. no human interaction + required). ### Bugfixes - In some use cases (e.g. authenticating a script), logging in happens without a redirection. The architecture so far prevented this -from being possible, and now after a login that does not require a redirect, the current session may be authenticated. + from being possible, and now after a login that does not require a redirect, the current session may be authenticated. - Logging in a browser app will now clear OIDC-specific query params from the URL, which prevents a crash on refresh. ### New features @@ -620,9 +620,9 @@ from being possible, and now after a login that does not require a redirect, the ### New features -- NodeJS support: a new NPM package, `@inrupt/solid-client-authn-node`, is now available to use authentication in a server environment. -- In addition to the features supported by the browser version, `@inrupt/solid-client-authn-node` supports the refresh token grant, which -makes it possible to maintain long-lived sessions without re-involving the user. +- NodeJS support: a new NPM package, `@inrupt/solid-client-authn-node`, is now available to use authentication in a server environment. +- In addition to the features supported by the browser version, `@inrupt/solid-client-authn-node` supports the refresh token grant, which + makes it possible to maintain long-lived sessions without re-involving the user. ### Bugfixes @@ -681,7 +681,7 @@ we will bump the major version when we change our publicly documented interface. ### Internal refactor: - Uses [oidc-client-js](https://github.com/IdentityModel/oidc-client-js) now to -perform the Auth Code Flow (replacing lots of hand-rolled code). + perform the Auth Code Flow (replacing lots of hand-rolled code). ### Bugfixes @@ -696,19 +696,21 @@ perform the Auth Code Flow (replacing lots of hand-rolled code). - Browser - Login now clears the local storage, so that you can log into a different server -even if not logged out properly. + even if not logged out properly. ### Internal refactor: + - Created multiple sub-packages, specifically the core and oidc-dpop-client-browser. - Moved interfaces down into Core. - Removed TSyringe annotations from the implementation of StorageUtility in the - Core package and extended it in the browser module (where they we re-applied to - allow injection again). + Core package and extended it in the browser module (where they we re-applied to + allow injection again). - Refactored the StorageUtility code to fix up mock usage. ## [0.1.2] - 2020-09-07 ### Internal refactor: + - Moved to Lerna (currently only the browser module is available). ## [0.1.1] - 2020-08-14 diff --git a/packages/browser/src/Session.spec.ts b/packages/browser/src/Session.spec.ts index 21727724bb..3b1ceeaefc 100644 --- a/packages/browser/src/Session.spec.ts +++ b/packages/browser/src/Session.spec.ts @@ -65,8 +65,6 @@ const mockLocation = (mockedLocation: string) => { window.history.replaceState = jest.fn(); }; -jest.mock("../src/iframe"); - describe("Session", () => { describe("constructor", () => { it("accepts an empty config", async () => { @@ -394,22 +392,6 @@ describe("Session", () => { ); expect(incomingRedirectHandler).toHaveBeenCalled(); }); - - it("posts the redirect IRI to the parent if in an iframe", async () => { - // Pretend we are in an iframe. - const frameElement = jest.spyOn(window, "frameElement", "get"); - frameElement.mockReturnValueOnce({} as Element); - - const mySession = new Session({}, "mySession"); - const iframe = jest.requireMock("../src/iframe"); - const postIri = jest.spyOn(iframe as any, "postRedirectUrlToParent"); - await mySession.handleIncomingRedirect({ - url: "https://some.redirect.url?code=someCode&state=someState", - }); - expect(postIri).toHaveBeenCalledWith( - "https://some.redirect.url?code=someCode&state=someState" - ); - }); }); describe("onError", () => { @@ -587,7 +569,6 @@ describe("Session", () => { clientId: "some client ID", clientSecret: "some client secret", redirectUrl: "https://some.redirect/url", - inIframe: false, }, expect.anything() ); @@ -897,168 +878,6 @@ describe("Session", () => { }); }); - describe("on tokenRenewal signal", () => { - it("triggers silent authentication in an iframe when receiving the signal", async () => { - const sessionId = "mySession"; - mockLocalStorage({ - [KEY_CURRENT_SESSION]: sessionId, - }); - mockLocation("https://mock.current/location"); - const mockedStorage = new StorageUtility( - mockStorage({ - [`${USER_SESSION_PREFIX}:${sessionId}`]: { - isLoggedIn: "true", - }, - }), - mockStorage({}) - ); - const clientAuthentication = mockClientAuthentication({ - sessionInfoManager: mockSessionInfoManager(mockedStorage), - }); - const validateCurrentSessionPromise = Promise.resolve({ - issuer: "https://some.issuer", - clientAppId: "some client ID", - clientAppSecret: "some client secret", - redirectUrl: "https://some.redirect/url", - tokenType: "DPoP", - }); - clientAuthentication.validateCurrentSession = jest - .fn() - .mockReturnValue( - validateCurrentSessionPromise - ) as typeof clientAuthentication.validateCurrentSession; - const incomingRedirectPromise = Promise.resolve(); - clientAuthentication.handleIncomingRedirect = jest - .fn() - .mockReturnValueOnce( - incomingRedirectPromise - ) as typeof clientAuthentication.handleIncomingRedirect; - clientAuthentication.login = jest.fn(); - const mySession = new Session({ clientAuthentication }, sessionId); - // Send the signal to the session - mySession.emit("tokenRenewal"); - await incomingRedirectPromise; - await validateCurrentSessionPromise; - - expect(clientAuthentication.login).toHaveBeenCalledWith( - { - sessionId: "mySession", - tokenType: "DPoP", - oidcIssuer: "https://some.issuer", - prompt: "none", - clientId: "some client ID", - clientSecret: "some client secret", - redirectUrl: "https://some.redirect/url", - inIframe: true, - }, - expect.anything() - ); - // Check that second parameter is of type session - expect( - (clientAuthentication.login as any).mock.calls[0][1] - ).toBeInstanceOf(Session); - }); - - it("sets the updated session info after silently refreshing", async () => { - const clientAuthentication = mockClientAuthentication(); - const incomingRedirectPromise = Promise.resolve({ - isLoggedIn: true, - webId: "https://some.pod/profile#me", - sessionId: "someSessionId", - expirationDate: 961106400, - }); - clientAuthentication.handleIncomingRedirect = jest - .fn() - .mockReturnValueOnce( - incomingRedirectPromise - ) as typeof clientAuthentication.handleIncomingRedirect; - - const windowAddEventListener = jest.spyOn(window, "addEventListener"); - // ../src/iframe is mocked for other tests, - // but we need `setupIframeListener` to actually be executed - // so that the callback gets called: - const iframeMock = jest.requireMock("../src/iframe") as any; - const iframeActual = jest.requireActual("../src/iframe") as any; - iframeMock.setupIframeListener.mockImplementationOnce( - iframeActual.setupIframeListener - ); - const mySession = new Session({ clientAuthentication }); - - // `window.addEventListener` gets called once with ("message", handler) - // — get that handler. - const messageEventHandler = windowAddEventListener.mock - .calls[0][1] as EventListener; - const mockedEvent = { - origin: window.location.origin, - source: null, - data: { - redirectUrl: "http://arbitrary.com", - }, - } as MessageEvent; - // This handler will call `clientAuthentication.handleIncomingRedirect`, - // which will return our sessionInfo values: - messageEventHandler(mockedEvent); - - await incomingRedirectPromise; - expect(mySession.info.webId).toBe("https://some.pod/profile#me"); - expect(mySession.info.sessionId).toBe("someSessionId"); - expect(mySession.info.expirationDate).toBe(961106400); - }); - - it("does not change the existing session if silent authentication failed", async () => { - const clientAuthentication = mockClientAuthentication(); - const incomingRedirectPromise = Promise.resolve({ - isLoggedIn: false, - }); - clientAuthentication.handleIncomingRedirect = jest - .fn() - .mockReturnValueOnce( - incomingRedirectPromise - ) as typeof clientAuthentication.handleIncomingRedirect; - - const windowAddEventListener = jest.spyOn(window, "addEventListener"); - // ../src/iframe is mocked for other tests, - // but we need `setupIframeListener` to actually be executed - // so that the callback gets called: - const iframeMock = jest.requireMock("../src/iframe") as any; - const iframeActual = jest.requireActual("../src/iframe") as any; - iframeMock.setupIframeListener.mockImplementationOnce( - iframeActual.setupIframeListener - ); - const mySession = new Session({ clientAuthentication }); - // The `any` assertion is necessary because Session.info is not meant to - // be written to; we only do so for tests to pretend we have an existing - // logged-in session that remains logged in after failed silent - // authentication. - (mySession as any).info = { - isLoggedIn: true, - webId: "https://some.pod/profile#me", - sessionId: "someSessionId", - expirationDate: 961106400, - }; - - // `window.addEventListener` gets called once with ("message", handler) - // — get that handler. - const messageEventHandler = windowAddEventListener.mock - .calls[0][1] as EventListener; - const mockedEvent = { - origin: window.location.origin, - source: null, - data: { - redirectUrl: "http://arbitrary.com", - }, - } as MessageEvent; - // This handler will call `clientAuthentication.handleIncomingRedirect`, - // which will return our sessionInfo values: - messageEventHandler(mockedEvent); - - await incomingRedirectPromise; - expect(mySession.info.webId).toBe("https://some.pod/profile#me"); - expect(mySession.info.sessionId).toBe("someSessionId"); - expect(mySession.info.expirationDate).toBe(961106400); - }); - }); - describe("onSessionExpiration", () => { it("calls the provided callback when receiving the appropriate event", async () => { const myCallback = jest.fn(); diff --git a/packages/browser/src/Session.ts b/packages/browser/src/Session.ts index 3f01ee88df..30103bca40 100644 --- a/packages/browser/src/Session.ts +++ b/packages/browser/src/Session.ts @@ -33,7 +33,6 @@ import { v4 } from "uuid"; import ClientAuthentication from "./ClientAuthentication"; import { getClientAuthenticationWithDependencies } from "./dependencies"; import { KEY_CURRENT_SESSION, KEY_CURRENT_URL } from "./constant"; -import { postRedirectUrlToParent, setupIframeListener } from "./iframe"; export interface ISessionOptions { /** @@ -98,11 +97,6 @@ export interface IHandleIncomingRedirectOptions { export async function silentlyAuthenticate( sessionId: string, clientAuthn: ClientAuthentication, - options: { - inIframe?: boolean; - } = { - inIframe: false, - }, session: Session ): Promise { const storedSessionInfo = await clientAuthn.validateCurrentSession(sessionId); @@ -121,7 +115,6 @@ export async function silentlyAuthenticate( clientId: storedSessionInfo.clientAppId, clientSecret: storedSessionInfo.clientAppSecret, tokenType: storedSessionInfo.tokenType ?? "DPoP", - inIframe: options.inIframe, }, session ); @@ -194,36 +187,6 @@ export class Session extends EventEmitter { }; } - // Listen for messages from children iframes. - setupIframeListener(async (redirectUrl: string) => { - const sessionInfo = - await this.clientAuthentication.handleIncomingRedirect( - redirectUrl, - this - ); - - // If silent authentication was not successful, do nothing; - // the existing session might still be valid for a while, - // and will expire by itself. - if (!isLoggedIn(sessionInfo)) { - return; - } - // After having revalidated the session, - // make sure to apply the new expiration time: - this.setSessionInfo(sessionInfo); - }); - // Listen for the 'tokenRenewal' signal to trigger the silent token renewal. - this.on("tokenRenewal", () => - silentlyAuthenticate( - this.info.sessionId, - this.clientAuthentication, - { - inIframe: true, - }, - this - ) - ); - // When a session is logged in, we want to track its ID in local storage to // enable silent refresh. The current session ID specifically stored in 'localStorage' // (as opposed to using our storage abstraction layer) because it is only @@ -321,13 +284,6 @@ export class Session extends EventEmitter { typeof inputOptions === "string" ? { url: inputOptions } : inputOptions; const url = options.url ?? window.location.href; - if (window.frameElement !== null) { - // This is being loaded from an iframe, so send the redirect - // URL to the parent window on the same origin. - postRedirectUrlToParent(url); - return undefined; - } - this.tokenRequestInProgress = true; const sessionInfo = await this.clientAuthentication.handleIncomingRedirect( url, @@ -357,13 +313,9 @@ export class Session extends EventEmitter { // ...if not, then there is no ID token, and so silent authentication cannot happen, but // if we do have a stored session ID, attempt to re-authenticate now silently. if (storedSessionId !== null) { - // TODO: iframe-based authentication being still experimental, it is disabled - // by default here. When it settles down, the following could be set to true, - // in which case the unresolving promise afterwards would need to be changed. const attemptedSilentAuthentication = await silentlyAuthenticate( storedSessionId, this.clientAuthentication, - undefined, this ); // At this point, we know that the main window will imminently be redirected. diff --git a/packages/browser/src/iframe.spec.ts b/packages/browser/src/iframe.spec.ts deleted file mode 100644 index 3e9e34734a..0000000000 --- a/packages/browser/src/iframe.spec.ts +++ /dev/null @@ -1,193 +0,0 @@ -/* - * Copyright 2022 Inrupt Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, - * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A - * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE - * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -import { jest, it, describe, expect } from "@jest/globals"; -import { - postRedirectUrlToParent, - redirectInIframe, - setupIframeListener, -} from "./iframe"; - -describe("redirectInIframe", () => { - it("creates an iframe with the appropriate attributes and performs the redirection in it", () => { - redirectInIframe("http://some.iri"); - - const iframe = document.getElementsByTagName("iframe")[0]; - - // This verifies that the iframe has been added to the DOM, i.e. that - // window.document.body.appendChild has been called. - expect(iframe).toBeDefined(); - - // The iframe has the appropriate attributes. - expect(iframe.getAttribute("hidden")).toBe("true"); - expect(iframe.getAttribute("sandbox")).toBe( - "allow-scripts allow-same-origin" - ); - expect(iframe.getAttribute("src")).toBe("http://some.iri"); - }); -}); - -describe("setupIframeListener", () => { - const setupDom = (originMatch: boolean, sourceMatch: boolean) => { - jest.spyOn(window, "location", "get").mockReturnValue({ - // The test iframe message has "" as an origin. - origin: originMatch ? "" : "https://some.other/origin", - } as Location); - - // Mock mismatching window - redirectInIframe("http://some.iri"); - const iframe = document.getElementsByTagName("iframe")[0]; - jest - .spyOn(iframe, "contentWindow", "get") - .mockReturnValue((sourceMatch ? null : ({} as unknown)) as Window); - }; - - const mockEventListener = () => { - let messageReceived = false; - // This promis prevents the test from completing while the message event - // hasn't been received, to ensure the listener callback is executed. - - // The following promise has an async executor to be able to sleep - // while waiting for a change. - // eslint-disable-next-line no-async-promise-executor - const blockingPromise = new Promise(async (resolve) => { - while (!messageReceived) { - // Wait for the message event to be processed. - // eslint-disable-next-line no-await-in-loop - await new Promise((resolveSleep) => setTimeout(resolveSleep, 100)); - } - resolve(undefined); - }); - window.addEventListener("message", () => { - messageReceived = true; - }); - return blockingPromise; - }; - - it("ignores message from iframes on different origins", async () => { - const callback = jest.fn(); - const blockingPromise = mockEventListener(); - setupDom(false, true); - setupIframeListener(callback as any); - - window.postMessage( - { - redirectUrl: "http://some.redirect/url", - }, - "http://localhost" - ); - - await blockingPromise; - - expect(callback).not.toHaveBeenCalled(); - }); - - it("ignores messages from iframes with an unknown source", async () => { - const callback = jest.fn(); - const blockingPromise = mockEventListener(); - setupDom(true, false); - setupIframeListener(callback as any); - - window.postMessage( - { - redirectUrl: "http://some.redirect/url", - }, - "http://localhost" - ); - - await blockingPromise; - - expect(callback).not.toHaveBeenCalled(); - }); - - it("ignores messages from valid iframes but with an unexpected structure", async () => { - const callback = jest.fn(); - const blockingPromise = mockEventListener(); - setupDom(true, true); - setupIframeListener(callback as any); - - window.postMessage( - { - // We expect a message with a 'redirectUrl' property. - someUnknownkey: "some value", - }, - "http://localhost" - ); - - await blockingPromise; - - expect(callback).not.toHaveBeenCalled(); - }); - - it("calls the given callback", async () => { - const callback = jest.fn(); - const blockingPromise = mockEventListener(); - setupDom(true, true); - setupIframeListener(callback as any); - - window.postMessage( - { - redirectUrl: "http://some.redirect/url", - }, - "http://localhost" - ); - - await blockingPromise; - - expect(callback).toHaveBeenCalled(); - }); - - it("cleans up the iframe after the message is received", async () => { - const callback = jest.fn(); - const blockingPromise = mockEventListener(); - setupDom(true, true); - setupIframeListener(callback as any); - - expect(document.getElementsByTagName("iframe")[0]).toBeDefined(); - - window.postMessage( - { - redirectUrl: "http://some.redirect/url", - }, - "http://localhost" - ); - - await blockingPromise; - - // Verify that the iframe has correctly been removed from the DOM - expect(document.getElementsByTagName("iframe")[0]).toBeUndefined(); - }); -}); - -describe("postRedirectUrlToParent", () => { - it("posts a message to the parent window with the provided url", () => { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const spyPost = jest.spyOn(window.top!, "postMessage"); - jest.spyOn(window, "location", "get").mockReturnValue({ - origin: "https://some.origin/", - } as unknown as Location); - postRedirectUrlToParent("https://some.redirect.url/"); - expect(spyPost).toHaveBeenCalledWith( - { redirectUrl: "https://some.redirect.url/" }, - "https://some.origin/" - ); - }); -}); diff --git a/packages/browser/src/iframe.ts b/packages/browser/src/iframe.ts deleted file mode 100644 index b69d5a1474..0000000000 --- a/packages/browser/src/iframe.ts +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright 2022 Inrupt Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, - * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A - * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE - * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -// This is an internal variable used to share some state between `redirectInIframe`, -// which initializes it, and `setupIframeListener`, which reads from it. It avoids -// creating a DOM element with identifying attributes such as an ID which may clash -// with some other element set up by the library user. -let redirectIframe: HTMLIFrameElement; - -// The iframe is dynamically created rather than on import, -// to avoid accessing `window` when an app is doing server-side rendering: -function getRedirectIframe(): HTMLIFrameElement { - if (typeof redirectIframe === "undefined") { - redirectIframe = window.document.createElement("iframe"); - redirectIframe.setAttribute("hidden", "true"); - redirectIframe.setAttribute("sandbox", "allow-scripts allow-same-origin"); - } - return redirectIframe; -} - -/** - * Redirects the browser to a provided IRI, but does such redirection in a child - * iframe. This is used to have a front-channel interaction with the Solid Identity - * Provider without having the user involved, and without refreshing the main window. - * - * @param redirectUrl The IRI to which the iframe should be redirected. - */ -export function redirectInIframe(redirectUrl: string): void { - const iframe = getRedirectIframe(); - window.document.body.appendChild(iframe); - iframe.src = redirectUrl; -} - -/** - * This function sets up an event listener that will receive iframe messages. - * It is only listening to messages coming from iframes that could have been - * opened by the library, and expects to be posted the IRI the iframe has been - * redirected to by the Solid Identity Provider. This way, the top window can - * perform the backchannel exchange to the token endpoint without performing - * the front-channel redirection. - * - * @param handleIframeRedirect Redirect URL sent by the iframe - */ -export function setupIframeListener( - handleIframeRedirect: (redirectUrl: string) => Promise -): void { - // If an app is doing server-side rendering, setting up an iframe listener - // is pointless (since there is no iframe, and there will be no expiring - // session to renew) and might even throw a `window` not defined error, so - // skip this in that case: - /* istanbul ignore if (window is always defined in tests) */ - if (typeof window === "undefined") { - return; - } - window.addEventListener("message", async (evt: MessageEvent) => { - const iframe = getRedirectIframe(); - if ( - evt.origin === window.location.origin && - evt.source === iframe.contentWindow - ) { - if (typeof evt.data.redirectUrl === "string") { - // The top-level window handles the redirect that happened in the iframe. - await handleIframeRedirect(evt.data.redirectUrl); - } - } - // Clean up the iframe from the DOM - if (window.document.body.contains(iframe)) { - window.document.body.removeChild(iframe); - } - }); -} - -/** - * This function bubbles up the result of the front-channel interaction with - * the authorization endpoint to the parent window. - */ -export function postRedirectUrlToParent(redirectUrl: string): void { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - window.top!.postMessage( - { - redirectUrl, - }, - window.location.origin - ); -} diff --git a/packages/browser/src/login/oidc/Redirector.spec.ts b/packages/browser/src/login/oidc/Redirector.spec.ts index 72ee7de90b..e745f5bcbb 100644 --- a/packages/browser/src/login/oidc/Redirector.spec.ts +++ b/packages/browser/src/login/oidc/Redirector.spec.ts @@ -29,8 +29,6 @@ import { } from "@jest/globals"; import Redirector from "./Redirector"; -jest.mock("../../../src/iframe"); - /** * Test for Redirector */ @@ -82,18 +80,6 @@ describe("Redirector", () => { expect(window.location.href).toBe("https://coolSite.com"); }); - it("redirects in an iframe if specified", () => { - const iframe = jest.requireMock("../../../src/iframe"); - const redirectInIframe = jest.spyOn(iframe as any, "redirectInIframe"); - const redirector = new Redirector(); - redirector.redirect("https://someUrl.com/redirect", { - redirectInIframe: true, - }); - expect(redirectInIframe).toHaveBeenCalledWith( - "https://someUrl.com/redirect" - ); - }); - it("calls redirect handler", () => { const handler = jest.fn(); const redirectUrl = "https://someUrl.com/redirect"; diff --git a/packages/browser/src/login/oidc/Redirector.ts b/packages/browser/src/login/oidc/Redirector.ts index cadb7651ac..4b94740ad2 100644 --- a/packages/browser/src/login/oidc/Redirector.ts +++ b/packages/browser/src/login/oidc/Redirector.ts @@ -28,7 +28,6 @@ import { IRedirector, IRedirectorOptions, } from "@inrupt/solid-client-authn-core"; -import { redirectInIframe } from "../../iframe"; /** * @hidden @@ -39,8 +38,6 @@ export default class Redirector implements IRedirector { options.handleRedirect(redirectUrl); } else if (options && options.redirectByReplacingState) { window.history.replaceState({}, "", redirectUrl); - } else if (options?.redirectInIframe) { - redirectInIframe(redirectUrl); } else { window.location.href = redirectUrl; } diff --git a/packages/browser/src/login/oidc/oidcHandlers/AuthorizationCodeWithPkceOidcHandler.ts b/packages/browser/src/login/oidc/oidcHandlers/AuthorizationCodeWithPkceOidcHandler.ts index 866663c645..df16f2b4fa 100644 --- a/packages/browser/src/login/oidc/oidcHandlers/AuthorizationCodeWithPkceOidcHandler.ts +++ b/packages/browser/src/login/oidc/oidcHandlers/AuthorizationCodeWithPkceOidcHandler.ts @@ -117,7 +117,6 @@ export default class AuthorizationCodeWithPkceOidcHandler redirector.redirect(signingRequest.url.toString(), { handleRedirect: oidcLoginOptions.handleRedirect, - redirectInIframe: oidcLoginOptions.inIframe, }); } catch (err: unknown) { // eslint-disable-next-line no-console diff --git a/packages/core/src/login/ILoginOptions.ts b/packages/core/src/login/ILoginOptions.ts index 55bbc56ba2..3f5eb80b0f 100644 --- a/packages/core/src/login/ILoginOptions.ts +++ b/packages/core/src/login/ILoginOptions.ts @@ -53,12 +53,4 @@ export default interface ILoginOptions extends ILoginInputOptions { * Event emitter enabling calling user-specified callbacks. */ eventEmitter?: EventEmitter; - - /** - * This boolean specifies redirection to the Identity Provider should happen in - * the main window or in an iframe, thus making the redirect invisible to the - * user. Such redirection may only succeed in the case of silent authentication, - * if a cookie is set for the IdP and this cookie is included by the iframe. - */ - inIframe?: boolean; } diff --git a/packages/core/src/login/oidc/IOidcOptions.ts b/packages/core/src/login/oidc/IOidcOptions.ts index 6327222002..9d97a63b10 100644 --- a/packages/core/src/login/oidc/IOidcOptions.ts +++ b/packages/core/src/login/oidc/IOidcOptions.ts @@ -64,7 +64,6 @@ export interface IOidcOptions { redirectUrl: string; handleRedirect?: (url: string) => unknown; eventEmitter?: EventEmitter; - inIframe?: boolean; } export default IOidcOptions; diff --git a/packages/core/src/login/oidc/IRedirector.ts b/packages/core/src/login/oidc/IRedirector.ts index 7d9bda7ec1..35c4114a4a 100644 --- a/packages/core/src/login/oidc/IRedirector.ts +++ b/packages/core/src/login/oidc/IRedirector.ts @@ -30,11 +30,6 @@ export interface IRedirectorOptions { handleRedirect?: (url: string) => unknown; redirectByReplacingState?: boolean; - /** - * If this is set to true, the redirect will happen in an iframe, as opposed - * to the main window. - */ - redirectInIframe?: boolean; } /**