Implement a generic OpenIdConnect auth-provider

This commit is contained in:
Brian Leathem
2020-11-16 14:04:24 -08:00
parent 17f4d7deb2
commit 8d948b243b
15 changed files with 727 additions and 3 deletions
+14
View File
@@ -235,6 +235,20 @@ auth:
$env: AUTH_OAUTH2_AUTH_URL
tokenUrl:
$env: AUTH_OAUTH2_TOKEN_URL
oidc:
development:
adUrl:
$env: AUTH_OIDC_AD_URL
clientId:
$env: AUTH_OIDC_CLIENT_ID
clientSecret:
$env: AUTH_OIDC_CLIENT_SECRET
authorizationUrl:
$env: AUTH_OIDC_AUTH_URL
tokenUrl:
$env: AUTH_OIDC_TOKEN_URL
tokenSignedResponseAlg:
$env: AUTH_OIDC_TOKEN_SIGNED_RESPONSE_ALG
auth0:
development:
clientId:
+7
View File
@@ -22,9 +22,16 @@ import {
samlAuthApiRef,
microsoftAuthApiRef,
oneloginAuthApiRef,
oidcApiRef,
} from '@backstage/core';
export const providers = [
{
id: 'oidc-auth-provider',
title: 'Oidc',
message: 'Sign In using OpenId Connect',
apiRef: oidcApiRef,
},
{
id: 'google-auth-provider',
title: 'Google',
@@ -311,6 +311,20 @@ export const oauth2ApiRef: ApiRef<
description: 'Example of how to use oauth2 custom provider',
});
/**
* Provides authentication for custom OpenID Connect identity providers.
*/
export const oidcApiRef: ApiRef<
OAuthApi &
OpenIdConnectApi &
ProfileInfoApi &
BackstageIdentityApi &
SessionApi
> = createApiRef({
id: 'core.auth.oidc',
description: 'Example of how to use oidc custom provider',
});
/**
* Provides authentication for saml based identity providers
*/
@@ -23,3 +23,4 @@ export * from './saml';
export * from './auth0';
export * from './microsoft';
export * from './onelogin';
export * from './oidc';
@@ -0,0 +1,152 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import OpenIdConnect from './Oidc';
const theFuture = new Date(Date.now() + 3600000);
const thePast = new Date(Date.now() - 10);
const PREFIX = 'https://www.googleapis.com/auth/';
const scopeTransform = (x: string[]) => x;
describe('OpenIdConnect', () => {
it('should get refreshed access token', async () => {
const getSession = jest.fn().mockResolvedValue({
providerInfo: { accessToken: 'access-token', expiresAt: theFuture },
});
const openIdConnect = new OpenIdConnect({
sessionManager: { getSession } as any,
scopeTransform,
});
expect(await openIdConnect.getAccessToken('my-scope my-scope2')).toBe(
'access-token',
);
expect(getSession).toBeCalledTimes(1);
expect(getSession.mock.calls[0][0].scopes).toEqual(
new Set(['my-scope', 'my-scope2']),
);
});
it('should transform scopes', async () => {
const getSession = jest.fn().mockResolvedValue({
providerInfo: { accessToken: 'access-token', expiresAt: theFuture },
});
const openIdConnect = new OpenIdConnect({
sessionManager: { getSession } as any,
scopeTransform: scopes => scopes.map(scope => `my-prefix/${scope}`),
});
expect(await openIdConnect.getAccessToken('my-scope')).toBe('access-token');
expect(getSession).toBeCalledTimes(1);
expect(getSession.mock.calls[0][0].scopes).toEqual(
new Set(['my-prefix/my-scope']),
);
});
it('should get refreshed id token', async () => {
const getSession = jest.fn().mockResolvedValue({
providerInfo: { idToken: 'id-token', expiresAt: theFuture },
});
const openIdConnect = new OpenIdConnect({
sessionManager: { getSession } as any,
scopeTransform,
});
expect(await openIdConnect.getIdToken()).toBe('id-token');
expect(getSession).toBeCalledTimes(1);
});
it('should get optional id token', async () => {
const getSession = jest.fn().mockResolvedValue({
providerInfo: { idToken: 'id-token', expiresAt: theFuture },
});
const openIdConnect = new OpenIdConnect({
sessionManager: { getSession } as any,
scopeTransform,
});
expect(await openIdConnect.getIdToken({ optional: true })).toBe('id-token');
expect(getSession).toBeCalledTimes(1);
});
it('should share popup closed errors', async () => {
const error = new Error('NOPE');
error.name = 'RejectedError';
const getSession = jest
.fn()
.mockResolvedValueOnce({
providerInfo: {
accessToken: 'access-token',
expiresAt: theFuture,
scopes: new Set([`${PREFIX}not-enough`]),
},
})
.mockRejectedValue(error);
const openIdConnect = new OpenIdConnect({
sessionManager: { getSession } as any,
scopeTransform,
});
// Make sure we have a session before we do the double request, so that we get past the !this.currentSession check
await expect(openIdConnect.getAccessToken()).resolves.toBe('access-token');
const promise1 = openIdConnect.getAccessToken('more');
const promise2 = openIdConnect.getAccessToken('more');
await expect(promise1).rejects.toBe(error);
await expect(promise2).rejects.toBe(error);
expect(getSession).toBeCalledTimes(3);
});
it('should wait for all session refreshes', async () => {
const initialSession = {
providerInfo: {
idToken: 'token1',
expiresAt: theFuture,
scopes: new Set(),
},
};
const getSession = jest
.fn()
.mockResolvedValueOnce(initialSession)
.mockResolvedValue({
providerInfo: {
idToken: 'token2',
expiresAt: theFuture,
scopes: new Set(),
},
});
const openIdConnect = new OpenIdConnect({
sessionManager: { getSession } as any,
scopeTransform,
});
// Grab the expired session first
await expect(openIdConnect.getIdToken()).resolves.toBe('token1');
expect(getSession).toBeCalledTimes(1);
initialSession.providerInfo.expiresAt = thePast;
const promise1 = openIdConnect.getIdToken();
const promise2 = openIdConnect.getIdToken();
const promise3 = openIdConnect.getIdToken();
await expect(promise1).resolves.toBe('token2');
await expect(promise2).resolves.toBe('token2');
await expect(promise3).resolves.toBe('token2');
expect(getSession).toBeCalledTimes(4); // De-duping of session requests happens in client
});
});
@@ -0,0 +1,183 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import OpenIdConnectIcon from '@material-ui/icons/AcUnit';
import { DefaultAuthConnector } from '../../../../lib/AuthConnector';
import { RefreshingAuthSessionManager } from '../../../../lib/AuthSessionManager';
import { SessionManager } from '../../../../lib/AuthSessionManager/types';
import { Observable } from '../../../../types';
import {
AuthRequestOptions,
BackstageIdentity,
OAuthApi,
OpenIdConnectApi,
ProfileInfo,
ProfileInfoApi,
SessionState,
SessionApi,
BackstageIdentityApi,
} from '../../../definitions/auth';
import { OpenIdConnectSession } from './types';
import { OAuthApiCreateOptions } from '../types';
type Options = {
sessionManager: SessionManager<OpenIdConnectSession>;
scopeTransform: (scopes: string[]) => string[];
};
type CreateOptions = OAuthApiCreateOptions & {
scopeTransform?: (scopes: string[]) => string[];
};
export type OpenIdConnectResponse = {
providerInfo: {
accessToken: string;
idToken: string;
scope: string;
expiresInSeconds: number;
};
profile: ProfileInfo;
backstageIdentity: BackstageIdentity;
};
const DEFAULT_PROVIDER = {
id: 'oidc',
title: 'Open ID Connect Identity Provider',
icon: OpenIdConnectIcon,
};
class OpenIdConnect
implements
OAuthApi,
OpenIdConnectApi,
ProfileInfoApi,
BackstageIdentityApi,
SessionApi {
static create({
discoveryApi,
environment = 'development',
provider = DEFAULT_PROVIDER,
oauthRequestApi,
defaultScopes = [],
scopeTransform = x => x,
}: CreateOptions) {
const connector = new DefaultAuthConnector({
discoveryApi,
environment,
provider,
oauthRequestApi: oauthRequestApi,
sessionTransform(res: OpenIdConnectResponse): OpenIdConnectSession {
return {
...res,
providerInfo: {
idToken: res.providerInfo.idToken,
accessToken: res.providerInfo.accessToken,
scopes: OpenIdConnect.normalizeScopes(
scopeTransform,
res.providerInfo.scope,
),
expiresAt: new Date(
Date.now() + res.providerInfo.expiresInSeconds * 1000,
),
},
};
},
});
const sessionManager = new RefreshingAuthSessionManager({
connector,
defaultScopes: new Set(defaultScopes),
sessionScopes: (session: OpenIdConnectSession) =>
session.providerInfo.scopes,
sessionShouldRefresh: (session: OpenIdConnectSession) => {
const expiresInSec =
(session.providerInfo.expiresAt.getTime() - Date.now()) / 1000;
return expiresInSec < 60 * 5;
},
});
return new OpenIdConnect({ sessionManager, scopeTransform });
}
private readonly sessionManager: SessionManager<OpenIdConnectSession>;
private readonly scopeTransform: (scopes: string[]) => string[];
constructor(options: Options) {
this.sessionManager = options.sessionManager;
this.scopeTransform = options.scopeTransform;
}
async signIn() {
await this.getAccessToken();
}
async signOut() {
await this.sessionManager.removeSession();
}
sessionState$(): Observable<SessionState> {
return this.sessionManager.sessionState$();
}
async getAccessToken(
scope?: string | string[],
options?: AuthRequestOptions,
) {
const normalizedScopes = OpenIdConnect.normalizeScopes(
this.scopeTransform,
scope,
);
const session = await this.sessionManager.getSession({
...options,
scopes: normalizedScopes,
});
return session?.providerInfo.accessToken ?? '';
}
async getIdToken(options: AuthRequestOptions = {}) {
const session = await this.sessionManager.getSession(options);
return session?.providerInfo.idToken ?? '';
}
async getBackstageIdentity(
options: AuthRequestOptions = {},
): Promise<BackstageIdentity | undefined> {
const session = await this.sessionManager.getSession(options);
return session?.backstageIdentity;
}
async getProfile(options: AuthRequestOptions = {}) {
const session = await this.sessionManager.getSession(options);
return session?.profile;
}
private static normalizeScopes(
scopeTransform: (scopes: string[]) => string[],
scopes?: string | string[],
): Set<string> {
if (!scopes) {
return new Set();
}
const scopeList = Array.isArray(scopes)
? scopes
: scopes.split(/[\s|,]/).filter(Boolean);
return new Set(scopeTransform(scopeList));
}
}
export default OpenIdConnect;
@@ -0,0 +1,18 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { default as OpenIdConnect } from './Oidc';
export * from './types';
@@ -0,0 +1,28 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ProfileInfo, BackstageIdentity } from '../../../definitions';
export type OpenIdConnectSession = {
providerInfo: {
idToken: string;
accessToken: string;
scopes: Set<string>;
expiresAt: Date;
};
profile: ProfileInfo;
backstageIdentity: BackstageIdentity;
};
@@ -46,6 +46,8 @@ import {
SamlAuth,
oneloginAuthApiRef,
OneLoginAuth,
oidcApiRef,
OpenIdConnect,
} from '@backstage/core-api';
export const defaultApis = [
@@ -153,4 +155,13 @@ export const defaultApis = [
factory: ({ discoveryApi, oauthRequestApi }) =>
OneLoginAuth.create({ discoveryApi, oauthRequestApi }),
}),
createApiFactory({
api: oidcApiRef,
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
},
factory: ({ discoveryApi, oauthRequestApi }) =>
OpenIdConnect.create({ discoveryApi, oauthRequestApi }),
}),
];
+4
View File
@@ -25,12 +25,15 @@
"@backstage/catalog-model": "^0.2.0",
"@backstage/config": "^0.1.1",
"@types/express": "^4.17.6",
"@types/express-session": "^1.17.2",
"@types/openid-client": "^3.7.0",
"compression": "^1.7.4",
"cookie-parser": "^1.4.5",
"cors": "^2.8.5",
"cross-fetch": "^3.0.6",
"express": "^4.17.1",
"express-promise-router": "^3.0.3",
"express-session": "^1.17.1",
"fs-extra": "^9.0.0",
"got": "^11.5.2",
"helmet": "^4.0.0",
@@ -39,6 +42,7 @@
"knex": "^0.21.6",
"moment": "^2.26.0",
"morgan": "^1.10.0",
"openid-client": "^4.2.1",
"passport": "^0.4.1",
"passport-github2": "^0.1.12",
"passport-gitlab2": "^5.0.0",
@@ -18,6 +18,7 @@ import { createGithubProvider } from './github';
import { createGitlabProvider } from './gitlab';
import { createGoogleProvider } from './google';
import { createOAuth2Provider } from './oauth2';
import { createOidcProvider } from './oidc';
import { createOktaProvider } from './okta';
import { createSamlProvider } from './saml';
import { createAuth0Provider } from './auth0';
@@ -34,6 +35,7 @@ const factories: { [providerId: string]: AuthProviderFactory } = {
auth0: createAuth0Provider,
microsoft: createMicrosoftProvider,
oauth2: createOAuth2Provider,
oidc: createOidcProvider,
onelogin: createOneLoginProvider,
};
@@ -0,0 +1,17 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { createOidcProvider } from './provider';
@@ -0,0 +1,216 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import express from 'express';
import {
Issuer,
Client,
Strategy as OidcStrategy,
TokenSet,
UserinfoResponse,
} from 'openid-client';
import {
OAuthAdapter,
OAuthProviderOptions,
OAuthHandlers,
OAuthResponse,
OAuthEnvironmentHandler,
OAuthStartRequest,
encodeState,
OAuthRefreshRequest,
} from '../../lib/oauth';
import {
executeFetchUserProfileStrategy,
executeFrameHandlerStrategy,
executeRedirectStrategy,
executeRefreshTokenStrategy,
PassportDoneCallback,
} from '../../lib/passport';
import { RedirectInfo, AuthProviderFactory, ProfileInfo } from '../types';
type PrivateInfo = {
refreshToken: string;
};
export type OidcAuthProviderOptions = OAuthProviderOptions & {
authorizationUrl: string;
tokenUrl: string;
adUrl: string;
tokenSignedResponseAlg?: string;
};
export class OidcAuthProvider implements OAuthHandlers {
private readonly _strategy: Promise<OidcStrategy<UserinfoResponse, Client>>;
constructor(options: OidcAuthProviderOptions) {
this._strategy = this.setupStrategy(options);
}
async start(req: OAuthStartRequest): Promise<RedirectInfo> {
const strategy = await this._strategy;
return await executeRedirectStrategy(req, strategy, {
accessType: 'offline',
prompt: 'consent',
scope: `${req.scope} default`,
state: encodeState(req.state),
});
}
async handler(
req: express.Request,
): Promise<{ response: OAuthResponse; refreshToken: string }> {
const strategy = await this._strategy;
const { response, privateInfo } = await executeFrameHandlerStrategy<
OAuthResponse,
PrivateInfo
>(req, strategy);
return {
response: await this.populateIdentity(response),
refreshToken: privateInfo.refreshToken,
};
}
async refresh(req: OAuthRefreshRequest): Promise<OAuthResponse> {
const strategy = await this._strategy;
const refreshTokenResponse = await executeRefreshTokenStrategy(
strategy,
req.refreshToken,
req.scope,
);
const {
accessToken,
params,
refreshToken: updatedRefreshToken,
} = refreshTokenResponse;
const profile = await executeFetchUserProfileStrategy(
strategy,
accessToken,
params.id_token,
);
return this.populateIdentity({
providerInfo: {
accessToken,
refreshToken: updatedRefreshToken,
idToken: params.id_token,
expiresInSeconds: params.expires_in,
scope: params.scope,
},
profile,
});
}
private async setupStrategy(
options: OidcAuthProviderOptions,
): Promise<OidcStrategy<UserinfoResponse, Client>> {
const issuer = await Issuer.discover(options.adUrl);
// console.log('Discovered issuer %s %O', issuer.issuer, issuer.metadata);
const client = new issuer.Client({
client_id: options.clientId,
client_secret: options.clientSecret,
redirect_uris: [options.callbackUrl],
response_types: ['code'],
id_token_signed_response_alg: options.tokenSignedResponseAlg || 'RS256',
});
const strategy = new OidcStrategy(
{
client,
passReqToCallback: false as true,
},
(
tokenset: TokenSet,
userinfo: UserinfoResponse,
done: PassportDoneCallback<OAuthResponse, PrivateInfo>,
) => {
const profile: ProfileInfo = {
displayName: userinfo.name,
email: userinfo.email,
picture: userinfo.picture,
};
done(
undefined,
{
providerInfo: {
idToken: tokenset.id_token || '',
accessToken: tokenset.access_token || '',
scope: tokenset.scope || '',
expiresInSeconds: tokenset.expires_in,
},
profile,
},
{
refreshToken: tokenset.refresh_token || '',
},
);
},
);
strategy.error = console.error;
return strategy;
}
// Use this function to grab the user profile info from the token
// Then populate the profile with it
private async populateIdentity(
response: OAuthResponse,
): Promise<OAuthResponse> {
const { profile } = response;
if (!profile.email) {
throw new Error('Profile does not contain an email');
}
const id = profile.email.split('@')[0];
return { ...response, backstageIdentity: { id } };
}
}
export const createOidcProvider: AuthProviderFactory = ({
globalConfig,
config,
tokenIssuer,
}) =>
OAuthEnvironmentHandler.mapConfig(config, envConfig => {
const providerId = 'oidc';
const clientId = envConfig.getString('clientId');
const clientSecret = envConfig.getString('clientSecret');
const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`;
const authorizationUrl = envConfig.getString('authorizationUrl');
const adUrl = envConfig.getString('adUrl');
const tokenUrl = envConfig.getString('tokenUrl');
const tokenSignedResponseAlg = envConfig.getString(
'tokenSignedResponseAlg',
);
const provider = new OidcAuthProvider({
clientId,
clientSecret,
callbackUrl,
authorizationUrl,
tokenUrl,
tokenSignedResponseAlg,
adUrl,
});
return OAuthAdapter.fromConfig(globalConfig, provider, {
disableRefresh: false,
providerId,
tokenIssuer,
});
});
+4 -1
View File
@@ -27,6 +27,7 @@ import Router from 'express-promise-router';
import { Logger } from 'winston';
import { createOidcRouter, DatabaseKeyStore, TokenFactory } from '../identity';
import { createAuthProvider } from '../providers';
import session from 'express-session';
export interface RouterOptions {
logger: Logger;
@@ -59,7 +60,9 @@ export async function createRouter({
});
const catalogApi = new CatalogClient({ discoveryApi: discovery });
router.use(cookieParser());
const secret = 'backstage secret'; // TODO: Allow an override here
router.use(cookieParser(secret));
router.use(session({ secret }));
router.use(express.urlencoded({ extended: false }));
router.use(express.json());
+56 -2
View File
@@ -5138,6 +5138,13 @@
"@types/qs" "*"
"@types/range-parser" "*"
"@types/express-session@^1.17.2":
version "1.17.2"
resolved "https://registry.npmjs.org/@types/express-session/-/express-session-1.17.2.tgz#ed6a36dd9f267c7fef86004f653bfb9b5cea3c21"
integrity sha512-QRm/fUuvr/BAosL9CvK351SDQP7wpD8+h3S8ZEE/8IvHJ/ZqHrjZbjx/flYfazyPw7yNi9O5fbjFZbh0vZ1ccg==
dependencies:
"@types/express" "*"
"@types/express@*", "@types/express@4.17.7", "@types/express@^4.17.6", "@types/express@^4.17.7":
version "4.17.7"
resolved "https://registry.npmjs.org/@types/express/-/express-4.17.7.tgz#42045be6475636d9801369cd4418ef65cdb0dd59"
@@ -5566,6 +5573,13 @@
dependencies:
"@types/node" "*"
"@types/openid-client@^3.7.0":
version "3.7.0"
resolved "https://registry.npmjs.org/@types/openid-client/-/openid-client-3.7.0.tgz#8406e12798d16083df09cc3625973f5a00dd57fa"
integrity sha512-hW+QbAeUyfUHABwUjUECOcv56Mf5tN29xK3GPN7wy3R3XfIQdkm37XELA4wbe2RNG9GYUpN8l2ytfoW05XmpgQ==
dependencies:
openid-client "*"
"@types/ora@^3.2.0":
version "3.2.0"
resolved "https://registry.npmjs.org/@types/ora/-/ora-3.2.0.tgz#b2f65d1283a8f36d8b0f9ee767e0732a2f429362"
@@ -11564,6 +11578,20 @@ express-promise-router@^3.0.3:
lodash.flattendeep "^4.0.0"
methods "^1.0.0"
express-session@^1.17.1:
version "1.17.1"
resolved "https://registry.npmjs.org/express-session/-/express-session-1.17.1.tgz#36ecbc7034566d38c8509885c044d461c11bf357"
integrity sha512-UbHwgqjxQZJiWRTMyhvWGvjBQduGCSBDhhZXYenziMFjxst5rMV+aJZ6hKPHZnPyHGsrqRICxtX8jtEbm/z36Q==
dependencies:
cookie "0.4.0"
cookie-signature "1.0.6"
debug "2.6.9"
depd "~2.0.0"
on-headers "~1.0.2"
parseurl "~1.3.3"
safe-buffer "5.2.0"
uid-safe "~2.1.5"
express@^4.0.0, express@^4.17.0, express@^4.17.1:
version "4.17.1"
resolved "https://registry.npmjs.org/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134"
@@ -12792,7 +12820,7 @@ got@^11.6.2:
p-cancelable "^2.0.0"
responselike "^2.0.0"
got@^11.7.0:
got@^11.7.0, got@^11.8.0:
version "11.8.0"
resolved "https://registry.npmjs.org/got/-/got-11.8.0.tgz#be0920c3586b07fd94add3b5b27cb28f49e6545f"
integrity sha512-k9noyoIIY9EejuhaBNLyZ31D5328LeqnyPNXJQb2XlJZcKakLqN5m6O/ikhq/0lw56kUYS54fVm+D1x57YC9oQ==
@@ -17740,6 +17768,20 @@ opencollective-postinstall@^2.0.2:
resolved "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.2.tgz#5657f1bede69b6e33a45939b061eb53d3c6c3a89"
integrity sha512-pVOEP16TrAO2/fjej1IdOyupJY8KDUM1CvsaScRbw6oddvpQoOfGk4ywha0HKKVAD6RkW4x6Q+tNBwhf3Bgpuw==
openid-client@*, openid-client@^4.2.1:
version "4.2.1"
resolved "https://registry.npmjs.org/openid-client/-/openid-client-4.2.1.tgz#8200c0ab6a3b8e954727dfa790847dc5cb8999c2"
integrity sha512-07eOcJeMH3ZHNvx5DVMZQmy3vZSTQqKSSunbtM1pXb+k5LBPi5hMum1vJCFReXlo4wuLEqZ/OgbsZvXPhbGRtA==
dependencies:
base64url "^3.0.1"
got "^11.8.0"
jose "^2.0.2"
lru-cache "^6.0.0"
make-error "^1.3.6"
object-hash "^2.0.1"
oidc-token-hash "^5.0.0"
p-any "^3.0.0"
openid-client@^4.1.1:
version "4.1.1"
resolved "https://registry.npmjs.org/openid-client/-/openid-client-4.1.1.tgz#3e8a25584c4292e9b9b03e60358f5549fb85197a"
@@ -19573,6 +19615,11 @@ ramda@^0.26, ramda@~0.26.1:
resolved "https://registry.npmjs.org/ramda/-/ramda-0.26.1.tgz#8d41351eb8111c55353617fc3bbffad8e4d35d06"
integrity sha512-hLWjpy7EnsDBb0p+Z3B7rPi3GDeRG5ZtiI33kJhTt+ORCd38AbAIjB/9zRIUoeTbE/AVX5ZkU7m6bznsvrf8eQ==
random-bytes@~1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz#4f68a1dc0ae58bd3fb95848c30324db75d64360b"
integrity sha1-T2ih3Arli9P7lYSMMDJNt11kNgs=
randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0:
version "2.1.0"
resolved "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
@@ -21086,7 +21133,7 @@ safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0:
safe-buffer@5.2.0, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0:
version "5.2.0"
resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519"
integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==
@@ -23357,6 +23404,13 @@ uid-number@0.0.6:
resolved "https://registry.npmjs.org/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81"
integrity sha1-DqEOgDXo61uOREnwbaHHMGY7qoE=
uid-safe@~2.1.5:
version "2.1.5"
resolved "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz#2b3d5c7240e8fc2e58f8aa269e5ee49c0857bd3a"
integrity sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==
dependencies:
random-bytes "~1.0.0"
uid2@0.0.3, uid2@0.0.x:
version "0.0.3"
resolved "https://registry.npmjs.org/uid2/-/uid2-0.0.3.tgz#483126e11774df2f71b8b639dcd799c376162b82"