removed flow, passport and more

Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
Fredrik Adelöw
2025-04-15 22:05:48 +02:00
parent 5205cc9c4e
commit f6e3bc9dce
12 changed files with 2 additions and 1044 deletions
@@ -1,203 +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 {
safelyEncodeURIComponent,
ensuresXRequestedWith,
postMessageResponse,
} from './authFlowHelpers';
import { WebMessageResponse } from './types';
describe('oauth helpers', () => {
describe('safelyEncodeURIComponent', () => {
it('encodes all occurrences of single quotes', () => {
expect(safelyEncodeURIComponent("a'ö'b")).toBe('a%27%C3%B6%27b');
});
});
describe('postMessageResponse', () => {
const appOrigin = 'http://localhost:3000';
it('should post a message back with payload success', () => {
const mockResponse = {
end: jest.fn().mockReturnThis(),
setHeader: jest.fn().mockReturnThis(),
} as unknown as express.Response;
const data: WebMessageResponse = {
type: 'authorization_response',
response: {
providerInfo: {
accessToken: 'ACCESS_TOKEN',
idToken: 'ID_TOKEN',
expiresInSeconds: 10,
scope: 'email',
},
profile: {
email: 'foo@bar.com',
},
backstageIdentity: {
token: 'a.b.c',
identity: {
type: 'user',
ownershipEntityRefs: [],
userEntityRef: 'a',
},
},
},
};
const encoded = safelyEncodeURIComponent(JSON.stringify(data));
postMessageResponse(mockResponse, appOrigin, data);
expect(mockResponse.setHeader).toHaveBeenCalledTimes(3);
expect(mockResponse.end).toHaveBeenCalledTimes(1);
expect(mockResponse.end).toHaveBeenCalledWith(
expect.stringContaining(encoded),
);
});
it('should post a message back with payload error', () => {
const mockResponse = {
end: jest.fn().mockReturnThis(),
setHeader: jest.fn().mockReturnThis(),
} as unknown as express.Response;
const data: WebMessageResponse = {
type: 'authorization_response',
error: new Error('Unknown error occurred'),
};
const encoded = safelyEncodeURIComponent(JSON.stringify(data));
postMessageResponse(mockResponse, appOrigin, data);
expect(mockResponse.setHeader).toHaveBeenCalledTimes(3);
expect(mockResponse.end).toHaveBeenCalledTimes(1);
expect(mockResponse.end).toHaveBeenCalledWith(
expect.stringContaining(encoded),
);
});
it('should call postMessage twice but only one of them with target *', () => {
let responseBody = '';
const mockResponse = {
end: jest.fn(body => {
responseBody = body;
return this;
}),
setHeader: jest.fn().mockReturnThis(),
} as unknown as express.Response;
const data: WebMessageResponse = {
type: 'authorization_response',
response: {
providerInfo: {
accessToken: 'ACCESS_TOKEN',
idToken: 'ID_TOKEN',
expiresInSeconds: 10,
scope: 'email',
},
profile: {
email: 'foo@bar.com',
},
backstageIdentity: {
token: 'a.b.c',
identity: {
type: 'user',
ownershipEntityRefs: [],
userEntityRef: 'a',
},
},
},
};
postMessageResponse(mockResponse, appOrigin, data);
expect(responseBody.match(/.postMessage\(/g)).toHaveLength(2);
expect(
responseBody.match(/.postMessage\([a-zA-Z.()]*, \'\*\'\)/g),
).toHaveLength(1);
const errData: WebMessageResponse = {
type: 'authorization_response',
error: new Error('Unknown error occurred'),
};
postMessageResponse(mockResponse, appOrigin, errData);
expect(responseBody.match(/.postMessage\(/g)).toHaveLength(2);
expect(
responseBody.match(/.postMessage\([a-zA-Z.()]*, \'\*\'\)/g),
).toHaveLength(1);
});
it('handles single quotes and unicode chars safely', () => {
const mockResponse = {
end: jest.fn().mockReturnThis(),
setHeader: jest.fn().mockReturnThis(),
} as unknown as express.Response;
const data: WebMessageResponse = {
type: 'authorization_response',
response: {
providerInfo: {
accessToken: 'ACCESS_TOKEN',
idToken: 'ID_TOKEN',
expiresInSeconds: 10,
scope: 'email',
},
profile: {
email: 'foo@bar.com',
displayName: "Adam l'Hôpital",
},
backstageIdentity: {
token: 'a.b.c',
identity: {
type: 'user',
ownershipEntityRefs: [],
userEntityRef: 'a',
},
},
},
};
postMessageResponse(mockResponse, appOrigin, data);
expect(mockResponse.setHeader).toHaveBeenCalledTimes(3);
expect(mockResponse.end).toHaveBeenCalledTimes(1);
expect(mockResponse.end).toHaveBeenCalledWith(
expect.stringContaining('Adam%20l%27H%C3%B4pital'),
);
});
});
describe('ensuresXRequestedWith', () => {
it('should return false if no header present', () => {
const mockRequest = {
header: () => jest.fn(),
} as unknown as express.Request;
expect(ensuresXRequestedWith(mockRequest)).toBe(false);
});
it('should return false if header present with incorrect value', () => {
const mockRequest = {
header: () => 'INVALID',
} as unknown as express.Request;
expect(ensuresXRequestedWith(mockRequest)).toBe(false);
});
it('should return true if header present with correct value', () => {
const mockRequest = {
header: () => 'XMLHttpRequest',
} as unknown as express.Request;
expect(ensuresXRequestedWith(mockRequest)).toBe(true);
});
});
});
@@ -1,85 +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 crypto from 'crypto';
import { WebMessageResponse } from './types';
export const safelyEncodeURIComponent = (value: string) => {
// Note the g at the end of the regex; all occurrences of single quotes must
// be replaced, which encodeURIComponent does not do itself by default
return encodeURIComponent(value).replace(/'/g, '%27');
};
/**
* @public
* @deprecated Use `sendWebMessageResponse` from `@backstage/plugin-auth-node` instead
*/
export const postMessageResponse = (
res: express.Response,
appOrigin: string,
response: WebMessageResponse,
) => {
const jsonData = JSON.stringify(response);
const base64Data = safelyEncodeURIComponent(jsonData);
const base64Origin = safelyEncodeURIComponent(appOrigin);
// NOTE: It is absolutely imperative that we use the safe encoder above, to
// be sure that the js code below does not allow the injection of malicious
// data.
// TODO: Make target app origin configurable globally
//
// postMessage fails silently if the targetOrigin is disallowed.
// So 2 postMessages are sent from the popup to the parent window.
// First, the origin being used to post the actual authorization response is
// shared with the parent window with a postMessage with targetOrigin '*'.
// Second, the actual authorization response is sent with the app origin
// as the targetOrigin.
// If the first message was received but the actual auth response was
// never received, the event listener can conclude that targetOrigin
// was disallowed, indicating potential misconfiguration.
//
const script = `
var authResponse = decodeURIComponent('${base64Data}');
var origin = decodeURIComponent('${base64Origin}');
var originInfo = {'type': 'config_info', 'targetOrigin': origin};
(window.opener || window.parent).postMessage(originInfo, '*');
(window.opener || window.parent).postMessage(JSON.parse(authResponse), origin);
setTimeout(() => {
window.close();
}, 100); // same as the interval of the core-app-api lib/loginPopup.ts (to address race conditions)
`;
const hash = crypto.createHash('sha256').update(script).digest('base64');
res.setHeader('Content-Type', 'text/html');
res.setHeader('X-Frame-Options', 'sameorigin');
res.setHeader('Content-Security-Policy', `script-src 'sha256-${hash}'`);
res.end(`<html><body><script>${script}</script></body></html>`);
};
/**
* @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') {
return false;
}
return true;
};
@@ -1,19 +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.
*/
export { ensuresXRequestedWith, postMessageResponse } from './authFlowHelpers';
export type { WebMessageResponse } from './types';
@@ -1,23 +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 { WebMessageResponse as _WebMessageResponse } from '@backstage/plugin-auth-node';
/**
* @public
* @deprecated import from `@backstage/plugin-auth-node` instead
*/
export type WebMessageResponse = _WebMessageResponse;
@@ -1,360 +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 { UnsecuredJWT } from 'jose';
import passport from 'passport';
import { InternalOAuthError } from 'passport-oauth2';
import {
executeRedirectStrategy,
executeFrameHandlerStrategy,
executeRefreshTokenStrategy,
makeProfileInfo,
} from './PassportStrategyHelper';
import { PassportProfile } from './types';
const mockRequest = {} as unknown as express.Request;
describe('PassportStrategyHelper', () => {
describe('makeProfileInfo', () => {
it('retrieves email from passport profile', () => {
const profile: PassportProfile = {
emails: [{ value: 'email' }],
provider: '',
id: '',
displayName: '',
};
const profileInfo = makeProfileInfo(profile);
expect(profileInfo.email).toEqual('email');
});
it('retrieves picture from passport profile avatarUrl', () => {
const profile: PassportProfile = {
avatarUrl: 'avatarUrl',
provider: '',
id: '',
displayName: '',
};
const profileInfo = makeProfileInfo(profile);
expect(profileInfo.picture).toEqual('avatarUrl');
});
it('falls back to picture from passport profile photos field', () => {
const profile: PassportProfile = {
photos: [{ value: 'picture' }],
provider: '',
id: '',
displayName: '',
};
const profileInfo = makeProfileInfo(profile);
expect(profileInfo.picture).toEqual('picture');
});
it('falls back to email from ID token', async () => {
const profile: PassportProfile = {
provider: '',
id: '',
displayName: '',
};
const profileInfo = makeProfileInfo(
profile,
await new UnsecuredJWT({ email: 'email' }).encode(),
);
expect(profileInfo.email).toEqual('email');
});
it('falls back to picture from ID token', async () => {
const profile: PassportProfile = {
provider: '',
id: '',
displayName: '',
};
const profileInfo = makeProfileInfo(
profile,
await new UnsecuredJWT({ picture: 'picture' }).encode(),
);
expect(profileInfo.picture).toEqual('picture');
});
it('falls back to name from ID token', async () => {
const profile: PassportProfile = {
provider: '',
id: '',
displayName: '',
};
const profileInfo = makeProfileInfo(
profile,
await new UnsecuredJWT({ name: 'name' }).encode(),
);
expect(profileInfo.displayName).toEqual('name');
});
it('fails when attempting to fall back to invalid JWT', () => {
const profile: PassportProfile = {
provider: '',
id: '',
displayName: '',
};
expect(() => makeProfileInfo(profile, 'invalid JWT')).toThrow(
'Failed to parse id token and get profile info',
);
});
});
class MyCustomRedirectStrategy extends passport.Strategy {
authenticate() {
this.redirect('a', 302);
}
}
describe('executeRedirectStrategy', () => {
it('should call authenticate and resolve with OAuthStartResponse', async () => {
const mockStrategy = new MyCustomRedirectStrategy();
const spyAuthenticate = jest.spyOn(mockStrategy, 'authenticate');
const redirectStrategyPromise = executeRedirectStrategy(
mockRequest,
mockStrategy,
{},
);
expect(spyAuthenticate).toHaveBeenCalledTimes(1);
await expect(redirectStrategyPromise).resolves.toStrictEqual(
expect.objectContaining({ url: 'a', status: 302 }),
);
});
});
describe('executeFrameHandlerStrategy', () => {
class MyCustomAuthSuccessStrategy extends passport.Strategy {
authenticate() {
this.success(
{ accessToken: 'ACCESS_TOKEN' },
{ refreshToken: 'REFRESH_TOKEN' },
);
}
}
class MyCustomAuthErrorStrategy extends passport.Strategy {
authenticate() {
this.error(
new InternalOAuthError('MyCustomAuth error', {
data: '{ "message": "Custom message" }',
}),
);
}
}
class MyCustomAuthRedirectStrategy extends passport.Strategy {
authenticate() {
this.redirect('URL', 302);
}
}
class MyCustomAuthFailStrategy extends passport.Strategy {
authenticate() {
this.fail('challenge', 302);
}
}
it('should resolve with user and info on success', async () => {
const mockStrategy = new MyCustomAuthSuccessStrategy();
const spyAuthenticate = jest.spyOn(mockStrategy, 'authenticate');
const frameHandlerStrategyPromise = executeFrameHandlerStrategy(
mockRequest,
mockStrategy,
);
expect(spyAuthenticate).toHaveBeenCalledTimes(1);
await expect(frameHandlerStrategyPromise).resolves.toStrictEqual(
expect.objectContaining({
result: { accessToken: 'ACCESS_TOKEN' },
privateInfo: { refreshToken: 'REFRESH_TOKEN' },
}),
);
});
it('should reject on error', async () => {
const mockStrategy = new MyCustomAuthErrorStrategy();
const spyAuthenticate = jest.spyOn(mockStrategy, 'authenticate');
const frameHandlerStrategyPromise = executeFrameHandlerStrategy(
mockRequest,
mockStrategy,
);
expect(spyAuthenticate).toHaveBeenCalledTimes(1);
await expect(frameHandlerStrategyPromise).rejects.toThrow(
'Authentication failed, MyCustomAuth error - Custom message',
);
});
it('should reject on redirect', async () => {
const mockStrategy = new MyCustomAuthRedirectStrategy();
const spyAuthenticate = jest.spyOn(mockStrategy, 'authenticate');
const frameHandlerStrategyPromise = executeFrameHandlerStrategy(
mockRequest,
mockStrategy,
);
expect(spyAuthenticate).toHaveBeenCalledTimes(1);
await expect(frameHandlerStrategyPromise).rejects.toThrow(
'Unexpected redirect',
);
});
it('should reject on fail', async () => {
const mockStrategy = new MyCustomAuthFailStrategy();
const spyAuthenticate = jest.spyOn(mockStrategy, 'authenticate');
const frameHandlerStrategyPromise = executeFrameHandlerStrategy(
mockRequest,
mockStrategy,
);
expect(spyAuthenticate).toHaveBeenCalledTimes(1);
await expect(frameHandlerStrategyPromise).rejects.toThrow();
});
});
describe('executeRefreshTokenStrategy', () => {
it('should resolve with a new access token, scope and expiry', async () => {
class MyCustomOAuth2Success {
getOAuthAccessToken(
_refreshToken: string,
_options: any,
callback: Function,
) {
callback(null, 'ACCESS_TOKEN', 'REFRESH_TOKEN', {
scope: 'a',
expires_in: 10,
});
}
}
class MyCustomRefreshTokenSuccess extends passport.Strategy {
_oauth2 = new MyCustomOAuth2Success();
userProfile(_accessToken: string, callback: Function) {
callback(null, {
provider: 'a',
email: 'b',
name: 'c',
picture: 'd',
});
}
}
const mockStrategy = new MyCustomRefreshTokenSuccess();
const refreshTokenPromise = executeRefreshTokenStrategy(
mockStrategy,
'REFRESH_TOKEN',
'a',
);
await expect(refreshTokenPromise).resolves.toStrictEqual(
expect.objectContaining({
accessToken: 'ACCESS_TOKEN',
params: expect.objectContaining({ scope: 'a', expires_in: 10 }),
}),
);
});
it('should forward simple errors', async () => {
class MyCustomRefreshTokenSuccess extends passport.Strategy {
_oauth2 = new (class {
getOAuthAccessToken(_r: string, _o: any, cb: Function) {
cb(new Error('Unknown error'));
}
})();
}
await expect(
executeRefreshTokenStrategy(
new MyCustomRefreshTokenSuccess(),
'REFRESH_TOKEN',
'a',
),
).rejects.toThrow(
'Failed to refresh access token; caused by Error: Unknown error',
);
});
it('should forward string errors', async () => {
class MyCustomRefreshTokenSuccess extends passport.Strategy {
_oauth2 = new (class {
getOAuthAccessToken(_r: string, _o: any, cb: Function) {
cb('some silly string error');
}
})();
}
await expect(
executeRefreshTokenStrategy(
new MyCustomRefreshTokenSuccess(),
'REFRESH_TOKEN',
'a',
),
).rejects.toThrow(
"Failed to refresh access token; caused by unknown error 'some silly string error'",
);
});
it('should forward object errors', async () => {
class MyCustomRefreshTokenSuccess extends passport.Strategy {
_oauth2 = new (class {
getOAuthAccessToken(_r: string, _o: any, cb: Function) {
cb({ name: 'SomeError', message: 'some message' });
}
})();
}
await expect(
executeRefreshTokenStrategy(
new MyCustomRefreshTokenSuccess(),
'REFRESH_TOKEN',
'a',
),
).rejects.toThrow(
'Failed to refresh access token; caused by SomeError: some message',
);
});
it('should reject with an error if access token missing in refresh callback', async () => {
class MyCustomOAuth2AccessTokenMissing {
getOAuthAccessToken(
_refreshToken: string,
_options: any,
callback: Function,
) {
callback(null, '');
}
}
class MyCustomRefreshTokenSuccess extends passport.Strategy {
_oauth2 = new MyCustomOAuth2AccessTokenMissing();
}
const mockStrategy = new MyCustomRefreshTokenSuccess();
const refreshTokenPromise = executeRefreshTokenStrategy(
mockStrategy,
'REFRESH_TOKEN',
'a',
);
await expect(refreshTokenPromise).rejects.toThrow(
'Failed to refresh access token, no access token received',
);
});
});
});
@@ -1,224 +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 passport from 'passport';
import { decodeJwt } from 'jose';
import { InternalOAuthError } from 'passport-oauth2';
import { ProfileInfo } from '@backstage/plugin-auth-node';
import { PassportProfile } from './types';
import { OAuthStartResponse } from '../../providers/types';
import { ForwardedError } from '@backstage/errors';
export type PassportDoneCallback<Res, Private = never> = (
err?: Error,
response?: Res,
privateInfo?: Private,
) => void;
export const makeProfileInfo = (
profile: PassportProfile,
idToken?: string,
): ProfileInfo => {
let email: string | undefined = undefined;
if (profile.emails && profile.emails.length > 0) {
const [firstEmail] = profile.emails;
email = firstEmail.value;
}
let picture: string | undefined = undefined;
if (profile.avatarUrl) {
picture = profile.avatarUrl;
} else if (profile.photos && profile.photos.length > 0) {
const [firstPhoto] = profile.photos;
picture = firstPhoto.value;
}
let displayName: string | undefined =
profile.displayName ?? profile.username ?? profile.id;
if ((!email || !picture || !displayName) && idToken) {
try {
const decoded = decodeJwt(idToken) as {
email?: string;
name?: string;
picture?: string;
};
if (!email && decoded.email) {
email = decoded.email;
}
if (!picture && decoded.picture) {
picture = decoded.picture;
}
if (!displayName && decoded.name) {
displayName = decoded.name;
}
} catch (e) {
throw new ForwardedError(
`Failed to parse id token and get profile info`,
e,
);
}
}
return {
email,
picture,
displayName,
};
};
export const executeRedirectStrategy = async (
req: express.Request,
providerStrategy: passport.Strategy,
options: Record<string, string>,
): Promise<OAuthStartResponse> => {
return new Promise(resolve => {
const strategy = Object.create(providerStrategy);
strategy.redirect = (url: string, status?: number) => {
resolve({ url, status: status ?? undefined });
};
strategy.authenticate(req, { ...options });
});
};
export const executeFrameHandlerStrategy = async <Result, PrivateInfo = never>(
req: express.Request,
providerStrategy: passport.Strategy,
options?: Record<string, string>,
) => {
return new Promise<{ result: Result; privateInfo: PrivateInfo }>(
(resolve, reject) => {
const strategy = Object.create(providerStrategy);
strategy.success = (result: any, privateInfo: any) => {
resolve({ result, privateInfo });
};
strategy.fail = (
info: { type: 'success' | 'error'; message?: string },
// _status: number,
) => {
reject(new Error(`Authentication rejected, ${info.message ?? ''}`));
};
strategy.error = (error: InternalOAuthError) => {
let message = `Authentication failed, ${error.message}`;
if (error.oauthError?.data) {
try {
const errorData = JSON.parse(error.oauthError.data);
if (errorData.message) {
message += ` - ${errorData.message}`;
}
} catch (parseError) {
message += ` - ${error.oauthError}`;
}
}
reject(new Error(message));
};
strategy.redirect = () => {
reject(new Error('Unexpected redirect'));
};
strategy.authenticate(req, { ...(options ?? {}) });
},
);
};
type RefreshTokenResponse = {
/**
* An access token issued for the signed in user.
*/
accessToken: string;
/**
* Optionally, the server can issue a new Refresh Token for the user
*/
refreshToken?: string;
params: any;
};
export const executeRefreshTokenStrategy = async (
providerStrategy: passport.Strategy,
refreshToken: string,
scope: string,
): Promise<RefreshTokenResponse> => {
return new Promise((resolve, reject) => {
const anyStrategy = providerStrategy as any;
const OAuth2 = anyStrategy._oauth2.constructor;
const oauth2 = new OAuth2(
anyStrategy._oauth2._clientId,
anyStrategy._oauth2._clientSecret,
anyStrategy._oauth2._baseSite,
anyStrategy._oauth2._authorizeUrl,
anyStrategy._refreshURL || anyStrategy._oauth2._accessTokenUrl,
anyStrategy._oauth2._customHeaders,
);
oauth2.getOAuthAccessToken(
refreshToken,
{
scope,
grant_type: 'refresh_token',
},
(
err: Error | null,
accessToken: string,
newRefreshToken: string,
params: any,
) => {
if (err) {
reject(new ForwardedError(`Failed to refresh access token`, err));
}
if (!accessToken) {
reject(
new Error(
`Failed to refresh access token, no access token received`,
),
);
}
resolve({
accessToken,
refreshToken: newRefreshToken,
params,
});
},
);
});
};
type ProviderStrategy = {
userProfile(accessToken: string, callback: Function): void;
};
export const executeFetchUserProfileStrategy = async (
providerStrategy: passport.Strategy,
accessToken: string,
): Promise<PassportProfile> => {
return new Promise((resolve, reject) => {
const anyStrategy = providerStrategy as unknown as ProviderStrategy;
anyStrategy.userProfile(
accessToken,
(error: Error, rawProfile: PassportProfile) => {
if (error) {
reject(error);
} else {
resolve(rawProfile);
}
},
);
});
};
@@ -1,24 +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.
*/
export {
executeFetchUserProfileStrategy,
executeFrameHandlerStrategy,
executeRedirectStrategy,
executeRefreshTokenStrategy,
makeProfileInfo,
} from './PassportStrategyHelper';
export type { PassportDoneCallback } from './PassportStrategyHelper';
@@ -1,20 +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 passport from 'passport';
export type PassportProfile = passport.Profile & {
avatarUrl?: string;
};
@@ -39,16 +39,7 @@ import {
} from '@backstage/plugin-auth-node';
import { CatalogIdentityClient } from '../catalog';
/**
* Uses the default ownership resolution logic to return an array
* of entity refs that the provided entity claims ownership through.
*
* A reference to the entity itself will also be included in the returned array.
*
* @public
* @deprecated use `ctx.resolveOwnershipEntityRefs(entity)` from the provided `AuthResolverContext` instead.
*/
export function getDefaultOwnershipEntityRefs(entity: Entity) {
function getDefaultOwnershipEntityRefs(entity: Entity) {
const membershipRefs =
entity.relations
?.filter(
@@ -59,9 +50,6 @@ export function getDefaultOwnershipEntityRefs(entity: Entity) {
return Array.from(new Set([stringifyEntityRef(entity), ...membershipRefs]));
}
/**
* @internal
*/
export class CatalogAuthResolverContext implements AuthResolverContext {
static create(options: {
logger: LoggerService;
@@ -14,7 +14,4 @@
* limitations under the License.
*/
export {
CatalogAuthResolverContext,
getDefaultOwnershipEntityRefs,
} from './CatalogAuthResolverContext';
export { CatalogAuthResolverContext } from './CatalogAuthResolverContext';
@@ -15,9 +15,3 @@
*/
export { createOriginFilter, type ProviderFactories } from './router';
export type {
AuthHandler,
AuthHandlerResult,
OAuthStartResponse,
} from './types';
@@ -1,63 +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 {
AuthResolverContext as _AuthResolverContext,
ProfileInfo as _ProfileInfo,
} from '@backstage/plugin-auth-node';
/**
* @public
* @deprecated Use `createOAuthAuthenticator` from `@backstage/plugin-auth-node` instead
*/
export type OAuthStartResponse = {
/**
* URL to redirect to
*/
url: string;
/**
* Status code to use for the redirect
*/
status?: number;
};
/**
* 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 };
/**
* The AuthHandler function is called every time the user authenticates using
* the provider.
*
* The handler should return a profile that represents the session for the user
* in the frontend.
*
* Throwing an error in the function will cause the authentication to fail,
* making it possible to use this function as a way to limit access to a certain
* group of users.
*
* @public
* @deprecated Use `createOAuthRouteHandlers` from `@backstage/plugin-auth-node` instead
*/
export type AuthHandler<TAuthResult> = (
input: TAuthResult,
context: _AuthResolverContext,
) => Promise<AuthHandlerResult>;