Merge pull request #8283 from backstage/mob/identity-api
core-plugin-api: stabilize IdentityApi
This commit is contained in:
@@ -103,7 +103,7 @@ export interface AuthProviderRouteHandlers {
|
||||
export type AuthResponse<ProviderInfo> = {
|
||||
providerInfo: ProviderInfo;
|
||||
profile: ProfileInfo;
|
||||
backstageIdentity?: BackstageIdentity;
|
||||
backstageIdentity?: BackstageIdentityResponse;
|
||||
};
|
||||
|
||||
// Warning: (ae-missing-release-tag) "AwsAlbProviderOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
@@ -116,14 +116,28 @@ export type AwsAlbProviderOptions = {
|
||||
};
|
||||
};
|
||||
|
||||
// Warning: (ae-missing-release-tag) "BackstageIdentity" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export type BackstageIdentity = {
|
||||
id: string;
|
||||
idToken?: string;
|
||||
token?: string;
|
||||
// @public @deprecated
|
||||
export type BackstageIdentity = BackstageSignInResult;
|
||||
|
||||
// @public
|
||||
export interface BackstageIdentityResponse extends BackstageSignInResult {
|
||||
identity: BackstageUserIdentity;
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface BackstageSignInResult {
|
||||
// @deprecated
|
||||
entity?: Entity;
|
||||
// @deprecated
|
||||
id: string;
|
||||
token: string;
|
||||
}
|
||||
|
||||
// @public
|
||||
export type BackstageUserIdentity = {
|
||||
type: 'user';
|
||||
userEntityRef: string;
|
||||
ownershipEntityRefs: string[];
|
||||
};
|
||||
|
||||
// Warning: (ae-missing-release-tag) "BitbucketOAuthResult" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
@@ -352,7 +366,7 @@ export type GoogleProviderOptions = {
|
||||
// @public
|
||||
export class IdentityClient {
|
||||
constructor(options: { discovery: PluginEndpointDiscovery; issuer: string });
|
||||
authenticate(token: string | undefined): Promise<BackstageIdentity>;
|
||||
authenticate(token: string | undefined): Promise<BackstageIdentityResponse>;
|
||||
static getBearerToken(
|
||||
authorizationHeader: string | undefined,
|
||||
): string | undefined;
|
||||
@@ -439,7 +453,7 @@ export interface OAuthHandlers {
|
||||
// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
|
||||
// Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}'
|
||||
handler(req: express.Request): Promise<{
|
||||
response: AuthResponse<OAuthProviderInfo>;
|
||||
response: OAuthResponse;
|
||||
refreshToken?: string;
|
||||
}>;
|
||||
logout?(): Promise<void>;
|
||||
@@ -447,7 +461,7 @@ export interface OAuthHandlers {
|
||||
// Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}'
|
||||
// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
|
||||
// Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}'
|
||||
refresh?(req: OAuthRefreshRequest): Promise<AuthResponse<OAuthProviderInfo>>;
|
||||
refresh?(req: OAuthRefreshRequest): Promise<OAuthResponse>;
|
||||
// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
|
||||
// Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}'
|
||||
// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
|
||||
@@ -482,10 +496,12 @@ export type OAuthRefreshRequest = express.Request<{}> & {
|
||||
refreshToken: string;
|
||||
};
|
||||
|
||||
// Warning: (ae-missing-release-tag) "OAuthResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export type OAuthResponse = AuthResponse<OAuthProviderInfo>;
|
||||
// @public
|
||||
export type OAuthResponse = {
|
||||
profile: ProfileInfo;
|
||||
providerInfo: OAuthProviderInfo;
|
||||
backstageIdentity?: BackstageSignInResult;
|
||||
};
|
||||
|
||||
// Warning: (ae-missing-release-tag) "OAuthResult" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
@@ -542,8 +558,11 @@ export const postMessageResponse: (
|
||||
response: WebMessageResponse,
|
||||
) => void;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "ProfileInfo" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
export function prepareBackstageIdentityResponse(
|
||||
result: BackstageSignInResult,
|
||||
): BackstageIdentityResponse;
|
||||
|
||||
// @public
|
||||
export type ProfileInfo = {
|
||||
email?: string;
|
||||
@@ -628,5 +647,4 @@ export type WebMessageResponse =
|
||||
// src/providers/github/provider.d.ts:71:68 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@"
|
||||
// src/providers/github/provider.d.ts:78:5 - (ae-forgotten-export) The symbol "StateEncoder" needs to be exported by the entry point index.d.ts
|
||||
// src/providers/types.d.ts:100:5 - (ae-forgotten-export) The symbol "AuthProviderConfig" needs to be exported by the entry point index.d.ts
|
||||
// src/providers/types.d.ts:122:8 - (tsdoc-missing-deprecation-message) The @deprecated block must include a deprecation message, e.g. describing the recommended alternative
|
||||
```
|
||||
|
||||
@@ -96,7 +96,15 @@ describe('IdentityClient', () => {
|
||||
it('should accept fresh token', async () => {
|
||||
const token = await factory.issueToken({ claims: { sub: 'foo' } });
|
||||
const response = await client.authenticate(token);
|
||||
expect(response).toEqual({ id: 'foo', idToken: token });
|
||||
expect(response).toEqual({
|
||||
id: 'foo',
|
||||
token: token,
|
||||
identity: {
|
||||
ownershipEntityRefs: [],
|
||||
type: 'user',
|
||||
userEntityRef: 'foo',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw on incorrect issuer', async () => {
|
||||
@@ -159,7 +167,15 @@ describe('IdentityClient', () => {
|
||||
jest.spyOn(Date, 'now').mockImplementation(() => fixedTime);
|
||||
const token = await factory.issueToken({ claims: { sub: 'foo' } });
|
||||
const response = await client.authenticate(token);
|
||||
expect(response).toEqual({ id: 'foo', idToken: token });
|
||||
expect(response).toEqual({
|
||||
id: 'foo',
|
||||
token: token,
|
||||
identity: {
|
||||
ownershipEntityRefs: [],
|
||||
type: 'user',
|
||||
userEntityRef: 'foo',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should not be fooled by the none algorithm', async () => {
|
||||
|
||||
@@ -16,8 +16,9 @@
|
||||
|
||||
import fetch from 'node-fetch';
|
||||
import { JWK, JWT, JWKS, JSONWebKey } from 'jose';
|
||||
import { BackstageIdentity } from '../providers';
|
||||
import { PluginEndpointDiscovery } from '@backstage/backend-common';
|
||||
import { AuthenticationError } from '@backstage/errors';
|
||||
import { BackstageIdentityResponse } from '../providers/types';
|
||||
|
||||
const CLOCK_MARGIN_S = 10;
|
||||
|
||||
@@ -45,15 +46,17 @@ export class IdentityClient {
|
||||
* Returns a BackstageIdentity (user) matching the token.
|
||||
* The method throws an error if verification fails.
|
||||
*/
|
||||
async authenticate(token: string | undefined): Promise<BackstageIdentity> {
|
||||
async authenticate(
|
||||
token: string | undefined,
|
||||
): Promise<BackstageIdentityResponse> {
|
||||
// Extract token from header
|
||||
if (!token) {
|
||||
throw new Error('No token specified');
|
||||
throw new AuthenticationError('No token specified');
|
||||
}
|
||||
// Get signing key matching token
|
||||
const key = await this.getKey(token);
|
||||
if (!key) {
|
||||
throw new Error('No signing key matching token found');
|
||||
throw new AuthenticationError('No signing key matching token found');
|
||||
}
|
||||
// Verify token claims and signature
|
||||
// Note: Claims must match those set by TokenFactory when issuing tokens
|
||||
@@ -62,12 +65,21 @@ export class IdentityClient {
|
||||
algorithms: ['ES256'],
|
||||
audience: 'backstage',
|
||||
issuer: this.issuer,
|
||||
}) as { sub: string };
|
||||
}) as { sub: string; ent: string[] };
|
||||
// Verified, return the matching user as BackstageIdentity
|
||||
// TODO: Settle internal user format/properties
|
||||
const user: BackstageIdentity = {
|
||||
if (!decoded.sub) {
|
||||
throw new AuthenticationError('No user sub found in token');
|
||||
}
|
||||
|
||||
const user: BackstageIdentityResponse = {
|
||||
id: decoded.sub,
|
||||
idToken: token,
|
||||
token,
|
||||
identity: {
|
||||
type: 'user',
|
||||
userEntityRef: decoded.sub,
|
||||
ownershipEntityRefs: decoded.ent ?? [],
|
||||
},
|
||||
};
|
||||
return user;
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ export * from './providers';
|
||||
// ensuresXRequestedWith and postMessageResponse to safely handle CORS requests for login. The WebMessageResponse type in flow is used to type the response from the login-popup
|
||||
export * from './lib/flow';
|
||||
|
||||
// OAuth wrapper over a passport or a custom `startegy`.
|
||||
// OAuth wrapper over a passport or a custom `strategy`.
|
||||
export * from './lib/oauth';
|
||||
|
||||
export * from './lib/catalog';
|
||||
|
||||
@@ -51,7 +51,12 @@ describe('oauth helpers', () => {
|
||||
},
|
||||
backstageIdentity: {
|
||||
id: 'a',
|
||||
idToken: 'a.b.c',
|
||||
token: 'a.b.c',
|
||||
identity: {
|
||||
type: 'user',
|
||||
ownershipEntityRefs: [],
|
||||
userEntityRef: 'a',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -106,7 +111,12 @@ describe('oauth helpers', () => {
|
||||
},
|
||||
backstageIdentity: {
|
||||
id: 'a',
|
||||
idToken: 'a.b.c',
|
||||
token: 'a.b.c',
|
||||
identity: {
|
||||
type: 'user',
|
||||
ownershipEntityRefs: [],
|
||||
userEntityRef: 'a',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -148,7 +158,12 @@ describe('oauth helpers', () => {
|
||||
},
|
||||
backstageIdentity: {
|
||||
id: 'a',
|
||||
idToken: 'a.b.c',
|
||||
token: 'a.b.c',
|
||||
identity: {
|
||||
type: 'user',
|
||||
ownershipEntityRefs: [],
|
||||
userEntityRef: 'a',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -31,6 +31,8 @@ const mockResponseData = {
|
||||
},
|
||||
backstageIdentity: {
|
||||
id: 'foo',
|
||||
token:
|
||||
'eyblob.eyJzdWIiOiJqaW1teW1hcmt1bSIsImVudCI6WyJ1c2VyOmRlZmF1bHQvamltbXltYXJrdW0iXX0=.eyblob',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -217,7 +219,13 @@ describe('OAuthAdapter', () => {
|
||||
...mockResponseData,
|
||||
backstageIdentity: {
|
||||
id: mockResponseData.backstageIdentity.id,
|
||||
token: 'my-id-token',
|
||||
token: mockResponseData.backstageIdentity.token,
|
||||
idToken: mockResponseData.backstageIdentity.token,
|
||||
identity: {
|
||||
ownershipEntityRefs: ['user:default/jimmymarkum'],
|
||||
type: 'user',
|
||||
userEntityRef: 'jimmymarkum',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,8 +19,9 @@ import crypto from 'crypto';
|
||||
import { URL } from 'url';
|
||||
import {
|
||||
AuthProviderRouteHandlers,
|
||||
BackstageIdentity,
|
||||
AuthProviderConfig,
|
||||
BackstageIdentityResponse,
|
||||
BackstageSignInResult,
|
||||
} from '../../providers/types';
|
||||
import {
|
||||
AuthenticationError,
|
||||
@@ -37,6 +38,7 @@ import {
|
||||
OAuthRefreshRequest,
|
||||
OAuthState,
|
||||
} from './types';
|
||||
import { prepareBackstageIdentityResponse } from '../../providers/prepareBackstageIdentityResponse';
|
||||
|
||||
export const THOUSAND_DAYS_MS = 1000 * 24 * 60 * 60 * 1000;
|
||||
export const TEN_MINUTES_MS = 600 * 1000;
|
||||
@@ -150,12 +152,12 @@ export class OAuthAdapter implements AuthProviderRouteHandlers {
|
||||
this.setRefreshTokenCookie(res, refreshToken);
|
||||
}
|
||||
|
||||
await this.populateIdentity(response.backstageIdentity);
|
||||
const identity = await this.populateIdentity(response.backstageIdentity);
|
||||
|
||||
// post message back to popup if successful
|
||||
return postMessageResponse(res, appOrigin, {
|
||||
type: 'authorization_response',
|
||||
response,
|
||||
response: { ...response, backstageIdentity: identity },
|
||||
});
|
||||
} catch (error) {
|
||||
const { name, message } = isError(error)
|
||||
@@ -209,7 +211,9 @@ export class OAuthAdapter implements AuthProviderRouteHandlers {
|
||||
forwardReq as OAuthRefreshRequest,
|
||||
);
|
||||
|
||||
await this.populateIdentity(response.backstageIdentity);
|
||||
const backstageIdentity = await this.populateIdentity(
|
||||
response.backstageIdentity,
|
||||
);
|
||||
|
||||
if (
|
||||
response.providerInfo.refreshToken &&
|
||||
@@ -218,7 +222,7 @@ export class OAuthAdapter implements AuthProviderRouteHandlers {
|
||||
this.setRefreshTokenCookie(res, response.providerInfo.refreshToken);
|
||||
}
|
||||
|
||||
res.status(200).json(response);
|
||||
res.status(200).json({ ...response, backstageIdentity });
|
||||
} catch (error) {
|
||||
throw new AuthenticationError('Refresh failed', error);
|
||||
}
|
||||
@@ -228,18 +232,22 @@ export class OAuthAdapter implements AuthProviderRouteHandlers {
|
||||
* If the response from the OAuth provider includes a Backstage identity, we
|
||||
* make sure it's populated with all the information we can derive from the user ID.
|
||||
*/
|
||||
private async populateIdentity(identity?: BackstageIdentity) {
|
||||
private async populateIdentity(
|
||||
identity?: BackstageSignInResult,
|
||||
): Promise<BackstageIdentityResponse | undefined> {
|
||||
if (!identity) {
|
||||
return;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!(identity.token || identity.idToken)) {
|
||||
identity.token = await this.options.tokenIssuer.issueToken({
|
||||
claims: { sub: identity.id },
|
||||
});
|
||||
} else if (!identity.token && identity.idToken) {
|
||||
identity.token = identity.idToken;
|
||||
if (identity.token) {
|
||||
return prepareBackstageIdentityResponse(identity);
|
||||
}
|
||||
|
||||
const token = await this.options.tokenIssuer.issueToken({
|
||||
claims: { sub: identity.id },
|
||||
});
|
||||
|
||||
return prepareBackstageIdentityResponse({ ...identity, token });
|
||||
}
|
||||
|
||||
private setNonceCookie = (res: express.Response, nonce: string) => {
|
||||
|
||||
@@ -16,7 +16,11 @@
|
||||
|
||||
import express from 'express';
|
||||
import { Profile as PassportProfile } from 'passport';
|
||||
import { AuthResponse, RedirectInfo } from '../../providers/types';
|
||||
import {
|
||||
RedirectInfo,
|
||||
BackstageSignInResult,
|
||||
ProfileInfo,
|
||||
} from '../../providers/types';
|
||||
|
||||
/**
|
||||
* Common options for passport.js-based OAuth providers
|
||||
@@ -47,7 +51,16 @@ export type OAuthResult = {
|
||||
refreshToken?: string;
|
||||
};
|
||||
|
||||
export type OAuthResponse = AuthResponse<OAuthProviderInfo>;
|
||||
/**
|
||||
* The expected response from an OAuth flow.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type OAuthResponse = {
|
||||
profile: ProfileInfo;
|
||||
providerInfo: OAuthProviderInfo;
|
||||
backstageIdentity?: BackstageSignInResult;
|
||||
};
|
||||
|
||||
export type OAuthProviderInfo = {
|
||||
/**
|
||||
@@ -108,7 +121,7 @@ export interface OAuthHandlers {
|
||||
* @param {express.Request} req
|
||||
*/
|
||||
handler(req: express.Request): Promise<{
|
||||
response: AuthResponse<OAuthProviderInfo>;
|
||||
response: OAuthResponse;
|
||||
refreshToken?: string;
|
||||
}>;
|
||||
|
||||
@@ -117,7 +130,7 @@ export interface OAuthHandlers {
|
||||
* @param {string} refreshToken
|
||||
* @param {string} scope
|
||||
*/
|
||||
refresh?(req: OAuthRefreshRequest): Promise<AuthResponse<OAuthProviderInfo>>;
|
||||
refresh?(req: OAuthRefreshRequest): Promise<OAuthResponse>;
|
||||
|
||||
/**
|
||||
* (Optional) Sign out of the auth provider.
|
||||
|
||||
@@ -151,7 +151,7 @@ export class Auth0AuthProvider implements OAuthHandlers {
|
||||
|
||||
const id = profile.email.split('@')[0];
|
||||
|
||||
return { ...response, backstageIdentity: { id } };
|
||||
return { ...response, backstageIdentity: { id, token: '' } };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -122,7 +122,11 @@ describe('AwsAlbAuthProvider', () => {
|
||||
profile: makeProfileInfo(fullProfile),
|
||||
}),
|
||||
signInResolver: async () => {
|
||||
return { id: 'user.name', token: 'TOKEN' };
|
||||
return {
|
||||
id: 'user.name',
|
||||
token:
|
||||
'eyblob.eyJzdWIiOiJqaW1teW1hcmt1bSIsImVudCI6WyJ1c2VyOmRlZmF1bHQvamltbXltYXJrdW0iXX0=.eyblob',
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -133,7 +137,15 @@ describe('AwsAlbAuthProvider', () => {
|
||||
expect(mockResponse.json).toHaveBeenCalledWith({
|
||||
backstageIdentity: {
|
||||
id: 'user.name',
|
||||
token: 'TOKEN',
|
||||
token:
|
||||
'eyblob.eyJzdWIiOiJqaW1teW1hcmt1bSIsImVudCI6WyJ1c2VyOmRlZmF1bHQvamltbXltYXJrdW0iXX0=.eyblob',
|
||||
idToken:
|
||||
'eyblob.eyJzdWIiOiJqaW1teW1hcmt1bSIsImVudCI6WyJ1c2VyOmRlZmF1bHQvamltbXltYXJrdW0iXX0=.eyblob',
|
||||
identity: {
|
||||
ownershipEntityRefs: ['user:default/jimmymarkum'],
|
||||
type: 'user',
|
||||
userEntityRef: 'jimmymarkum',
|
||||
},
|
||||
},
|
||||
profile: {
|
||||
displayName: 'User Name',
|
||||
|
||||
@@ -32,6 +32,7 @@ import { CatalogIdentityClient } from '../../lib/catalog';
|
||||
import { Profile as PassportProfile } from 'passport';
|
||||
import { makeProfileInfo } from '../../lib/passport';
|
||||
import { AuthenticationError } from '@backstage/errors';
|
||||
import { prepareBackstageIdentityResponse } from '../prepareBackstageIdentityResponse';
|
||||
|
||||
export const ALB_JWT_HEADER = 'x-amzn-oidc-data';
|
||||
export const ALB_ACCESSTOKEN_HEADER = 'x-amzn-oidc-accesstoken';
|
||||
@@ -198,7 +199,7 @@ export class AwsAlbAuthProvider implements AuthProviderRouteHandlers {
|
||||
accessToken: result.accessToken,
|
||||
expiresInSeconds: result.expiresInSeconds,
|
||||
},
|
||||
backstageIdentity,
|
||||
backstageIdentity: prepareBackstageIdentityResponse(backstageIdentity),
|
||||
profile,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -38,4 +38,13 @@ export type {
|
||||
|
||||
// These types are needed for a postMessage from the login pop-up
|
||||
// to the frontend
|
||||
export type { AuthResponse, BackstageIdentity, ProfileInfo } from './types';
|
||||
export type {
|
||||
AuthResponse,
|
||||
BackstageIdentity,
|
||||
BackstageUserIdentity,
|
||||
BackstageIdentityResponse,
|
||||
BackstageSignInResult,
|
||||
ProfileInfo,
|
||||
} from './types';
|
||||
|
||||
export { prepareBackstageIdentityResponse } from './prepareBackstageIdentityResponse';
|
||||
|
||||
@@ -205,6 +205,7 @@ export class OidcAuthProvider implements OAuthHandlers {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,7 +148,7 @@ export class OneLoginProvider implements OAuthHandlers {
|
||||
|
||||
const id = profile.email.split('@')[0];
|
||||
|
||||
return { ...response, backstageIdentity: { id } };
|
||||
return { ...response, backstageIdentity: { id, token: '' } };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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 { BackstageIdentityResponse, BackstageSignInResult } from './types';
|
||||
|
||||
function parseJwtPayload(token: string) {
|
||||
const [_header, payload, _signature] = token.split('.');
|
||||
return JSON.parse(Buffer.from(payload, 'base64').toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses token and decorates the BackstageIdentityResponse with identity information sourced from the token
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function prepareBackstageIdentityResponse(
|
||||
result: BackstageSignInResult,
|
||||
): BackstageIdentityResponse {
|
||||
const { sub, ent } = parseJwtPayload(result.token);
|
||||
return {
|
||||
...{
|
||||
// TODO: idToken is for backwards compatibility and can be removed in the future
|
||||
idToken: result.token,
|
||||
...result,
|
||||
},
|
||||
identity: {
|
||||
type: 'user',
|
||||
userEntityRef: sub,
|
||||
ownershipEntityRefs: ent ?? [],
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -38,6 +38,7 @@ import { TokenIssuer } from '../../identity/types';
|
||||
import { isError } from '@backstage/errors';
|
||||
import { CatalogIdentityClient } from '../../lib/catalog';
|
||||
import { Logger } from 'winston';
|
||||
import { prepareBackstageIdentityResponse } from '../prepareBackstageIdentityResponse';
|
||||
|
||||
/** @public */
|
||||
export type SamlAuthResult = {
|
||||
@@ -105,7 +106,7 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers {
|
||||
};
|
||||
|
||||
if (this.signInResolver) {
|
||||
response.backstageIdentity = await this.signInResolver(
|
||||
const signInResponse = await this.signInResolver(
|
||||
{
|
||||
result,
|
||||
profile,
|
||||
@@ -116,6 +117,9 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers {
|
||||
logger: this.logger,
|
||||
},
|
||||
);
|
||||
|
||||
response.backstageIdentity =
|
||||
prepareBackstageIdentityResponse(signInResponse);
|
||||
}
|
||||
|
||||
return postMessageResponse(res, this.appUrl, {
|
||||
|
||||
@@ -137,42 +137,92 @@ export type AuthProviderFactory = (
|
||||
export type AuthResponse<ProviderInfo> = {
|
||||
providerInfo: ProviderInfo;
|
||||
profile: ProfileInfo;
|
||||
backstageIdentity?: BackstageIdentity;
|
||||
backstageIdentity?: BackstageIdentityResponse;
|
||||
};
|
||||
|
||||
export type BackstageIdentity = {
|
||||
/**
|
||||
* User identity information within Backstage.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type BackstageUserIdentity = {
|
||||
/**
|
||||
* The type of identity that this structure represents. In the frontend app
|
||||
* this will currently always be 'user'.
|
||||
*/
|
||||
type: 'user';
|
||||
|
||||
/**
|
||||
* The entityRef of the user in the catalog.
|
||||
* For example User:default/sandra
|
||||
*/
|
||||
userEntityRef: string;
|
||||
|
||||
/**
|
||||
* The user and group entities that the user claims ownership through
|
||||
*/
|
||||
ownershipEntityRefs: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* A representation of a successful Backstage sign-in.
|
||||
*
|
||||
* Compared to the {@link BackstageIdentityResponse} this type omits
|
||||
* the decoded identity information embedded in the token.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface BackstageSignInResult {
|
||||
/**
|
||||
* An opaque ID that uniquely identifies the user within Backstage.
|
||||
*
|
||||
* This is typically the same as the user entity `metadata.name`.
|
||||
*
|
||||
* @deprecated Use the `identity` field instead
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* This is deprecated, use `token` instead.
|
||||
* @deprecated
|
||||
*/
|
||||
idToken?: string;
|
||||
|
||||
/**
|
||||
* The token used to authenticate the user within Backstage.
|
||||
*/
|
||||
token?: string;
|
||||
|
||||
/**
|
||||
* The entity that the user is represented by within Backstage.
|
||||
*
|
||||
* This entity may or may not exist within the Catalog, and it can be used
|
||||
* to read and store additional metadata about the user.
|
||||
*
|
||||
* @deprecated Use the `identity` field instead.
|
||||
*/
|
||||
entity?: Entity;
|
||||
};
|
||||
|
||||
/**
|
||||
* The token used to authenticate the user within Backstage.
|
||||
*/
|
||||
token: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The old exported symbol for {@link BackstageSignInResult}.
|
||||
* @public
|
||||
* @deprecated Use the `BackstageSignInResult` type instead.
|
||||
*/
|
||||
export type BackstageIdentity = BackstageSignInResult;
|
||||
|
||||
/**
|
||||
* Response object containing the {@link BackstageUserIdentity} and the token from the authentication provider.
|
||||
* @public
|
||||
*/
|
||||
export interface BackstageIdentityResponse extends BackstageSignInResult {
|
||||
/**
|
||||
* A plaintext description of the identity that is encapsulated within the token.
|
||||
*/
|
||||
identity: BackstageUserIdentity;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
export type ProfileInfo = {
|
||||
/**
|
||||
@@ -209,7 +259,7 @@ export type SignInResolver<AuthResult> = (
|
||||
catalogIdentityClient: CatalogIdentityClient;
|
||||
logger: Logger;
|
||||
},
|
||||
) => Promise<BackstageIdentity>;
|
||||
) => Promise<BackstageSignInResult>;
|
||||
|
||||
export type AuthHandlerResult = { profile: ProfileInfo };
|
||||
|
||||
|
||||
Reference in New Issue
Block a user