|
|
|
@ -2,10 +2,13 @@ import {OIDCConfig} from "app/server/lib/OIDCConfig";
|
|
|
|
|
import {assert} from "chai";
|
|
|
|
|
import {EnvironmentSnapshot} from "../testUtils";
|
|
|
|
|
import Sinon from "sinon";
|
|
|
|
|
import {Client} from "openid-client";
|
|
|
|
|
import {Client, generators} from "openid-client";
|
|
|
|
|
import express from "express";
|
|
|
|
|
import log from "app/server/lib/log";
|
|
|
|
|
import _ from "lodash";
|
|
|
|
|
|
|
|
|
|
class OIDCConfigStubbed extends OIDCConfig {
|
|
|
|
|
public static async build(clientStub?: Client): Promise<OIDCConfig> {
|
|
|
|
|
public static async build(clientStub?: Client): Promise<OIDCConfigStubbed> {
|
|
|
|
|
const result = new OIDCConfigStubbed();
|
|
|
|
|
if (clientStub) {
|
|
|
|
|
result._initClient = Sinon.spy(() => {
|
|
|
|
@ -20,6 +23,8 @@ class OIDCConfigStubbed extends OIDCConfig {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class ClientStub {
|
|
|
|
|
public static FAKE_REDIRECT_URL = 'FAKE_REDIRECT_URL';
|
|
|
|
|
public authorizationUrl = Sinon.stub().returns(ClientStub.FAKE_REDIRECT_URL);
|
|
|
|
|
public issuer: {
|
|
|
|
|
metadata: {
|
|
|
|
|
end_session_endpoint: string | undefined;
|
|
|
|
@ -29,11 +34,18 @@ class ClientStub {
|
|
|
|
|
end_session_endpoint: 'http://localhost:8484/logout',
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
public asClient() {
|
|
|
|
|
return this as unknown as Client;
|
|
|
|
|
}
|
|
|
|
|
public getAuthorizationUrlStub() {
|
|
|
|
|
return this.authorizationUrl;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
describe('OIDCConfig', () => {
|
|
|
|
|
let oldEnv: EnvironmentSnapshot;
|
|
|
|
|
let sandbox: Sinon.SinonSandbox;
|
|
|
|
|
let logInfoStub: Sinon.SinonStub;
|
|
|
|
|
|
|
|
|
|
before(() => {
|
|
|
|
|
oldEnv = new EnvironmentSnapshot();
|
|
|
|
@ -41,6 +53,7 @@ describe('OIDCConfig', () => {
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
sandbox = Sinon.createSandbox();
|
|
|
|
|
logInfoStub = sandbox.stub(log, 'info');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
@ -53,7 +66,6 @@ describe('OIDCConfig', () => {
|
|
|
|
|
process.env.GRIST_OIDC_IDP_CLIENT_ID = 'client id';
|
|
|
|
|
process.env.GRIST_OIDC_IDP_CLIENT_SECRET = 'secret';
|
|
|
|
|
process.env.GRIST_OIDC_IDP_ISSUER = 'http://localhost:8000';
|
|
|
|
|
process.env.GRIST_OIDC_IDP_SCOPES = 'openid email profile';
|
|
|
|
|
process.env.GRIST_OIDC_SP_PROFILE_NAME_ATTR = ''; // use the default behavior
|
|
|
|
|
process.env.GRIST_OIDC_SP_PROFILE_EMAIL_ATTR = ''; // use the default behavior
|
|
|
|
|
}
|
|
|
|
@ -80,6 +92,23 @@ describe('OIDCConfig', () => {
|
|
|
|
|
await assert.isRejected(promise, 'client init failed');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should create a client with passed information', async () => {
|
|
|
|
|
setEnvVars();
|
|
|
|
|
const client = new ClientStub();
|
|
|
|
|
const config = await OIDCConfigStubbed.build(client.asClient());
|
|
|
|
|
assert.isTrue(config._initClient.calledOnce);
|
|
|
|
|
assert.deepEqual(config._initClient.firstCall.args, [{
|
|
|
|
|
clientId: process.env.GRIST_OIDC_IDP_CLIENT_ID,
|
|
|
|
|
clientSecret: process.env.GRIST_OIDC_IDP_CLIENT_SECRET,
|
|
|
|
|
issuerUrl: process.env.GRIST_OIDC_IDP_ISSUER,
|
|
|
|
|
}]);
|
|
|
|
|
assert.isTrue(logInfoStub.calledOnce);
|
|
|
|
|
assert.deepEqual(
|
|
|
|
|
logInfoStub.firstCall.args,
|
|
|
|
|
[`OIDCConfig: initialized with issuer ${process.env.GRIST_OIDC_IDP_ISSUER}`]
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('End Session Endpoint', () => {
|
|
|
|
|
[
|
|
|
|
|
{
|
|
|
|
@ -120,11 +149,13 @@ describe('OIDCConfig', () => {
|
|
|
|
|
Object.assign(process.env, ctx.env);
|
|
|
|
|
const client = new ClientStub();
|
|
|
|
|
client.issuer.metadata.end_session_endpoint = ctx.end_session_endpoint;
|
|
|
|
|
const promise = OIDCConfigStubbed.build(client as Client);
|
|
|
|
|
const promise = OIDCConfigStubbed.build(client.asClient());
|
|
|
|
|
if (ctx.errorMsg) {
|
|
|
|
|
await assert.isRejected(promise, ctx.errorMsg);
|
|
|
|
|
assert.isFalse(logInfoStub.calledOnce);
|
|
|
|
|
} else {
|
|
|
|
|
await assert.isFulfilled(promise);
|
|
|
|
|
assert.isTrue(logInfoStub.calledOnce);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
@ -142,7 +173,7 @@ describe('OIDCConfig', () => {
|
|
|
|
|
it('should successfully change the supported protections', async function () {
|
|
|
|
|
setEnvVars();
|
|
|
|
|
process.env.GRIST_OIDC_IDP_ENABLED_PROTECTIONS = 'NONCE';
|
|
|
|
|
const config = await OIDCConfigStubbed.build(new ClientStub() as Client);
|
|
|
|
|
const config = await OIDCConfigStubbed.build((new ClientStub()).asClient());
|
|
|
|
|
assert.isTrue(config.supportsProtection("NONCE"));
|
|
|
|
|
assert.isFalse(config.supportsProtection("PKCE"));
|
|
|
|
|
assert.isFalse(config.supportsProtection("STATE"));
|
|
|
|
@ -150,10 +181,163 @@ describe('OIDCConfig', () => {
|
|
|
|
|
|
|
|
|
|
it('if omitted, should defaults to "STATE,PKCE"', async function () {
|
|
|
|
|
setEnvVars();
|
|
|
|
|
const config = await OIDCConfigStubbed.build(new ClientStub() as Client);
|
|
|
|
|
const config = await OIDCConfigStubbed.build((new ClientStub()).asClient());
|
|
|
|
|
assert.isFalse(config.supportsProtection("NONCE"));
|
|
|
|
|
assert.isTrue(config.supportsProtection("PKCE"));
|
|
|
|
|
assert.isTrue(config.supportsProtection("STATE"));
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('getLoginRedirectUrl', () => {
|
|
|
|
|
const FAKE_NONCE = 'fake-nonce';
|
|
|
|
|
const FAKE_STATE = 'fake-state';
|
|
|
|
|
const FAKE_CODE_VERIFIER = 'fake-code-verifier';
|
|
|
|
|
const FAKE_CODE_CHALLENGE = 'fake-code-challenge';
|
|
|
|
|
const TARGET_URL = 'http://localhost:8484/';
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
sandbox.stub(generators, 'nonce').returns(FAKE_NONCE);
|
|
|
|
|
sandbox.stub(generators, 'state').returns(FAKE_STATE);
|
|
|
|
|
sandbox.stub(generators, 'codeVerifier').returns(FAKE_CODE_VERIFIER);
|
|
|
|
|
sandbox.stub(generators, 'codeChallenge').returns(FAKE_CODE_CHALLENGE);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
[
|
|
|
|
|
{
|
|
|
|
|
itMsg: 'should forge the url with default values',
|
|
|
|
|
expectedCalledWith: [{
|
|
|
|
|
scope: 'openid email profile',
|
|
|
|
|
acr_values: undefined,
|
|
|
|
|
code_challenge: FAKE_CODE_CHALLENGE,
|
|
|
|
|
code_challenge_method: 'S256',
|
|
|
|
|
state: FAKE_STATE,
|
|
|
|
|
}],
|
|
|
|
|
expectedSession: {
|
|
|
|
|
oidc: {
|
|
|
|
|
codeVerifier: FAKE_CODE_VERIFIER,
|
|
|
|
|
state: FAKE_STATE,
|
|
|
|
|
targetUrl: TARGET_URL,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
itMsg: 'should forge the URL with passed GRIST_OIDC_IDP_SCOPES',
|
|
|
|
|
env: {
|
|
|
|
|
GRIST_OIDC_IDP_SCOPES: 'my scopes',
|
|
|
|
|
},
|
|
|
|
|
expectedCalledWith: [{
|
|
|
|
|
scope: 'my scopes',
|
|
|
|
|
acr_values: undefined,
|
|
|
|
|
code_challenge: FAKE_CODE_CHALLENGE,
|
|
|
|
|
code_challenge_method: 'S256',
|
|
|
|
|
state: FAKE_STATE,
|
|
|
|
|
}],
|
|
|
|
|
expectedSession: {
|
|
|
|
|
oidc: {
|
|
|
|
|
codeVerifier: FAKE_CODE_VERIFIER,
|
|
|
|
|
state: FAKE_STATE,
|
|
|
|
|
targetUrl: TARGET_URL,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
itMsg: 'should pass the nonce when GRIST_OIDC_IDP_ENABLED_PROTECTIONS includes NONCE',
|
|
|
|
|
env: {
|
|
|
|
|
GRIST_OIDC_IDP_ENABLED_PROTECTIONS: 'STATE,NONCE,PKCE',
|
|
|
|
|
},
|
|
|
|
|
expectedCalledWith: [{
|
|
|
|
|
scope: 'openid email profile',
|
|
|
|
|
acr_values: undefined,
|
|
|
|
|
code_challenge: FAKE_CODE_CHALLENGE,
|
|
|
|
|
code_challenge_method: 'S256',
|
|
|
|
|
state: FAKE_STATE,
|
|
|
|
|
nonce: FAKE_NONCE,
|
|
|
|
|
}],
|
|
|
|
|
expectedSession: {
|
|
|
|
|
oidc: {
|
|
|
|
|
codeVerifier: FAKE_CODE_VERIFIER,
|
|
|
|
|
nonce: FAKE_NONCE,
|
|
|
|
|
state: FAKE_STATE,
|
|
|
|
|
targetUrl: TARGET_URL,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
itMsg: 'should not pass the code_challenge when PKCE is omitted in GRIST_OIDC_IDP_ENABLED_PROTECTIONS',
|
|
|
|
|
env: {
|
|
|
|
|
GRIST_OIDC_IDP_ENABLED_PROTECTIONS: 'STATE,NONCE',
|
|
|
|
|
},
|
|
|
|
|
expectedCalledWith: [{
|
|
|
|
|
scope: 'openid email profile',
|
|
|
|
|
acr_values: undefined,
|
|
|
|
|
state: FAKE_STATE,
|
|
|
|
|
nonce: FAKE_NONCE,
|
|
|
|
|
}],
|
|
|
|
|
expectedSession: {
|
|
|
|
|
oidc: {
|
|
|
|
|
nonce: FAKE_NONCE,
|
|
|
|
|
state: FAKE_STATE,
|
|
|
|
|
targetUrl: TARGET_URL,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
].forEach(ctx => {
|
|
|
|
|
it(ctx.itMsg, async () => {
|
|
|
|
|
setEnvVars();
|
|
|
|
|
Object.assign(process.env, ctx.env);
|
|
|
|
|
const clientStub = new ClientStub();
|
|
|
|
|
const config = await OIDCConfigStubbed.build(clientStub.asClient());
|
|
|
|
|
const session = {};
|
|
|
|
|
const req = {
|
|
|
|
|
session
|
|
|
|
|
} as unknown as express.Request;
|
|
|
|
|
const url = await config.getLoginRedirectUrl(req, new URL(TARGET_URL));
|
|
|
|
|
assert.equal(url, ClientStub.FAKE_REDIRECT_URL);
|
|
|
|
|
assert.isTrue(clientStub.authorizationUrl.calledOnce);
|
|
|
|
|
assert.deepEqual(clientStub.authorizationUrl.firstCall.args, ctx.expectedCalledWith);
|
|
|
|
|
assert.deepEqual(session, ctx.expectedSession);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// describe('handleCallback', () => {
|
|
|
|
|
// const FAKE_STATE = 'fake-state';
|
|
|
|
|
// const FAKE_CODE_VERIFIER = 'fake-code-verifier';
|
|
|
|
|
// const FAKE_CODE_CHALLENGE = 'fake-code-challenge';
|
|
|
|
|
// const FAKE_NONCE = 'fake-nonce';
|
|
|
|
|
// const FAKE_CODE = 'fake-code';
|
|
|
|
|
// let fakeRes: express.Response;
|
|
|
|
|
|
|
|
|
|
// beforeEach(() => {
|
|
|
|
|
// fakeRes = {
|
|
|
|
|
// redirect: sandbox.stub(),
|
|
|
|
|
// status: sandbox.stub().returnsThis(),
|
|
|
|
|
// send: sandbox.stub().returnsThis(),
|
|
|
|
|
// } as unknown as express.Response;
|
|
|
|
|
// });
|
|
|
|
|
|
|
|
|
|
// [
|
|
|
|
|
// {
|
|
|
|
|
// itMsg: 'should reject when the state is not found in the session',
|
|
|
|
|
// session: {},
|
|
|
|
|
// expectedErrorMsg: /Login or logout failed to complete/,
|
|
|
|
|
// }
|
|
|
|
|
// ].forEach(ctx => {
|
|
|
|
|
// it(ctx.itMsg, async () => {
|
|
|
|
|
// setEnvVars();
|
|
|
|
|
// const clientStub = new ClientStub();
|
|
|
|
|
// const config = await OIDCConfigStubbed.build(clientStub.asClient());
|
|
|
|
|
// const req = {
|
|
|
|
|
// session: ctx.session,
|
|
|
|
|
// query: {
|
|
|
|
|
// state: FAKE_STATE,
|
|
|
|
|
// code: FAKE_CODE,
|
|
|
|
|
// }
|
|
|
|
|
// } as unknown as express.Request;
|
|
|
|
|
// const promise = config.handleCallback(req, fakeRes);
|
|
|
|
|
// await assert.isRejected(promise, ctx.expectedErrorMsg);
|
|
|
|
|
// });
|
|
|
|
|
// });
|
|
|
|
|
// });
|
|
|
|
|
|
|
|
|
|
});
|
|
|
|
|