gristlabs_grist-core/app/server/lib/oidc/Protections.ts
Florent fde6c8142d
Support nonce and acr with OIDC + other improvements and tests (#883)
* Introduces new configuration variables for OIDC:
  - GRIST_OIDC_IDP_ENABLED_PROTECTIONS
  - GRIST_OIDC_IDP_ACR_VALUES
  - GRIST_OIDC_IDP_EXTRA_CLIENT_METADATA
* Implements all supported protections in oidc/Protections.ts
* Includes a better error page for failed OIDC logins
* Includes some other improvements, e.g. to logging, to OIDC
* Adds a large unit test for OIDCConfig
* Adds support for SERVER_NODE_OPTIONS for running tests
* Adds to documentation/develop.md info about GREP_TESTS, VERBOSE, and SERVER_NODE_OPTIONS.
2024-08-08 15:35:37 -04:00

122 lines
3.6 KiB
TypeScript

import { StringUnion } from 'app/common/StringUnion';
import { SessionOIDCInfo } from 'app/server/lib/BrowserSession';
import { AuthorizationParameters, generators, OpenIDCallbackChecks } from 'openid-client';
export const EnabledProtection = StringUnion(
"STATE",
"NONCE",
"PKCE",
);
export type EnabledProtectionString = typeof EnabledProtection.type;
interface Protection {
generateSessionInfo(): SessionOIDCInfo;
forgeAuthUrlParams(sessionInfo: SessionOIDCInfo): AuthorizationParameters;
getCallbackChecks(sessionInfo: SessionOIDCInfo): OpenIDCallbackChecks;
}
function checkIsSet(value: string|undefined, message: string): string {
if (!value) { throw new Error(message); }
return value;
}
class PKCEProtection implements Protection {
public generateSessionInfo(): SessionOIDCInfo {
return {
code_verifier: generators.codeVerifier()
};
}
public forgeAuthUrlParams(sessionInfo: SessionOIDCInfo): AuthorizationParameters {
return {
code_challenge: generators.codeChallenge(checkIsSet(sessionInfo.code_verifier, "Login is stale")),
code_challenge_method: 'S256'
};
}
public getCallbackChecks(sessionInfo: SessionOIDCInfo): OpenIDCallbackChecks {
return {
code_verifier: checkIsSet(sessionInfo.code_verifier, "Login is stale")
};
}
}
class NonceProtection implements Protection {
public generateSessionInfo(): SessionOIDCInfo {
return {
nonce: generators.nonce()
};
}
public forgeAuthUrlParams(sessionInfo: SessionOIDCInfo): AuthorizationParameters {
return {
nonce: sessionInfo.nonce
};
}
public getCallbackChecks(sessionInfo: SessionOIDCInfo): OpenIDCallbackChecks {
return {
nonce: checkIsSet(sessionInfo.nonce, "Login is stale")
};
}
}
class StateProtection implements Protection {
public generateSessionInfo(): SessionOIDCInfo {
return {
state: generators.state()
};
}
public forgeAuthUrlParams(sessionInfo: SessionOIDCInfo): AuthorizationParameters {
return {
state: sessionInfo.state
};
}
public getCallbackChecks(sessionInfo: SessionOIDCInfo): OpenIDCallbackChecks {
return {
state: checkIsSet(sessionInfo.state, "Login or logout failed to complete")
};
}
}
export class ProtectionsManager implements Protection {
private _protections: Protection[] = [];
constructor(private _enabledProtections: Set<EnabledProtectionString>) {
if (this._enabledProtections.has('STATE')) {
this._protections.push(new StateProtection());
}
if (this._enabledProtections.has('NONCE')) {
this._protections.push(new NonceProtection());
}
if (this._enabledProtections.has('PKCE')) {
this._protections.push(new PKCEProtection());
}
}
public generateSessionInfo(): SessionOIDCInfo {
const sessionInfo: SessionOIDCInfo = {};
for (const protection of this._protections) {
Object.assign(sessionInfo, protection.generateSessionInfo());
}
return sessionInfo;
}
public forgeAuthUrlParams(sessionInfo: SessionOIDCInfo): AuthorizationParameters {
const authParams: AuthorizationParameters = {};
for (const protection of this._protections) {
Object.assign(authParams, protection.forgeAuthUrlParams(sessionInfo));
}
return authParams;
}
public getCallbackChecks(sessionInfo: SessionOIDCInfo): OpenIDCallbackChecks {
const checks: OpenIDCallbackChecks = {};
for (const protection of this._protections) {
Object.assign(checks, protection.getCallbackChecks(sessionInfo));
}
return checks;
}
public supportsProtection(protection: EnabledProtectionString) {
return this._enabledProtections.has(protection);
}
}