Merge pull request #2613 from spotify/rugvip/idlookup
auth-backend: initial implementation of catalog user lookup
This commit is contained in:
@@ -21,6 +21,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-common": "^0.1.1-alpha.23",
|
||||
"@backstage/catalog-model": "^0.1.1-alpha.23",
|
||||
"@backstage/config": "^0.1.1-alpha.23",
|
||||
"@types/express": "^4.17.6",
|
||||
"compression": "^1.7.4",
|
||||
@@ -36,6 +37,7 @@
|
||||
"knex": "^0.21.1",
|
||||
"moment": "^2.26.0",
|
||||
"morgan": "^1.10.0",
|
||||
"node-fetch": "^2.6.1",
|
||||
"passport": "^0.4.1",
|
||||
"passport-github2": "^0.1.12",
|
||||
"passport-gitlab2": "^5.0.0",
|
||||
@@ -53,6 +55,7 @@
|
||||
"@types/body-parser": "^1.19.0",
|
||||
"@types/cookie-parser": "^1.4.2",
|
||||
"@types/jwt-decode": "2.2.1",
|
||||
"@types/node-fetch": "^2.5.7",
|
||||
"@types/passport": "^1.0.3",
|
||||
"@types/passport-github2": "^1.2.4",
|
||||
"@types/passport-google-oauth20": "^2.0.3",
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 fetch from 'node-fetch';
|
||||
import { UserEntity } from '@backstage/catalog-model';
|
||||
import { PluginEndpointDiscovery } from '@backstage/backend-common';
|
||||
|
||||
type UserQuery = {
|
||||
annotations: Record<string, string>;
|
||||
};
|
||||
|
||||
/**
|
||||
* A catalog client tailored for reading out identity data from the catalog.
|
||||
*/
|
||||
export class CatalogIdentityClient {
|
||||
private readonly discovery: PluginEndpointDiscovery;
|
||||
|
||||
constructor(options: { discovery: PluginEndpointDiscovery }) {
|
||||
this.discovery = options.discovery;
|
||||
}
|
||||
|
||||
async findUser(query: UserQuery): Promise<UserEntity[]> {
|
||||
const params = new URLSearchParams();
|
||||
params.append('kind', 'User');
|
||||
|
||||
for (const [key, value] of Object.entries(query.annotations)) {
|
||||
params.append(`metadata.annotations.${key}`, value);
|
||||
}
|
||||
|
||||
const baseUrl = await this.discovery.getBaseUrl('catalog');
|
||||
const response = await fetch(`${baseUrl}/entities?${params}`);
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
throw new Error(
|
||||
`Request failed with ${response.status} ${response.statusText}, ${text}`,
|
||||
);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { CatalogIdentityClient } from './CatalogIdentityClient';
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
import express from 'express';
|
||||
import { Logger } from 'winston';
|
||||
import { Strategy as GoogleStrategy } from 'passport-google-oauth20';
|
||||
import {
|
||||
executeFrameHandlerStrategy,
|
||||
@@ -36,15 +37,25 @@ import {
|
||||
OAuthRefreshRequest,
|
||||
} from '../../lib/oauth';
|
||||
import passport from 'passport';
|
||||
import { CatalogIdentityClient } from '../../lib/catalog';
|
||||
|
||||
type PrivateInfo = {
|
||||
refreshToken: string;
|
||||
};
|
||||
|
||||
export type GoogleAuthProviderOptions = OAuthProviderOptions & {
|
||||
logger: Logger;
|
||||
identityClient: CatalogIdentityClient;
|
||||
};
|
||||
|
||||
export class GoogleAuthProvider implements OAuthHandlers {
|
||||
private readonly _strategy: GoogleStrategy;
|
||||
private readonly logger: Logger;
|
||||
private readonly identityClient: CatalogIdentityClient;
|
||||
|
||||
constructor(options: OAuthProviderOptions) {
|
||||
constructor(options: GoogleAuthProviderOptions) {
|
||||
this.logger = options.logger;
|
||||
this.identityClient = options.identityClient;
|
||||
// TODO: throw error if env variables not set?
|
||||
this._strategy = new GoogleStrategy(
|
||||
{
|
||||
@@ -138,17 +149,44 @@ export class GoogleAuthProvider implements OAuthHandlers {
|
||||
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];
|
||||
const users = await this.identityClient.findUser({
|
||||
annotations: {
|
||||
'google.com/email': profile.email,
|
||||
},
|
||||
});
|
||||
if (users.length !== 1) {
|
||||
if (users.length > 1) {
|
||||
this.logger.info(
|
||||
`Multiple identities found for Google user ${profile.email}`,
|
||||
);
|
||||
} else {
|
||||
this.logger.info(`No identity found for Google user ${profile.email}`);
|
||||
}
|
||||
|
||||
return { ...response, backstageIdentity: { id } };
|
||||
this.logger.warn(
|
||||
`Falling back to allowing login based on email pattern, this will probably break in the future`,
|
||||
);
|
||||
return {
|
||||
...response,
|
||||
backstageIdentity: { id: profile.email.split('@')[0] },
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...response,
|
||||
backstageIdentity: {
|
||||
id: users[0].metadata.name,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const createGoogleProvider: AuthProviderFactory = ({
|
||||
globalConfig,
|
||||
config,
|
||||
logger,
|
||||
tokenIssuer,
|
||||
discovery,
|
||||
}) =>
|
||||
OAuthEnvironmentHandler.mapConfig(config, envConfig => {
|
||||
const providerId = 'google';
|
||||
@@ -160,6 +198,8 @@ export const createGoogleProvider: AuthProviderFactory = ({
|
||||
clientId,
|
||||
clientSecret,
|
||||
callbackUrl,
|
||||
logger,
|
||||
identityClient: new CatalogIdentityClient({ discovery }),
|
||||
});
|
||||
|
||||
return OAuthAdapter.fromConfig(globalConfig, provider, {
|
||||
|
||||
@@ -18,6 +18,7 @@ import express from 'express';
|
||||
import { Logger } from 'winston';
|
||||
import { TokenIssuer } from '../identity';
|
||||
import { Config } from '@backstage/config';
|
||||
import { PluginEndpointDiscovery } from '@backstage/backend-common';
|
||||
|
||||
export type AuthProviderConfig = {
|
||||
/**
|
||||
@@ -115,6 +116,7 @@ export type AuthProviderFactoryOptions = {
|
||||
config: Config;
|
||||
logger: Logger;
|
||||
tokenIssuer: TokenIssuer;
|
||||
discovery: PluginEndpointDiscovery;
|
||||
};
|
||||
|
||||
export type AuthProviderFactory = (
|
||||
|
||||
@@ -34,20 +34,20 @@ export interface RouterOptions {
|
||||
discovery: PluginEndpointDiscovery;
|
||||
}
|
||||
|
||||
export async function createRouter(
|
||||
options: RouterOptions,
|
||||
): Promise<express.Router> {
|
||||
export async function createRouter({
|
||||
logger,
|
||||
config,
|
||||
discovery,
|
||||
database,
|
||||
}: RouterOptions): Promise<express.Router> {
|
||||
const router = Router();
|
||||
const logger = options.logger.child({ plugin: 'auth' });
|
||||
|
||||
const appUrl = options.config.getString('app.baseUrl');
|
||||
const authUrl = await options.discovery.getExternalBaseUrl('auth');
|
||||
const appUrl = config.getString('app.baseUrl');
|
||||
const authUrl = await discovery.getExternalBaseUrl('auth');
|
||||
|
||||
const keyDurationSeconds = 3600;
|
||||
|
||||
const keyStore = await DatabaseKeyStore.create({
|
||||
database: options.database,
|
||||
});
|
||||
const keyStore = await DatabaseKeyStore.create({ database });
|
||||
const tokenIssuer = new TokenFactory({
|
||||
issuer: authUrl,
|
||||
keyStore,
|
||||
@@ -59,7 +59,7 @@ export async function createRouter(
|
||||
router.use(express.urlencoded({ extended: false }));
|
||||
router.use(express.json());
|
||||
|
||||
const providersConfig = options.config.getConfig('auth.providers');
|
||||
const providersConfig = config.getConfig('auth.providers');
|
||||
const providers = providersConfig.keys();
|
||||
|
||||
for (const providerId of providers) {
|
||||
@@ -70,6 +70,7 @@ export async function createRouter(
|
||||
config: providersConfig.getConfig(providerId),
|
||||
logger,
|
||||
tokenIssuer,
|
||||
discovery,
|
||||
});
|
||||
|
||||
const r = Router();
|
||||
|
||||
Reference in New Issue
Block a user