Merge pull request #4047 from backjo/feature/AwsAlbReverseProxyAuthentication
Add auth-backend provider for AWS ALB OIDC reverse proxy
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-auth-backend': patch
|
||||
---
|
||||
|
||||
Add AWS ALB OIDC reverse proxy authentication provider
|
||||
@@ -16,6 +16,11 @@
|
||||
"url": "https://github.com/backstage/backstage",
|
||||
"directory": "plugins/auth-backend"
|
||||
},
|
||||
"jest": {
|
||||
"moduleNameMapper": {
|
||||
"^jose/(.*)$": "<rootDir>/../../../node_modules/jose/dist/node/cjs/$1"
|
||||
}
|
||||
},
|
||||
"keywords": [
|
||||
"backstage"
|
||||
],
|
||||
@@ -44,11 +49,12 @@
|
||||
"fs-extra": "^9.0.0",
|
||||
"got": "^11.5.2",
|
||||
"helmet": "^4.0.0",
|
||||
"jose": "^1.27.1",
|
||||
"jose": "^3.5.1",
|
||||
"jwt-decode": "^3.1.0",
|
||||
"knex": "^0.21.6",
|
||||
"moment": "^2.26.0",
|
||||
"morgan": "^1.10.0",
|
||||
"node-cache": "^5.1.2",
|
||||
"openid-client": "^4.2.1",
|
||||
"passport": "^0.4.1",
|
||||
"passport-github2": "^0.1.12",
|
||||
|
||||
@@ -13,12 +13,23 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { TextDecoder, TextEncoder } from 'util';
|
||||
|
||||
// These two statements are structured like this because Jest doesn't include these in the default
|
||||
// test environment, even though they exist in Node.
|
||||
global.TextEncoder = TextEncoder;
|
||||
// @ts-ignore
|
||||
global.TextDecoder = TextDecoder;
|
||||
|
||||
import { utc } from 'moment';
|
||||
import { TokenFactory } from './TokenFactory';
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { KeyStore, AnyJWK, StoredKey } from './types';
|
||||
import { JWKS, JSONWebKey, JWT } from 'jose';
|
||||
import { AnyJWK, KeyStore, StoredKey } from './types';
|
||||
import jwtVerify from 'jose/jwt/verify';
|
||||
import parseJwk from 'jose/jwk/parse';
|
||||
import decodeProtectedHeader, {
|
||||
ProtectedHeaderParameters,
|
||||
} from 'jose/util/decode_protected_header';
|
||||
|
||||
const logger = getVoidLogger();
|
||||
|
||||
@@ -52,10 +63,8 @@ class MemoryKeyStore implements KeyStore {
|
||||
}
|
||||
|
||||
function jwtKid(jwt: string): string {
|
||||
const { header } = JWT.decode(jwt, { complete: true }) as {
|
||||
header: { kid: string };
|
||||
};
|
||||
return header.kid;
|
||||
const header = decodeProtectedHeader(jwt) as ProtectedHeaderParameters;
|
||||
return header.kid as string;
|
||||
}
|
||||
|
||||
describe('TokenFactory', () => {
|
||||
@@ -72,14 +81,14 @@ describe('TokenFactory', () => {
|
||||
const token = await factory.issueToken({ claims: { sub: 'foo' } });
|
||||
|
||||
const { keys } = await factory.listPublicKeys();
|
||||
const keyStore = JWKS.asKeyStore({
|
||||
keys: keys.map(key => key as JSONWebKey),
|
||||
});
|
||||
const keyMap = Object.fromEntries(keys.map(key => [key.kid, key]));
|
||||
|
||||
const payload = JWT.verify(token, keyStore) as object & {
|
||||
iat: number;
|
||||
exp: number;
|
||||
};
|
||||
const payload = (
|
||||
await jwtVerify(token, async header => {
|
||||
const kid = header.kid as string;
|
||||
return await parseJwk(keyMap[kid]);
|
||||
})
|
||||
).payload;
|
||||
expect(payload).toEqual({
|
||||
iss: 'my-issuer',
|
||||
aud: 'backstage',
|
||||
@@ -87,7 +96,7 @@ describe('TokenFactory', () => {
|
||||
iat: expect.any(Number),
|
||||
exp: expect.any(Number),
|
||||
});
|
||||
expect(payload.exp).toBe(payload.iat + keyDurationSeconds);
|
||||
expect(payload.exp).toBe(Number(payload.iat) + keyDurationSeconds);
|
||||
});
|
||||
|
||||
it('should generate new signing keys when the current one expires', async () => {
|
||||
|
||||
@@ -16,7 +16,10 @@
|
||||
|
||||
import moment from 'moment';
|
||||
import { TokenIssuer, TokenParams, KeyStore, AnyJWK } from './types';
|
||||
import { JSONWebKey, JWK, JWS } from 'jose';
|
||||
import parseJwk, { JWK } from 'jose/jwk/parse';
|
||||
import SignJWT from 'jose/jwt/sign';
|
||||
import generateKeyPair from 'jose/util/generate_key_pair';
|
||||
import fromKeyLike from 'jose/jwk/from_key_like';
|
||||
import { Logger } from 'winston';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
@@ -53,7 +56,7 @@ export class TokenFactory implements TokenIssuer {
|
||||
private readonly keyDurationSeconds: number;
|
||||
|
||||
private keyExpiry?: moment.Moment;
|
||||
private privateKeyPromise?: Promise<JSONWebKey>;
|
||||
private privateKeyPromise?: Promise<JWK>;
|
||||
|
||||
constructor(options: Options) {
|
||||
this.issuer = options.issuer;
|
||||
@@ -64,7 +67,7 @@ export class TokenFactory implements TokenIssuer {
|
||||
|
||||
async issueToken(params: TokenParams): Promise<string> {
|
||||
const key = await this.getKey();
|
||||
|
||||
const keyLike = await parseJwk(key);
|
||||
const iss = this.issuer;
|
||||
const sub = params.claims.sub;
|
||||
const aud = 'backstage';
|
||||
@@ -72,11 +75,9 @@ export class TokenFactory implements TokenIssuer {
|
||||
const exp = iat + this.keyDurationSeconds;
|
||||
|
||||
this.logger.info(`Issuing token for ${sub}`);
|
||||
|
||||
return JWS.sign({ iss, sub, aud, iat, exp }, key, {
|
||||
alg: key.alg,
|
||||
kid: key.kid,
|
||||
});
|
||||
return new SignJWT({ iss, sub, aud, iat, exp })
|
||||
.setProtectedHeader({ alg: key.alg, typ: 'JWT', kid: key.kid })
|
||||
.sign(keyLike);
|
||||
}
|
||||
|
||||
// This will be called by other services that want to verify ID tokens.
|
||||
@@ -114,7 +115,7 @@ export class TokenFactory implements TokenIssuer {
|
||||
return { keys: validKeys.map(({ key }) => key) };
|
||||
}
|
||||
|
||||
private async getKey(): Promise<JSONWebKey> {
|
||||
private async getKey(): Promise<JWK> {
|
||||
// Make sure that we only generate one key at a time
|
||||
if (this.privateKeyPromise) {
|
||||
if (this.keyExpiry?.isAfter()) {
|
||||
@@ -127,23 +128,30 @@ export class TokenFactory implements TokenIssuer {
|
||||
this.keyExpiry = moment().add(this.keyDurationSeconds, 'seconds');
|
||||
const promise = (async () => {
|
||||
// This generates a new signing key to be used to sign tokens until the next key rotation
|
||||
const key = await JWK.generate('EC', 'P-256', {
|
||||
use: 'sig',
|
||||
kid: uuid(),
|
||||
alg: 'ES256',
|
||||
});
|
||||
|
||||
const key = await generateKeyPair('ES256');
|
||||
const kid = uuid();
|
||||
const jwk = await fromKeyLike(key.privateKey);
|
||||
|
||||
// JOSE Library provides optional for most fields - and TS does not distinguish between missing/undefined.
|
||||
// Because AnyJWK requires keys to have type "string", this throws a TypeError - though in practice, if the field
|
||||
// is undefined, JOSE will not send it back as key.
|
||||
const storedJwk: AnyJWK = {
|
||||
...jwk,
|
||||
alg: 'ES256',
|
||||
kid: kid,
|
||||
use: 'sig',
|
||||
} as AnyJWK;
|
||||
// We're not allowed to use the key until it has been successfully stored
|
||||
// TODO: some token verification implementations aggressively cache the list of keys, and
|
||||
// don't attempt to fetch new ones even if they encounter an unknown kid. Therefore we
|
||||
// may want to keep using the existing key for some period of time until we switch to
|
||||
// the new one. This also needs to be implemented cross-service though, meaning new services
|
||||
// that boot up need to be able to grab an existing key to use for signing.
|
||||
this.logger.info(`Created new signing key ${key.kid}`);
|
||||
await this.keyStore.addKey((key.toJWK(false) as unknown) as AnyJWK);
|
||||
|
||||
this.logger.info(`Created new signing key ${jwk.kid}`);
|
||||
await this.keyStore.addKey(storedJwk);
|
||||
// At this point we are allowed to start using the new key
|
||||
return key as JSONWebKey;
|
||||
return storedJwk;
|
||||
})();
|
||||
|
||||
this.privateKeyPromise = promise;
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright 2021 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 { createAwsAlbProvider } from './provider';
|
||||
@@ -0,0 +1,192 @@
|
||||
/*
|
||||
* 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 { getVoidLogger } from '@backstage/backend-common';
|
||||
import express from 'express';
|
||||
import * as jwtVerify from 'jose/jwt/verify';
|
||||
|
||||
import { AwsAlbAuthProvider } from './provider';
|
||||
import { AuthResponse } from '../types';
|
||||
|
||||
const mockedJwtVerify = jwtVerify as jest.Mocked<any>;
|
||||
|
||||
const mockKey = async () => {
|
||||
return `-----BEGIN PUBLIC KEY-----
|
||||
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEnuN4LlaJhaUpx+qZFTzYCrSBLk0I
|
||||
yOlxJ2VW88mLAQGJ7HPAvOdylxZsItMnzCuqNzZvie8m/NJsOjhDncVkrw==
|
||||
-----END PUBLIC KEY-----
|
||||
`;
|
||||
};
|
||||
|
||||
jest.mock('cross-fetch', () => ({
|
||||
__esModule: true,
|
||||
default: async () => {
|
||||
return {
|
||||
json: async () => {
|
||||
return mockKey();
|
||||
},
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('jose/jwt/verify', () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const identityResolutionCallbackMock = async (): Promise<AuthResponse<any>> => {
|
||||
return {
|
||||
backstageIdentity: {
|
||||
id: 'foo',
|
||||
idToken: '',
|
||||
},
|
||||
profile: {
|
||||
displayName: 'Foo Bar',
|
||||
},
|
||||
providerInfo: {},
|
||||
};
|
||||
};
|
||||
|
||||
const identityResolutionCallbackRejectedMock = async (): Promise<
|
||||
AuthResponse<any>
|
||||
> => {
|
||||
throw new Error('failed');
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('AwsALBAuthProvider', () => {
|
||||
const catalogApi = {
|
||||
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
|
||||
addLocation: jest.fn(),
|
||||
getEntities: jest.fn(),
|
||||
getLocationByEntity: jest.fn(),
|
||||
getLocationById: jest.fn(),
|
||||
removeEntityByUid: jest.fn(),
|
||||
getEntityByName: jest.fn(),
|
||||
};
|
||||
|
||||
const mockResponseSend = jest.fn();
|
||||
const mockRequest = ({
|
||||
header: jest.fn(() => {
|
||||
return 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImZvbyIsImlzc3VlciI6ImZvbyJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.zUkMYAuMwC1T0tyHMpxXrkbFDa4aGhB8d9um_tI2hsI';
|
||||
}),
|
||||
} as unknown) as express.Request;
|
||||
const mockRequestWithoutJwt = ({
|
||||
header: jest.fn(() => {
|
||||
return undefined;
|
||||
}),
|
||||
} as unknown) as express.Request;
|
||||
const mockResponse = ({
|
||||
header: () => jest.fn(),
|
||||
send: mockResponseSend,
|
||||
} as unknown) as express.Response;
|
||||
|
||||
describe('should transform to type OAuthResponse', () => {
|
||||
it('when JWT is valid and identity is resolved successfully', async () => {
|
||||
const provider = new AwsAlbAuthProvider(getVoidLogger(), catalogApi, {
|
||||
region: 'us-west-2',
|
||||
identityResolutionCallback: identityResolutionCallbackMock,
|
||||
issuer: 'foo',
|
||||
});
|
||||
|
||||
mockedJwtVerify.default.mockImplementationOnce(async () => {
|
||||
return {
|
||||
payload: {
|
||||
sub: 'foo',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
await provider.refresh(mockRequest, mockResponse);
|
||||
|
||||
expect(mockResponseSend.mock.calls[0][0]).toEqual({
|
||||
backstageIdentity: {
|
||||
id: 'foo',
|
||||
idToken: '',
|
||||
},
|
||||
profile: {
|
||||
displayName: 'Foo Bar',
|
||||
},
|
||||
providerInfo: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('should fail when', () => {
|
||||
it('JWT is missing', async () => {
|
||||
const provider = new AwsAlbAuthProvider(getVoidLogger(), catalogApi, {
|
||||
region: 'us-west-2',
|
||||
identityResolutionCallback: identityResolutionCallbackMock,
|
||||
issuer: 'foo',
|
||||
});
|
||||
|
||||
await provider.refresh(mockRequestWithoutJwt, mockResponse);
|
||||
|
||||
expect(mockResponseSend.mock.calls[0][0]).toEqual(401);
|
||||
});
|
||||
|
||||
it('JWT is invalid', async () => {
|
||||
const provider = new AwsAlbAuthProvider(getVoidLogger(), catalogApi, {
|
||||
region: 'us-west-2',
|
||||
identityResolutionCallback: identityResolutionCallbackMock,
|
||||
issuer: 'foo',
|
||||
});
|
||||
|
||||
mockedJwtVerify.default.mockImplementationOnce(async () => {
|
||||
throw new Error('bad JWT');
|
||||
});
|
||||
|
||||
await provider.refresh(mockRequest, mockResponse);
|
||||
|
||||
expect(mockResponseSend.mock.calls[0][0]).toEqual(401);
|
||||
});
|
||||
|
||||
it('issuer is invalid', async () => {
|
||||
const provider = new AwsAlbAuthProvider(getVoidLogger(), catalogApi, {
|
||||
region: 'us-west-2',
|
||||
identityResolutionCallback: identityResolutionCallbackMock,
|
||||
issuer: 'foobar',
|
||||
});
|
||||
|
||||
mockedJwtVerify.default.mockImplementationOnce(async () => {
|
||||
return {};
|
||||
});
|
||||
|
||||
await provider.refresh(mockRequest, mockResponse);
|
||||
|
||||
expect(mockResponseSend.mock.calls[0][0]).toEqual(401);
|
||||
});
|
||||
|
||||
it('identity resolution callback rejects', async () => {
|
||||
const provider = new AwsAlbAuthProvider(getVoidLogger(), catalogApi, {
|
||||
region: 'us-west-2',
|
||||
identityResolutionCallback: identityResolutionCallbackRejectedMock,
|
||||
issuer: 'foo',
|
||||
});
|
||||
|
||||
mockedJwtVerify.default.mockImplementationOnce(async () => {
|
||||
return {};
|
||||
});
|
||||
|
||||
await provider.refresh(mockRequest, mockResponse);
|
||||
|
||||
expect(mockResponseSend.mock.calls[0][0]).toEqual(401);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Copyright 2021 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 {
|
||||
AuthProviderFactoryOptions,
|
||||
AuthProviderRouteHandlers,
|
||||
ExperimentalIdentityResolver,
|
||||
} from '../types';
|
||||
import express from 'express';
|
||||
import fetch from 'cross-fetch';
|
||||
import * as crypto from 'crypto';
|
||||
import { KeyObject } from 'crypto';
|
||||
import { Logger } from 'winston';
|
||||
import NodeCache from 'node-cache';
|
||||
import jwtVerify from 'jose/jwt/verify';
|
||||
import { CatalogApi } from '@backstage/catalog-client';
|
||||
|
||||
const ALB_JWT_HEADER = 'x-amzn-oidc-data';
|
||||
/**
|
||||
* A callback function that receives a verified JWT and returns a UserEntity
|
||||
* @param {payload} The verified JWT payload
|
||||
*/
|
||||
type AwsAlbAuthProviderOptions = {
|
||||
region: string;
|
||||
issuer: string;
|
||||
identityResolutionCallback: ExperimentalIdentityResolver;
|
||||
};
|
||||
export const getJWTHeaders = (input: string) => {
|
||||
const encoded = input.split('.')[0];
|
||||
return JSON.parse(Buffer.from(encoded, 'base64').toString('utf8'));
|
||||
};
|
||||
|
||||
export class AwsAlbAuthProvider implements AuthProviderRouteHandlers {
|
||||
private logger: Logger;
|
||||
private readonly catalogClient: CatalogApi;
|
||||
private options: AwsAlbAuthProviderOptions;
|
||||
private readonly keyCache: NodeCache;
|
||||
|
||||
constructor(
|
||||
logger: Logger,
|
||||
catalogClient: CatalogApi,
|
||||
options: AwsAlbAuthProviderOptions,
|
||||
) {
|
||||
this.logger = logger;
|
||||
this.catalogClient = catalogClient;
|
||||
this.options = options;
|
||||
this.keyCache = new NodeCache({ stdTTL: 3600 });
|
||||
}
|
||||
frameHandler(): Promise<void> {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
async refresh(req: express.Request, res: express.Response): Promise<void> {
|
||||
const jwt = req.header(ALB_JWT_HEADER);
|
||||
if (jwt !== undefined) {
|
||||
try {
|
||||
const headers = getJWTHeaders(jwt);
|
||||
const key = await this.getKey(headers.kid);
|
||||
const verifiedToken = await jwtVerify(jwt, key, {});
|
||||
|
||||
if (
|
||||
this.options.issuer !== '' &&
|
||||
headers.issuer !== this.options.issuer
|
||||
) {
|
||||
throw new Error('issuer mismatch on JWT');
|
||||
}
|
||||
|
||||
const resolvedEntity = await this.options.identityResolutionCallback(
|
||||
verifiedToken.payload,
|
||||
this.catalogClient,
|
||||
);
|
||||
res.send(resolvedEntity);
|
||||
} catch (e) {
|
||||
this.logger.error('exception occurred during JWT processing', e);
|
||||
res.send(401);
|
||||
}
|
||||
} else {
|
||||
res.send(401);
|
||||
}
|
||||
}
|
||||
|
||||
start(): Promise<void> {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
async getKey(keyId: string): Promise<KeyObject> {
|
||||
const optionalCacheKey = this.keyCache.get<KeyObject>(keyId);
|
||||
if (optionalCacheKey) {
|
||||
return optionalCacheKey;
|
||||
}
|
||||
const keyText: string = await fetch(
|
||||
`https://public-keys.auth.elb.${this.options.region}.amazonaws.com/${keyId}`,
|
||||
).then(response => response.json());
|
||||
const keyValue = crypto.createPublicKey(keyText);
|
||||
this.keyCache.set(keyId, keyValue);
|
||||
return keyValue;
|
||||
}
|
||||
}
|
||||
|
||||
export const createAwsAlbProvider = ({
|
||||
logger,
|
||||
catalogApi,
|
||||
config,
|
||||
identityResolver,
|
||||
}: AuthProviderFactoryOptions) => {
|
||||
const region = config.getString('region');
|
||||
const issuer = config.getString('iss');
|
||||
if (identityResolver !== undefined) {
|
||||
return new AwsAlbAuthProvider(logger, catalogApi, {
|
||||
region,
|
||||
issuer,
|
||||
identityResolutionCallback: identityResolver,
|
||||
});
|
||||
}
|
||||
throw new Error(
|
||||
'Identity resolver is required to use this authentication provider',
|
||||
);
|
||||
};
|
||||
@@ -25,6 +25,7 @@ import { createAuth0Provider } from './auth0';
|
||||
import { createMicrosoftProvider } from './microsoft';
|
||||
import { createOneLoginProvider } from './onelogin';
|
||||
import { AuthProviderFactory } from './types';
|
||||
import { createAwsAlbProvider } from './aws-alb';
|
||||
|
||||
export const factories: { [providerId: string]: AuthProviderFactory } = {
|
||||
google: createGoogleProvider,
|
||||
@@ -37,4 +38,5 @@ export const factories: { [providerId: string]: AuthProviderFactory } = {
|
||||
oauth2: createOAuth2Provider,
|
||||
oidc: createOidcProvider,
|
||||
onelogin: createOneLoginProvider,
|
||||
awsalb: createAwsAlbProvider,
|
||||
};
|
||||
|
||||
@@ -13,13 +13,17 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { TextDecoder, TextEncoder } from 'util';
|
||||
|
||||
// @ts-ignore
|
||||
global.TextDecoder = TextDecoder;
|
||||
global.TextEncoder = TextEncoder;
|
||||
import express from 'express';
|
||||
import { Session } from 'express-session';
|
||||
import nock from 'nock';
|
||||
import { ClientMetadata, IssuerMetadata } from 'openid-client';
|
||||
import { createOidcProvider, OidcAuthProvider } from './provider';
|
||||
import { JWT, JWK } from 'jose';
|
||||
import UnsecuredJWT from 'jose/jwt/unsecured';
|
||||
import { AuthProviderFactoryOptions } from '../types';
|
||||
import { Config } from '@backstage/config';
|
||||
import { OAuthAdapter } from '../../lib/oauth';
|
||||
@@ -71,6 +75,7 @@ describe('OidcAuthProvider', () => {
|
||||
const jwt = {
|
||||
sub: 'alice',
|
||||
iss: 'https://oidc.test',
|
||||
iat: Date.now(),
|
||||
aud: clientMetadata.clientId,
|
||||
exp: Date.now() + 10000,
|
||||
};
|
||||
@@ -79,7 +84,7 @@ describe('OidcAuthProvider', () => {
|
||||
.reply(200, issuerMetadata)
|
||||
.post('/as/token.oauth2')
|
||||
.reply(200, {
|
||||
id_token: JWT.sign(jwt, JWK.None),
|
||||
id_token: new UnsecuredJWT(jwt).encode(),
|
||||
access_token: 'test',
|
||||
authorization_signed_response_alg: 'HS256',
|
||||
})
|
||||
|
||||
@@ -112,6 +112,19 @@ export interface AuthProviderRouteHandlers {
|
||||
logout?(req: express.Request, res: express.Response): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* EXPERIMENTAL - this will almost certainly break in a future release.
|
||||
*
|
||||
* Used to resolve an identity from auth information in some auth providers.
|
||||
*/
|
||||
export type ExperimentalIdentityResolver = (
|
||||
/**
|
||||
* An object containing information specific to the auth provider.
|
||||
*/
|
||||
payload: object,
|
||||
catalogApi: CatalogApi,
|
||||
) => Promise<AuthResponse<any>>;
|
||||
|
||||
export type AuthProviderFactoryOptions = {
|
||||
providerId: string;
|
||||
globalConfig: AuthProviderConfig;
|
||||
@@ -120,6 +133,7 @@ export type AuthProviderFactoryOptions = {
|
||||
tokenIssuer: TokenIssuer;
|
||||
discovery: PluginEndpointDiscovery;
|
||||
catalogApi: CatalogApi;
|
||||
identityResolver?: ExperimentalIdentityResolver;
|
||||
};
|
||||
|
||||
export type AuthProviderFactory = (
|
||||
|
||||
@@ -10126,6 +10126,11 @@ clone-response@^1.0.2:
|
||||
dependencies:
|
||||
mimic-response "^1.0.0"
|
||||
|
||||
clone@2.x:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f"
|
||||
integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=
|
||||
|
||||
clone@^1.0.2:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
|
||||
@@ -16549,19 +16554,17 @@ jest@^26.0.1:
|
||||
import-local "^3.0.2"
|
||||
jest-cli "^26.6.3"
|
||||
|
||||
jose@^1.27.1:
|
||||
version "1.27.1"
|
||||
resolved "https://registry.npmjs.org/jose/-/jose-1.27.1.tgz#a1de2ecb5b3ae1ae28f0d9d0cc536349ada27ec8"
|
||||
integrity sha512-VyHM6IJPw0TTGqHVNlPWg16/ASDPAmcChcLqSb3WNBvwWFoWPeFqlmAUCm8/oIG1GjZwAlUDuRKFfycowarcVA==
|
||||
jose@^2.0.2:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.npmjs.org/jose/-/jose-2.0.3.tgz#9c931ab3e13e2d16a5b9e6183e60b2fc40a8e1b8"
|
||||
integrity sha512-L+RlDgjO0Tk+Ki6/5IXCSEnmJCV8iMFZoBuEgu2vPQJJ4zfG/k3CAqZUMKDYNRHIDyy0QidJpOvX0NgpsAqFlw==
|
||||
dependencies:
|
||||
"@panva/asn1.js" "^1.0.0"
|
||||
|
||||
jose@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.npmjs.org/jose/-/jose-2.0.2.tgz#fb22385b80c658cc7a0cae05b7086c04c6be49f4"
|
||||
integrity sha512-yD93lsiMA1go/qxSY/vXWBodmIZJIxeB7QhFi8z1yQ3KUwKENqI9UA8VCHlQ5h3x1zWuWZjoY87ByQzkQbIrQg==
|
||||
dependencies:
|
||||
"@panva/asn1.js" "^1.0.0"
|
||||
jose@^3.5.1:
|
||||
version "3.5.1"
|
||||
resolved "https://registry.npmjs.org/jose/-/jose-3.5.1.tgz#adc0d5000dbf64a7f4db171d449dbcf6b556b6f0"
|
||||
integrity sha512-BQQJafCDvsmtc/LaK57cjfhU/1AKBhJSbJhKPq+uhrjMoV/sh3/dbk9cm08nAeSGS6j+sjhWoY+LZPUXiKzYzw==
|
||||
|
||||
joycon@^2.2.5:
|
||||
version "2.2.5"
|
||||
@@ -18808,6 +18811,13 @@ node-addon-api@2.0.0:
|
||||
resolved "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.0.tgz#f9afb8d777a91525244b01775ea0ddbe1125483b"
|
||||
integrity sha512-ASCL5U13as7HhOExbT6OlWJJUV/lLzL2voOSP1UVehpRD8FbSrSDjfScK/KwAvVTI5AS6r4VwbOMlIqtvRidnA==
|
||||
|
||||
node-cache@^5.1.2:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz#f264dc2ccad0a780e76253a694e9fd0ed19c398d"
|
||||
integrity sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==
|
||||
dependencies:
|
||||
clone "2.x"
|
||||
|
||||
node-dir@^0.1.10:
|
||||
version "0.1.17"
|
||||
resolved "https://registry.npmjs.org/node-dir/-/node-dir-0.1.17.tgz#5f5665d93351335caabef8f1c554516cf5f1e4e5"
|
||||
|
||||
Reference in New Issue
Block a user