Merge pull request #19280 from backstage/rugvip/auth-migration

auth-backend: migrate to new backend system + new authenticators pattern
This commit is contained in:
Patrik Oldsberg
2023-08-16 09:47:52 +02:00
committed by GitHub
104 changed files with 5914 additions and 1347 deletions
+101 -192
View File
@@ -5,98 +5,69 @@
```ts
/// <reference types="node" />
import { BackstageIdentityResponse } from '@backstage/plugin-auth-node';
import { AuthProviderConfig as AuthProviderConfig_2 } from '@backstage/plugin-auth-node';
import { AuthProviderFactory as AuthProviderFactory_2 } from '@backstage/plugin-auth-node';
import { AuthProviderRouteHandlers as AuthProviderRouteHandlers_2 } from '@backstage/plugin-auth-node';
import { AuthResolverCatalogUserQuery as AuthResolverCatalogUserQuery_2 } from '@backstage/plugin-auth-node';
import { AuthResolverContext as AuthResolverContext_2 } from '@backstage/plugin-auth-node';
import { BackstageSignInResult } from '@backstage/plugin-auth-node';
import { CacheService } from '@backstage/backend-plugin-api';
import { CatalogApi } from '@backstage/catalog-client';
import { ClientAuthResponse } from '@backstage/plugin-auth-node';
import { Config } from '@backstage/config';
import { CookieConfigurer as CookieConfigurer_2 } from '@backstage/plugin-auth-node';
import { decodeOAuthState } from '@backstage/plugin-auth-node';
import { encodeOAuthState } from '@backstage/plugin-auth-node';
import { Entity } from '@backstage/catalog-model';
import express from 'express';
import { GetEntitiesRequest } from '@backstage/catalog-client';
import { GcpIapResult as GcpIapResult_2 } from '@backstage/plugin-auth-backend-module-gcp-iap-provider';
import { GcpIapTokenInfo as GcpIapTokenInfo_2 } from '@backstage/plugin-auth-backend-module-gcp-iap-provider';
import { IncomingHttpHeaders } from 'http';
import { JsonValue } from '@backstage/types';
import { Logger } from 'winston';
import { LoggerService } from '@backstage/backend-plugin-api';
import { OAuthEnvironmentHandler as OAuthEnvironmentHandler_2 } from '@backstage/plugin-auth-node';
import { OAuthState as OAuthState_2 } from '@backstage/plugin-auth-node';
import { PluginDatabaseManager } from '@backstage/backend-common';
import { PluginEndpointDiscovery } from '@backstage/backend-common';
import { prepareBackstageIdentityResponse as prepareBackstageIdentityResponse_2 } from '@backstage/plugin-auth-node';
import { Profile } from 'passport';
import { ProfileInfo as ProfileInfo_2 } from '@backstage/plugin-auth-node';
import { SignInInfo as SignInInfo_2 } from '@backstage/plugin-auth-node';
import { SignInResolver as SignInResolver_2 } from '@backstage/plugin-auth-node';
import { TokenManager } from '@backstage/backend-common';
import { TokenParams as TokenParams_2 } from '@backstage/plugin-auth-node';
import { TokenSet } from 'openid-client';
import { UserEntity } from '@backstage/catalog-model';
import { UserinfoResponse } from 'openid-client';
import { WebMessageResponse as WebMessageResponse_2 } from '@backstage/plugin-auth-node';
// @public
// @public @deprecated
export type AuthHandler<TAuthResult> = (
input: TAuthResult,
context: AuthResolverContext,
) => Promise<AuthHandlerResult>;
// @public
// @public @deprecated
export type AuthHandlerResult = {
profile: ProfileInfo;
};
// @public (undocumented)
export type AuthProviderConfig = {
baseUrl: string;
appUrl: string;
isOriginAllowed: (origin: string) => boolean;
cookieConfigurer?: CookieConfigurer;
};
// @public @deprecated (undocumented)
export type AuthProviderConfig = AuthProviderConfig_2;
// @public (undocumented)
export type AuthProviderFactory = (options: {
providerId: string;
globalConfig: AuthProviderConfig;
config: Config;
logger: Logger;
resolverContext: AuthResolverContext;
}) => AuthProviderRouteHandlers;
// @public @deprecated (undocumented)
export type AuthProviderFactory = AuthProviderFactory_2;
// @public
export interface AuthProviderRouteHandlers {
frameHandler(req: express.Request, res: express.Response): Promise<void>;
logout?(req: express.Request, res: express.Response): Promise<void>;
refresh?(req: express.Request, res: express.Response): Promise<void>;
start(req: express.Request, res: express.Response): Promise<void>;
}
// @public @deprecated (undocumented)
export type AuthProviderRouteHandlers = AuthProviderRouteHandlers_2;
// @public
export type AuthResolverCatalogUserQuery =
| {
entityRef:
| string
| {
kind?: string;
namespace?: string;
name: string;
};
}
| {
annotations: Record<string, string>;
}
| {
filter: Exclude<GetEntitiesRequest['filter'], undefined>;
};
// @public @deprecated (undocumented)
export type AuthResolverCatalogUserQuery = AuthResolverCatalogUserQuery_2;
// @public
export type AuthResolverContext = {
issueToken(params: TokenParams): Promise<{
token: string;
}>;
findCatalogUser(query: AuthResolverCatalogUserQuery): Promise<{
entity: Entity;
}>;
signInWithCatalogUser(
query: AuthResolverCatalogUserQuery,
): Promise<BackstageSignInResult>;
};
// @public @deprecated (undocumented)
export type AuthResolverContext = AuthResolverContext_2;
// @public (undocumented)
export type AuthResponse<ProviderInfo> = {
providerInfo: ProviderInfo;
profile: ProfileInfo;
backstageIdentity?: BackstageIdentityResponse;
};
// @public @deprecated (undocumented)
export type AuthResponse<TProviderInfo> = ClientAuthResponse<TProviderInfo>;
// @public (undocumented)
export type AwsAlbResult = {
@@ -151,7 +122,7 @@ export class CatalogIdentityClient {
findUser(query: { annotations: Record<string, string> }): Promise<UserEntity>;
resolveCatalogMembership(query: {
entityRefs: string[];
logger?: Logger;
logger?: LoggerService;
}): Promise<string[]>;
}
@@ -191,18 +162,8 @@ export type CloudflareAccessResult = {
token: string;
};
// @public
export type CookieConfigurer = (ctx: {
providerId: string;
baseUrl: string;
callbackUrl: string;
appOrigin: string;
}) => {
domain: string;
path: string;
secure: boolean;
sameSite?: 'none' | 'lax' | 'strict';
};
// @public @deprecated (undocumented)
export type CookieConfigurer = CookieConfigurer_2;
// @public
export function createAuthProviderIntegration<
@@ -235,23 +196,17 @@ export type EasyAuthResult = {
accessToken?: string;
};
// @public (undocumented)
export const encodeState: (state: OAuthState) => string;
// @public @deprecated (undocumented)
export const encodeState: typeof encodeOAuthState;
// @public (undocumented)
// @public @deprecated (undocumented)
export const ensuresXRequestedWith: (req: express.Request) => boolean;
// @public
export type GcpIapResult = {
iapToken: GcpIapTokenInfo;
};
// @public @deprecated
export type GcpIapResult = GcpIapResult_2;
// @public
export type GcpIapTokenInfo = {
sub: string;
email: string;
[key: string]: JsonValue;
};
// @public @deprecated
export type GcpIapTokenInfo = GcpIapTokenInfo_2;
// @public
export function getDefaultOwnershipEntityRefs(entity: Entity): string[];
@@ -276,7 +231,7 @@ export type OAuth2ProxyResult<JWTPayload = {}> = {
getHeader(name: string): string | undefined;
};
// @public (undocumented)
// @public @deprecated (undocumented)
export class OAuthAdapter implements AuthProviderRouteHandlers {
constructor(handlers: OAuthHandlers, options: OAuthAdapterOptions);
// (undocumented)
@@ -298,7 +253,7 @@ export class OAuthAdapter implements AuthProviderRouteHandlers {
start(req: express.Request, res: express.Response): Promise<void>;
}
// @public (undocumented)
// @public @deprecated (undocumented)
export type OAuthAdapterOptions = {
providerId: string;
persistScopes?: boolean;
@@ -309,25 +264,10 @@ export type OAuthAdapterOptions = {
callbackUrl: string;
};
// @public (undocumented)
export class OAuthEnvironmentHandler implements AuthProviderRouteHandlers {
constructor(handlers: Map<string, AuthProviderRouteHandlers>);
// (undocumented)
frameHandler(req: express.Request, res: express.Response): Promise<void>;
// (undocumented)
logout(req: express.Request, res: express.Response): Promise<void>;
// (undocumented)
static mapConfig(
config: Config,
factoryFunc: (envConfig: Config) => AuthProviderRouteHandlers,
): OAuthEnvironmentHandler;
// (undocumented)
refresh(req: express.Request, res: express.Response): Promise<void>;
// (undocumented)
start(req: express.Request, res: express.Response): Promise<void>;
}
// @public @deprecated (undocumented)
export const OAuthEnvironmentHandler: typeof OAuthEnvironmentHandler_2;
// @public
// @public @deprecated (undocumented)
export interface OAuthHandlers {
handler(req: express.Request): Promise<{
response: OAuthResponse;
@@ -341,12 +281,12 @@ export interface OAuthHandlers {
start(req: OAuthStartRequest): Promise<OAuthStartResponse>;
}
// @public (undocumented)
// @public @deprecated (undocumented)
export type OAuthLogoutRequest = express.Request<{}> & {
refreshToken: string;
};
// @public (undocumented)
// @public @deprecated (undocumented)
export type OAuthProviderInfo = {
accessToken: string;
idToken?: string;
@@ -354,59 +294,53 @@ export type OAuthProviderInfo = {
scope: string;
};
// @public
// @public @deprecated
export type OAuthProviderOptions = {
clientId: string;
clientSecret: string;
callbackUrl: string;
};
// @public (undocumented)
// @public @deprecated (undocumented)
export type OAuthRefreshRequest = express.Request<{}> & {
scope: string;
refreshToken: string;
};
// @public
// @public @deprecated (undocumented)
export type OAuthResponse = {
profile: ProfileInfo;
providerInfo: OAuthProviderInfo;
backstageIdentity?: BackstageSignInResult;
};
// @public (undocumented)
// @public @deprecated (undocumented)
export type OAuthResult = {
fullProfile: Profile;
params: {
id_token?: string;
scope: string;
token_type?: string;
expires_in: number;
};
accessToken: string;
refreshToken?: string;
};
// @public (undocumented)
// @public @deprecated (undocumented)
export type OAuthStartRequest = express.Request<{}> & {
scope: string;
state: OAuthState;
};
// @public (undocumented)
// @public @deprecated (undocumented)
export type OAuthStartResponse = {
url: string;
status?: number;
};
// @public (undocumented)
export type OAuthState = {
nonce: string;
env: string;
origin?: string;
scope?: string;
redirectUrl?: string;
flow?: string;
};
// @public @deprecated (undocumented)
export type OAuthState = OAuthState_2;
// @public
export type OidcAuthResult = {
@@ -414,24 +348,18 @@ export type OidcAuthResult = {
userinfo: UserinfoResponse;
};
// @public (undocumented)
// @public @deprecated (undocumented)
export const postMessageResponse: (
res: express.Response,
appOrigin: string,
response: WebMessageResponse,
) => void;
// @public
export function prepareBackstageIdentityResponse(
result: BackstageSignInResult,
): BackstageIdentityResponse;
// @public @deprecated (undocumented)
export const prepareBackstageIdentityResponse: typeof prepareBackstageIdentityResponse_2;
// @public
export type ProfileInfo = {
email?: string;
displayName?: string;
picture?: string;
};
// @public @deprecated (undocumented)
export type ProfileInfo = ProfileInfo_2;
// @public (undocumented)
export type ProviderFactories = {
@@ -452,7 +380,7 @@ export const providers: Readonly<{
| undefined;
}
| undefined,
) => AuthProviderFactory;
) => AuthProviderFactory_2;
resolvers: never;
}>;
auth0: Readonly<{
@@ -467,7 +395,7 @@ export const providers: Readonly<{
| undefined;
}
| undefined,
) => AuthProviderFactory;
) => AuthProviderFactory_2;
resolvers: never;
}>;
awsAlb: Readonly<{
@@ -480,7 +408,7 @@ export const providers: Readonly<{
};
}
| undefined,
) => AuthProviderFactory;
) => AuthProviderFactory_2;
resolvers: never;
}>;
bitbucket: Readonly<{
@@ -495,7 +423,7 @@ export const providers: Readonly<{
| undefined;
}
| undefined,
) => AuthProviderFactory;
) => AuthProviderFactory_2;
resolvers: Readonly<{
usernameMatchingUserEntityAnnotation(): SignInResolver<OAuthResult>;
userIdMatchingUserEntityAnnotation(): SignInResolver<OAuthResult>;
@@ -513,7 +441,7 @@ export const providers: Readonly<{
| undefined;
}
| undefined,
) => AuthProviderFactory;
) => AuthProviderFactory_2;
resolvers: Readonly<{
emailMatchingUserEntityProfileEmail: () => SignInResolver<BitbucketServerOAuthResult>;
}>;
@@ -525,18 +453,18 @@ export const providers: Readonly<{
resolver: SignInResolver<CloudflareAccessResult>;
};
cache?: CacheService | undefined;
}) => AuthProviderFactory;
}) => AuthProviderFactory_2;
resolvers: Readonly<{
emailMatchingUserEntityProfileEmail: () => SignInResolver<unknown>;
}>;
}>;
gcpIap: Readonly<{
create: (options: {
authHandler?: AuthHandler<GcpIapResult> | undefined;
authHandler?: AuthHandler<GcpIapResult_2> | undefined;
signIn: {
resolver: SignInResolver<GcpIapResult>;
resolver: SignInResolver<GcpIapResult_2>;
};
}) => AuthProviderFactory;
}) => AuthProviderFactory_2;
resolvers: never;
}>;
github: Readonly<{
@@ -552,7 +480,7 @@ export const providers: Readonly<{
stateEncoder?: StateEncoder | undefined;
}
| undefined,
) => AuthProviderFactory;
) => AuthProviderFactory_2;
resolvers: Readonly<{
usernameMatchingUserEntityName: () => SignInResolver<GithubOAuthResult>;
}>;
@@ -569,7 +497,7 @@ export const providers: Readonly<{
| undefined;
}
| undefined,
) => AuthProviderFactory;
) => AuthProviderFactory_2;
resolvers: never;
}>;
google: Readonly<{
@@ -584,11 +512,11 @@ export const providers: Readonly<{
| undefined;
}
| undefined,
) => AuthProviderFactory;
) => AuthProviderFactory_2;
resolvers: Readonly<{
emailLocalPartMatchingUserEntityName: () => SignInResolver<unknown>;
emailMatchingUserEntityProfileEmail: () => SignInResolver<unknown>;
emailMatchingUserEntityAnnotation(): SignInResolver<OAuthResult>;
emailMatchingUserEntityProfileEmail: () => SignInResolver_2<OAuthResult>;
emailLocalPartMatchingUserEntityName: () => SignInResolver_2<OAuthResult>;
emailMatchingUserEntityAnnotation: () => SignInResolver_2<OAuthResult>;
}>;
}>;
microsoft: Readonly<{
@@ -603,7 +531,7 @@ export const providers: Readonly<{
| undefined;
}
| undefined,
) => AuthProviderFactory;
) => AuthProviderFactory_2;
resolvers: Readonly<{
emailLocalPartMatchingUserEntityName: () => SignInResolver<unknown>;
emailMatchingUserEntityProfileEmail: () => SignInResolver<unknown>;
@@ -622,7 +550,7 @@ export const providers: Readonly<{
| undefined;
}
| undefined,
) => AuthProviderFactory;
) => AuthProviderFactory_2;
resolvers: never;
}>;
oauth2Proxy: Readonly<{
@@ -631,7 +559,7 @@ export const providers: Readonly<{
signIn: {
resolver: SignInResolver<OAuth2ProxyResult<unknown>>;
};
}) => AuthProviderFactory;
}) => AuthProviderFactory_2;
resolvers: never;
}>;
oidc: Readonly<{
@@ -646,7 +574,7 @@ export const providers: Readonly<{
| undefined;
}
| undefined,
) => AuthProviderFactory;
) => AuthProviderFactory_2;
resolvers: Readonly<{
emailLocalPartMatchingUserEntityName: () => SignInResolver<unknown>;
emailMatchingUserEntityProfileEmail: () => SignInResolver<unknown>;
@@ -664,7 +592,7 @@ export const providers: Readonly<{
| undefined;
}
| undefined,
) => AuthProviderFactory;
) => AuthProviderFactory_2;
resolvers: Readonly<{
emailLocalPartMatchingUserEntityName: () => SignInResolver<unknown>;
emailMatchingUserEntityProfileEmail: () => SignInResolver<unknown>;
@@ -683,7 +611,7 @@ export const providers: Readonly<{
| undefined;
}
| undefined,
) => AuthProviderFactory;
) => AuthProviderFactory_2;
resolvers: never;
}>;
saml: Readonly<{
@@ -698,7 +626,7 @@ export const providers: Readonly<{
| undefined;
}
| undefined,
) => AuthProviderFactory;
) => AuthProviderFactory_2;
resolvers: Readonly<{
nameIdMatchingUserEntityName(): SignInResolver<SamlAuthResult>;
}>;
@@ -713,13 +641,13 @@ export const providers: Readonly<{
};
}
| undefined,
) => AuthProviderFactory;
) => AuthProviderFactory_2;
resolvers: never;
}>;
}>;
// @public (undocumented)
export const readState: (stateString: string) => OAuthState;
// @public @deprecated (undocumented)
export const readState: typeof decodeOAuthState;
// @public (undocumented)
export interface RouterOptions {
@@ -732,7 +660,7 @@ export interface RouterOptions {
// (undocumented)
discovery: PluginEndpointDiscovery;
// (undocumented)
logger: Logger;
logger: LoggerService;
// (undocumented)
providerFactories?: ProviderFactories;
// (undocumented)
@@ -746,42 +674,23 @@ export type SamlAuthResult = {
fullProfile: any;
};
// @public
export type SignInInfo<TAuthResult> = {
profile: ProfileInfo;
result: TAuthResult;
};
// @public @deprecated (undocumented)
export type SignInInfo<TAuthResult> = SignInInfo_2<TAuthResult>;
// @public
export type SignInResolver<TAuthResult> = (
info: SignInInfo<TAuthResult>,
context: AuthResolverContext,
) => Promise<BackstageSignInResult>;
// @public @deprecated (undocumented)
export type SignInResolver<TAuthResult> = SignInResolver_2<TAuthResult>;
// @public (undocumented)
// @public @deprecated (undocumented)
export type StateEncoder = (req: OAuthStartRequest) => Promise<{
encodedState: string;
}>;
// @public
export type TokenParams = {
claims: {
sub: string;
ent?: string[];
} & Record<string, JsonValue>;
};
// @public @deprecated (undocumented)
export type TokenParams = TokenParams_2;
// @public (undocumented)
// @public @deprecated (undocumented)
export const verifyNonce: (req: express.Request, providerId: string) => void;
// @public
export type WebMessageResponse =
| {
type: 'authorization_response';
response: AuthResponse<unknown>;
}
| {
type: 'authorization_response';
error: Error;
};
// @public @deprecated (undocumented)
export type WebMessageResponse = WebMessageResponse_2;
```
@@ -0,0 +1,262 @@
<svg host="65bd71144e" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="801px" height="271px" viewBox="-0.5 -0.5 801 271" content="&lt;mxfile&gt;&lt;diagram id=&quot;m9H7LUwiOP4VaYd3QZaE&quot; name=&quot;Page-1&quot;&gt;zVnLjpswFP2abCpNBSbP5TSTaboYNWoqtbO04AasGoyMk5B+fU2ww8NklGkbw2Iy+Pp5jo/v9WPkLeP8M8dp9MICoCPkBPnIexoh5LqLqfxXWE6lBTmTeWkJOQlUqcqwJb9BGR1l3ZMAskZBwRgVJG0afZYk4IuGDXPOjs1iO0abvaY4BMOw9TE1rT9IIKLS6o2dyr4GEka657nKiLEuqwxZhAN2rJm81chbcsZE+RXnS6AFeZqWst7zldzLuDgk4pYKqKxwwHSvoG1wlqWMi63gWEB4UuMUJ42ds30SQFHfGXmfjhERsE2xX+Qe5XRLWyRiKlOu/FTNAxeQXx2iewEuFQMsBsFlt46q8OBqspRa3IVKHyvqL2WiGu16OrCa7fDSdsWI/FCkdBM0NvBDIKWgkglLoAtwUeZtuLJJtue+KoWU/AXmIYja1JiscKBYkEOz+S6EquqGEdlxxebsCpm6iXJYqlaLp8swbqIOLaxwN+mgbv6P3N2qjomxfL4+7kVU/MkhEx8LxntfQPPWjKOO5TO90/JxPSsamJoaWFiSgDvpC6E7tgVx3hvEqSWIl/FUEO+Cx5Yqp4Zj2nCWn+TPQe6c+DP2pWfqP7aPW75pbNM1OQb8e6i6I7Jbc02mqN9GCDkRP4vJ/zhRqddazlOudHFOnFTiv7EyscXKrK95t+fNepO2NQeH7ITdrn25rbCLehMqsiXUuRGmzvvnoYWpHqPUwmBIkrMjFL5znGQ7xuPe2emTHnfcxU9+GtYJrL3NsXsEM/eCj74PWbaMwP/VOzdt9XRd7tyNGnN1bUmYfEm+QcaohDU4dh5ci/Sg9x7e7e0fC0M7atm69NF911SDpbt5YcGewgcdu3qXTls5Nt0yMt3yObCvkgPhLIklhDVOAjoAmszrZZs8mb65vEAMcCqGSM7MJjnm7lA/Tpi3rGug6SAIm7bU5JmMIedOjHnvPfD/nfPVkOrO19aJQfc9cOfb3vHd0/vKZPVgWL7TVM+u3uoP&lt;/diagram&gt;&lt;/mxfile&gt;">
<defs/>
<g>
<rect x="40" y="230" width="120" height="40" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 250px; margin-left: 41px;">
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
PassportStrategy
</div>
</div>
</div>
</foreignObject>
<text x="100" y="254" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
PassportStrategy
</text>
</switch>
</g>
<path d="M 100 210 L 100 223.63" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 100 228.88 L 96.5 221.88 L 100 223.63 L 103.5 221.88 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 240 183.64 L 206.36 185.17" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 201.12 185.4 L 207.95 181.59 L 206.36 185.17 L 208.27 188.58 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/>
<rect x="240" y="160" width="160" height="40" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 158px; height: 1px; padding-top: 180px; margin-left: 241px;">
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
OAuthAuthenticator
</div>
</div>
</div>
</foreignObject>
<text x="320" y="184" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
OAuthAuthenticator
</text>
</switch>
</g>
<path d="M 640 100 L 606.37 100" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 601.12 100 L 608.12 96.5 L 606.37 100 L 608.12 103.5 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 720 120 L 720 153.63" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 720 158.88 L 716.5 151.88 L 720 153.63 L 723.5 151.88 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 640 120 L 606.18 128.46" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 601.08 129.73 L 607.03 124.64 L 606.18 128.46 L 608.72 131.43 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 640 80 L 606.18 71.54" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 601.08 70.27 L 608.72 68.57 L 606.18 71.54 L 607.03 75.36 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/>
<rect x="640" y="80" width="160" height="40" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 158px; height: 1px; padding-top: 100px; margin-left: 641px;">
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
ProxyProviderFactory
</div>
</div>
</div>
</foreignObject>
<text x="720" y="104" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
ProxyProviderFactory
</text>
</switch>
</g>
<path d="M 400 100 L 433.63 100" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 438.88 100 L 431.88 103.5 L 433.63 100 L 431.88 96.5 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 320 120 L 320 153.63" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 320 158.88 L 316.5 151.88 L 320 153.63 L 323.5 151.88 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 400 120 L 433.82 128.46" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 438.92 129.73 L 431.28 131.43 L 433.82 128.46 L 432.97 124.64 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 400 80 L 433.82 71.54" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 438.92 70.27 L 432.97 75.36 L 433.82 71.54 L 431.28 68.57 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 240 88 L 206.3 82.94" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 201.11 82.17 L 208.55 79.74 L 206.3 82.94 L 207.51 86.67 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 240 112 L 206.3 117.06" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 201.11 117.83 L 207.51 113.33 L 206.3 117.06 L 208.55 120.26 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/>
<rect x="240" y="80" width="160" height="40" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 158px; height: 1px; padding-top: 100px; margin-left: 241px;">
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
OAuthProviderFactory
</div>
</div>
</div>
</foreignObject>
<text x="320" y="104" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
OAuthProviderFactory
</text>
</switch>
</g>
<rect x="440" y="80" width="160" height="40" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 158px; height: 1px; padding-top: 100px; margin-left: 441px;">
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
ProfileTransform
</div>
</div>
</div>
</foreignObject>
<text x="520" y="104" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
ProfileTransform
</text>
</switch>
</g>
<rect x="640" y="160" width="160" height="40" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 158px; height: 1px; padding-top: 180px; margin-left: 641px;">
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
ProxyAuthenticator
</div>
</div>
</div>
</foreignObject>
<text x="720" y="184" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
ProxyAuthenticator
</text>
</switch>
</g>
<rect x="440" y="130" width="160" height="40" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 158px; height: 1px; padding-top: 150px; margin-left: 441px;">
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
AccessCheck
</div>
</div>
</div>
</foreignObject>
<text x="520" y="154" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
AccessCheck
</text>
</switch>
</g>
<rect x="440" y="30" width="160" height="40" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 158px; height: 1px; padding-top: 50px; margin-left: 441px;">
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
SignInResolver
</div>
</div>
</div>
</foreignObject>
<text x="520" y="54" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
SignInResolver
</text>
</switch>
</g>
<path d="M 320 40 L 320 73.63" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 320 78.88 L 316.5 71.88 L 320 73.63 L 323.5 71.88 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/>
<rect x="240" y="0" width="160" height="40" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 158px; height: 1px; padding-top: 20px; margin-left: 241px;">
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
authModule*Provider
</div>
</div>
</div>
</foreignObject>
<text x="320" y="24" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
authModule*Provider
</text>
</switch>
</g>
<rect x="40" y="50" width="160" height="40" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 158px; height: 1px; padding-top: 70px; margin-left: 41px;">
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
OAuthEnvironmentHandler
</div>
</div>
</div>
</foreignObject>
<text x="120" y="74" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
OAuthEnvironmentHandler
</text>
</switch>
</g>
<rect x="40" y="110" width="160" height="40" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 158px; height: 1px; padding-top: 130px; margin-left: 41px;">
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
OAuthAdapter
</div>
</div>
</div>
</foreignObject>
<text x="120" y="134" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
OAuthAdapter
</text>
</switch>
</g>
<rect x="0" y="170" width="200" height="40" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 198px; height: 1px; padding-top: 190px; margin-left: 1px;">
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
PassportOAuthAuthenticatorHelper
</div>
</div>
</div>
</foreignObject>
<text x="100" y="194" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
PassportOAuthAuthenticatorHelper
</text>
</switch>
</g>
<path d="M 720 40 L 720 73.63" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 720 78.88 L 716.5 71.88 L 720 73.63 L 723.5 71.88 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/>
<rect x="640" y="0" width="160" height="40" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 158px; height: 1px; padding-top: 20px; margin-left: 641px;">
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
authModule*Provider
</div>
</div>
</div>
</foreignObject>
<text x="720" y="24" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
authModule*Provider
</text>
</switch>
</g>
</g>
<switch>
<g requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"/>
<a transform="translate(0,-5)" xlink:href="https://www.diagrams.net/doc/faq/svg-export-text-problems" target="_blank">
<text text-anchor="middle" font-size="10px" x="50%" y="100%">
Text is not SVG - cannot display
</text>
</a>
</switch>
</svg>

