Use BackstageUserIdentity, fix tests
Co-authored-by: blam <ben@blam.sh> Co-authored-by: Fredrik Adelöw <freben@gmail.com> Co-authored-by: Patrik Oldsberg <poldsberg@gmail.com> Signed-off-by: Johan Haals <johan.haals@gmail.com>
This commit is contained in:
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { ApiRef, createApiRef } from '../system';
|
||||
import { ProfileInfo } from './auth';
|
||||
import { BackstageUserIdentity, ProfileInfo } from './auth';
|
||||
|
||||
/*
|
||||
|
||||
@@ -24,26 +24,6 @@ import { ProfileInfo } from './auth';
|
||||
|
||||
*/
|
||||
|
||||
/**
|
||||
* User identity information within Backstage.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type BackstageUserIdentity = {
|
||||
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[];
|
||||
};
|
||||
|
||||
/**
|
||||
* The Identity API used to identify and get information about the signed in user.
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ describe('oauth helpers', () => {
|
||||
},
|
||||
backstageIdentity: {
|
||||
id: 'a',
|
||||
idToken: 'a.b.c',
|
||||
token: 'a.b.c',
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -106,7 +106,7 @@ describe('oauth helpers', () => {
|
||||
},
|
||||
backstageIdentity: {
|
||||
id: 'a',
|
||||
idToken: 'a.b.c',
|
||||
token: 'a.b.c',
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -148,7 +148,7 @@ describe('oauth helpers', () => {
|
||||
},
|
||||
backstageIdentity: {
|
||||
id: 'a',
|
||||
idToken: 'a.b.c',
|
||||
token: 'a.b.c',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -31,6 +31,7 @@ const mockResponseData = {
|
||||
},
|
||||
backstageIdentity: {
|
||||
id: 'foo',
|
||||
token: '',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -19,8 +19,8 @@ import crypto from 'crypto';
|
||||
import { URL } from 'url';
|
||||
import {
|
||||
AuthProviderRouteHandlers,
|
||||
BackstageIdentity,
|
||||
AuthProviderConfig,
|
||||
BackstageIdentityResponse,
|
||||
} from '../../providers/types';
|
||||
import {
|
||||
AuthenticationError,
|
||||
@@ -228,17 +228,17 @@ 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?: BackstageIdentityResponse) {
|
||||
if (!identity) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(identity.token || identity.idToken)) {
|
||||
if (!(identity.token || identity.token)) {
|
||||
identity.token = await this.options.tokenIssuer.issueToken({
|
||||
claims: { sub: identity.id },
|
||||
});
|
||||
} else if (!identity.token && identity.idToken) {
|
||||
identity.token = identity.idToken;
|
||||
} else if (!identity.token && identity.token) {
|
||||
identity.token = identity.token;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -151,7 +151,7 @@ export class Auth0AuthProvider implements OAuthHandlers {
|
||||
|
||||
const id = profile.email.split('@')[0];
|
||||
|
||||
return { ...response, backstageIdentity: { id } };
|
||||
return { ...response, backstageIdentity: { id, token: '' } };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,4 +38,4 @@ 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, BackstageUserIdentity, ProfileInfo } from './types';
|
||||
|
||||
@@ -205,7 +205,6 @@ 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: '' } };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -140,6 +140,30 @@ export type AuthResponse<ProviderInfo> = {
|
||||
backstageIdentity?: BackstageIdentityResponse;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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[];
|
||||
};
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@@ -171,20 +195,7 @@ export type BackstageIdentityResponse = {
|
||||
/**
|
||||
* A plaintext description of the identity that is encapsulated within the token.
|
||||
*/
|
||||
identity?: {
|
||||
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[];
|
||||
};
|
||||
identity?: BackstageUserIdentity;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -79,6 +79,9 @@ describe('CatalogImportClient', () => {
|
||||
signOut: () => {
|
||||
return Promise.resolve();
|
||||
},
|
||||
getProfileInfo: jest.fn(),
|
||||
getBackstageIdentity: jest.fn(),
|
||||
getCredentials: jest.fn(),
|
||||
};
|
||||
|
||||
const scmIntegrationsApi = ScmIntegrations.fromConfig(
|
||||
|
||||
@@ -37,6 +37,9 @@ describe('<DefaultImportPage />', () => {
|
||||
signOut: () => {
|
||||
return Promise.resolve();
|
||||
},
|
||||
getProfileInfo: jest.fn(),
|
||||
getBackstageIdentity: jest.fn(),
|
||||
getCredentials: jest.fn(),
|
||||
};
|
||||
|
||||
let apis: TestApiRegistry;
|
||||
|
||||
@@ -43,6 +43,9 @@ describe('<ImportPage />', () => {
|
||||
signOut: () => {
|
||||
return Promise.resolve();
|
||||
},
|
||||
getProfileInfo: jest.fn(),
|
||||
getBackstageIdentity: jest.fn(),
|
||||
getCredentials: jest.fn(),
|
||||
};
|
||||
|
||||
let apis: TestApiRegistry;
|
||||
|
||||
@@ -40,6 +40,9 @@ const identityApi: IdentityApi = {
|
||||
async signOut() {
|
||||
return Promise.resolve();
|
||||
},
|
||||
getProfileInfo: jest.fn(),
|
||||
getBackstageIdentity: jest.fn(),
|
||||
getCredentials: jest.fn(),
|
||||
};
|
||||
const guestIdentityApi: IdentityApi = {
|
||||
getUserId() {
|
||||
@@ -54,6 +57,9 @@ const guestIdentityApi: IdentityApi = {
|
||||
async signOut() {
|
||||
return Promise.resolve();
|
||||
},
|
||||
getProfileInfo: jest.fn(),
|
||||
getBackstageIdentity: jest.fn(),
|
||||
getCredentials: jest.fn(),
|
||||
};
|
||||
|
||||
describe('CatalogClientWrapper', () => {
|
||||
|
||||
@@ -187,6 +187,9 @@ export const MockCostInsightsApiProvider = ({
|
||||
getIdToken: jest.fn(),
|
||||
getUserId: jest.fn(),
|
||||
signOut: jest.fn(),
|
||||
getProfileInfo: jest.fn(),
|
||||
getBackstageIdentity: jest.fn(),
|
||||
getCredentials: jest.fn(),
|
||||
};
|
||||
|
||||
const defaultCostInsightsApi: CostInsightsApi = {
|
||||
|
||||
@@ -24,20 +24,11 @@ import { UrlPatternDiscovery } from '@backstage/core-app-api';
|
||||
|
||||
const server = setupServer();
|
||||
|
||||
const identityApi: IdentityApi = {
|
||||
getUserId() {
|
||||
return 'jane-fonda';
|
||||
},
|
||||
getProfile() {
|
||||
return { email: 'jane-fonda@spotify.com' };
|
||||
},
|
||||
const identityApi = {
|
||||
async getIdToken() {
|
||||
return Promise.resolve('fake-id-token');
|
||||
},
|
||||
async signOut() {
|
||||
return Promise.resolve();
|
||||
},
|
||||
};
|
||||
} as IdentityApi;
|
||||
|
||||
describe('FossaClient', () => {
|
||||
setupRequestMockHandlers(server);
|
||||
|
||||
@@ -34,6 +34,9 @@ describe('apis', () => {
|
||||
getUserId: jest.fn(),
|
||||
getProfile: jest.fn(),
|
||||
signOut: jest.fn(),
|
||||
getProfileInfo: jest.fn(),
|
||||
getBackstageIdentity: jest.fn(),
|
||||
getCredentials: jest.fn(),
|
||||
});
|
||||
|
||||
const client = new SearchClient({
|
||||
|
||||
@@ -37,6 +37,9 @@ const identityApiAuthenticated: IdentityApi = {
|
||||
async signOut() {
|
||||
return Promise.resolve();
|
||||
},
|
||||
getProfileInfo: jest.fn(),
|
||||
getBackstageIdentity: jest.fn(),
|
||||
getCredentials: jest.fn(),
|
||||
};
|
||||
const identityApiGuest: IdentityApi = {
|
||||
getUserId() {
|
||||
@@ -51,6 +54,9 @@ const identityApiGuest: IdentityApi = {
|
||||
async signOut() {
|
||||
return Promise.resolve();
|
||||
},
|
||||
getProfileInfo: jest.fn(),
|
||||
getBackstageIdentity: jest.fn(),
|
||||
getCredentials: jest.fn(),
|
||||
};
|
||||
|
||||
describe('SonarQubeClient', () => {
|
||||
|
||||
@@ -44,6 +44,9 @@ describe('TechDocsStorageClient', () => {
|
||||
getProfile: jest.fn(),
|
||||
getUserId: jest.fn(),
|
||||
signOut: jest.fn(),
|
||||
getProfileInfo: jest.fn(),
|
||||
getBackstageIdentity: jest.fn(),
|
||||
getCredentials: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
Reference in New Issue
Block a user