diff --git a/.gitignore b/.gitignore index 7c5d5ec..f96d6f1 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,16 @@ jspm_packages # Optional npm cache directory .npm +# yarn cache directory +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions + + # Optional REPL history .node_repl_history @@ -44,4 +54,4 @@ jspm_packages /build_keycloak /build -/storybook-static \ No newline at end of file +/storybook-static diff --git a/README.md b/README.md index 49bf3f5..8bddea7 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Keycloak theme that goes along with it. If you are only looking to create a theme (and not a theme + an App) there are a lot of things that you can remove from this starter: [Please read this section of the README](#standalone-keycloak-theme). > ❗️ WARNING ❗️: Don't waste time trying to port this setup to [Vite](https://vitejs.dev/). -> Currently Keycloakify only works collocated with Webpack projects but [we are working toward enabling collocation with Vite and Next.js projects](https://github.com/keycloakify/keycloakify/pull/275)! +> Currently Keycloakify only works collocated with Webpack projects but [we are working toward enabling collocation with Vite](https://github.com/keycloakify/keycloakify/pull/275)! # Quick start @@ -220,4 +220,4 @@ jobs: EOF ``` -You can also remove `jwt-decode`, `keycloak-js`, `powerhooks` and `tsafe` from your dependencies. +You can also remove `oidc-spa`, `powerhooks` and `tsafe` from your dependencies. diff --git a/package.json b/package.json index 5f3ca1b..6050665 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "keycloakify-starter", "homepage": "https://starter.keycloakify.dev", - "version": "4.7.0", + "version": "4.9.1", "description": "A starter/demo project for keycloakify", "repository": { "type": "git", @@ -26,8 +26,7 @@ "keywords": [], "dependencies": { "evt": "^2.4.15", - "jwt-decode": "^3.1.2", - "keycloak-js": "^21.0.1", + "oidc-spa": "^2.0.2", "keycloakify": "9.0.0-rc.0", "powerhooks": "^0.26.8", "react": "18.1.0", @@ -84,4 +83,4 @@ "last 1 safari version" ] } -} \ No newline at end of file +} diff --git a/public/index.html b/public/index.html index 0bcab1a..6b39543 100644 --- a/public/index.html +++ b/public/index.html @@ -3,7 +3,7 @@ - + + + + + + + @@ -78,4 +84,4 @@ --> - \ No newline at end of file + diff --git a/src/App/App.tsx b/src/App/App.tsx index b6146dc..ed71f44 100644 --- a/src/App/App.tsx +++ b/src/App/App.tsx @@ -1,93 +1,145 @@ import "./App.css"; import logo from "./logo.svg"; import myimg from "./myimg.png"; -import { createOidcClientProvider, useOidcClient } from "./oidc"; -import { addFooToQueryParams, addBarToQueryParams } from "../keycloak-theme/login/valuesTransferredOverUrl"; -import jwt_decode from "jwt-decode"; -import { addParamToUrl } from "powerhooks/tools/urlSearchParams"; +import { useMemo } from "react"; +import { createOidcProvider, useOidc } from "oidc-spa/react"; +import { decodeJwt } from "oidc-spa"; +import { assert } from "tsafe/assert"; //On older Keycloak version you need the /auth (e.g: http://localhost:8080/auth) //On newer version you must remove it (e.g: http://localhost:8080 ) const keycloakUrl = "https://auth.code.gouv.fr/auth"; const keycloakRealm = "keycloakify"; -const keycloakClient= "starter"; +const keycloakClientId= "starter"; -const { OidcClientProvider } = createOidcClientProvider({ - url: keycloakUrl, - realm: keycloakRealm, - clientId: keycloakClient, - //This function will be called just before redirecting, - //it should return the current langue. - //kcContext.locale.currentLanguageTag will be what this function returned just before redirecting. - getUiLocales: () => "en", - transformUrlBeforeRedirect: url => - [url] - //Instead of foo and bar you could have isDark for example or any other state that you wish to - //transfer from the main app to the login pages. - .map(url => addFooToQueryParams({ url, value: { foo: 42 } })) - .map(url => addBarToQueryParams({ url, value: "value of bar transferred to login page" })) - [0], - log: console.log +const { OidcProvider } = createOidcProvider({ + issuerUri: `${keycloakUrl}/realms/${keycloakRealm}`, + clientId: keycloakClientId, + // NOTE: You can also pass queries params when calling oidc.login() + getExtraQueryParams: () => ({ + // This adding ui_locales to the url will ensure the consistency of the language between the app and the login pages + // If your app implements a i18n system (like i18nifty.dev for example) you should use this and replace "en" by the + // current language of the app. + // On the other side you will find kcContext.locale.currentLanguageTag to be whatever you set here. + "ui_locales": "en", + "my_custom_param": "value of foo transferred to login page" + }), + /* + * This parameter have to be provided provide if your App is not hosted at the origin of the subdomain. + * For example if your site is hosted by navigating to `https://www.example.com` + * you don't have to provide this parameter. + * On the other end if your site is hosted by navigating to `https://www.example.com/my-app` + * Then you want to set publicUrl to `/my-app` + * + * Be mindful that `${window.location.origin}${publicUrl}/silent-sso.html` must return the `silent-sso.html` that + * you are supposed to have created in your `public/` directory. + * + * If your are still using `create-react-app` (like we are for now) you can just set + * publicUrl to `process.env.PUBLIC_URL` and don't have to think about it further. + */ + publicUrl: process.env.PUBLIC_URL }); export default function App() { return ( - + - + ); } + function ContextualizedApp() { - const { oidcClient } = useOidcClient(); - - let accountUrl = `${keycloakUrl}/realms/${keycloakRealm}/account`; - - // Set the language the user will get on the account page - accountUrl = addParamToUrl({ - url: accountUrl, - name: "kc_locale", - value: "en" - }).newUrl; - - // Enable to redirect to the app from the account page we'll get the referrer_uri under kcContext.referrer.url - // It's useful to avoid hard coding the app url in the keycloak config - accountUrl = addParamToUrl({ - url: accountUrl, - name: "referrer", - value: keycloakClient - }).newUrl; - - accountUrl = addParamToUrl({ - url: accountUrl, - name: "referrer_uri", - value: window.location.href - }).newUrl; + const { oidc } = useOidc(); return (
{ - oidcClient.isUserLoggedIn ? - <> -

You are authenticated !

- {/* On older Keycloak version its /auth/realms instead of /realms */} - Link to your Keycloak account -
{JSON.stringify(jwt_decode(oidcClient.getAccessToken()), null, 2)}
- - + oidc.isUserLoggedIn ? + oidc.logout({ redirectTo: "home" })} /> : - <> - - + } logo test_image

