Merge pull request #1433 from spotify/rugvip/authid
auth-backend: leave it to the providers to figure out the backstage identity
This commit is contained in:
@@ -173,7 +173,7 @@ export type ProfileInfo = {
|
||||
/**
|
||||
* Email ID.
|
||||
*/
|
||||
email: string;
|
||||
email?: string;
|
||||
|
||||
/**
|
||||
* Display name that can be presented to the user.
|
||||
|
||||
@@ -34,14 +34,16 @@ export const UserProfile: FC<{ open: boolean; setOpen: Function }> = ({
|
||||
}) => {
|
||||
const ref = useRef<Element>(); // for scrolling down when collapse item opens
|
||||
const classes = useStyles();
|
||||
const profile = useApi(identityApiRef).getProfile();
|
||||
const identityApi = useApi(identityApiRef);
|
||||
|
||||
const handleClick = () => {
|
||||
setOpen(!open);
|
||||
setTimeout(() => ref.current?.scrollIntoView({ behavior: 'smooth' }), 300);
|
||||
};
|
||||
|
||||
const displayName = profile.displayName ?? profile.email;
|
||||
const userId = identityApi.getUserId();
|
||||
const profile = identityApi.getProfile();
|
||||
const displayName = profile.displayName ?? userId;
|
||||
const SignInAvatar = () => (
|
||||
<Avatar src={profile.picture} className={classes.avatar}>
|
||||
{displayName[0]}
|
||||
|
||||
@@ -23,13 +23,9 @@ import {
|
||||
verifyNonce,
|
||||
OAuthProvider,
|
||||
} from './OAuthProvider';
|
||||
import {
|
||||
WebMessageResponse,
|
||||
OAuthProviderHandlers,
|
||||
OAuthResponse,
|
||||
} from '../providers/types';
|
||||
import { WebMessageResponse, OAuthProviderHandlers } from '../providers/types';
|
||||
|
||||
const mockResponseData: OAuthResponse = {
|
||||
const mockResponseData = {
|
||||
providerInfo: {
|
||||
accessToken: 'ACCESS_TOKEN',
|
||||
idToken: 'ID_TOKEN',
|
||||
@@ -39,6 +35,9 @@ const mockResponseData: OAuthResponse = {
|
||||
profile: {
|
||||
email: 'foo@bar.com',
|
||||
},
|
||||
backstageIdentity: {
|
||||
id: 'foo',
|
||||
},
|
||||
};
|
||||
|
||||
describe('OAuthProvider Utils', () => {
|
||||
@@ -350,7 +349,7 @@ describe('OAuthProvider', () => {
|
||||
expect(mockResponse.send).toHaveBeenCalledWith({
|
||||
...mockResponseData,
|
||||
backstageIdentity: {
|
||||
id: mockResponseData.profile.email,
|
||||
id: mockResponseData.backstageIdentity.id,
|
||||
idToken: 'my-id-token',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -18,10 +18,10 @@ import express from 'express';
|
||||
import crypto from 'crypto';
|
||||
import { URL } from 'url';
|
||||
import {
|
||||
AuthResponse,
|
||||
AuthProviderRouteHandlers,
|
||||
OAuthProviderHandlers,
|
||||
WebMessageResponse,
|
||||
BackstageIdentity,
|
||||
} from '../providers/types';
|
||||
import { InputError } from '@backstage/backend-common';
|
||||
import { TokenIssuer } from '../identity';
|
||||
@@ -147,19 +147,12 @@ export class OAuthProvider implements AuthProviderRouteHandlers {
|
||||
this.setRefreshTokenCookie(res, refreshToken);
|
||||
}
|
||||
|
||||
const id = response.profile.email;
|
||||
const idToken = await this.options.tokenIssuer.issueToken({
|
||||
claims: { sub: id },
|
||||
});
|
||||
const fullResponse: AuthResponse<unknown> = {
|
||||
...response,
|
||||
backstageIdentity: { id, idToken },
|
||||
};
|
||||
await this.populateIdentity(response.backstageIdentity);
|
||||
|
||||
// post message back to popup if successful
|
||||
return postMessageResponse(res, this.options.appOrigin, {
|
||||
type: 'authorization_response',
|
||||
response: fullResponse,
|
||||
response,
|
||||
});
|
||||
} catch (error) {
|
||||
// post error message back to popup if failure
|
||||
@@ -213,21 +206,30 @@ export class OAuthProvider implements AuthProviderRouteHandlers {
|
||||
// get new access_token
|
||||
const response = await this.providerHandlers.refresh(refreshToken, scope);
|
||||
|
||||
const id = response.profile.email;
|
||||
const idToken = await this.options.tokenIssuer.issueToken({
|
||||
claims: { sub: id },
|
||||
});
|
||||
const fullResponse: AuthResponse<unknown> = {
|
||||
...response,
|
||||
backstageIdentity: { id, idToken },
|
||||
};
|
||||
await this.populateIdentity(response.backstageIdentity);
|
||||
|
||||
res.send(fullResponse);
|
||||
res.send(response);
|
||||
} catch (error) {
|
||||
res.status(401).send(`${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
if (!identity) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!identity.idToken) {
|
||||
identity.idToken = await this.options.tokenIssuer.issueToken({
|
||||
claims: { sub: identity.id },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private setNonceCookie = (res: express.Response, nonce: string) => {
|
||||
res.cookie(`${this.options.providerId}-nonce`, nonce, {
|
||||
maxAge: TEN_MINUTES_MS,
|
||||
|
||||
@@ -57,10 +57,6 @@ export const makeProfileInfo = (
|
||||
}
|
||||
}
|
||||
|
||||
if (!email) {
|
||||
throw new Error('No email received in profile info');
|
||||
}
|
||||
|
||||
return {
|
||||
email,
|
||||
picture,
|
||||
|
||||
@@ -72,15 +72,13 @@ export class GithubAuthProvider implements OAuthProviderHandlers {
|
||||
return await executeRedirectStrategy(req, this._strategy, options);
|
||||
}
|
||||
|
||||
async handler(req: express.Request): Promise<{ response: OAuthResponse }> {
|
||||
const result = await executeFrameHandlerStrategy<OAuthResponse>(
|
||||
async handler(req: express.Request) {
|
||||
const { response } = await executeFrameHandlerStrategy<OAuthResponse>(
|
||||
req,
|
||||
this._strategy,
|
||||
);
|
||||
|
||||
return {
|
||||
response: result.response,
|
||||
};
|
||||
return { response };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -97,14 +97,14 @@ export class GoogleAuthProvider implements OAuthProviderHandlers {
|
||||
async handler(
|
||||
req: express.Request,
|
||||
): Promise<{ response: OAuthResponse; refreshToken: string }> {
|
||||
const result = await executeFrameHandlerStrategy<
|
||||
const { response, privateInfo } = await executeFrameHandlerStrategy<
|
||||
OAuthResponse,
|
||||
PrivateInfo
|
||||
>(req, this._strategy);
|
||||
|
||||
return {
|
||||
response: result.response,
|
||||
refreshToken: result.privateInfo.refreshToken,
|
||||
response: await this.populateIdentity(response),
|
||||
refreshToken: privateInfo.refreshToken,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ export class GoogleAuthProvider implements OAuthProviderHandlers {
|
||||
params.id_token,
|
||||
);
|
||||
|
||||
return {
|
||||
return this.populateIdentity({
|
||||
providerInfo: {
|
||||
accessToken,
|
||||
idToken: params.id_token,
|
||||
@@ -129,7 +129,22 @@ export class GoogleAuthProvider implements OAuthProviderHandlers {
|
||||
scope: params.scope,
|
||||
},
|
||||
profile,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private async populateIdentity(
|
||||
response: OAuthResponse,
|
||||
): Promise<OAuthResponse> {
|
||||
const { profile } = response;
|
||||
|
||||
if (!profile.email) {
|
||||
throw new Error('Google profile contained no email');
|
||||
}
|
||||
|
||||
// TODO(Rugvip): Hardcoded to the local part of the email for now
|
||||
const id = profile.email.split('@')[0];
|
||||
|
||||
return { ...response, backstageIdentity: { id } };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -100,14 +100,20 @@ export interface OAuthProviderHandlers {
|
||||
*/
|
||||
handler(
|
||||
req: express.Request,
|
||||
): Promise<{ response: OAuthResponse; refreshToken?: string }>;
|
||||
): Promise<{
|
||||
response: AuthResponse<OAuthProviderInfo>;
|
||||
refreshToken?: string;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* (Optional) Given a refresh token and scope fetches a new access token from the auth provider.
|
||||
* @param {string} refreshToken
|
||||
* @param {string} scope
|
||||
*/
|
||||
refresh?(refreshToken: string, scope: string): Promise<OAuthResponse>;
|
||||
refresh?(
|
||||
refreshToken: string,
|
||||
scope: string,
|
||||
): Promise<AuthResponse<OAuthProviderInfo>>;
|
||||
|
||||
/**
|
||||
* (Optional) Sign out of the auth provider.
|
||||
@@ -192,13 +198,10 @@ export type AuthProviderFactory = (
|
||||
export type AuthResponse<ProviderInfo> = {
|
||||
providerInfo: ProviderInfo;
|
||||
profile: ProfileInfo;
|
||||
backstageIdentity: BackstageIdentity;
|
||||
backstageIdentity?: BackstageIdentity;
|
||||
};
|
||||
|
||||
export type OAuthResponse = Omit<
|
||||
AuthResponse<OAuthProviderInfo>,
|
||||
'backstageIdentity'
|
||||
>;
|
||||
export type OAuthResponse = AuthResponse<OAuthProviderInfo>;
|
||||
|
||||
export type BackstageIdentity = {
|
||||
/**
|
||||
@@ -209,7 +212,7 @@ export type BackstageIdentity = {
|
||||
/**
|
||||
* An ID token that can be used to authenticate the user within Backstage.
|
||||
*/
|
||||
idToken: string;
|
||||
idToken?: string;
|
||||
};
|
||||
|
||||
export type OAuthProviderInfo = {
|
||||
@@ -279,7 +282,7 @@ export type ProfileInfo = {
|
||||
/**
|
||||
* Email ID of the signed in user.
|
||||
*/
|
||||
email: string;
|
||||
email?: string;
|
||||
/**
|
||||
* Display name that can be presented to the signed in user.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user