diff --git a/.env.local.keycloak-example b/.env.local.keycloak-example new file mode 100644 index 00000000..737d7364 --- /dev/null +++ b/.env.local.keycloak-example @@ -0,0 +1,11 @@ +REACT_APP_OIDC_SERVER_TYPE=KEYCLOAK +REACT_APP_OIDC_RETURN_TYPE="code" +REACT_APP_OIDC_AUTHORITY=https://tunnistus.test.hel.ninja/auth/realms/helsinki-tunnistus/ +REACT_APP_OIDC_CLIENT_ID="kukkuu-admin-ui-dev" +REACT_APP_OIDC_KUKKUU_API_CLIENT_ID="kukkuu-api-dev" +REACT_APP_OIDC_SCOPE="openid profile" +REACT_APP_OIDC_AUDIENCES=kukkuu-api-dev +# REACT_APP_API_URI=https://kukkuu.api.test.hel.ninja/graphql +REACT_APP_API_URI=http://localhost:8081/graphql +REACT_APP_SENTRY_DSN= +REACT_APP_FEATURE_FLAG_EXTERNAL_TICKET_SYSTEM_SUPPORT=true diff --git a/Dockerfile b/Dockerfile index e59e3157..e72e666c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -52,9 +52,13 @@ FROM appbase AS staticbuilder # =================================== ARG REACT_APP_API_URI +ARG REACT_APP_OIDC_SERVER_TYPE +ARG REACT_APP_OIDC_RETURN_TYPE ARG REACT_APP_OIDC_AUTHORITY ARG REACT_APP_OIDC_CLIENT_ID +ARG REACT_APP_OIDC_KUKKUU_API_CLIENT_ID ARG REACT_APP_OIDC_SCOPE +ARG REACT_APP_OIDC_AUDIENCES ARG REACT_APP_KUKKUU_API_OIDC_SCOPE ARG REACT_APP_ENVIRONMENT ARG REACT_APP_SENTRY_DSN diff --git a/README.md b/README.md index 1ef65780..2cf6ca8f 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,36 @@ and execute the following four commands inside your docker container: To make Kukkuu Admin use the local Tunnistamo set `REACT_APP_OIDC_AUTHORITY="http://tunnistamo-backend:8000"` for example in file `.env.local`. +#### Using the Helsinki-Profile Keycloak instead of Tunnistamo + +> It is planned that the Tunnistamo will be replaced with Helsinki-Profile Keycloak during the summer of 2024. + +There is an [example of Keycloak environment variables](./.env.local.keycloak-example) that can be used, when a local Kukkuu Admin UI is wanted to be connected to the Helsinki-Profile Keycloak of a test environment. + +The example file should include some what the following variables, that are telling the app to change the behavior of the authorization provider a bit, compared to how it is with Tunnistamo. + +- `REACT_APP_OIDC_SERVER_TYPE=KEYCLOAK` is to add some parameters to the token-request that the Keycloak service needs. As a comparison, by default it is working as `REACT_APP_OIDC_SERVER_TYPE=TUNNISTAMO`). +- `REACT_APP_OIDC_RETURN_TYPE=code` is to use authorization code flow instead of deprecated (and even removed from `oidc-client-ts`) implicit flow. +- `REACT_APP_OIDC_AUTHORITY` tells where the authorization service is located and who the issuer of the JWT is. +- `REACT_APP_OIDC_CLIENT_ID` is the unique client id that is used when the client is configured to auth service. +- `REACT_APP_OIDC_SCOPE="openid profile"` tells that the Kukkuu Admin UI needs the openid and profile information to be included in the JWT. +- `REACT_APP_OIDC_AUDIENCES=kukkuu-api-dev` means that when the authorization is given, the access is needed to these clients too, so the api-tokens needs to be generated. +- `REACT_APP_OIDC_KUKKUU_API_CLIENT_ID` is used collect the proper auth token for communication between the Admin UI and the API. + +Example configuration when a local Kukkuu API is used with a local Kukkuu Admin UI and Helsinki-Profile Keycloak from the test environment: + +```shell +REACT_APP_OIDC_SERVER_TYPE=KEYCLOAK +REACT_APP_OIDC_RETURN_TYPE="code" +REACT_APP_OIDC_AUTHORITY=https://tunnistus.test.hel.ninja/auth/realms/helsinki-tunnistus/ +REACT_APP_OIDC_CLIENT_ID="kukkuu-admin-ui-dev" +REACT_APP_OIDC_KUKKUU_API_CLIENT_ID="kukkuu-api-dev" +REACT_APP_OIDC_SCOPE="openid profile" +REACT_APP_OIDC_AUDIENCES=kukkuu-api-dev +# REACT_APP_API_URI=https://kukkuu.api.test.hel.ninja/graphql +REACT_APP_API_URI=http://localhost:8081/graphql +``` + #### Install Kukkuu API locally Clone the repository (https://github.com/City-of-Helsinki/kukkuu). Follow the instructions for running kukkuu with docker. Before running `docker-compose up` set the following settings in kukkuu roots `docker-compose.env.yaml`: diff --git a/package.json b/package.json index f87ef9ad..af93bd19 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ "history": "^5.3.0", "moment": "^2.29.4", "moment-timezone": "^0.5.43", - "oidc-client": "^1.11.5", + "oidc-client-ts": "^3.0.1", "prettier": "^2.8.8", "prop-types": "^15.8.1", "query-string": "^8.1.0", diff --git a/public/silent_renew.html b/public/silent_renew.html index fe58735c..bf88b1ac 100644 --- a/public/silent_renew.html +++ b/public/silent_renew.html @@ -1,13 +1,20 @@ - - - - - - - + + + + Silent renewal + + + + + + + + \ No newline at end of file diff --git a/src/api/apolloClient/handleApolloError.ts b/src/api/apolloClient/handleApolloError.ts index 162cd385..463c16a5 100644 --- a/src/api/apolloClient/handleApolloError.ts +++ b/src/api/apolloClient/handleApolloError.ts @@ -32,6 +32,7 @@ const handleApolloError: ErrorHandler = ({ ) { // If JWT is expired it means that we want people to log in again. We don't need to log this to sentry. console.error('JWT expired'); + authService.resetAuthState(); } else if (errorCode === 'PERMISSION_DENIED_ERROR') { // Most permission errors happen when user authentication // expires or when the user accesses the application before diff --git a/src/domain/application/AppConfig.ts b/src/domain/application/AppConfig.ts new file mode 100644 index 00000000..13180205 --- /dev/null +++ b/src/domain/application/AppConfig.ts @@ -0,0 +1,95 @@ +class AppConfig { + static get apiUrl() { + return getEnvOrError(process.env.REACT_APP_API_URI, 'REACT_APP_API_URI'); + } + + static get oidcAuthority() { + const origin = getEnvOrError( + process.env.REACT_APP_OIDC_AUTHORITY, + 'REACT_APP_OIDC_AUTHORITY' + ); + return new URL(origin).href; + } + + /** + * The audiences used in the OIDC. + * + * @example + * // In Tunnistamo it can be left as undefined, + * // because it is not included in the request done bythe OIDC client. + * ["https://api.hel.fi/auth/kukkuu"] + * // In Keycloak: + * [ + 'kukkuu-api-test', + 'profile-api-test', + ] + */ + static get oidcAudience() { + return process.env.REACT_APP_OIDC_AUDIENCES; + } + + static get oidcClientId() { + return getEnvOrError( + process.env.REACT_APP_OIDC_CLIENT_ID, + 'REACT_APP_OIDC_CLIENT_ID' + ); + } + + static get oidcScope() { + return getEnvOrError( + process.env.REACT_APP_OIDC_SCOPE, + 'REACT_APP_OIDC_SCOPE,' + ); + } + + static get oidcReturnType() { + // "code" for authorization code flow. + return process.env.REACT_APP_OIDC_RETURN_TYPE ?? 'code'; + } + + static get oidcKukkuuApiClientId() { + return ( + process.env.REACT_APP_OIDC_KUKKUU_API_CLIENT_ID ?? this.oidcKukkuuAPIScope + ); + } + + static get oidcKukkuuApiTokensUrl() { + return this.oidcServerType === 'KEYCLOAK' + ? `${this.oidcAuthority}protocol/openid-connect/token` + : `${this.oidcAuthority}api-tokens/`; + } + + static get oidcKukkuuAPIScope() { + return getEnvOrError( + process.env.REACT_APP_KUKKUU_API_OIDC_SCOPE, + 'REACT_APP_KUKKUU_API_OIDC_SCOPE' + ); + } + + /** + * NOTE: The oidcServerType is not an OIDC client attribute. + * It's purely used to help to select a configuration for the LoginProvider. + * */ + static get oidcServerType(): 'KEYCLOAK' | 'TUNNISTAMO' { + const oidcServerType = + process.env.REACT_APP_OIDC_SERVER_TYPE ?? 'TUNNISTAMO'; + if (oidcServerType === 'KEYCLOAK' || oidcServerType === 'TUNNISTAMO') { + return oidcServerType; + } + throw new Error(`Invalid OIDC server type: ${oidcServerType}`); + } +} + +// Accept both variable and name so that variable can be correctly replaced +// by build. +// process.env.VAR => value +// process.env["VAR"] => no value +// Name is used to make debugging easier. +function getEnvOrError(variable?: string, name?: string) { + if (!variable) { + throw Error(`Environment variable with name ${name} was not found`); + } + return variable; +} + +export default AppConfig; diff --git a/src/domain/authentication/CallbackPage.tsx b/src/domain/authentication/CallbackPage.tsx index 0ea154c5..b2199431 100644 --- a/src/domain/authentication/CallbackPage.tsx +++ b/src/domain/authentication/CallbackPage.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from 'react'; import { useTranslate, useDataProvider, Loading } from 'react-admin'; import * as Sentry from '@sentry/browser'; -import type { User } from 'oidc-client'; +import type { User } from 'oidc-client-ts'; import { useNavigate, useLocation } from 'react-router-dom'; import authService from './authService'; @@ -37,7 +37,7 @@ function CallBackPage() { if (role === 'none') { navigate('/unauthorized', { replace: true }); } else { - navigate(getRedirectPath(user.state?.path, pathname), { + navigate(getRedirectPath(user.url_state, pathname), { replace: true, }); } diff --git a/src/domain/authentication/__tests__/__snapshots__/authService.test.js.snap b/src/domain/authentication/__tests__/__snapshots__/authService.test.js.snap index f71ac99f..f2f30834 100644 --- a/src/domain/authentication/__tests__/__snapshots__/authService.test.js.snap +++ b/src/domain/authentication/__tests__/__snapshots__/authService.test.js.snap @@ -1,13 +1,17 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`authService fetchApiToken should call axios.get with the right arguments 1`] = ` +exports[`authService fetchApiToken should call axios with the right arguments 1`] = ` Array [ "https://tunnistamo.test.kuva.hel.ninja/api-tokens/", Object { - "baseURL": "https://tunnistamo.test.kuva.hel.ninja", + "baseURL": "https://tunnistamo.test.kuva.hel.ninja/", + "data": Object {}, "headers": Object { + "Accept": "application/json", "Authorization": "bearer db237bc3-e197-43de-8c86-3feea4c5f886", + "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8", }, + "method": "post", }, ] `; diff --git a/src/domain/authentication/__tests__/authService.test.js b/src/domain/authentication/__tests__/authService.test.js index 1c57b8bc..eebad461 100644 --- a/src/domain/authentication/__tests__/authService.test.js +++ b/src/domain/authentication/__tests__/authService.test.js @@ -3,12 +3,13 @@ import axios from 'axios'; import dataProvider from '../../../api/dataProvider'; import authService, { API_TOKEN } from '../authService'; import authorizationService from '../authorizationService'; +import AppConfig from '../../application/AppConfig'; jest.mock('axios'); describe('authService', () => { const userManager = authService.userManager; - const oidcUserKey = `oidc.user:${process.env.REACT_APP_OIDC_AUTHORITY}:${process.env.REACT_APP_OIDC_CLIENT_ID}`; + const oidcUserKey = `oidc.user:${AppConfig.oidcAuthority}:${AppConfig.oidcClientId}`; beforeEach(() => { jest.spyOn(dataProvider, 'getMyAdminProfile').mockResolvedValue({}); @@ -37,7 +38,7 @@ describe('authService', () => { it('should get API_TOKENS from localStorage', () => { authService.getToken(); - expect(localStorage.getItem).toHaveBeenNthCalledWith(2, API_TOKEN); + expect(localStorage.getItem).toHaveBeenNthCalledWith(1, API_TOKEN); }); }); @@ -87,12 +88,14 @@ describe('authService', () => { authService.login(path); - expect(signinRedirect).toHaveBeenNthCalledWith(1, { data: { path } }); + expect(signinRedirect).toHaveBeenNthCalledWith(1, { + url_state: path, + }); }); }); describe('endLogin', () => { - axios.get.mockResolvedValue({ data: {} }); + axios.mockResolvedValue({ data: {} }); const access_token = 'db237bc3-e197-43de-8c86-3feea4c5f886'; const mockUser = { name: 'Penelope Krajcik', @@ -235,9 +238,9 @@ describe('authService', () => { }; beforeEach(() => { - axios.get.mockReset(); + axios.mockReset(); - axios.get.mockResolvedValue({ + axios.mockResolvedValue({ data: { firstToken: '71ffd52c-5985-46d3-b445-490554f4012a', secondToken: 'de7c2a83-07f2-46bf-8417-8f648adbc7be', @@ -245,12 +248,12 @@ describe('authService', () => { }); }); - it('should call axios.get with the right arguments', async () => { + it('should call axios with the right arguments', async () => { expect.assertions(2); await authService.fetchApiToken(mockUser); - expect(axios.get).toHaveBeenCalledTimes(1); - expect(axios.get.mock.calls[0]).toMatchSnapshot(); + expect(axios).toHaveBeenCalledTimes(1); + expect(axios.mock.calls[0]).toMatchSnapshot(); }); it('should call localStorage.setItem with the right arguments', async () => { diff --git a/src/domain/authentication/authService.ts b/src/domain/authentication/authService.ts index 149c2b7d..cbe8f0c3 100644 --- a/src/domain/authentication/authService.ts +++ b/src/domain/authentication/authService.ts @@ -1,42 +1,79 @@ -import type { User, UserManagerSettings } from 'oidc-client'; -import { UserManager, Log, WebStorageStateStore } from 'oidc-client'; +import type { User, UserManagerSettings } from 'oidc-client-ts'; +import { UserManager, Log, WebStorageStateStore } from 'oidc-client-ts'; import axios from 'axios'; import * as Sentry from '@sentry/browser'; import projectService from '../projects/projectService'; import authorizationService from './authorizationService'; +import AppConfig from '../application/AppConfig'; const origin = window.location.origin; export const API_TOKEN = 'apiToken'; +export type ApiTokenClientProps = { + url: string; + queryProps?: { permission: string; grant_type: string }; + audiences?: string[]; +}; + export class AuthService { private userManager: UserManager; + private apiTokensClientConfig: ApiTokenClientProps; + private authServerType: 'KEYCLOAK' | 'TUNNISTAMO'; + private audience: string; constructor() { + this.authServerType = AppConfig.oidcServerType; + this.audience = AppConfig.oidcAudience ?? AppConfig.oidcKukkuuApiClientId; + const settings: UserManagerSettings = { loadUserInfo: true, userStore: new WebStorageStateStore({ store: window.localStorage }), - authority: process.env.REACT_APP_OIDC_AUTHORITY, - client_id: process.env.REACT_APP_OIDC_CLIENT_ID, + response_type: AppConfig.oidcReturnType, + authority: AppConfig.oidcAuthority, + client_id: AppConfig.oidcClientId, + scope: AppConfig.oidcScope, redirect_uri: `${origin}/callback`, - // For debugging, set it to 1 minute by removing comment: - // accessTokenExpiringNotificationTime: 59.65 * 60, - automaticSilentRenew: false, - silent_redirect_uri: `${origin}/silent_renew.html`, - response_type: 'id_token token', - scope: process.env.REACT_APP_OIDC_SCOPE, post_logout_redirect_uri: `${origin}/`, + // TODO: The silent renew support needs to be added to the React-admin authProvider as well. + // More about this: + // - https://marmelab.com/blog/2020/07/02/manage-your-jwt-react-admin-authentication-in-memory.html + // - https://marmelab.com/react-admin/addRefreshAuthToAuthProvider.html + // - https://marmelab.com/react-admin/addRefreshAuthToDataProvider.html + automaticSilentRenew: false, + // silent_redirect_uri: `${origin}/silent_renew.html`, }; - // Show oidc debugging info in the console only while developing + if (!settings.automaticSilentRenew) { + // eslint-disable-next-line no-console + console.info('Auth token silent renew is disabled.'); + } + if (process.env.NODE_ENV === 'development') { - Log.logger = console; - Log.level = Log.INFO; + // Show oidc debugging info in the console only while developing + Log.setLogger(console); + Log.setLevel(Log.INFO); } // User Manager instance this.userManager = new UserManager(settings); + // Api tokens client configuration + this.apiTokensClientConfig = { + url: AppConfig.oidcKukkuuApiTokensUrl, + queryProps: + this.authServerType === 'KEYCLOAK' + ? { + permission: '#access', + grant_type: 'urn:ietf:params:oauth:grant-type:uma-ticket', + } + : undefined, + audiences: + this.authServerType === 'KEYCLOAK' && AppConfig.oidcAudience + ? [AppConfig.oidcAudience] + : undefined, + }; + // Public methods this.getUser = this.getUser.bind(this); this.getToken = this.getToken.bind(this); @@ -46,6 +83,7 @@ export class AuthService { this.renewToken = this.renewToken.bind(this); this.logout = this.logout.bind(this); this.resetAuthState = this.resetAuthState.bind(this); + this.fetchApiToken = this.fetchApiToken.bind(this); // Events this.userManager.events.addAccessTokenExpired(() => { @@ -74,8 +112,12 @@ export class AuthService { return localStorage.getItem(API_TOKEN); } + public getUserStorageKey(): string { + return `oidc.user:${AppConfig.oidcAuthority}:${AppConfig.oidcClientId}`; + } + public isAuthenticated() { - const userKey = `oidc.user:${process.env.REACT_APP_OIDC_AUTHORITY}:${process.env.REACT_APP_OIDC_CLIENT_ID}`; + const userKey = this.getUserStorageKey(); const oidcStorage = localStorage.getItem(userKey); const apiTokens = this.getToken(); @@ -86,7 +128,7 @@ export class AuthService { public async login(path = '/'): Promise { try { - return this.userManager.signinRedirect({ data: { path } }); + return this.userManager.signinRedirect({ url_state: path }); } catch (error) { if (error instanceof Error) { if (error.message !== 'Network Error') { @@ -105,7 +147,7 @@ export class AuthService { return user; } - public renewToken(): Promise { + public renewToken(): Promise { return this.userManager.signinSilent(); } @@ -122,17 +164,36 @@ export class AuthService { } private async fetchApiToken(user: User): Promise { - const url = `${process.env.REACT_APP_OIDC_AUTHORITY}/api-tokens/`; - const { data: apiTokens } = await axios.get(url, { - baseURL: process.env.REACT_APP_OIDC_AUTHORITY, - headers: { - Authorization: `bearer ${user.access_token}`, - }, - }); - const apiToken = - apiTokens[process.env.REACT_APP_KUKKUU_API_OIDC_SCOPE as string]; - - localStorage.setItem(API_TOKEN, apiToken); + const accessToken = user.access_token; + try { + const { data } = await axios(this.apiTokensClientConfig.url, { + method: 'post', + baseURL: AppConfig.oidcAuthority, + headers: { + Authorization: `bearer ${accessToken}`, + Accept: 'application/json', + 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', + }, + data: + this.authServerType === 'KEYCLOAK' + ? { + audience: this.audience, + ...this.apiTokensClientConfig.queryProps, + } + : {}, + }); + + const apiToken = + this.authServerType === 'KEYCLOAK' + ? data.access_token + : data[AppConfig.oidcKukkuuApiClientId]; + + localStorage.setItem(API_TOKEN, apiToken); + } catch (error) { + // eslint-disable-next-line no-console + console.error('Failed to fetch API token', error); + Sentry.captureException(error); + } } } diff --git a/yarn.lock b/yarn.lock index 45621056..e2c8ec2d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4001,7 +4001,7 @@ acorn-walk@^7.1.1: resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc" integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== -acorn@^7.1.1, acorn@^7.4.1: +acorn@^7.1.1: version "7.4.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== @@ -4630,7 +4630,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.3.1, base64-js@^1.5.1: +base64-js@^1.3.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== @@ -5370,7 +5370,7 @@ core-js@^2.4.0: resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec" integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ== -core-js@^3.19.2, core-js@^3.8.3: +core-js@^3.19.2: version "3.33.0" resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.33.0.tgz#70366dbf737134761edb017990cf5ce6c6369c40" integrity sha512-HoZr92+ZjFEKar5HS6MC776gYslNOKHt75mEBKWKnPeFDpZ6nH5OeF3S6HFT1mUAUZKrzkez05VboaX8myjSuw== @@ -5442,11 +5442,6 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" -crypto-js@^4.0.0: - 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-md5@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/crypto-md5/-/crypto-md5-1.0.0.tgz#ccc8da750c753c7edcbabc542967472a384e86bb" @@ -9223,6 +9218,11 @@ jss@10.10.0, jss@^10.10.0: object.assign "^4.1.4" object.values "^1.1.6" +jwt-decode@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-4.0.0.tgz#2270352425fd413785b2faf11f6e755c5151bd4b" + integrity sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA== + keyv@^4.5.3: version "4.5.4" resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" @@ -9957,16 +9957,12 @@ 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@^1.11.5: - version "1.11.5" - resolved "https://registry.yarnpkg.com/oidc-client/-/oidc-client-1.11.5.tgz#020aa193d68a3e1f87a24fcbf50073b738de92bb" - integrity sha512-LcKrKC8Av0m/KD/4EFmo9Sg8fSQ+WFJWBrmtWd+tZkNn3WT/sQG3REmPANE9tzzhbjW6VkTNy4xhAXCfPApAOg== +oidc-client-ts@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/oidc-client-ts/-/oidc-client-ts-3.0.1.tgz#be264fb87c89f74f73863646431c32cd06f5ceb7" + integrity sha512-xX8unZNtmtw3sOz4FPSqDhkLFnxCDsdo2qhFEH2opgWnF/iXMFoYdBQzkwCxAZVgt3FT3DnuBY3k80EZHT0RYg== dependencies: - acorn "^7.4.1" - base64-js "^1.5.1" - core-js "^3.8.3" - crypto-js "^4.0.0" - serialize-javascript "^4.0.0" + jwt-decode "^4.0.0" on-finished@2.4.1: version "2.4.1"