address review comments

Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
Fredrik Adelöw
2022-01-13 11:16:16 +01:00
parent 28b61f32b4
commit be610c5744
10 changed files with 70 additions and 61 deletions
+10 -10
View File
@@ -202,16 +202,6 @@ export type CustomProviderClassKey = 'form' | 'button';
// @public (undocumented)
export function DashboardIcon(props: IconComponentProps): JSX.Element;
// @public
export const DelegatedSignInPage: (
props: DelegatedSignInPageProps,
) => JSX.Element | null;
// @public
export type DelegatedSignInPageProps = SignInPageProps & {
provider: string;
};
// @public
type DependencyEdge<T = {}> = T & {
from: string;
@@ -765,6 +755,16 @@ export function Progress(
props: PropsWithChildren<LinearProgressProps>,
): JSX.Element;
// @public
export const ProxiedSignInPage: (
props: ProxiedSignInPageProps,
) => JSX.Element | null;
// @public
export type ProxiedSignInPageProps = SignInPageProps & {
provider: string;
};
// Warning: (ae-missing-release-tag) "Ranker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
@@ -15,20 +15,19 @@
*/
import { setupRequestMockHandlers } from '@backstage/test-utils';
import fetch from 'cross-fetch';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import {
DEFAULTS,
DelegatedSignInIdentity,
ProxiedSignInIdentity,
tokenToExpiry,
} from './DelegatedSignInIdentity';
} from './ProxiedSignInIdentity';
const validBackstageTokenExpClaim = 1641216199;
const validBackstageToken =
'eyJhbGciOiJFUzI1NiIsImtpZCI6ImMxNTMzNDRiLWZjYzktNGIwOS1iN2ZhLTU3ZmM5MDhjMjBiNiJ9.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjcwMDcvYXBpL2F1dGgiLCJzdWIiOiJmcmViZW4iLCJhdWQiOiJiYWNrc3RhZ2UiLCJpYXQiOjE2NDEyMTI1OTksImV4cCI6MTY0MTIxNjE5OSwiZW50IjpbInVzZXI6ZGVmYXVsdC9mcmViZW4iXX0.4nOTmPHPwhzaKTzikgUsHcszfcP-JamcojMnRfyfsKhyHCCEywe6uLFlvvmK5NbaX5Z7IIji-kg7bxKU58kwoQ';
describe('DelegatedSignInIdentity', () => {
describe('ProxiedSignInIdentity', () => {
describe('tokenToExpiry', () => {
beforeEach(() => jest.useFakeTimers('modern'));
afterEach(() => jest.useRealTimers());
@@ -46,9 +45,17 @@ describe('DelegatedSignInIdentity', () => {
),
);
});
it('handles a token that has no exp', async () => {
const [a, _b, c] = validBackstageToken.split('.');
const botched = `${a}.${btoa(JSON.stringify({}))}.${c}`;
expect(tokenToExpiry(botched)).toEqual(
new Date(new Date(Date.now() + DEFAULTS.defaultTokenExpiryMillis)),
);
});
});
describe('DelegatedSignInIdentity', () => {
describe('ProxiedSignInIdentity', () => {
beforeEach(() => jest.useFakeTimers('modern'));
afterEach(() => jest.useRealTimers());
@@ -61,7 +68,7 @@ describe('DelegatedSignInIdentity', () => {
function makeToken() {
const iat = Math.floor(Date.now() / 1000);
const exp = iat + 100;
const exp = iat + 3600;
return {
providerInfo: {
stuff: 1,
@@ -107,10 +114,9 @@ describe('DelegatedSignInIdentity', () => {
),
);
const identity = new DelegatedSignInIdentity({
const identity = new ProxiedSignInIdentity({
provider: 'foo',
discoveryApi: { getBaseUrl },
fetchApi: { fetch },
});
getBaseUrl.mockResolvedValue('http://example.com/api/auth');
@@ -126,7 +132,7 @@ describe('DelegatedSignInIdentity', () => {
// Use a fairly large margin (1000) since the iat and exp are clamped to
// full seconds, but the "local current time" isn't
jest.advanceTimersByTime(
100 * 1000 - DEFAULTS.tokenExpiryMarginMillis - 1000,
3600 * 1000 - DEFAULTS.tokenExpiryMarginMillis - 1000,
);
await identity.getSessionAsync(); // still no need to fetch again
expect(serverCalled).toBeCalledTimes(1);
@@ -17,38 +17,40 @@
import {
BackstageUserIdentity,
discoveryApiRef,
fetchApiRef,
IdentityApi,
ProfileInfo,
} from '@backstage/core-plugin-api';
import { ResponseError } from '@backstage/errors';
import { DelegatedSession, delegatedSessionSchema } from './types';
import { ProxiedSession, proxiedSessionSchema } from './types';
export const DEFAULTS = {
// The amount of time between token refreshes, if we fail to get an actual
// value out of the exp claim
defaultTokenExpiryMillis: 60_000,
defaultTokenExpiryMillis: 5 * 60 * 1000,
// The amount of time before the actual expiry of the Backstage token, that we
// shall start trying to get a new one
tokenExpiryMarginMillis: 30_000,
tokenExpiryMarginMillis: 5 * 60 * 1000,
} as const;
// When the token expires, with some margin
export function tokenToExpiry(jwtToken: string | undefined): Date {
const fallback = new Date(Date.now() + DEFAULTS.defaultTokenExpiryMillis);
if (!jwtToken) {
return new Date(Date.now() + DEFAULTS.defaultTokenExpiryMillis);
return fallback;
}
const [_header, rawPayload, _signature] = jwtToken.split('.');
const payload = JSON.parse(atob(rawPayload));
if (typeof payload.exp !== 'number') {
return fallback;
}
return new Date(payload.exp * 1000 - DEFAULTS.tokenExpiryMarginMillis);
}
type DelegatedSignInIdentityOptions = {
type ProxiedSignInIdentityOptions = {
provider: string;
discoveryApi: typeof discoveryApiRef.T;
fetchApi: typeof fetchApiRef.T;
};
type State =
@@ -57,12 +59,12 @@ type State =
}
| {
type: 'fetching';
promise: Promise<DelegatedSession>;
previous: DelegatedSession | undefined;
promise: Promise<ProxiedSession>;
previous: ProxiedSession | undefined;
}
| {
type: 'active';
session: DelegatedSession;
session: ProxiedSession;
expiresAt: Date;
}
| {
@@ -74,12 +76,12 @@ type State =
* An identity API that gets the user auth information solely based on a
* provider's `/refresh` endpoint.
*/
export class DelegatedSignInIdentity implements IdentityApi {
private readonly options: DelegatedSignInIdentityOptions;
export class ProxiedSignInIdentity implements IdentityApi {
private readonly options: ProxiedSignInIdentityOptions;
private readonly abortController: AbortController;
private state: State;
constructor(options: DelegatedSignInIdentityOptions) {
constructor(options: ProxiedSignInIdentityOptions) {
this.options = options;
this.abortController = new AbortController();
this.state = { type: 'empty' };
@@ -133,7 +135,7 @@ export class DelegatedSignInIdentity implements IdentityApi {
this.abortController.abort();
}
getSessionSync(): DelegatedSession {
getSessionSync(): ProxiedSession {
if (this.state.type === 'active') {
return this.state.session;
} else if (this.state.type === 'fetching' && this.state.previous) {
@@ -142,7 +144,7 @@ export class DelegatedSignInIdentity implements IdentityApi {
throw new Error('No session available. Try reloading your browser page.');
}
async getSessionAsync(): Promise<DelegatedSession> {
async getSessionAsync(): Promise<ProxiedSession> {
if (this.state.type === 'fetching') {
return this.state.promise;
} else if (
@@ -182,10 +184,13 @@ export class DelegatedSignInIdentity implements IdentityApi {
return promise;
}
async fetchSession(): Promise<DelegatedSession> {
async fetchSession(): Promise<ProxiedSession> {
const baseUrl = await this.options.discoveryApi.getBaseUrl('auth');
const response = await this.options.fetchApi.fetch(
// Note that we do not use the fetchApi here, since this all happens before
// sign-in completes so there can be no automatic token injection and
// similar.
const response = await fetch(
`${baseUrl}/${this.options.provider}/refresh`,
{
signal: this.abortController.signal,
@@ -198,6 +203,6 @@ export class DelegatedSignInIdentity implements IdentityApi {
throw await ResponseError.fromResponse(response);
}
return delegatedSessionSchema.parse(await response.json());
return proxiedSessionSchema.parse(await response.json());
}
}
@@ -16,7 +16,6 @@
import {
discoveryApiRef,
fetchApiRef,
SignInPageProps,
useApi,
} from '@backstage/core-plugin-api';
@@ -24,14 +23,14 @@ import React from 'react';
import useAsync from 'react-use/lib/useAsync';
import { ErrorPanel } from '../../components/ErrorPanel';
import { Progress } from '../../components/Progress';
import { DelegatedSignInIdentity } from './DelegatedSignInIdentity';
import { ProxiedSignInIdentity } from './ProxiedSignInIdentity';
/**
* Props for {@link DelegatedSignInPage}.
* Props for {@link ProxiedSignInPage}.
*
* @public
*/
export type DelegatedSignInPageProps = SignInPageProps & {
export type ProxiedSignInPageProps = SignInPageProps & {
/**
* The provider to use, e.g. "gcp-iap" or "aws-alb". This must correspond to
* a properly configured auth provider ID in the auth backend.
@@ -40,9 +39,9 @@ export type DelegatedSignInPageProps = SignInPageProps & {
};
/**
* A sign-in page that has no user interface of its own. Instead, it delegates
* sign-in to some reverse authenticating proxy that Backstage is deployed
* behind, and leverages its session handling.
* A sign-in page that has no user interface of its own. Instead, it relies on
* sign-in being performed by a reverse authenticating proxy that Backstage is
* deployed behind, and leverages its session handling.
*
* @remarks
*
@@ -54,15 +53,13 @@ export type DelegatedSignInPageProps = SignInPageProps & {
*
* @public
*/
export const DelegatedSignInPage = (props: DelegatedSignInPageProps) => {
export const ProxiedSignInPage = (props: ProxiedSignInPageProps) => {
const discoveryApi = useApi(discoveryApiRef);
const fetchApi = useApi(fetchApiRef);
const { loading, error } = useAsync(async () => {
const identity = new DelegatedSignInIdentity({
const identity = new ProxiedSignInIdentity({
provider: props.provider,
discoveryApi,
fetchApi,
});
await identity.start();
@@ -14,5 +14,5 @@
* limitations under the License.
*/
export { DelegatedSignInPage } from './DelegatedSignInPage';
export type { DelegatedSignInPageProps } from './DelegatedSignInPage';
export { ProxiedSignInPage } from './ProxiedSignInPage';
export type { ProxiedSignInPageProps } from './ProxiedSignInPage';
@@ -15,10 +15,10 @@
*/
import { TypeOf } from 'zod';
import { DelegatedSession, delegatedSessionSchema } from './types';
import { ProxiedSession, proxiedSessionSchema } from './types';
describe('types', () => {
const responseData: DelegatedSession = {
const responseData: ProxiedSession = {
providerInfo: {
stuff: 1,
},
@@ -39,8 +39,8 @@ describe('types', () => {
};
it('has a compatible schema type', () => {
function f(_b: TypeOf<typeof delegatedSessionSchema>) {}
function f(_b: TypeOf<typeof proxiedSessionSchema>) {}
f(responseData); // no tsc errors
expect(delegatedSessionSchema.parse(responseData)).toEqual(responseData);
expect(proxiedSessionSchema.parse(responseData)).toEqual(responseData);
});
});
@@ -20,7 +20,7 @@ import {
} from '@backstage/core-plugin-api';
import { z } from 'zod';
export const delegatedSessionSchema = z.object({
export const proxiedSessionSchema = z.object({
providerInfo: z.object({}).catchall(z.unknown()).optional(),
profile: z.object({
email: z.string().optional(),
@@ -39,12 +39,12 @@ export const delegatedSessionSchema = z.object({
});
/**
* Generic session information for delegated sign-in providers, e.g. common
* Generic session information for proxied sign-in providers, e.g. common
* reverse authenticating proxy implementations.
*
* @public
*/
export type DelegatedSession = {
export type ProxiedSession = {
providerInfo?: { [key: string]: unknown };
profile: ProfileInfo;
backstageIdentity: BackstageIdentityResponse;
+1 -1
View File
@@ -17,7 +17,6 @@
export * from './BottomLink';
export * from './Content';
export * from './ContentHeader';
export * from './DelegatedSignInPage';
export * from './ErrorBoundary';
export * from './ErrorPage';
export * from './Header';
@@ -27,6 +26,7 @@ export * from './HomepageTimer';
export * from './InfoCard';
export * from './ItemCard';
export * from './Page';
export * from './ProxiedSignInPage';
export * from './Sidebar';
export * from './SignInPage';
export * from './TabbedCard';
@@ -15,3 +15,4 @@
*/
import '@testing-library/jest-dom';
import 'cross-fetch/polyfill';