Hello world

-

Check out all keycloak pages in the Storybook!

+

Check out all keycloak pages in the Storybook!

Once you've identified the ones you want to customize run npx eject-keycloak-page

); + } + +function AuthenticatedRoute(props: { logout: () => void; }) { + + const { logout } = props; + + const { user } = useUser(); + + return ( + <> +

Hello {user.name} !

+ Link to your Keycloak account + +
{JSON.stringify(user, null, 2)}
+ + ); + +} + +function useUser() { + const { oidc } = useOidc(); + + assert(oidc.isUserLoggedIn, "This hook can only be used when the user is logged in"); + + const { idToken } = oidc.getTokens(); + + const user = useMemo( + () => + decodeJwt<{ + // Use https://jwt.io/ to tell what's in your idToken + // It will depend of your Keycloak configuration. + // Here I declare only two field on the type but actually there are + // Many more things available. + sub: string; + name: string; + preferred_username: string; + // This is a custom attribute set up in our Keycloak configuration + // it's not present by default. + // See https://docs.keycloakify.dev/realtime-input-validation#getting-your-custom-user-attribute-to-be-included-in-the-jwt + favorite_pet: "cat" | "dog" | "bird"; + }>(idToken), + [idToken] + ); + + return { user }; +} + +function buildAccountUrl( + params: { + locale: string; + } +){ + const { locale } = params; + + const accountUrl = new URL(`${keycloakUrl}/realms/${keycloakRealm}/account`); + + const searchParams = new URLSearchParams(); + + searchParams.append("kc_locale", locale); + searchParams.append("referrer", keycloakClientId); + searchParams.append("referrer_uri", window.location.href); + + accountUrl.search = searchParams.toString(); + + return accountUrl.toString(); +} \ No newline at end of file diff --git a/src/App/oidc.tsx b/src/App/oidc.tsx deleted file mode 100644 index 44eb9e1..0000000 --- a/src/App/oidc.tsx +++ /dev/null @@ -1,213 +0,0 @@ -import { useState, useContext, createContext, useEffect } from "react"; -import Keycloak_js from "keycloak-js"; -import { id } from "tsafe/id"; -import { addParamToUrl } from "powerhooks/tools/urlSearchParams"; -import type { ReturnType } from "tsafe/ReturnType"; -import type { Param0 } from "tsafe/Param0"; -import { assert } from "tsafe/assert"; -import { createKeycloakAdapter } from "keycloakify"; -import jwt_decode from "jwt-decode"; -import { Evt } from "evt"; - -export declare type OidcClient = OidcClient.LoggedIn | OidcClient.NotLoggedIn; - -export declare namespace OidcClient { - export type NotLoggedIn = { - isUserLoggedIn: false; - login: (params: { - //To prevent infinite loop if the user access a page that requires to - //be authenticated but cancel (clicks back). - doesCurrentHrefRequiresAuth: boolean; - }) => Promise; - }; - - export type LoggedIn = { - isUserLoggedIn: true; - getAccessToken: () => string; - logout: (params: { redirectTo: "home" | "current page" }) => Promise; - //If we have sent a API request to change user's email for example - //and we want that jwt_decode(oidcClient.getAccessToken()).email be the new email - //in this case we would call this method... - updateTokenInfos: () => Promise; - }; -} - -type Params = { - url: string; - realm: string; - clientId: string; - transformUrlBeforeRedirect?: (url: string) => string; - getUiLocales?: () => string; - log?: typeof console.log; -}; - -async function createKeycloakOidcClient(params: Params): Promise { - const { - url, - realm, - clientId, - transformUrlBeforeRedirect, - getUiLocales, - log - } = params; - - const keycloakInstance = new Keycloak_js({ url, realm, clientId }); - - let redirectMethod: ReturnType< - Param0["getRedirectMethod"] - > = "overwrite location.href"; - - const isAuthenticated = await keycloakInstance - .init({ - onLoad: "check-sso", - silentCheckSsoRedirectUri: `${window.location.origin}/silent-sso.html`, - responseMode: "query", - checkLoginIframe: false, - adapter: createKeycloakAdapter({ - transformUrlBeforeRedirect: url => - [url] - .map(transformUrlBeforeRedirect ?? (url => url)) - .map( - getUiLocales === undefined ? - (url => url) : - url => - addParamToUrl({ - url, - "name": "ui_locales", - "value": getUiLocales() - }).newUrl - ) - [0], - keycloakInstance, - getRedirectMethod: () => redirectMethod - }) - }) - .catch((error: Error) => error); - - //TODO: Make sure that result is always an object. - if (isAuthenticated instanceof Error) { - throw isAuthenticated; - } - - const login: OidcClient.NotLoggedIn["login"] = async ({ - doesCurrentHrefRequiresAuth - }) => { - if (doesCurrentHrefRequiresAuth) { - redirectMethod = "location.replace"; - } - - await keycloakInstance.login({ "redirectUri": window.location.href }); - - return new Promise(() => { }); - }; - - if (!isAuthenticated) { - return id({ - "isUserLoggedIn": false, - login - }); - } - - let currentAccessToken = keycloakInstance.token!; - - const oidcClient = id({ - "isUserLoggedIn": true, - "getAccessToken": () => currentAccessToken, - "logout": async ({ redirectTo }) => { - await keycloakInstance.logout({ - "redirectUri": (() => { - switch (redirectTo) { - case "current page": - return window.location.href; - case "home": - return window.location.origin; - } - })() - }); - - return new Promise(() => { }); - }, - "updateTokenInfos": async () => { - await keycloakInstance.updateToken(-1); - - currentAccessToken = keycloakInstance.token!; - } - }); - - (function callee() { - const msBeforeExpiration = jwt_decode<{ exp: number }>(currentAccessToken)["exp"] * 1000 - Date.now(); - - setTimeout(async () => { - - log?.(`OIDC access token will expire in ${minValiditySecond} seconds, waiting for user activity before renewing`); - - await Evt.merge([ - Evt.from(document, "mousemove"), - Evt.from(document, "keydown") - ]).waitFor(); - - log?.("User activity detected. Refreshing access token now"); - - const error = await keycloakInstance.updateToken(-1).then( - () => undefined, - (error: Error) => error - ); - - if (error) { - log?.("Can't refresh OIDC access token, getting a new one"); - //NOTE: Never resolves - await login({ "doesCurrentHrefRequiresAuth": true }); - } - - currentAccessToken = keycloakInstance.token!; - - callee(); - - }, msBeforeExpiration - minValiditySecond * 1000); - })(); - - return oidcClient; -} - -const minValiditySecond = 25; - -const oidcClientContext = createContext(undefined); - -export function createOidcClientProvider(params: Params) { - - - const prOidcClient = createKeycloakOidcClient(params); - - function OidcClientProvider(props: { children: React.ReactNode; }) { - - const { children } = props; - - const [oidcClient, setOidcClient] = useState(undefined); - - useEffect(() => { - - prOidcClient.then(setOidcClient); - - }, []); - - if (oidcClient === undefined) { - return null; - } - - return ( - - {children} - - ); - - } - - return { OidcClientProvider }; - -} - -export function useOidcClient() { - const oidcClient = useContext(oidcClientContext); - assert(oidcClient !== undefined); - return { oidcClient }; -} diff --git a/src/keycloak-theme/account/Template.tsx b/src/keycloak-theme/account/Template.tsx index 0c33a05..7f8236a 100644 --- a/src/keycloak-theme/account/Template.tsx +++ b/src/keycloak-theme/account/Template.tsx @@ -24,7 +24,7 @@ export default function Template(props: TemplateProps) { `${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly-additions.min.css`, `${url.resourcesPath}/css/account.css` ], - "htmlClassName": undefined, + "htmlClassName": getClassName("kcHtmlClass"), "bodyClassName": clsx("admin-console", "user", getClassName("kcBodyClass")) }); diff --git a/src/keycloak-theme/login/KcApp.tsx b/src/keycloak-theme/login/KcApp.tsx index 120ec1d..683a7c2 100644 --- a/src/keycloak-theme/login/KcApp.tsx +++ b/src/keycloak-theme/login/KcApp.tsx @@ -7,10 +7,6 @@ import { useI18n } from "./i18n"; const Template = lazy(() => import("./Template")); const DefaultTemplate = lazy(() => import("keycloakify/login/Template")); -// You can uncomment this to see the values passed by the main app before redirecting. -//import { foo, bar } from "./valuesTransferredOverUrl"; -//console.log(`Values passed by the main app in the URL parameter:`, { foo, bar }); - const Login = lazy(() => import("./pages/Login")); // If you can, favor register-user-profile.ftl over register.ftl, see: https://docs.keycloakify.dev/realtime-input-validation const Register = lazy(() => import("./pages/Register")); diff --git a/src/keycloak-theme/login/Template.tsx b/src/keycloak-theme/login/Template.tsx index 3b60e09..cb663b9 100644 --- a/src/keycloak-theme/login/Template.tsx +++ b/src/keycloak-theme/login/Template.tsx @@ -40,7 +40,7 @@ export default function Template(props: TemplateProps) { `${url.resourcesPath}/css/login.css` ], "htmlClassName": getClassName("kcHtmlClass"), - "bodyClassName": undefined + "bodyClassName": getClassName("kcBodyClass") }); if (!isReady) { @@ -50,8 +50,12 @@ export default function Template(props: TemplateProps) { return (
-
- {msg("loginTitleHtml", realm.displayNameHtml)} +
+ {msg("loginTitleHtml", realm.displayNameHtml)}!!!
diff --git a/src/keycloak-theme/login/createPageStory.tsx b/src/keycloak-theme/login/createPageStory.tsx index cdf570f..24a5d70 100644 --- a/src/keycloak-theme/login/createPageStory.tsx +++ b/src/keycloak-theme/login/createPageStory.tsx @@ -15,7 +15,13 @@ export function createPageStory(params: { storyPartialKcContext: params.kcContext }); - return ; + return ( + <> + {/* If you import custom fonts in your index.html you have to import them in storybook as well*/} + + + + ); } diff --git a/src/keycloak-theme/login/pages/Login.tsx b/src/keycloak-theme/login/pages/Login.tsx index 1de3f27..abc250b 100644 --- a/src/keycloak-theme/login/pages/Login.tsx +++ b/src/keycloak-theme/login/pages/Login.tsx @@ -5,6 +5,17 @@ import type { PageProps } from "keycloakify/login/pages/PageProps"; import { useGetClassName } from "keycloakify/login/lib/useGetClassName"; import type { KcContext } from "../kcContext"; import type { I18n } from "../i18n"; +import { retrieveQueryParamFromUrl } from "oidc-spa/tools/urlQueryParams"; + +const result = retrieveQueryParamFromUrl({ + "url": window.location.href, + "name": "my_custom_param", +}); + +if (result.wasPresent) { + console.log("my_custom_param", result.value); +} + export default function Login(props: PageProps, I18n>) { const { kcContext, i18n, doUseDefaultCss, Template, classes } = props; @@ -60,7 +71,7 @@ export default function Login(props: PageProps {realm.password && ( @@ -71,8 +82,8 @@ export default function Login(props: PageProps {msg("rememberMe")} @@ -149,8 +160,8 @@ export default function Login(props: PageProps { - const queryParamName = "foo"; - - type Type = { foo: number; }; - - const value = (()=> { - - const unparsedValue = read({ queryParamName }); - - if( unparsedValue === undefined ){ - return undefined; - } - - return JSON.parse(unparsedValue) as Type; - - })(); - - function addToUrlQueryParams(params: { - url: string; - value: Type; - }): string { - const { url, value } = params; - - return addParamToUrl({ - url, - "name": queryParamName, - "value": JSON.stringify(value) - }).newUrl; - } - - const out = { - [queryParamName]: value, - [`add${capitalize(queryParamName)}ToQueryParams` as const]: addToUrlQueryParams - } as const; - - return out; -})(); - -export const { bar, addBarToQueryParams } = (() => { - const queryParamName = "bar"; - - type Type = string; - - const value = (()=> { - - const unparsedValue = read({ queryParamName }); - - if( unparsedValue === undefined ){ - return undefined; - } - - return JSON.parse(unparsedValue) as Type; - - })(); - - function addToUrlQueryParams(params: { - url: string; - value: Type; - }): string { - const { url, value } = params; - - return addParamToUrl({ - url, - "name": queryParamName, - "value": JSON.stringify(value) - }).newUrl; - } - - const out = { - [queryParamName]: value, - [`add${capitalize(queryParamName)}ToQueryParams` as const]: addToUrlQueryParams - } as const; - - return out; -})(); - - -function read(params: { queryParamName: string }): string | undefined { - if (kcContext === undefined || process.env.NODE_ENV !== "production") { - //NOTE: We do something only if we are really in Keycloak - return undefined; - } - - const { queryParamName } = params; - - read_from_url: { - const result = retrieveParamFromUrl({ - "url": window.location.href, - "name": queryParamName - }); - - if (!result.wasPresent) { - break read_from_url; - } - - const { newUrl, value: serializedValue } = result; - - updateSearchBarUrl(newUrl); - - localStorage.setItem(queryParamName, serializedValue); - - return serializedValue; - } - - //Reading from local storage - const serializedValue = localStorage.getItem(queryParamName); - - if (serializedValue === null) { - throw new Error( - `Missing ${queryParamName} in URL when redirecting to login page` - ); - } - - return serializedValue; -} diff --git a/yarn.lock b/yarn.lock index 99c50c1..32d91cd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4706,7 +4706,7 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== -base64-js@^1.0.2, base64-js@^1.5.1: +base64-js@^1.0.2: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== @@ -5801,6 +5801,11 @@ crypto-browserify@^3.11.0: randombytes "^2.0.0" randomfill "^1.0.3" +crypto-js@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.1.1.tgz#9e485bcf03521041bd85844786b83fb7619736cf" + integrity sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw== + crypto-random-string@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" @@ -9613,11 +9618,6 @@ js-sdsl@^4.1.4: resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.4.0.tgz#8b437dbe642daa95760400b602378ed8ffea8430" integrity sha512-FfVSdx6pJ41Oa+CF7RDaFmTnCaFhua+SNYQX74riGOpl96x+2jQCqEfQ2bnXu/5DPCqlRuiqyvTJM0Qjz26IVg== -js-sha256@^0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/js-sha256/-/js-sha256-0.9.0.tgz#0b89ac166583e91ef9123644bd3c5334ce9d0966" - integrity sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA== - js-string-escape@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/js-string-escape/-/js-string-escape-1.0.1.tgz#e2625badbc0d67c7533e9edc1068c587ae4137ef" @@ -9772,6 +9772,7 @@ keycloakify@9.0.0-rc.0: version "9.0.0-rc.0" resolved "https://registry.yarnpkg.com/keycloakify/-/keycloakify-9.0.0-rc.0.tgz#8982f96ef8fc0ce33b6d3aff7b1225a1f4baeeec" integrity sha512-z+D1NQIpWN20i8D3h2IHBSZKpuVMWNjRmKFJMjTOHh5uIeHiPJm/yAT7PtqOw/hRGH/xgNk5J1q2hZf+sFCfiw== + dependencies: "@babel/generator" "^7.22.9" "@babel/parser" "^7.22.7" @@ -10917,6 +10918,23 @@ obuf@^1.0.0, obuf@^1.1.2: resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== +oidc-client-ts@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/oidc-client-ts/-/oidc-client-ts-2.3.0.tgz#43c90f1f0cc3be2e4ede38b8c68642ba00bfa8f6" + integrity sha512-7RUKU+TJFQo+4X9R50IGJAIDF18uRBaFXyZn4VVCfwmwbSUhKcdDnw4zgeut3uEXkiD3NqURq+d88sDPxjf1FA== + dependencies: + crypto-js "^4.1.1" + jwt-decode "^3.1.2" + +oidc-spa@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/oidc-spa/-/oidc-spa-2.0.2.tgz#7a6c64f0e92c0f9349742974648627a85865a8b8" + integrity sha512-PILedvHPxy5U1Kwkpukdw5DW3adST/Bo6vZz70o7hwe59kwBVmVATBlAikZlscZn4x5pAeOyWuD1fir0N1WrJg== + dependencies: + jwt-decode "^3.1.2" + oidc-client-ts "^2.3.0" + tsafe "^1.6.5" + on-finished@2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" @@ -14294,6 +14312,11 @@ tsafe@^1.6.0: resolved "https://registry.yarnpkg.com/tsafe/-/tsafe-1.6.0.tgz#48a9bd0a4c43df43d289bdfc1d89f0d7fffbd612" integrity sha512-wlUeRBnyN3EN2chXznpLm7vBEvJLEOziDU+MN6NRlD99AkwmXgtChNQhp+V97VyRa3Bp05IaL4Cocsc7JlyEUg== +tsafe@^1.6.5: + version "1.6.5" + resolved "https://registry.yarnpkg.com/tsafe/-/tsafe-1.6.5.tgz#74943b69190069168a53d2accd6d07891cf1cce8" + integrity sha512-895zss8xqqHKTc28sHGIfZKnt3C5jrstB1DyPr/h3/flK0zojsZUMQL1/W4ytdDW6KI4Oth62nb9rrxmA3s3Iw== + tsconfig-paths@^3.14.1: version "3.14.2" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz#6e32f1f79412decd261f92d633a9dc1cfa99f088"