Merge pull request #13252 from backstage/freben/api-well-ok-then

When the 🧹 s go marching in
This commit is contained in:
Fredrik Adelöw
2022-08-23 13:55:58 +02:00
committed by GitHub
273 changed files with 2469 additions and 1529 deletions
@@ -26,17 +26,10 @@ import {
} from '@backstage/catalog-model';
import { TokenManager } from '@backstage/backend-common';
type UserQuery = {
annotations: Record<string, string>;
};
type MemberClaimQuery = {
entityRefs: string[];
logger?: Logger;
};
/**
* A catalog client tailored for reading out identity data from the catalog.
*
* @public
*/
export class CatalogIdentityClient {
private readonly catalogApi: CatalogApi;
@@ -52,7 +45,9 @@ export class CatalogIdentityClient {
*
* Throws a NotFoundError or ConflictError if 0 or multiple users are found.
*/
async findUser(query: UserQuery): Promise<UserEntity> {
async findUser(query: {
annotations: Record<string, string>;
}): Promise<UserEntity> {
const filter: Record<string, string> = {
kind: 'user',
};
@@ -81,7 +76,10 @@ export class CatalogIdentityClient {
*
* Returns a superset of the entity names that can be passed directly to `issueToken` as `ent`.
*/
async resolveCatalogMembership(query: MemberClaimQuery): Promise<string[]> {
async resolveCatalogMembership(query: {
entityRefs: string[];
logger?: Logger;
}): Promise<string[]> {
const { entityRefs, logger } = query;
const resolvedEntityRefs = entityRefs
.map((ref: string) => {
@@ -24,6 +24,7 @@ export const safelyEncodeURIComponent = (value: string) => {
return encodeURIComponent(value).replace(/'/g, '%27');
};
/** @public */
export const postMessageResponse = (
res: express.Response,
appOrigin: string,
@@ -68,6 +69,7 @@ export const postMessageResponse = (
res.end(`<html><body><script>${script}</script></body></html>`);
};
/** @public */
export const ensuresXRequestedWith = (req: express.Request) => {
const requiredHeader = req.header('X-Requested-With');
if (!requiredHeader || requiredHeader !== 'XMLHttpRequest') {
@@ -19,6 +19,8 @@ import { AuthResponse } from '../../providers/types';
/**
* Payload sent as a post message after the auth request is complete.
* If successful then has a valid payload with Auth information else contains an error.
*
* @public
*/
export type WebMessageResponse =
| {
@@ -44,7 +44,8 @@ import { prepareBackstageIdentityResponse } from '../../providers/prepareBacksta
export const THOUSAND_DAYS_MS = 1000 * 24 * 60 * 60 * 1000;
export const TEN_MINUTES_MS = 600 * 1000;
export type Options = {
/** @public */
export type OAuthAdapterOptions = {
providerId: string;
secure: boolean;
persistScopes?: boolean;
@@ -54,11 +55,16 @@ export type Options = {
isOriginAllowed: (origin: string) => boolean;
callbackUrl: string;
};
/** @public */
export class OAuthAdapter implements AuthProviderRouteHandlers {
static fromConfig(
config: AuthProviderConfig,
handlers: OAuthHandlers,
options: Pick<Options, 'providerId' | 'persistScopes' | 'callbackUrl'>,
options: Pick<
OAuthAdapterOptions,
'providerId' | 'persistScopes' | 'callbackUrl'
>,
): OAuthAdapter {
const { origin: appOrigin } = new URL(config.appUrl);
@@ -83,7 +89,7 @@ export class OAuthAdapter implements AuthProviderRouteHandlers {
constructor(
private readonly handlers: OAuthHandlers,
private readonly options: Options,
private readonly options: OAuthAdapterOptions,
) {
this.baseCookieOptions = {
httpOnly: true,
@@ -20,6 +20,7 @@ import { InputError, NotFoundError } from '@backstage/errors';
import { readState } from './helpers';
import { AuthProviderRouteHandlers } from '../../providers/types';
/** @public */
export class OAuthEnvironmentHandler implements AuthProviderRouteHandlers {
static mapConfig(
config: Config,
@@ -19,6 +19,7 @@ import { OAuthState } from './types';
import pickBy from 'lodash/pickBy';
import { CookieConfigurer } from '../../providers/types';
/** @public */
export const readState = (stateString: string): OAuthState => {
const state = Object.fromEntries(
new URLSearchParams(Buffer.from(stateString, 'hex').toString('utf-8')),
@@ -35,6 +36,7 @@ export const readState = (stateString: string): OAuthState => {
return state as OAuthState;
};
/** @public */
export const encodeState = (state: OAuthState): string => {
const stateString = new URLSearchParams(
pickBy<string>(state, value => value !== undefined),
@@ -43,6 +45,7 @@ export const encodeState = (state: OAuthState): string => {
return Buffer.from(stateString, 'utf-8').toString('hex');
};
/** @public */
export const verifyNonce = (req: express.Request, providerId: string) => {
const cookieNonce = req.cookies[`${providerId}-nonce`];
const state: OAuthState = readState(req.query.state?.toString() ?? '');
@@ -15,6 +15,7 @@
*/
export { OAuthEnvironmentHandler } from './OAuthEnvironmentHandler';
export type { OAuthAdapterOptions } from './OAuthAdapter';
export { OAuthAdapter } from './OAuthAdapter';
export { encodeState, verifyNonce, readState } from './helpers';
export type {
+9 -2
View File
@@ -17,10 +17,12 @@
import express from 'express';
import { Profile as PassportProfile } from 'passport';
import { BackstageSignInResult } from '@backstage/plugin-auth-node';
import { RedirectInfo, ProfileInfo } from '../../providers/types';
import { OAuthStartResponse, ProfileInfo } from '../../providers/types';
/**
* Common options for passport.js-based OAuth providers
*
* @public
*/
export type OAuthProviderOptions = {
/**
@@ -37,6 +39,7 @@ export type OAuthProviderOptions = {
callbackUrl: string;
};
/** @public */
export type OAuthResult = {
fullProfile: PassportProfile;
params: {
@@ -59,6 +62,7 @@ export type OAuthResponse = {
backstageIdentity?: BackstageSignInResult;
};
/** @public */
export type OAuthProviderInfo = {
/**
* An access token issued for the signed in user.
@@ -78,6 +82,7 @@ export type OAuthProviderInfo = {
scope: string;
};
/** @public */
export type OAuthState = {
/* A type for the serialized value in the `state` parameter of the OAuth authorization flow
*/
@@ -87,11 +92,13 @@ export type OAuthState = {
scope?: string;
};
/** @public */
export type OAuthStartRequest = express.Request<{}> & {
scope: string;
state: OAuthState;
};
/** @public */
export type OAuthRefreshRequest = express.Request<{}> & {
scope: string;
refreshToken: string;
@@ -108,7 +115,7 @@ export interface OAuthHandlers {
/**
* Initiate a sign in request with an auth provider.
*/
start(req: OAuthStartRequest): Promise<RedirectInfo>;
start(req: OAuthStartRequest): Promise<OAuthStartResponse>;
/**
* Handle the redirect from the auth provider when the user has signed in.
@@ -33,7 +33,7 @@ describe('PassportStrategyHelper', () => {
}
describe('executeRedirectStrategy', () => {
it('should call authenticate and resolve with RedirectInfo', async () => {
it('should call authenticate and resolve with OAuthStartResponse', async () => {
const mockStrategy = new MyCustomRedirectStrategy();
const spyAuthenticate = jest.spyOn(mockStrategy, 'authenticate');
const redirectStrategyPromise = executeRedirectStrategy(
@@ -20,7 +20,7 @@ import jwtDecoder from 'jwt-decode';
import { InternalOAuthError } from 'passport-oauth2';
import { PassportProfile } from './types';
import { ProfileInfo, RedirectInfo } from '../../providers/types';
import { ProfileInfo, OAuthStartResponse } from '../../providers/types';
export type PassportDoneCallback<Res, Private = never> = (
err?: Error,
@@ -77,7 +77,7 @@ export const executeRedirectStrategy = async (
req: express.Request,
providerStrategy: passport.Strategy,
options: Record<string, string>,
): Promise<RedirectInfo> => {
): Promise<OAuthStartResponse> => {
return new Promise(resolve => {
const strategy = Object.create(providerStrategy);
strategy.redirect = (url: string, status?: number) => {
@@ -14,4 +14,4 @@
* limitations under the License.
*/
export { atlassian, AtlassianAuthProvider } from './provider';
export { atlassian } from './provider';
@@ -38,12 +38,13 @@ import {
import {
AuthHandler,
AuthResolverContext,
RedirectInfo,
OAuthStartResponse,
SignInResolver,
} from '../types';
import express from 'express';
import { createAuthProviderIntegration } from '../createAuthProviderIntegration';
/** @public */
export type AtlassianAuthProviderOptions = OAuthProviderOptions & {
scopes: string;
signInResolver?: SignInResolver<OAuthResult>;
@@ -58,10 +59,6 @@ export const atlassianDefaultAuthHandler: AuthHandler<OAuthResult> = async ({
profile: makeProfileInfo(fullProfile, params.id_token),
});
/**
* @public
* @deprecated This export is deprecated and will be removed in the future.
*/
export class AtlassianAuthProvider implements OAuthHandlers {
private readonly _strategy: AtlassianStrategy;
private readonly signInResolver?: SignInResolver<OAuthResult>;
@@ -97,7 +94,7 @@ export class AtlassianAuthProvider implements OAuthHandlers {
);
}
async start(req: OAuthStartRequest): Promise<RedirectInfo> {
async start(req: OAuthStartRequest): Promise<OAuthStartResponse> {
return await executeRedirectStrategy(req, this._strategy, {
state: encodeState(req.state),
});
@@ -37,7 +37,7 @@ import {
PassportDoneCallback,
} from '../../lib/passport';
import {
RedirectInfo,
OAuthStartResponse,
AuthHandler,
SignInResolver,
AuthResolverContext,
@@ -98,7 +98,7 @@ export class Auth0AuthProvider implements OAuthHandlers {
);
}
async start(req: OAuthStartRequest): Promise<RedirectInfo> {
async start(req: OAuthStartRequest): Promise<OAuthStartResponse> {
return await executeRedirectStrategy(req, this._strategy, {
accessType: 'offline',
prompt: 'consent',
@@ -39,7 +39,7 @@ import {
import { createAuthProviderIntegration } from '../createAuthProviderIntegration';
import {
AuthHandler,
RedirectInfo,
OAuthStartResponse,
SignInResolver,
AuthResolverContext,
} from '../types';
@@ -54,6 +54,7 @@ type Options = OAuthProviderOptions & {
resolverContext: AuthResolverContext;
};
/** @public */
export type BitbucketOAuthResult = {
fullProfile: BitbucketPassportProfile;
params: {
@@ -65,6 +66,7 @@ export type BitbucketOAuthResult = {
refreshToken?: string;
};
/** @public */
export type BitbucketPassportProfile = PassportProfile & {
id?: string;
displayName?: string;
@@ -119,7 +121,7 @@ export class BitbucketAuthProvider implements OAuthHandlers {
);
}
async start(req: OAuthStartRequest): Promise<RedirectInfo> {
async start(req: OAuthStartRequest): Promise<OAuthStartResponse> {
return await executeRedirectStrategy(req, this._strategy, {
accessType: 'offline',
prompt: 'consent',
@@ -146,7 +146,6 @@ export type CloudflareAccessIdentityProfile = {
};
/**
*
* @public
*/
export type CloudflareAccessResult = {
@@ -22,6 +22,8 @@ import { AuthProviderFactory, SignInResolver } from './types';
*
* The returned object facilitates the creation of provider instances, and
* supplies built-in sign-in resolvers for the specific provider.
*
* @public
*/
export function createAuthProviderIntegration<
TCreateOptions extends unknown[],
@@ -26,7 +26,7 @@ import {
PassportDoneCallback,
} from '../../lib/passport';
import {
RedirectInfo,
OAuthStartResponse,
AuthHandler,
SignInResolver,
StateEncoder,
@@ -52,6 +52,7 @@ type PrivateInfo = {
refreshToken?: string;
};
/** @public */
export type GithubOAuthResult = {
fullProfile: PassportProfile;
params: {
@@ -106,7 +107,7 @@ export class GithubAuthProvider implements OAuthHandlers {
);
}
async start(req: OAuthStartRequest): Promise<RedirectInfo> {
async start(req: OAuthStartRequest): Promise<OAuthStartResponse> {
return await executeRedirectStrategy(req, this._strategy, {
scope: req.scope,
state: (await this.stateEncoder(req)).encodedState,
@@ -25,7 +25,7 @@ import {
PassportDoneCallback,
} from '../../lib/passport';
import {
RedirectInfo,
OAuthStartResponse,
SignInResolver,
AuthHandler,
AuthResolverContext,
@@ -110,7 +110,7 @@ export class GitlabAuthProvider implements OAuthHandlers {
);
}
async start(req: OAuthStartRequest): Promise<RedirectInfo> {
async start(req: OAuthStartRequest): Promise<OAuthStartResponse> {
return await executeRedirectStrategy(req, this._strategy, {
scope: req.scope,
state: encodeState(req.state),
@@ -39,7 +39,7 @@ import {
import {
AuthHandler,
AuthResolverContext,
RedirectInfo,
OAuthStartResponse,
SignInResolver,
} from '../types';
import { createAuthProviderIntegration } from '../createAuthProviderIntegration';
@@ -98,7 +98,7 @@ export class GoogleAuthProvider implements OAuthHandlers {
);
}
async start(req: OAuthStartRequest): Promise<RedirectInfo> {
async start(req: OAuthStartRequest): Promise<OAuthStartResponse> {
return await executeRedirectStrategy(req, this.strategy, {
accessType: 'offline',
prompt: 'consent',
+1 -1
View File
@@ -14,7 +14,6 @@
* limitations under the License.
*/
export { AtlassianAuthProvider } from './atlassian';
export type { AwsAlbResult } from './aws-alb';
export type {
BitbucketOAuthResult,
@@ -50,6 +49,7 @@ export type {
StateEncoder,
AuthResponse,
ProfileInfo,
OAuthStartResponse,
} from './types';
export { prepareBackstageIdentityResponse } from './prepareBackstageIdentityResponse';
@@ -38,7 +38,7 @@ import {
} from '../../lib/passport';
import {
AuthHandler,
RedirectInfo,
OAuthStartResponse,
SignInResolver,
AuthResolverContext,
} from '../types';
@@ -97,7 +97,7 @@ export class MicrosoftAuthProvider implements OAuthHandlers {
);
}
async start(req: OAuthStartRequest): Promise<RedirectInfo> {
async start(req: OAuthStartRequest): Promise<OAuthStartResponse> {
return await executeRedirectStrategy(req, this._strategy, {
scope: req.scope,
state: encodeState(req.state),
@@ -39,7 +39,7 @@ import {
import {
AuthHandler,
AuthResolverContext,
RedirectInfo,
OAuthStartResponse,
SignInResolver,
} from '../types';
import { createAuthProviderIntegration } from '../createAuthProviderIntegration';
@@ -114,7 +114,7 @@ export class OAuth2AuthProvider implements OAuthHandlers {
);
}
async start(req: OAuthStartRequest): Promise<RedirectInfo> {
async start(req: OAuthStartRequest): Promise<OAuthStartResponse> {
return await executeRedirectStrategy(req, this._strategy, {
accessType: 'offline',
prompt: 'consent',
@@ -40,7 +40,7 @@ import {
import {
AuthHandler,
AuthResolverContext,
RedirectInfo,
OAuthStartResponse,
SignInResolver,
} from '../types';
import { createAuthProviderIntegration } from '../createAuthProviderIntegration';
@@ -91,7 +91,7 @@ export class OidcAuthProvider implements OAuthHandlers {
this.resolverContext = options.resolverContext;
}
async start(req: OAuthStartRequest): Promise<RedirectInfo> {
async start(req: OAuthStartRequest): Promise<OAuthStartResponse> {
const { strategy } = await this.implementation;
const options: Record<string, string> = {
scope: req.scope || this.scope || 'openid profile email',
@@ -38,7 +38,7 @@ import {
} from '../../lib/passport';
import {
AuthHandler,
RedirectInfo,
OAuthStartResponse,
SignInResolver,
AuthResolverContext,
} from '../types';
@@ -125,7 +125,7 @@ export class OktaAuthProvider implements OAuthHandlers {
);
}
async start(req: OAuthStartRequest): Promise<RedirectInfo> {
async start(req: OAuthStartRequest): Promise<OAuthStartResponse> {
return await executeRedirectStrategy(req, this.strategy, {
accessType: 'offline',
prompt: 'consent',
@@ -37,7 +37,7 @@ import {
PassportDoneCallback,
} from '../../lib/passport';
import {
RedirectInfo,
OAuthStartResponse,
AuthHandler,
SignInResolver,
AuthResolverContext,
@@ -95,7 +95,7 @@ export class OneLoginProvider implements OAuthHandlers {
},
);
}
async start(req: OAuthStartRequest): Promise<RedirectInfo> {
async start(req: OAuthStartRequest): Promise<OAuthStartResponse> {
return await executeRedirectStrategy(req, this._strategy, {
accessType: 'offline',
prompt: 'consent',
+5 -1
View File
@@ -126,7 +126,8 @@ export type AuthProviderConfig = {
cookieConfigurer?: CookieConfigurer;
};
export type RedirectInfo = {
/** @public */
export type OAuthStartResponse = {
/**
* URL to redirect to
*/
@@ -147,6 +148,8 @@ export type RedirectInfo = {
* `/auth/[provider]/handler/frame -> frameHandler`
* `/auth/[provider]/refresh -> refresh`
* `/auth/[provider]/logout -> logout`
*
* @public
*/
export interface AuthProviderRouteHandlers {
/**
@@ -192,6 +195,7 @@ export interface AuthProviderRouteHandlers {
logout?(req: express.Request, res: express.Response): Promise<void>;
}
/** @public */
export type AuthProviderFactory = (options: {
providerId: string;
globalConfig: AuthProviderConfig;
+5 -1
View File
@@ -36,8 +36,10 @@ import passport from 'passport';
import { Minimatch } from 'minimatch';
import { CatalogAuthResolverContext } from '../lib/resolvers';
type ProviderFactories = { [s: string]: AuthProviderFactory };
/** @public */
export type ProviderFactories = { [s: string]: AuthProviderFactory };
/** @public */
export interface RouterOptions {
logger: Logger;
database: PluginDatabaseManager;
@@ -48,6 +50,7 @@ export interface RouterOptions {
providerFactories?: ProviderFactories;
}
/** @public */
export async function createRouter(
options: RouterOptions,
): Promise<express.Router> {
@@ -187,6 +190,7 @@ export async function createRouter(
return router;
}
/** @public */
export function createOriginFilter(
config: Config,
): (origin: string) => boolean {