After

Width:  |  Height:  |  Size: 23 KiB

+3
View File
@@ -33,10 +33,13 @@
},
"dependencies": {
"@backstage/backend-common": "workspace:^",
"@backstage/backend-plugin-api": "workspace:^",
"@backstage/catalog-client": "workspace:^",
"@backstage/catalog-model": "workspace:^",
"@backstage/config": "workspace:^",
"@backstage/errors": "workspace:^",
"@backstage/plugin-auth-backend-module-gcp-iap-provider": "workspace:^",
"@backstage/plugin-auth-backend-module-google-provider": "workspace:^",
"@backstage/plugin-auth-node": "workspace:^",
"@backstage/types": "workspace:^",
"@davidzemon/passport-okta-oauth": "^0.0.5",
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { Logger } from 'winston';
import { LoggerService } from '@backstage/backend-plugin-api';
import {
DocumentData,
Firestore,
@@ -57,7 +57,7 @@ export class FirestoreKeyStore implements KeyStore {
static async verifyConnection(
keyStore: FirestoreKeyStore,
logger?: Logger,
logger?: LoggerService,
): Promise<void> {
try {
await keyStore.verify();
@@ -15,7 +15,7 @@
*/
import { pickBy } from 'lodash';
import { Logger } from 'winston';
import { LoggerService } from '@backstage/backend-plugin-api';
import { Config } from '@backstage/config';
@@ -26,7 +26,7 @@ import { MemoryKeyStore } from './MemoryKeyStore';
import { KeyStore } from './types';
type Options = {
logger: Logger;
logger: LoggerService;
database: AuthDatabase;
};
@@ -18,14 +18,14 @@ import { AuthenticationError } from '@backstage/errors';
import { exportJWK, generateKeyPair, importJWK, JWK, SignJWT } from 'jose';
import { DateTime } from 'luxon';
import { v4 as uuid } from 'uuid';
import { Logger } from 'winston';
import { LoggerService } from '@backstage/backend-plugin-api';
import { AnyJWK, KeyStore, TokenIssuer, TokenParams } from './types';
const MS_IN_S = 1000;
type Options = {
logger: Logger;
logger: LoggerService;
/** Value of the issuer claim in issued tokens */
issuer: string;
/** Key store used for storing signing keys */
@@ -57,7 +57,7 @@ type Options = {
*/
export class TokenFactory implements TokenIssuer {
private readonly issuer: string;
private readonly logger: Logger;
private readonly logger: LoggerService;
private readonly keyStore: KeyStore;
private readonly keyDurationSeconds: number;
private readonly algorithm: string;
+3 -19
View File
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { JsonValue } from '@backstage/types';
import { TokenParams as _TokenParams } from '@backstage/plugin-auth-node';
/** Represents any form of serializable JWK */
export interface AnyJWK extends Record<string, string> {
@@ -25,26 +25,10 @@ export interface AnyJWK extends Record<string, string> {
}
/**
* Parameters used to issue new ID Tokens
*
* @public
* @deprecated import from `@backstage/plugin-auth-node` instead
*/
export type TokenParams = {
/**
* The claims that will be embedded within the token. At a minimum, this should include
* the subject claim, `sub`. It is common to also list entity ownership relations in the
* `ent` list. Additional claims may also be added at the developer's discretion except
* for the following list, which will be overwritten by the TokenIssuer: `iss`, `aud`,
* `iat`, and `exp`. The Backstage team also maintains the right add new claims in the future
* without listing the change as a "breaking change".
*/
claims: {
/** The token subject, i.e. User ID */
sub: string;
/** A list of entity references that the user claims ownership through */
ent?: string[];
} & Record<string, JsonValue>;
};
export type TokenParams = _TokenParams;
/**
* A TokenIssuer is able to issue verifiable ID Tokens on demand.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { Logger } from 'winston';
import { LoggerService } from '@backstage/backend-plugin-api';
import { ConflictError, NotFoundError } from '@backstage/errors';
import { CatalogApi } from '@backstage/catalog-client';
import {
@@ -78,7 +78,7 @@ export class CatalogIdentityClient {
*/
async resolveCatalogMembership(query: {
entityRefs: string[];
logger?: Logger;
logger?: LoggerService;
}): Promise<string[]> {
const { entityRefs, logger } = query;
const resolvedEntityRefs = entityRefs
@@ -24,7 +24,10 @@ export const safelyEncodeURIComponent = (value: string) => {
return encodeURIComponent(value).replace(/'/g, '%27');
};
/** @public */
/**
* @public
* @deprecated Use `sendWebMessageResponse` from `@backstage/plugin-auth-node` instead
*/
export const postMessageResponse = (
res: express.Response,
appOrigin: string,
@@ -69,7 +72,10 @@ export const postMessageResponse = (
res.end(`<html><body><script>${script}</script></body></html>`);
};
/** @public */
/**
* @public
* @deprecated Use inline logic to check that the `X-Requested-With` header is set to `'XMLHttpRequest'` instead.
*/
export const ensuresXRequestedWith = (req: express.Request) => {
const requiredHeader = req.header('X-Requested-With');
if (!requiredHeader || requiredHeader !== 'XMLHttpRequest') {
+3 -13
View File
@@ -14,20 +14,10 @@
* limitations under the License.
*/
import { AuthResponse } from '../../providers/types';
import { WebMessageResponse as _WebMessageResponse } from '@backstage/plugin-auth-node';
/**
* 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
* @deprecated import from `@backstage/plugin-auth-node` instead
*/
export type WebMessageResponse =
| {
type: 'authorization_response';
response: AuthResponse<unknown>;
}
| {
type: 'authorization_response';
error: Error;
};
export type WebMessageResponse = _WebMessageResponse;
@@ -0,0 +1,61 @@
/*
* Copyright 2023 The Backstage Authors
*
* 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 { AuthResolverContext } from '@backstage/plugin-auth-node';
import { AuthHandler } from '../../providers';
import { OAuthResult } from '../oauth';
import { PassportProfile } from '../passport/types';
import { adaptLegacyOAuthHandler } from './adaptLegacyOAuthHandler';
describe('adaptLegacyOAuthHandler', () => {
it('should pass through undefined', () => {
expect(adaptLegacyOAuthHandler(undefined)).toBeUndefined();
});
it('should convert an old auth handler to a new profile transform', () => {
const authHandler: AuthHandler<OAuthResult> = jest.fn();
const profileTransform = adaptLegacyOAuthHandler(authHandler);
profileTransform?.(
{
fullProfile: { id: 'id' } as PassportProfile,
session: {
accessToken: 'token',
expiresInSeconds: 3,
scope: 'sco pe',
tokenType: 'bear',
idToken: 'id-token',
refreshToken: 'refresh-token',
},
},
{ ctx: 'ctx' } as unknown as AuthResolverContext,
);
expect(authHandler).toHaveBeenCalledWith(
{
fullProfile: { id: 'id' },
accessToken: 'token',
params: {
scope: 'sco pe',
id_token: 'id-token',
expires_in: 3,
token_type: 'bear',
},
},
{ ctx: 'ctx' },
);
});
});
@@ -0,0 +1,46 @@
/*
* Copyright 2023 The Backstage Authors
*
* 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 {
OAuthAuthenticatorResult,
ProfileTransform,
} from '@backstage/plugin-auth-node';
import { AuthHandler } from '../../providers';
import { OAuthResult } from '../oauth';
import { PassportProfile } from '../passport/types';
/** @internal */
export function adaptLegacyOAuthHandler(
authHandler?: AuthHandler<OAuthResult>,
): ProfileTransform<OAuthAuthenticatorResult<PassportProfile>> | undefined {
return (
authHandler &&
(async (result, ctx) =>
authHandler(
{
fullProfile: result.fullProfile,
accessToken: result.session.accessToken,
params: {
scope: result.session.scope,
id_token: result.session.idToken,
token_type: result.session.tokenType,
expires_in: result.session.expiresInSeconds,
},
},
ctx,
))
);
}
@@ -0,0 +1,69 @@
/*
* Copyright 2023 The Backstage Authors
*
* 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 {
AuthResolverContext,
PassportProfile,
} from '@backstage/plugin-auth-node';
import { adaptLegacyOAuthSignInResolver } from './adaptLegacyOAuthSignInResolver';
describe('adaptLegacyOAuthSignInResolver', () => {
it('should pass through undefined', () => {
expect(adaptLegacyOAuthSignInResolver(undefined)).toBeUndefined();
});
it('should convert a legacy resolver to a new one', () => {
const legacyResolver = jest.fn();
const newResolver = adaptLegacyOAuthSignInResolver(legacyResolver);
newResolver?.(
{
profile: { email: 'em@i.l' },
result: {
fullProfile: { id: 'id' } as PassportProfile,
session: {
accessToken: 'token',
expiresInSeconds: 3,
scope: 'sco pe',
tokenType: 'bear',
idToken: 'id-token',
refreshToken: 'refresh-token',
},
},
},
{ ctx: 'ctx' } as unknown as AuthResolverContext,
);
expect(legacyResolver).toHaveBeenCalledWith(
{
profile: { email: 'em@i.l' },
result: {
fullProfile: { id: 'id' },
accessToken: 'token',
refreshToken: 'refresh-token',
params: {
scope: 'sco pe',
id_token: 'id-token',
expires_in: 3,
token_type: 'bear',
},
},
},
{ ctx: 'ctx' },
);
});
});
@@ -0,0 +1,49 @@
/*
* Copyright 2023 The Backstage Authors
*
* 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 {
OAuthAuthenticatorResult,
PassportProfile,
SignInResolver,
} from '@backstage/plugin-auth-node';
import { OAuthResult } from '../oauth';
/** @internal */
export function adaptLegacyOAuthSignInResolver(
signInResolver?: SignInResolver<OAuthResult>,
): SignInResolver<OAuthAuthenticatorResult<PassportProfile>> | undefined {
return (
signInResolver &&
(async (input, ctx) =>
signInResolver(
{
profile: input.profile,
result: {
fullProfile: input.result.fullProfile,
accessToken: input.result.session.accessToken,
refreshToken: input.result.session.refreshToken,
params: {
scope: input.result.session.scope,
id_token: input.result.session.idToken,
token_type: input.result.session.tokenType,
expires_in: input.result.session.expiresInSeconds,
},
},
},
ctx,
))
);
}
@@ -0,0 +1,85 @@
/*
* Copyright 2023 The Backstage Authors
*
* 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 {
AuthResolverContext,
PassportProfile,
} from '@backstage/plugin-auth-node';
import { adaptOAuthSignInResolverToLegacy } from './adaptOAuthSignInResolverToLegacy';
describe('adaptOAuthSignInResolverToLegacy', () => {
it('should pass through an empty object', () => {
const legacyResolvers = adaptOAuthSignInResolverToLegacy({});
expect(legacyResolvers).toEqual({});
// @ts-expect-error
legacyResolvers.missing?.();
});
it('should adapt a collection of sign-in resolvers', () => {
const resolverA = jest.fn();
const resolverB = jest.fn();
const legacyResolvers = adaptOAuthSignInResolverToLegacy({
resolverA,
resolverB,
});
const legacyResolverA = legacyResolvers.resolverA();
legacyResolverA(
{
profile: { email: 'em@i.l' },
result: {
fullProfile: { id: 'id' } as PassportProfile,
accessToken: 'token',
refreshToken: 'refresh-token',
params: {
scope: 'sco pe',
id_token: 'id-token',
expires_in: 3,
token_type: 'bear',
},
},
},
{ ctx: 'ctx' } as unknown as AuthResolverContext,
);
expect(resolverA).toHaveBeenCalledWith(
{
profile: { email: 'em@i.l' },
result: {
fullProfile: { id: 'id' } as PassportProfile,
session: {
accessToken: 'token',
expiresInSeconds: 3,
scope: 'sco pe',
tokenType: 'bear',
idToken: 'id-token',
refreshToken: 'refresh-token',
},
},
},
{ ctx: 'ctx' },
);
expect(resolverB).not.toHaveBeenCalled();
legacyResolvers.resolverB()(
{ profile: {}, result: { params: {} } } as any,
{} as any,
);
expect(resolverB).toHaveBeenCalled();
});
});
@@ -0,0 +1,55 @@
/*
* Copyright 2023 The Backstage Authors
*
* 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 {
OAuthAuthenticatorResult,
PassportProfile,
SignInResolver,
} from '@backstage/plugin-auth-node';
import { OAuthResult } from '../oauth';
/** @internal */
export function adaptOAuthSignInResolverToLegacy<
TKeys extends string,
>(resolvers: {
[key in TKeys]: SignInResolver<OAuthAuthenticatorResult<PassportProfile>>;
}): { [key in TKeys]: () => SignInResolver<OAuthResult> } {
const legacyResolvers = {} as {
[key in TKeys]: () => SignInResolver<OAuthResult>;
};
for (const name of Object.keys(resolvers) as TKeys[]) {
const resolver = resolvers[name];
legacyResolvers[name] = () => async (input, ctx) =>
resolver(
{
profile: input.profile,
result: {
fullProfile: input.result.fullProfile,
session: {
accessToken: input.result.accessToken,
expiresInSeconds: input.result.params.expires_in,
scope: input.result.params.scope,
idToken: input.result.params.id_token,
tokenType: input.result.params.token_type ?? 'bearer',
refreshToken: input.result.refreshToken,
},
},
},
ctx,
);
}
return legacyResolvers;
}
@@ -0,0 +1,19 @@
/*
* Copyright 2023 The Backstage Authors
*
* 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 { adaptLegacyOAuthHandler } from './adaptLegacyOAuthHandler';
export { adaptLegacyOAuthSignInResolver } from './adaptLegacyOAuthSignInResolver';
export { adaptOAuthSignInResolverToLegacy } from './adaptOAuthSignInResolverToLegacy';
@@ -50,7 +50,10 @@ import { prepareBackstageIdentityResponse } from '../../providers/prepareBacksta
export const THOUSAND_DAYS_MS = 1000 * 24 * 60 * 60 * 1000;
export const TEN_MINUTES_MS = 600 * 1000;
/** @public */
/**
* @public
* @deprecated Use `createOAuthRouteHandlers` from `@backstage/plugin-auth-node` instead
*/
export type OAuthAdapterOptions = {
providerId: string;
persistScopes?: boolean;
@@ -61,7 +64,10 @@ export type OAuthAdapterOptions = {
callbackUrl: string;
};
/** @public */
/**
* @public
* @deprecated Use `createOAuthRouteHandlers` from `@backstage/plugin-auth-node` instead
*/
export class OAuthAdapter implements AuthProviderRouteHandlers {
static fromConfig(
config: AuthProviderConfig,
@@ -14,84 +14,10 @@
* limitations under the License.
*/
import express from 'express';
import { Config } from '@backstage/config';
import { InputError, NotFoundError } from '@backstage/errors';
import { readState } from './helpers';
import { AuthProviderRouteHandlers } from '../../providers/types';
import { OAuthEnvironmentHandler as _OAuthEnvironmentHandler } from '@backstage/plugin-auth-node';
/** @public */
export class OAuthEnvironmentHandler implements AuthProviderRouteHandlers {
static mapConfig(
config: Config,
factoryFunc: (envConfig: Config) => AuthProviderRouteHandlers,
) {
const envs = config.keys();
const handlers = new Map<string, AuthProviderRouteHandlers>();
for (const env of envs) {
const envConfig = config.getConfig(env);
const handler = factoryFunc(envConfig);
handlers.set(env, handler);
}
return new OAuthEnvironmentHandler(handlers);
}
constructor(
private readonly handlers: Map<string, AuthProviderRouteHandlers>,
) {}
async start(req: express.Request, res: express.Response): Promise<void> {
const provider = this.getProviderForEnv(req);
await provider.start(req, res);
}
async frameHandler(
req: express.Request,
res: express.Response,
): Promise<void> {
const provider = this.getProviderForEnv(req);
await provider.frameHandler(req, res);
}
async refresh(req: express.Request, res: express.Response): Promise<void> {
const provider = this.getProviderForEnv(req);
await provider.refresh?.(req, res);
}
async logout(req: express.Request, res: express.Response): Promise<void> {
const provider = this.getProviderForEnv(req);
await provider.logout?.(req, res);
}
private getRequestFromEnv(req: express.Request): string | undefined {
const reqEnv = req.query.env?.toString();
if (reqEnv) {
return reqEnv;
}
const stateParams = req.query.state?.toString();
if (!stateParams) {
return undefined;
}
const env = readState(stateParams).env;
return env;
}
private getProviderForEnv(req: express.Request): AuthProviderRouteHandlers {
const env: string | undefined = this.getRequestFromEnv(req);
if (!env) {
throw new InputError(`Must specify 'env' query to select environment`);
}
const handler = this.handlers.get(env);
if (!handler) {
throw new NotFoundError(
`No configuration available for the '${env}' environment of this provider.`,
);
}
return handler;
}
}
/**
* @public
* @deprecated import from `@backstage/plugin-auth-node` instead
*/
export const OAuthEnvironmentHandler = _OAuthEnvironmentHandler;
@@ -76,7 +76,7 @@ describe('OAuthProvider Utils', () => {
} as unknown as express.Request;
expect(() => {
verifyNonce(mockRequest, 'providera');
}).toThrow('Invalid state passed via request');
}).toThrow('OAuth state is invalid, missing env');
});
it('should throw error if nonce mismatch', () => {
+18 -26
View File
@@ -16,36 +16,28 @@
import express from 'express';
import { OAuthState } from './types';
import pickBy from 'lodash/pickBy';
import { CookieConfigurer } from '../../providers/types';
import {
decodeOAuthState,
encodeOAuthState,
} from '@backstage/plugin-auth-node';
/** @public */
export const readState = (stateString: string): OAuthState => {
const state = Object.fromEntries(
new URLSearchParams(Buffer.from(stateString, 'hex').toString('utf-8')),
);
if (
!state.nonce ||
!state.env ||
state.nonce?.length === 0 ||
state.env?.length === 0
) {
throw Error(`Invalid state passed via request`);
}
/**
* @public
* @deprecated Use `decodeOAuthState` from `@backstage/plugin-auth-node` instead
*/
export const readState = decodeOAuthState;
return state as OAuthState;
};
/**
* @public
* @deprecated Use `encodeOAuthState` from `@backstage/plugin-auth-node` instead
*/
export const encodeState = encodeOAuthState;
/** @public */
export const encodeState = (state: OAuthState): string => {
const stateString = new URLSearchParams(
pickBy<string>(state, value => value !== undefined),
).toString();
return Buffer.from(stateString, 'utf-8').toString('hex');
};
/** @public */
/**
* @public
* @deprecated Use inline logic to make sure the session and state nonce matches instead.
*/
export const verifyNonce = (req: express.Request, providerId: string) => {
const cookieNonce = req.cookies[`${providerId}-nonce`];
const state: OAuthState = readState(req.query.state?.toString() ?? '');
+33 -23
View File
@@ -16,13 +16,17 @@
import express from 'express';
import { Profile as PassportProfile } from 'passport';
import { BackstageSignInResult } from '@backstage/plugin-auth-node';
import {
BackstageSignInResult,
OAuthState as _OAuthState,
} from '@backstage/plugin-auth-node';
import { OAuthStartResponse, ProfileInfo } from '../../providers/types';
/**
* Common options for passport.js-based OAuth providers
*
* @public
* @deprecated No longer in use
*/
export type OAuthProviderOptions = {
/**
@@ -39,12 +43,16 @@ export type OAuthProviderOptions = {
callbackUrl: string;
};
/** @public */
/**
* @public
* @deprecated Use `OAuthAuthenticatorResult<PassportProfile>` from `@backstage/plugin-auth-node` instead
*/
export type OAuthResult = {
fullProfile: PassportProfile;
params: {
id_token?: string;
scope: string;
token_type?: string;
expires_in: number;
};
accessToken: string;
@@ -52,9 +60,8 @@ export type OAuthResult = {
};
/**
* The expected response from an OAuth flow.
*
* @public
* @deprecated Use `ClientAuthResponse` from `@backstage/plugin-auth-node` instead
*/
export type OAuthResponse = {
profile: ProfileInfo;
@@ -62,7 +69,10 @@ export type OAuthResponse = {
backstageIdentity?: BackstageSignInResult;
};
/** @public */
/**
* @public
* @deprecated Use `createOAuthRouteHandlers` from `@backstage/plugin-auth-node` instead
*/
export type OAuthProviderInfo = {
/**
* An access token issued for the signed in user.
@@ -82,41 +92,41 @@ export type OAuthProviderInfo = {
scope: string;
};
/** @public */
export type OAuthState = {
/* A type for the serialized value in the `state` parameter of the OAuth authorization flow
*/
nonce: string;
env: string;
origin?: string;
scope?: string;
redirectUrl?: string;
flow?: string;
};
/**
* @public
* @deprecated import from `@backstage/plugin-auth-node` instead
*/
export type OAuthState = _OAuthState;
/** @public */
/**
* @public
* @deprecated Use `createOAuthRouteHandlers` from `@backstage/plugin-auth-node` instead
*/
export type OAuthStartRequest = express.Request<{}> & {
scope: string;
state: OAuthState;
};
/** @public */
/**
* @public
* @deprecated Use `createOAuthRouteHandlers` from `@backstage/plugin-auth-node` instead
*/
export type OAuthRefreshRequest = express.Request<{}> & {
scope: string;
refreshToken: string;
};
/** @public */
/**
* @public
* @deprecated Use `createOAuthRouteHandlers` from `@backstage/plugin-auth-node` instead
*/
export type OAuthLogoutRequest = express.Request<{}> & {
refreshToken: string;
};
/**
* Any OAuth provider needs to implement this interface which has provider specific
* handlers for different methods to perform authentication, get access tokens,
* refresh tokens and perform sign out.
*
* @public
* @deprecated Use `createOAuthRouteHandlers` from `@backstage/plugin-auth-node` instead
*/
export interface OAuthHandlers {
/**
@@ -24,7 +24,7 @@ import {
stringifyEntityRef,
} from '@backstage/catalog-model';
import { ConflictError, InputError, NotFoundError } from '@backstage/errors';
import { Logger } from 'winston';
import { LoggerService } from '@backstage/backend-plugin-api';
import { TokenIssuer, TokenParams } from '../../identity/types';
import { AuthResolverContext } from '../../providers';
import { AuthResolverCatalogUserQuery } from '../../providers/types';
@@ -54,7 +54,7 @@ export function getDefaultOwnershipEntityRefs(entity: Entity) {
*/
export class CatalogAuthResolverContext implements AuthResolverContext {
static create(options: {
logger: Logger;
logger: LoggerService;
catalogApi: CatalogApi;
tokenIssuer: TokenIssuer;
tokenManager: TokenManager;
@@ -73,7 +73,7 @@ export class CatalogAuthResolverContext implements AuthResolverContext {
}
private constructor(
public readonly logger: Logger,
public readonly logger: LoggerService,
public readonly tokenIssuer: TokenIssuer,
public readonly catalogIdentityClient: CatalogIdentityClient,
private readonly catalogApi: CatalogApi,
@@ -1,134 +0,0 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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 { ConflictError } from '@backstage/errors';
import { OAuth2Client } from 'google-auth-library';
import { createTokenValidator, parseRequestToken } from './helpers';
const validJwt =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImZvbyIsImlzcyI6ImZvbyJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.T2BNS4G-6RoiFnXc8Q8TiwdWzTpNitY8jcsGM3N3-Yo';
beforeEach(() => {
jest.clearAllMocks();
});
describe('helpers', () => {
describe('createTokenValidator', () => {
it('runs the happy path', async () => {
const mockClient = {
getIapPublicKeys: async () => ({ pubkeys: '' }),
verifySignedJwtWithCertsAsync: async () => ({
getPayload: () => ({ sub: 's', email: 'e@mail.com' }),
}),
};
const validator = createTokenValidator(
'a',
mockClient as unknown as OAuth2Client,
);
await expect(validator(validJwt)).resolves.toMatchObject({
sub: 's',
email: 'e@mail.com',
});
});
it('throws if the client throws', async () => {
const mockClient = {
getIapPublicKeys: async () => {
throw new TypeError('bam');
},
};
const validator = createTokenValidator(
'a',
mockClient as unknown as OAuth2Client,
);
await expect(validator(validJwt)).rejects.toThrow(TypeError);
});
it('rejects empty payload', async () => {
const mockClient = {
getIapPublicKeys: async () => ({ pubkeys: '' }),
verifySignedJwtWithCertsAsync: async () => ({
getPayload: () => undefined,
}),
};
const validator = createTokenValidator(
'a',
mockClient as unknown as OAuth2Client,
);
await expect(validator(validJwt)).rejects.toMatchObject({
name: 'TypeError',
message: 'Token had no payload',
});
});
});
describe('parseRequestToken', () => {
it('runs the happy path', async () => {
await expect(
parseRequestToken(
validJwt,
async () => ({ sub: 's', email: 'e@mail.com' } as any),
),
).resolves.toMatchObject({
iapToken: {
sub: 's',
email: 'e@mail.com',
},
});
});
it('rejects bad tokens', async () => {
await expect(
parseRequestToken(7, undefined as any),
).rejects.toMatchObject({
name: 'AuthenticationError',
message: 'Missing Google IAP header',
});
await expect(
parseRequestToken(undefined, undefined as any),
).rejects.toMatchObject({
name: 'AuthenticationError',
message: 'Missing Google IAP header',
});
await expect(
parseRequestToken('', undefined as any),
).rejects.toMatchObject({
name: 'AuthenticationError',
message: 'Missing Google IAP header',
});
});
it('translates validator errors', async () => {
await expect(
parseRequestToken(validJwt, async () => {
throw new ConflictError('Ouch');
}),
).rejects.toMatchObject({
name: 'AuthenticationError',
message: 'Google IAP token verification failed, ConflictError: Ouch',
});
});
it('rejects bad token payloads', async () => {
await expect(
parseRequestToken(validJwt, async () => ({ sub: 'a' } as any)),
).rejects.toMatchObject({
name: 'AuthenticationError',
message: 'Google IAP token payload is missing sub and/or email claim',
});
});
});
});
@@ -1,82 +0,0 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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 { AuthenticationError } from '@backstage/errors';
import { OAuth2Client, TokenPayload } from 'google-auth-library';
import { AuthHandler } from '../types';
import { GcpIapResult } from './types';
export function createTokenValidator(
audience: string,
mockClient?: OAuth2Client,
): (token: string) => Promise<TokenPayload> {
const client = mockClient ?? new OAuth2Client();
return async function tokenValidator(token) {
// TODO(freben): Rate limit the public key reads. It may be sensible to
// cache these for some reasonable time rather than asking for the public
// keys on every single sign-in. But since the rate of events here is so
// slow, I decided to keep it simple for now.
const response = await client.getIapPublicKeys();
const ticket = await client.verifySignedJwtWithCertsAsync(
token,
response.pubkeys,
audience,
['https://cloud.google.com/iap'],
);
const payload = ticket.getPayload();
if (!payload) {
throw new TypeError('Token had no payload');
}
return payload;
};
}
export async function parseRequestToken(
jwtToken: unknown,
tokenValidator: (token: string) => Promise<TokenPayload>,
): Promise<GcpIapResult> {
if (typeof jwtToken !== 'string' || !jwtToken) {
throw new AuthenticationError('Missing Google IAP header');
}
let payload: TokenPayload;
try {
payload = await tokenValidator(jwtToken);
} catch (e) {
throw new AuthenticationError(`Google IAP token verification failed, ${e}`);
}
if (!payload.sub || !payload.email) {
throw new AuthenticationError(
'Google IAP token payload is missing sub and/or email claim',
);
}
return {
iapToken: {
...payload,
sub: payload.sub,
email: payload.email,
},
};
}
export const defaultAuthHandler: AuthHandler<GcpIapResult> = async ({
iapToken,
}) => ({ profile: { email: iapToken.email } });
@@ -1,75 +0,0 @@
/*
* Copyright 2020 The Backstage Authors
*
* 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 request from 'supertest';
import { AuthResolverContext } from '../types';
import { GcpIapProvider } from './provider';
import { DEFAULT_IAP_JWT_HEADER } from './types';
beforeEach(() => {
jest.clearAllMocks();
});
describe('GcpIapProvider', () => {
const authHandler = jest.fn();
const signInResolver = jest.fn();
const tokenValidator = jest.fn();
it.each([undefined, 'x-custom-header'])(
'runs the happy path',
async jwtHeader => {
const provider = new GcpIapProvider({
authHandler,
signInResolver,
tokenValidator,
resolverContext: {} as AuthResolverContext,
jwtHeader: jwtHeader,
});
// { "sub": "user:default/me", "ent": ["group:default/home"] }
const backstageToken =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyOmRlZmF1bHQvbWUiLCJlbnQiOlsiZ3JvdXA6ZGVmYXVsdC9ob21lIl19.CbmAKzFErGmtsnpRxyPc7dHv7WEjb5lY6206YCzR_Rc';
const iapToken = { sub: 's', email: 'e@mail.com' };
authHandler.mockResolvedValueOnce({ email: 'e@mail.com' });
signInResolver.mockResolvedValueOnce({ token: backstageToken });
tokenValidator.mockResolvedValueOnce(iapToken);
const app = express();
app.use('/refresh', provider.refresh.bind(provider));
const header = jwtHeader || DEFAULT_IAP_JWT_HEADER;
const response = await request(app).get('/refresh').set(header, 'token');
expect(response.status).toBe(200);
expect(response.get('content-type')).toBe(
'application/json; charset=utf-8',
);
expect(response.body).toEqual({
backstageIdentity: {
token: backstageToken,
identity: {
type: 'user',
userEntityRef: 'user:default/me',
ownershipEntityRefs: ['group:default/home'],
},
},
providerInfo: { iapToken },
});
},
);
});
@@ -14,70 +14,11 @@
* limitations under the License.
*/
import express from 'express';
import { TokenPayload } from 'google-auth-library';
import { gcpIapAuthenticator } from '@backstage/plugin-auth-backend-module-gcp-iap-provider';
import { createProxyAuthProviderFactory } from '@backstage/plugin-auth-node';
import { createAuthProviderIntegration } from '../createAuthProviderIntegration';
import { prepareBackstageIdentityResponse } from '../prepareBackstageIdentityResponse';
import {
AuthHandler,
AuthProviderRouteHandlers,
AuthResolverContext,
SignInResolver,
} from '../types';
import {
createTokenValidator,
defaultAuthHandler,
parseRequestToken,
} from './helpers';
import { GcpIapResponse, GcpIapResult, DEFAULT_IAP_JWT_HEADER } from './types';
export class GcpIapProvider implements AuthProviderRouteHandlers {
private readonly authHandler: AuthHandler<GcpIapResult>;
private readonly signInResolver: SignInResolver<GcpIapResult>;
private readonly tokenValidator: (token: string) => Promise<TokenPayload>;
private readonly resolverContext: AuthResolverContext;
private readonly jwtHeader: string;
constructor(options: {
authHandler: AuthHandler<GcpIapResult>;
signInResolver: SignInResolver<GcpIapResult>;
tokenValidator: (token: string) => Promise<TokenPayload>;
resolverContext: AuthResolverContext;
jwtHeader?: string;
}) {
this.authHandler = options.authHandler;
this.signInResolver = options.signInResolver;
this.tokenValidator = options.tokenValidator;
this.resolverContext = options.resolverContext;
this.jwtHeader = options?.jwtHeader || DEFAULT_IAP_JWT_HEADER;
}
async start() {}
async frameHandler() {}
async refresh(req: express.Request, res: express.Response): Promise<void> {
const result = await parseRequestToken(
req.header(this.jwtHeader),
this.tokenValidator,
);
const { profile } = await this.authHandler(result, this.resolverContext);
const backstageIdentity = await this.signInResolver(
{ profile, result },
this.resolverContext,
);
const response: GcpIapResponse = {
providerInfo: { iapToken: result.iapToken },
profile,
backstageIdentity: prepareBackstageIdentityResponse(backstageIdentity),
};
res.json(response);
}
}
import { AuthHandler, SignInResolver } from '../types';
import { GcpIapResult } from './types';
/**
* Auth provider integration for Google Identity-Aware Proxy auth
@@ -104,21 +45,10 @@ export const gcpIap = createAuthProviderIntegration({
resolver: SignInResolver<GcpIapResult>;
};
}) {
return ({ config, resolverContext }) => {
const audience = config.getString('audience');
const jwtHeader = config.getOptionalString('jwtHeader');
const authHandler = options.authHandler ?? defaultAuthHandler;
const signInResolver = options.signIn.resolver;
const tokenValidator = createTokenValidator(audience);
return new GcpIapProvider({
authHandler,
signInResolver,
tokenValidator,
resolverContext,
jwtHeader,
});
};
return createProxyAuthProviderFactory({
authenticator: gcpIapAuthenticator,
profileTransform: options?.authHandler,
signInResolver: options?.signIn?.resolver,
});
},
});
@@ -14,58 +14,24 @@
* limitations under the License.
*/
import { JsonValue } from '@backstage/types';
import { AuthResponse } from '../types';
/**
* The header name used by the IAP.
*/
export const DEFAULT_IAP_JWT_HEADER = 'x-goog-iap-jwt-assertion';
import {
GcpIapTokenInfo as _GcpIapTokenInfo,
GcpIapResult as _GcpIapResult,
} from '@backstage/plugin-auth-backend-module-gcp-iap-provider';
/**
* The data extracted from an IAP token.
*
* @public
* @deprecated import from `@backstage/plugin-auth-backend-module-gcp-iap-provider` instead
*/
export type GcpIapTokenInfo = {
/**
* The unique, stable identifier for the user.
*/
sub: string;
/**
* User email address.
*/
email: string;
/**
* Other fields.
*/
[key: string]: JsonValue;
};
export type GcpIapTokenInfo = _GcpIapTokenInfo;
/**
* The result of the initial auth challenge. This is the input to the auth
* callbacks.
*
* @public
* @deprecated import from `@backstage/plugin-auth-backend-module-gcp-iap-provider` instead
*/
export type GcpIapResult = {
/**
* The data extracted from the IAP token header.
*/
iapToken: GcpIapTokenInfo;
};
/**
* The provider info to return to the frontend.
*/
export type GcpIapProviderInfo = {
/**
* The data extracted from the IAP token header.
*/
iapToken: GcpIapTokenInfo;
};
/**
* The shape of the response to return to callers.
*/
export type GcpIapResponse = AuthResponse<GcpIapProviderInfo>;
export type GcpIapResult = _GcpIapResult;
@@ -14,74 +14,57 @@
* limitations under the License.
*/
import { GoogleAuthProvider } from './provider';
import * as helpers from '../../lib/passport/PassportStrategyHelper';
import { OAuthResult } from '../../lib/oauth';
import { AuthResolverContext } from '../types';
import { googleAuthenticator } from '@backstage/plugin-auth-backend-module-google-provider';
import { createOAuthProviderFactory } from '@backstage/plugin-auth-node';
import { google } from './provider';
jest.mock('../../lib/passport/PassportStrategyHelper', () => {
return {
executeFrameHandlerStrategy: jest.fn(),
executeRefreshTokenStrategy: jest.fn(),
executeFetchUserProfileStrategy: jest.fn(),
};
});
const mockFrameHandler = jest.spyOn(
helpers,
'executeFrameHandlerStrategy',
) as unknown as jest.MockedFunction<
() => Promise<{ result: OAuthResult; privateInfo: any }>
>;
jest.mock('@backstage/plugin-auth-node', () => ({
...jest.requireActual('@backstage/plugin-auth-node'),
createOAuthProviderFactory: jest.fn(() => 'provider-factory'),
}));
describe('createGoogleProvider', () => {
it('should auth', async () => {
const provider = new GoogleAuthProvider({
resolverContext: {} as AuthResolverContext,
authHandler: async ({ fullProfile }) => ({
profile: {
email: fullProfile.emails![0]!.value,
displayName: fullProfile.displayName,
picture: 'http://google.com/lols',
},
}),
clientId: 'mock',
clientSecret: 'mock',
callbackUrl: 'mock',
});
afterEach(() => jest.clearAllMocks());
mockFrameHandler.mockResolvedValueOnce({
result: {
fullProfile: {
emails: [{ value: 'conrad@example.com' }],
displayName: 'Conrad',
id: 'conrad',
provider: 'google',
},
params: {
id_token: 'idToken',
scope: 'scope',
expires_in: 123,
},
accessToken: 'accessToken',
},
privateInfo: {
refreshToken: 'wacka',
},
it('should be created', async () => {
expect(google.create()).toBe('provider-factory');
expect(createOAuthProviderFactory).toHaveBeenCalledWith({
authenticator: googleAuthenticator,
});
const { response } = await provider.handler({} as any);
expect(response).toEqual({
providerInfo: {
accessToken: 'accessToken',
expiresInSeconds: 123,
idToken: 'idToken',
scope: 'scope',
},
profile: {
email: 'conrad@example.com',
displayName: 'Conrad',
picture: 'http://google.com/lols',
},
});
it('should be created with sign-in resolver', async () => {
expect(google.create({ signIn: { resolver: jest.fn() } })).toBe(
'provider-factory',
);
expect(createOAuthProviderFactory).toHaveBeenCalledWith({
authenticator: googleAuthenticator,
signInResolver: expect.any(Function),
});
});
it('should be created with sign-in resolver and auth handler', async () => {
expect(
google.create({
signIn: { resolver: jest.fn() },
authHandler: jest.fn(),
}),
).toBe('provider-factory');
expect(createOAuthProviderFactory).toHaveBeenCalledWith({
authenticator: googleAuthenticator,
signInResolver: expect.any(Function),
profileTransform: expect.any(Function),
});
});
it('should have resolvers', () => {
expect(google.resolvers).toEqual({
emailLocalPartMatchingUserEntityName: expect.any(Function),
emailMatchingUserEntityAnnotation: expect.any(Function),
emailMatchingUserEntityProfileEmail: expect.any(Function),
});
});
});
@@ -14,166 +14,22 @@
* limitations under the License.
*/
import express from 'express';
import passport from 'passport';
import { OAuth2Client } from 'google-auth-library';
import { Strategy as GoogleStrategy } from 'passport-google-oauth20';
import {
encodeState,
OAuthAdapter,
OAuthEnvironmentHandler,
OAuthHandlers,
OAuthProviderOptions,
OAuthRefreshRequest,
OAuthResponse,
OAuthResult,
OAuthStartRequest,
OAuthLogoutRequest,
} from '../../lib/oauth';
googleAuthenticator,
googleSignInResolvers,
} from '@backstage/plugin-auth-backend-module-google-provider';
import {
executeFetchUserProfileStrategy,
executeFrameHandlerStrategy,
executeRedirectStrategy,
executeRefreshTokenStrategy,
makeProfileInfo,
PassportDoneCallback,
} from '../../lib/passport';
commonSignInResolvers,
createOAuthProviderFactory,
} from '@backstage/plugin-auth-node';
import {
AuthHandler,
AuthResolverContext,
OAuthStartResponse,
SignInResolver,
} from '../types';
adaptLegacyOAuthHandler,
adaptLegacyOAuthSignInResolver,
adaptOAuthSignInResolverToLegacy,
} from '../../lib/legacy';
import { OAuthResult } from '../../lib/oauth';
import { createAuthProviderIntegration } from '../createAuthProviderIntegration';
import {
commonByEmailLocalPartResolver,
commonByEmailResolver,
} from '../resolvers';
type PrivateInfo = {
refreshToken: string;
};
type Options = OAuthProviderOptions & {
signInResolver?: SignInResolver<OAuthResult>;
authHandler: AuthHandler<OAuthResult>;
resolverContext: AuthResolverContext;
};
export class GoogleAuthProvider implements OAuthHandlers {
private readonly strategy: GoogleStrategy;
private readonly signInResolver?: SignInResolver<OAuthResult>;
private readonly authHandler: AuthHandler<OAuthResult>;
private readonly resolverContext: AuthResolverContext;
constructor(options: Options) {
this.authHandler = options.authHandler;
this.signInResolver = options.signInResolver;
this.resolverContext = options.resolverContext;
this.strategy = new GoogleStrategy(
{
clientID: options.clientId,
clientSecret: options.clientSecret,
callbackURL: options.callbackUrl,
passReqToCallback: false,
},
(
accessToken: any,
refreshToken: any,
params: any,
fullProfile: passport.Profile,
done: PassportDoneCallback<OAuthResult, PrivateInfo>,
) => {
done(
undefined,
{
fullProfile,
params,
accessToken,
refreshToken,
},
{
refreshToken,
},
);
},
);
}
async start(req: OAuthStartRequest): Promise<OAuthStartResponse> {
return await executeRedirectStrategy(req, this.strategy, {
accessType: 'offline',
prompt: 'consent',
scope: req.scope,
state: encodeState(req.state),
});
}
async handler(req: express.Request) {
const { result, privateInfo } = await executeFrameHandlerStrategy<
OAuthResult,
PrivateInfo
>(req, this.strategy);
return {
response: await this.handleResult(result),
refreshToken: privateInfo.refreshToken,
};
}
async logout(req: OAuthLogoutRequest) {
const oauthClient = new OAuth2Client();
await oauthClient.revokeToken(req.refreshToken);
}
async refresh(req: OAuthRefreshRequest) {
const { accessToken, refreshToken, params } =
await executeRefreshTokenStrategy(
this.strategy,
req.refreshToken,
req.scope,
);
const fullProfile = await executeFetchUserProfileStrategy(
this.strategy,
accessToken,
);
return {
response: await this.handleResult({
fullProfile,
params,
accessToken,
}),
refreshToken,
};
}
private async handleResult(result: OAuthResult) {
const { profile } = await this.authHandler(result, this.resolverContext);
const response: OAuthResponse = {
providerInfo: {
idToken: result.params.id_token,
accessToken: result.accessToken,
scope: result.params.scope,
expiresInSeconds: result.params.expires_in,
},
profile,
};
if (this.signInResolver) {
response.backstageIdentity = await this.signInResolver(
{
result,
profile,
},
this.resolverContext,
);
}
return response;
}
}
import { AuthHandler, SignInResolver } from '../types';
/**
* Auth provider integration for Google auth
@@ -198,62 +54,18 @@ export const google = createAuthProviderIntegration({
resolver: SignInResolver<OAuthResult>;
};
}) {
return ({ providerId, globalConfig, config, resolverContext }) =>
OAuthEnvironmentHandler.mapConfig(config, envConfig => {
const clientId = envConfig.getString('clientId');
const clientSecret = envConfig.getString('clientSecret');
const customCallbackUrl = envConfig.getOptionalString('callbackUrl');
const callbackUrl =
customCallbackUrl ||
`${globalConfig.baseUrl}/${providerId}/handler/frame`;
const authHandler: AuthHandler<OAuthResult> = options?.authHandler
? options.authHandler
: async ({ fullProfile, params }) => ({
profile: makeProfileInfo(fullProfile, params.id_token),
});
const provider = new GoogleAuthProvider({
clientId,
clientSecret,
callbackUrl,
signInResolver: options?.signIn?.resolver,
authHandler,
resolverContext,
});
return OAuthAdapter.fromConfig(globalConfig, provider, {
providerId,
callbackUrl,
});
});
},
resolvers: {
/**
* Looks up the user by matching their email local part to the entity name.
*/
emailLocalPartMatchingUserEntityName: () => commonByEmailLocalPartResolver,
/**
* Looks up the user by matching their email to the entity email.
*/
emailMatchingUserEntityProfileEmail: () => commonByEmailResolver,
/**
* Looks up the user by matching their email to the `google.com/email` annotation.
*/
emailMatchingUserEntityAnnotation(): SignInResolver<OAuthResult> {
return async (info, ctx) => {
const { profile } = info;
if (!profile.email) {
throw new Error('Google profile contained no email');
}
return ctx.signInWithCatalogUser({
annotations: {
'google.com/email': profile.email,
},
});
};
},
return createOAuthProviderFactory({
authenticator: googleAuthenticator,
profileTransform: adaptLegacyOAuthHandler(options?.authHandler),
signInResolver: adaptLegacyOAuthSignInResolver(options?.signIn?.resolver),
});
},
resolvers: adaptOAuthSignInResolverToLegacy({
emailLocalPartMatchingUserEntityName:
commonSignInResolvers.emailLocalPartMatchingUserEntityName(),
emailMatchingUserEntityProfileEmail:
commonSignInResolvers.emailMatchingUserEntityProfileEmail(),
emailMatchingUserEntityAnnotation:
googleSignInResolvers.emailMatchingUserEntityAnnotation(),
}),
});
@@ -45,6 +45,9 @@ describe('MicrosoftAuthProvider', () => {
},
})({
providerId: 'microsoft',
baseUrl: 'http://backstage.test/api/auth',
appUrl: 'http://backstage.test',
isOriginAllowed: _ => true,
globalConfig: {
baseUrl: 'http://backstage.test/api/auth',
appUrl: 'http://backstage.test',
@@ -47,7 +47,7 @@ import {
commonByEmailLocalPartResolver,
commonByEmailResolver,
} from '../resolvers';
import { Logger } from 'winston';
import { LoggerService } from '@backstage/backend-plugin-api';
import fetch from 'node-fetch';
import { decodeJwt } from 'jose';
import { Profile as PassportProfile } from 'passport';
@@ -60,7 +60,7 @@ type PrivateInfo = {
type Options = OAuthProviderOptions & {
signInResolver?: SignInResolver<OAuthResult>;
authHandler: AuthHandler<OAuthResult>;
logger: Logger;
logger: LoggerService;
resolverContext: AuthResolverContext;
authorizationUrl?: string;
tokenUrl?: string;
@@ -70,7 +70,7 @@ export class MicrosoftAuthProvider implements OAuthHandlers {
private readonly _strategy: MicrosoftStrategy;
private readonly signInResolver?: SignInResolver<OAuthResult>;
private readonly authHandler: AuthHandler<OAuthResult>;
private readonly logger: Logger;
private readonly logger: LoggerService;
private readonly resolverContext: AuthResolverContext;
constructor(options: Options) {
@@ -22,7 +22,7 @@ jest.mock('@backstage/catalog-client');
import { AuthenticationError } from '@backstage/errors';
import express from 'express';
import * as jose from 'jose';
import { Logger } from 'winston';
import { LoggerService } from '@backstage/backend-plugin-api';
import { AuthHandler, AuthResolverContext, SignInResolver } from '../types';
import {
oauth2Proxy,
@@ -36,7 +36,7 @@ describe('Oauth2ProxyAuthProvider', () => {
'eyblob.eyJzdWIiOiJ1c2VyOmRlZmF1bHQvamltbXltYXJrdW0iLCJlbnQiOlsidXNlcjpkZWZhdWx0L2ppbW15bWFya3VtIl19.eyblob';
let provider: Oauth2ProxyAuthProvider<any>;
let logger: jest.Mocked<Logger>;
let logger: jest.Mocked<LoggerService>;
let signInResolver: jest.MockedFunction<
SignInResolver<OAuth2ProxyResult<any>>
>;
@@ -53,7 +53,7 @@ describe('Oauth2ProxyAuthProvider', () => {
>;
authHandler = jest.fn();
signInResolver = jest.fn();
logger = { error: jest.fn() } as unknown as jest.Mocked<Logger>;
logger = { error: jest.fn() } as unknown as jest.Mocked<LoggerService>;
mockResponse = {
status: jest.fn(),
@@ -1,41 +0,0 @@
/*
* Copyright 2020 The Backstage Authors
*
* 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 { prepareBackstageIdentityResponse } from './prepareBackstageIdentityResponse';
function mkToken(payload: unknown) {
return `a.${Buffer.from(JSON.stringify(payload), 'utf8').toString(
'base64',
)}.z`;
}
describe('prepareBackstageIdentityResponse', () => {
it('parses a complete token to determine the identity', () => {
const token = mkToken({ sub: 'k:ns/n', ent: ['k:ns/o'] });
expect(
prepareBackstageIdentityResponse({
token,
}),
).toEqual({
token,
identity: {
type: 'user',
userEntityRef: 'k:ns/n',
ownershipEntityRefs: ['k:ns/o'],
},
});
});
});
@@ -14,34 +14,11 @@
* limitations under the License.
*/
import {
BackstageIdentityResponse,
BackstageSignInResult,
} from '@backstage/plugin-auth-node';
function parseJwtPayload(token: string) {
const [_header, payload, _signature] = token.split('.');
return JSON.parse(Buffer.from(payload, 'base64').toString());
}
import { prepareBackstageIdentityResponse as _prepareBackstageIdentityResponse } from '@backstage/plugin-auth-node';
/**
* Parses a Backstage-issued token and decorates the
* {@link @backstage/plugin-auth-node#BackstageIdentityResponse} with identity information sourced from the
* token.
*
* @public
* @deprecated import from `@backstage/plugin-auth-node` instead
*/
export function prepareBackstageIdentityResponse(
result: BackstageSignInResult,
): BackstageIdentityResponse {
const { sub, ent } = parseJwtPayload(result.token);
return {
...result,
identity: {
type: 'user',
userEntityRef: sub,
ownershipEntityRefs: ent ?? [],
},
};
}
export const prepareBackstageIdentityResponse =
_prepareBackstageIdentityResponse;
+52 -217
View File
@@ -14,126 +14,42 @@
* limitations under the License.
*/
import { GetEntitiesRequest } from '@backstage/catalog-client';
import { Entity } from '@backstage/catalog-model';
import { Config } from '@backstage/config';
import {
BackstageIdentityResponse,
BackstageSignInResult,
AuthProviderConfig as _AuthProviderConfig,
AuthProviderRouteHandlers as _AuthProviderRouteHandlers,
AuthProviderFactory as _AuthProviderFactory,
AuthResolverCatalogUserQuery as _AuthResolverCatalogUserQuery,
AuthResolverContext as _AuthResolverContext,
ClientAuthResponse as _ClientAuthResponse,
CookieConfigurer as _CookieConfigurer,
ProfileInfo as _ProfileInfo,
SignInInfo as _SignInInfo,
SignInResolver as _SignInResolver,
} from '@backstage/plugin-auth-node';
import express from 'express';
import { Logger } from 'winston';
import { TokenParams } from '../identity/types';
import { OAuthStartRequest } from '../lib/oauth/types';
/**
* A query for a single user in the catalog.
*
* If `entityRef` is used, the default kind is `'User'`.
*
* If `annotations` are used, all annotations must be present and
* match the provided value exactly. Only entities of kind `'User'` will be considered.
*
* If `filter` are used they are passed on as they are to the `CatalogApi`.
*
* Regardless of the query method, the query must match exactly one entity
* in the catalog, or an error will be thrown.
*
* @public
* @deprecated import from `@backstage/plugin-auth-node` instead
*/
export type AuthResolverCatalogUserQuery =
| {
entityRef:
| string
| {
kind?: string;
namespace?: string;
name: string;
};
}
| {
annotations: Record<string, string>;
}
| {
filter: Exclude<GetEntitiesRequest['filter'], undefined>;
};
export type AuthResolverCatalogUserQuery = _AuthResolverCatalogUserQuery;
/**
* The context that is used for auth processing.
*
* @public
* @deprecated import from `@backstage/plugin-auth-node` instead
*/
export type AuthResolverContext = {
/**
* Issues a Backstage token using the provided parameters.
*/
issueToken(params: TokenParams): Promise<{ token: string }>;
/**
* Finds a single user in the catalog using the provided query.
*
* See {@link AuthResolverCatalogUserQuery} for details.
*/
findCatalogUser(
query: AuthResolverCatalogUserQuery,
): Promise<{ entity: Entity }>;
/**
* Finds a single user in the catalog using the provided query, and then
* issues an identity for that user using default ownership resolution.
*
* See {@link AuthResolverCatalogUserQuery} for details.
*/
signInWithCatalogUser(
query: AuthResolverCatalogUserQuery,
): Promise<BackstageSignInResult>;
};
export type AuthResolverContext = _AuthResolverContext;
/**
* The callback used to resolve the cookie configuration for auth providers that use cookies.
* @public
* @deprecated import from `@backstage/plugin-auth-node` instead
*/
export type CookieConfigurer = (ctx: {
/** ID of the auth provider that this configuration applies to */
providerId: string;
/** The externally reachable base URL of the auth-backend plugin */
baseUrl: string;
/** The configured callback URL of the auth provider */
callbackUrl: string;
/** The origin URL of the app */
appOrigin: string;
}) => {
domain: string;
path: string;
secure: boolean;
sameSite?: 'none' | 'lax' | 'strict';
};
export type CookieConfigurer = _CookieConfigurer;
/** @public */
export type AuthProviderConfig = {
/**
* The protocol://domain[:port] where the app is hosted. This is used to construct the
* callbackURL to redirect to once the user signs in to the auth provider.
*/
baseUrl: string;
/**
* The base URL of the app as provided by app.baseUrl
*/
appUrl: string;
/**
* A function that is called to check whether an origin is allowed to receive the authentication result.
*/
isOriginAllowed: (origin: string) => boolean;
/**
* The function used to resolve cookie configuration based on the auth provider options.
*/
cookieConfigurer?: CookieConfigurer;
};
/** @public */
/**
* @public
* @deprecated Use `createOAuthAuthenticator` from `@backstage/plugin-auth-node` instead
*/
export type OAuthStartResponse = {
/**
* URL to redirect to
@@ -146,138 +62,53 @@ export type OAuthStartResponse = {
};
/**
* Any Auth provider needs to implement this interface which handles the routes in the
* auth backend. Any auth API requests from the frontend reaches these methods.
*
* The routes in the auth backend API are tied to these methods like below
*
* `/auth/[provider]/start -> start`
* `/auth/[provider]/handler/frame -> frameHandler`
* `/auth/[provider]/refresh -> refresh`
* `/auth/[provider]/logout -> logout`
*
* @public
* @deprecated import from `@backstage/plugin-auth-node` instead
*/
export interface AuthProviderRouteHandlers {
/**
* Handles the start route of the API. This initiates a sign in request with an auth provider.
*
* Request
* - scopes for the auth request (Optional)
* Response
* - redirect to the auth provider for the user to sign in or consent.
* - sets a nonce cookie and also pass the nonce as 'state' query parameter in the redirect request
*/
start(req: express.Request, res: express.Response): Promise<void>;
/**
* Once the user signs in or consents in the OAuth screen, the auth provider redirects to the
* callbackURL which is handled by this method.
*
* Request
* - to contain a nonce cookie and a 'state' query parameter
* Response
* - postMessage to the window with a payload that contains accessToken, expiryInSeconds?, idToken? and scope.
* - sets a refresh token cookie if the auth provider supports refresh tokens
*/
frameHandler(req: express.Request, res: express.Response): Promise<void>;
/**
* (Optional) If the auth provider supports refresh tokens then this method handles
* requests to get a new access token.
*
* Request
* - to contain a refresh token cookie and scope (Optional) query parameter.
* Response
* - payload with accessToken, expiryInSeconds?, idToken?, scope and user profile information.
*/
refresh?(req: express.Request, res: express.Response): Promise<void>;
/**
* (Optional) Handles sign out requests
*
* Response
* - removes the refresh token cookie
*/
logout?(req: express.Request, res: express.Response): Promise<void>;
}
/** @public */
export type AuthProviderFactory = (options: {
providerId: string;
globalConfig: AuthProviderConfig;
config: Config;
logger: Logger;
resolverContext: AuthResolverContext;
}) => AuthProviderRouteHandlers;
/** @public */
export type AuthResponse<ProviderInfo> = {
providerInfo: ProviderInfo;
profile: ProfileInfo;
backstageIdentity?: BackstageIdentityResponse;
};
export type AuthProviderConfig = _AuthProviderConfig;
/**
* Used to display login information to user, i.e. sidebar popup.
*
* It is also temporarily used as the profile of the signed-in user's Backstage
* identity, but we want to replace that with data from identity and/org catalog
* service
*
* @public
* @deprecated import from `@backstage/plugin-auth-node` instead
*/
export type ProfileInfo = {
/**
* Email ID of the signed in user.
*/
email?: string;
/**
* Display name that can be presented to the signed in user.
*/
displayName?: string;
/**
* URL to an image that can be used as the display image or avatar of the
* signed in user.
*/
picture?: string;
};
export type AuthProviderRouteHandlers = _AuthProviderRouteHandlers;
/**
* Type of sign in information context. Includes the profile information and
* authentication result which contains auth related information.
*
* @public
* @deprecated import from `@backstage/plugin-auth-node` instead
*/
export type SignInInfo<TAuthResult> = {
/**
* The simple profile passed down for use in the frontend.
*/
profile: ProfileInfo;
/**
* The authentication result that was received from the authentication
* provider.
*/
result: TAuthResult;
};
export type AuthProviderFactory = _AuthProviderFactory;
/**
* Describes the function which handles the result of a successful
* authentication. Must return a valid {@link @backstage/plugin-auth-node#BackstageSignInResult}.
*
* @public
* @deprecated import `ClientAuthResponse` from `@backstage/plugin-auth-node` instead
*/
export type SignInResolver<TAuthResult> = (
info: SignInInfo<TAuthResult>,
context: AuthResolverContext,
) => Promise<BackstageSignInResult>;
export type AuthResponse<TProviderInfo> = _ClientAuthResponse<TProviderInfo>;
/**
* @public
* @deprecated import from `@backstage/plugin-auth-node` instead
*/
export type ProfileInfo = _ProfileInfo;
/**
* @public
* @deprecated import from `@backstage/plugin-auth-node` instead
*/
export type SignInInfo<TAuthResult> = _SignInInfo<TAuthResult>;
/**
* @public
* @deprecated import from `@backstage/plugin-auth-node` instead
*/
export type SignInResolver<TAuthResult> = _SignInResolver<TAuthResult>;
/**
* The return type of an authentication handler. Must contain valid profile
* information.
*
* @public
* @deprecated Use `createOAuthRouteHandlers` from `@backstage/plugin-auth-node` instead
*/
export type AuthHandlerResult = { profile: ProfileInfo };
@@ -293,13 +124,17 @@ export type AuthHandlerResult = { profile: ProfileInfo };
* group of users.
*
* @public
* @deprecated Use `createOAuthRouteHandlers` from `@backstage/plugin-auth-node` instead
*/
export type AuthHandler<TAuthResult> = (
input: TAuthResult,
context: AuthResolverContext,
) => Promise<AuthHandlerResult>;
/** @public */
/**
* @public
* @deprecated Use `createOAuthRouteHandlers` from `@backstage/plugin-auth-node` instead
*/
export type StateEncoder = (
req: OAuthStartRequest,
) => Promise<{ encodedState: string }>;
+5 -2
View File
@@ -17,7 +17,7 @@
import express from 'express';
import Router from 'express-promise-router';
import cookieParser from 'cookie-parser';
import { Logger } from 'winston';
import { LoggerService } from '@backstage/backend-plugin-api';
import {
defaultAuthProviderFactories,
AuthProviderFactory,
@@ -44,7 +44,7 @@ export type ProviderFactories = { [s: string]: AuthProviderFactory };
/** @public */
export interface RouterOptions {
logger: Logger;
logger: LoggerService;
database: PluginDatabaseManager;
config: Config;
discovery: PluginEndpointDiscovery;
@@ -130,6 +130,9 @@ export async function createRouter(
try {
const provider = providerFactory({
providerId,
appUrl,
baseUrl: authUrl,
isOriginAllowed,
globalConfig: {
baseUrl: authUrl,
appUrl,
@@ -23,11 +23,11 @@ import {
} from '@backstage/backend-common';
import { Server } from 'http';
import Knex from 'knex';
import { Logger } from 'winston';
import { LoggerService } from '@backstage/backend-plugin-api';
import { createRouter } from './router';
export interface ServerOptions {
logger: Logger;
logger: LoggerService;
}
export async function startStandaloneServer(