Encode env in OAuth flow into the state parameter. refs #1775
`Env` passed as a separate parameter in the request might break the flow of the authorization flow. Spotify considers the `env` as a first class abstraction and use it across their application stack. To ensure both the existing flow and newer flows are supported, use the `state` parameter to encode an object, consisting of the `nonce` and the `env` which is then passed to the authorization server. This parameter is returned to the application in the callbackURL
This commit is contained in:
@@ -25,13 +25,28 @@ export class EnvironmentHandler implements AuthProviderRouteHandlers {
|
||||
constructor(
|
||||
private readonly providerId: string,
|
||||
private readonly providers: EnvironmentHandlers,
|
||||
private readonly envIdentifier: (req: express.Request) => string,
|
||||
) {}
|
||||
|
||||
/* Return the value of `env` key, if it is exists, encoded within
|
||||
the `state` parameter in the request
|
||||
*/
|
||||
private getEnv(stateParams: Array<string>): string {
|
||||
const envParams = stateParams.filter(
|
||||
param => param.split('=')[0] === 'env',
|
||||
);
|
||||
|
||||
if (envParams.length > 0) {
|
||||
return envParams[0].split('=')[1];
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
private getProviderForEnv(
|
||||
req: express.Request,
|
||||
res: express.Response,
|
||||
): AuthProviderRouteHandlers | undefined {
|
||||
const env = req.query.env?.toString();
|
||||
const env: string = this.envIdentifier(req);
|
||||
|
||||
if (env && this.providers.hasOwnProperty(env)) {
|
||||
return this.providers[env];
|
||||
|
||||
@@ -39,6 +39,21 @@ export type Options = {
|
||||
tokenIssuer: TokenIssuer;
|
||||
};
|
||||
|
||||
/* Return the value of `env` key, if it is exists, encoded within
|
||||
the `state` parameter in the request
|
||||
*/
|
||||
const getEnv = (stateParams: Array<string>): string => {
|
||||
const envParams = stateParams.filter(param => param.split('=')[0] === 'env');
|
||||
|
||||
if (envParams.length > 0) {
|
||||
return envParams[0].split('=')[1];
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const readState = (stateString: string): Array<string> => {
|
||||
return decodeURIComponent(stateString).split('&');
|
||||
};
|
||||
export const verifyNonce = (req: express.Request, providerId: string) => {
|
||||
const cookieNonce = req.cookies[`${providerId}-nonce`];
|
||||
const stateNonce = req.query.state;
|
||||
@@ -103,6 +118,7 @@ export class OAuthProvider implements AuthProviderRouteHandlers {
|
||||
async start(req: express.Request, res: express.Response): Promise<void> {
|
||||
// retrieve scopes from request
|
||||
const scope = req.query.scope?.toString() ?? '';
|
||||
const env = req.query.env?.toString() ?? '';
|
||||
|
||||
if (this.options.persistScopes) {
|
||||
this.setScopesCookie(res, scope);
|
||||
@@ -112,9 +128,21 @@ export class OAuthProvider implements AuthProviderRouteHandlers {
|
||||
// set a nonce cookie before redirecting to oauth provider
|
||||
this.setNonceCookie(res, nonce);
|
||||
|
||||
// const stateObject: {nonce: string,
|
||||
// env: string}
|
||||
|
||||
const stateObject = { nonce: nonce, env: env };
|
||||
|
||||
const state = Object.keys(stateObject)
|
||||
.map(
|
||||
key =>
|
||||
`${encodeURIComponent(key)}=${encodeURIComponent(stateObject[key])}`,
|
||||
)
|
||||
.join('&');
|
||||
|
||||
const queryParameters = {
|
||||
scope,
|
||||
state: nonce,
|
||||
state: state,
|
||||
};
|
||||
|
||||
const { url, status } = await this.providerHandlers.start(
|
||||
@@ -224,6 +252,19 @@ export class OAuthProvider implements AuthProviderRouteHandlers {
|
||||
}
|
||||
}
|
||||
|
||||
identifyEnv(req: express.Request): string {
|
||||
const reqEnv = req.query.env?.toString();
|
||||
if (reqEnv) {
|
||||
return reqEnv;
|
||||
}
|
||||
const stateParam = req.query.state?.toString();
|
||||
if (!stateParam) {
|
||||
return '';
|
||||
}
|
||||
const env = getEnv(readState(stateParam));
|
||||
return env;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
import Router from 'express-promise-router';
|
||||
import express from 'express';
|
||||
import { Logger } from 'winston';
|
||||
import { TokenIssuer } from '../identity';
|
||||
import { createGithubProvider } from './github';
|
||||
@@ -54,16 +55,24 @@ export const createAuthProviderRouter = (
|
||||
const router = Router();
|
||||
const envs = providerConfig.keys();
|
||||
const envProviders: EnvironmentHandlers = {};
|
||||
let envIdentifier: (req: express.Request) => string;
|
||||
|
||||
for (const env of envs) {
|
||||
const envConfig = providerConfig.getConfig(env);
|
||||
const provider = factory(globalConfig, env, envConfig, logger, issuer);
|
||||
if (provider) {
|
||||
envProviders[env] = provider;
|
||||
if (envIdentifier === undefined) {
|
||||
envIdentifier = provider.identifyEnv;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handler = new EnvironmentHandler(providerId, envProviders);
|
||||
const handler = new EnvironmentHandler(
|
||||
providerId,
|
||||
envProviders,
|
||||
envIdentifier,
|
||||
);
|
||||
|
||||
router.get('/start', handler.start.bind(handler));
|
||||
router.get('/handler/frame', handler.frameHandler.bind(handler));
|
||||
|
||||
@@ -143,7 +143,7 @@ export class OAuth2AuthProvider implements OAuthProviderHandlers {
|
||||
|
||||
export function createOAuth2Provider(
|
||||
{ baseUrl }: AuthProviderConfig,
|
||||
env: string,
|
||||
_: string,
|
||||
envConfig: Config,
|
||||
logger: Logger,
|
||||
tokenIssuer: TokenIssuer,
|
||||
@@ -153,7 +153,7 @@ export function createOAuth2Provider(
|
||||
const appOrigin = envConfig.getString('appOrigin');
|
||||
const clientID = envConfig.getString('clientId');
|
||||
const clientSecret = envConfig.getString('clientSecret');
|
||||
const callbackURL = `${baseUrl}/${providerId}/handler/frame?env=${env}`;
|
||||
const callbackURL = `${baseUrl}/${providerId}/handler/frame`;
|
||||
const authorizationURL = envConfig.getString('authorizationURL');
|
||||
const tokenURL = envConfig.getString('tokenURL');
|
||||
|
||||
|
||||
@@ -106,6 +106,10 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers {
|
||||
async logout(_req: express.Request, res: express.Response): Promise<void> {
|
||||
res.send('noop');
|
||||
}
|
||||
|
||||
identifyEnv(_req: express.Request): string {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
type SAMLProviderOptions = {
|
||||
|
||||
@@ -203,6 +203,15 @@ export interface AuthProviderRouteHandlers {
|
||||
* @param {express.Response} res
|
||||
*/
|
||||
logout?(req: express.Request, res: express.Response): Promise<void>;
|
||||
|
||||
/**
|
||||
*(Optional) A method to identify the environment Context of the Request
|
||||
*
|
||||
*Request
|
||||
*- contains the environment context information encoded in the request
|
||||
* @param {express.Request} req
|
||||
*/
|
||||
identifyEnv?(req: express.Request): string;
|
||||
}
|
||||
|
||||
export type AuthProviderFactory = (
|
||||
|
||||
Reference in New Issue
Block a user