mirror of https://github.com/boxyhq/jackson.git
Enhancements (#584)
* Throw error if `entityID` is missing * Use `JacksonError` instead of Error * Type enhancements - use `SAMLSSORecord` * Better typing with `OIDCSSORecord` * Add types for response * Update swagger * Sync package lock * Assert connection record type in tests * Mark `@deprecated` for config methods * Mark `openid` as optional * Gaurd against nullish * Fix test * Add entityID check for update op, add tests * Cleanup `t.end()`, not required for `async` tests * Remove oidcPath check in defaultOpts * Return error if `oidcPath` is empty in authorize for OIDC Connection * Add missing `async` * Fail connection add/update if `oidcPath` is not set * Type alignment * Update swagger spec * Fix type for `oidcPath` * Cleanup * Add missing return types and fix type for `getConfig` * Bump up version * Update swagger spec * Remove uffizzi from ignore file Co-authored-by: Kiran <kiran@boxyhq.com>
This commit is contained in:
parent
ddc0c511a8
commit
2e5da524cf
|
@ -9,5 +9,4 @@ README.md
|
||||||
_dev
|
_dev
|
||||||
.vscode
|
.vscode
|
||||||
swagger
|
swagger
|
||||||
uffizzi
|
|
||||||
.husky
|
.husky
|
|
@ -9,6 +9,9 @@ import {
|
||||||
SAMLSSOConnectionWithEncodedMetadata,
|
SAMLSSOConnectionWithEncodedMetadata,
|
||||||
SAMLSSOConnectionWithRawMetadata,
|
SAMLSSOConnectionWithRawMetadata,
|
||||||
OIDCSSOConnection,
|
OIDCSSOConnection,
|
||||||
|
JacksonOption,
|
||||||
|
SAMLSSORecord,
|
||||||
|
OIDCSSORecord,
|
||||||
} from '../typings';
|
} from '../typings';
|
||||||
import { JacksonError } from './error';
|
import { JacksonError } from './error';
|
||||||
import { IndexNames } from './utils';
|
import { IndexNames } from './utils';
|
||||||
|
@ -17,9 +20,11 @@ import samlConnection from './connection/saml';
|
||||||
|
|
||||||
export class ConnectionAPIController implements IConnectionAPIController {
|
export class ConnectionAPIController implements IConnectionAPIController {
|
||||||
private connectionStore: Storable;
|
private connectionStore: Storable;
|
||||||
|
private opts: JacksonOption;
|
||||||
|
|
||||||
constructor({ connectionStore }) {
|
constructor({ connectionStore, opts }) {
|
||||||
this.connectionStore = connectionStore;
|
this.connectionStore = connectionStore;
|
||||||
|
this.opts = opts;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -144,6 +149,8 @@ export class ConnectionAPIController implements IConnectionAPIController {
|
||||||
* $ref: '#/definitions/validationErrorsPost'
|
* $ref: '#/definitions/validationErrorsPost'
|
||||||
* 401:
|
* 401:
|
||||||
* description: Unauthorized
|
* description: Unauthorized
|
||||||
|
* 500:
|
||||||
|
* description: Please set OpenID response handler path (oidcPath) on Jackson
|
||||||
* /api/v1/connections:
|
* /api/v1/connections:
|
||||||
* post:
|
* post:
|
||||||
* summary: Create SSO connection
|
* summary: Create SSO connection
|
||||||
|
@ -178,21 +185,29 @@ export class ConnectionAPIController implements IConnectionAPIController {
|
||||||
*/
|
*/
|
||||||
public async createSAMLConnection(
|
public async createSAMLConnection(
|
||||||
body: SAMLSSOConnectionWithEncodedMetadata | SAMLSSOConnectionWithRawMetadata
|
body: SAMLSSOConnectionWithEncodedMetadata | SAMLSSOConnectionWithRawMetadata
|
||||||
): Promise<any> {
|
): Promise<SAMLSSORecord> {
|
||||||
metrics.increment('createConnection');
|
metrics.increment('createConnection');
|
||||||
const record = await samlConnection.create(body, this.connectionStore);
|
|
||||||
return record;
|
return await samlConnection.create(body, this.connectionStore);
|
||||||
}
|
}
|
||||||
|
|
||||||
// For backwards compatibility
|
// For backwards compatibility
|
||||||
public async config(...args: Parameters<ConnectionAPIController['createSAMLConnection']>): Promise<any> {
|
public async config(
|
||||||
|
...args: Parameters<ConnectionAPIController['createSAMLConnection']>
|
||||||
|
): Promise<SAMLSSORecord> {
|
||||||
return this.createSAMLConnection(...args);
|
return this.createSAMLConnection(...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async createOIDCConnection(body: OIDCSSOConnection): Promise<any> {
|
public async createOIDCConnection(body: OIDCSSOConnection): Promise<OIDCSSORecord> {
|
||||||
metrics.increment('createConnection');
|
metrics.increment('createConnection');
|
||||||
const record = await oidcConnection.create(body, this.connectionStore);
|
|
||||||
return record;
|
if (!this.opts.oidcPath) {
|
||||||
|
throw new JacksonError('Please set OpenID response handler path (oidcPath) on Jackson', 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await oidcConnection.create(body, this.connectionStore);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @swagger
|
* @swagger
|
||||||
* definitions:
|
* definitions:
|
||||||
|
@ -324,6 +339,8 @@ export class ConnectionAPIController implements IConnectionAPIController {
|
||||||
* $ref: '#/definitions/validationErrorsPatch'
|
* $ref: '#/definitions/validationErrorsPatch'
|
||||||
* 401:
|
* 401:
|
||||||
* description: Unauthorized
|
* description: Unauthorized
|
||||||
|
* 500:
|
||||||
|
* description: Please set OpenID response handler path (oidcPath) on Jackson
|
||||||
*/
|
*/
|
||||||
public async updateSAMLConnection(
|
public async updateSAMLConnection(
|
||||||
body: (SAMLSSOConnectionWithEncodedMetadata | SAMLSSOConnectionWithRawMetadata) & {
|
body: (SAMLSSOConnectionWithEncodedMetadata | SAMLSSOConnectionWithRawMetadata) & {
|
||||||
|
@ -337,14 +354,20 @@ export class ConnectionAPIController implements IConnectionAPIController {
|
||||||
// For backwards compatibility
|
// For backwards compatibility
|
||||||
public async updateConfig(
|
public async updateConfig(
|
||||||
...args: Parameters<ConnectionAPIController['updateSAMLConnection']>
|
...args: Parameters<ConnectionAPIController['updateSAMLConnection']>
|
||||||
): Promise<any> {
|
): Promise<void> {
|
||||||
await this.updateSAMLConnection(...args);
|
await this.updateSAMLConnection(...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateOIDCConnection(
|
public async updateOIDCConnection(
|
||||||
body: OIDCSSOConnection & { clientID: string; clientSecret: string }
|
body: OIDCSSOConnection & { clientID: string; clientSecret: string }
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
if (!this.opts.oidcPath) {
|
||||||
|
throw new JacksonError('Please set OpenID response handler path (oidcPath) on Jackson', 500);
|
||||||
|
}
|
||||||
|
|
||||||
await oidcConnection.update(body, this.connectionStore, this.getConnections.bind(this));
|
await oidcConnection.update(body, this.connectionStore, this.getConnections.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @swagger
|
* @swagger
|
||||||
* parameters:
|
* parameters:
|
||||||
|
@ -363,6 +386,11 @@ export class ConnectionAPIController implements IConnectionAPIController {
|
||||||
* name: clientID
|
* name: clientID
|
||||||
* type: string
|
* type: string
|
||||||
* description: Client ID
|
* description: Client ID
|
||||||
|
* strategyParamGet:
|
||||||
|
* in: query
|
||||||
|
* name: strategy
|
||||||
|
* type: string
|
||||||
|
* description: Strategy which can help to filter connections with tenant/product query
|
||||||
* definitions:
|
* definitions:
|
||||||
* Connection:
|
* Connection:
|
||||||
* type: object
|
* type: object
|
||||||
|
@ -418,6 +446,7 @@ export class ConnectionAPIController implements IConnectionAPIController {
|
||||||
* - $ref: '#/parameters/tenantParamGet'
|
* - $ref: '#/parameters/tenantParamGet'
|
||||||
* - $ref: '#/parameters/productParamGet'
|
* - $ref: '#/parameters/productParamGet'
|
||||||
* - $ref: '#/parameters/clientIDParamGet'
|
* - $ref: '#/parameters/clientIDParamGet'
|
||||||
|
* - $ref: '#/parameters/strategyParamGet'
|
||||||
* operationId: get-connections
|
* operationId: get-connections
|
||||||
* tags: [Connections]
|
* tags: [Connections]
|
||||||
* responses:
|
* responses:
|
||||||
|
@ -428,7 +457,7 @@ export class ConnectionAPIController implements IConnectionAPIController {
|
||||||
* '401':
|
* '401':
|
||||||
* $ref: '#/responses/401Get'
|
* $ref: '#/responses/401Get'
|
||||||
*/
|
*/
|
||||||
public async getConnections(body: GetConnectionsQuery): Promise<Array<any>> {
|
public async getConnections(body: GetConnectionsQuery): Promise<Array<SAMLSSORecord | OIDCSSORecord>> {
|
||||||
const clientID = 'clientID' in body ? body.clientID : undefined;
|
const clientID = 'clientID' in body ? body.clientID : undefined;
|
||||||
const tenant = 'tenant' in body ? body.tenant : undefined;
|
const tenant = 'tenant' in body ? body.tenant : undefined;
|
||||||
const product = 'product' in body ? body.product : undefined;
|
const product = 'product' in body ? body.product : undefined;
|
||||||
|
@ -475,6 +504,7 @@ export class ConnectionAPIController implements IConnectionAPIController {
|
||||||
if (!filteredConnections.length) {
|
if (!filteredConnections.length) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return filteredConnections;
|
return filteredConnections;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -528,7 +558,7 @@ export class ConnectionAPIController implements IConnectionAPIController {
|
||||||
* '401':
|
* '401':
|
||||||
* $ref: '#/responses/401Get'
|
* $ref: '#/responses/401Get'
|
||||||
*/
|
*/
|
||||||
public async getConfig(body: GetConfigQuery): Promise<any> {
|
public async getConfig(body: GetConfigQuery): Promise<SAMLSSORecord | Record<string, never>> {
|
||||||
const clientID = 'clientID' in body ? body.clientID : undefined;
|
const clientID = 'clientID' in body ? body.clientID : undefined;
|
||||||
const tenant = 'tenant' in body ? body.tenant : undefined;
|
const tenant = 'tenant' in body ? body.tenant : undefined;
|
||||||
const product = 'product' in body ? body.product : undefined;
|
const product = 'product' in body ? body.product : undefined;
|
||||||
|
@ -584,7 +614,7 @@ export class ConnectionAPIController implements IConnectionAPIController {
|
||||||
* name: strategy
|
* name: strategy
|
||||||
* in: formData
|
* in: formData
|
||||||
* type: string
|
* type: string
|
||||||
* description: Strategy
|
* description: Strategy which can help to filter connections with tenant/product query
|
||||||
* /api/v1/connections:
|
* /api/v1/connections:
|
||||||
* delete:
|
* delete:
|
||||||
* parameters:
|
* parameters:
|
||||||
|
@ -662,6 +692,7 @@ export class ConnectionAPIController implements IConnectionAPIController {
|
||||||
if (!connections || !connections.length) {
|
if (!connections || !connections.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// filter if strategy is passed
|
// filter if strategy is passed
|
||||||
const filteredConnections = strategy
|
const filteredConnections = strategy
|
||||||
? connections.filter((connection) => {
|
? connections.filter((connection) => {
|
||||||
|
@ -688,6 +719,7 @@ export class ConnectionAPIController implements IConnectionAPIController {
|
||||||
|
|
||||||
throw new JacksonError('Please provide `clientID` and `clientSecret` or `tenant` and `product`.', 400);
|
throw new JacksonError('Please provide `clientID` and `clientSecret` or `tenant` and `product`.', 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async deleteConfig(body: DelConnectionsQuery): Promise<void> {
|
public async deleteConfig(body: DelConnectionsQuery): Promise<void> {
|
||||||
await this.deleteConnections({ ...body, strategy: 'saml' });
|
await this.deleteConnections({ ...body, strategy: 'saml' });
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import { IConnectionAPIController, OIDCSSOConnection, Storable } from '../../typings';
|
import { IConnectionAPIController, OIDCSSOConnection, OIDCSSORecord, Storable } from '../../typings';
|
||||||
import * as dbutils from '../../db/utils';
|
import * as dbutils from '../../db/utils';
|
||||||
import {
|
import {
|
||||||
extractHostName,
|
extractHostName,
|
||||||
|
@ -24,22 +24,15 @@ const oidc = {
|
||||||
oidcClientSecret = '',
|
oidcClientSecret = '',
|
||||||
} = body;
|
} = body;
|
||||||
|
|
||||||
let connectionClientSecret;
|
let connectionClientSecret: string;
|
||||||
|
|
||||||
validateSSOConnection(body, 'oidc');
|
validateSSOConnection(body, 'oidc');
|
||||||
|
|
||||||
const redirectUrlList = extractRedirectUrls(redirectUrl);
|
const redirectUrlList = extractRedirectUrls(redirectUrl);
|
||||||
|
|
||||||
validateRedirectUrl({ defaultRedirectUrl, redirectUrlList });
|
validateRedirectUrl({ defaultRedirectUrl, redirectUrlList });
|
||||||
|
|
||||||
const record: Partial<OIDCSSOConnection> & {
|
const record: Partial<OIDCSSORecord> = {
|
||||||
clientID: string; // set by Jackson
|
|
||||||
clientSecret: string; // set by Jackson
|
|
||||||
oidcProvider?: {
|
|
||||||
provider?: string;
|
|
||||||
discoveryUrl?: string;
|
|
||||||
clientId?: string;
|
|
||||||
clientSecret?: string;
|
|
||||||
};
|
|
||||||
} = {
|
|
||||||
defaultRedirectUrl,
|
defaultRedirectUrl,
|
||||||
redirectUrl: redirectUrlList,
|
redirectUrl: redirectUrlList,
|
||||||
tenant,
|
tenant,
|
||||||
|
@ -49,6 +42,7 @@ const oidc = {
|
||||||
clientID: '',
|
clientID: '',
|
||||||
clientSecret: '',
|
clientSecret: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
// from OpenID Provider
|
// from OpenID Provider
|
||||||
record.oidcProvider = {
|
record.oidcProvider = {
|
||||||
discoveryUrl: oidcDiscoveryUrl,
|
discoveryUrl: oidcDiscoveryUrl,
|
||||||
|
@ -79,8 +73,9 @@ const oidc = {
|
||||||
value: dbutils.keyFromParts(tenant, product),
|
value: dbutils.keyFromParts(tenant, product),
|
||||||
});
|
});
|
||||||
|
|
||||||
return record;
|
return record as OIDCSSORecord;
|
||||||
},
|
},
|
||||||
|
|
||||||
update: async (
|
update: async (
|
||||||
body: OIDCSSOConnection & { clientID: string; clientSecret: string },
|
body: OIDCSSOConnection & { clientID: string; clientSecret: string },
|
||||||
connectionStore: Storable,
|
connectionStore: Storable,
|
||||||
|
@ -96,25 +91,31 @@ const oidc = {
|
||||||
oidcClientSecret,
|
oidcClientSecret,
|
||||||
...clientInfo
|
...clientInfo
|
||||||
} = body;
|
} = body;
|
||||||
|
|
||||||
if (!clientInfo?.clientID) {
|
if (!clientInfo?.clientID) {
|
||||||
throw new JacksonError('Please provide clientID', 400);
|
throw new JacksonError('Please provide clientID', 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!clientInfo?.clientSecret) {
|
if (!clientInfo?.clientSecret) {
|
||||||
throw new JacksonError('Please provide clientSecret', 400);
|
throw new JacksonError('Please provide clientSecret', 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!clientInfo?.tenant) {
|
if (!clientInfo?.tenant) {
|
||||||
throw new JacksonError('Please provide tenant', 400);
|
throw new JacksonError('Please provide tenant', 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!clientInfo?.product) {
|
if (!clientInfo?.product) {
|
||||||
throw new JacksonError('Please provide product', 400);
|
throw new JacksonError('Please provide product', 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (description && description.length > 100) {
|
if (description && description.length > 100) {
|
||||||
throw new JacksonError('Description should not exceed 100 characters', 400);
|
throw new JacksonError('Description should not exceed 100 characters', 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
const redirectUrlList = redirectUrl ? extractRedirectUrls(redirectUrl) : null;
|
const redirectUrlList = redirectUrl ? extractRedirectUrls(redirectUrl) : null;
|
||||||
validateRedirectUrl({ defaultRedirectUrl, redirectUrlList });
|
validateRedirectUrl({ defaultRedirectUrl, redirectUrlList });
|
||||||
|
|
||||||
const _savedConnection = (await connectionsGetter(clientInfo))[0];
|
const _savedConnection = (await connectionsGetter(clientInfo))[0] as OIDCSSORecord;
|
||||||
|
|
||||||
if (_savedConnection.clientSecret !== clientInfo?.clientSecret) {
|
if (_savedConnection.clientSecret !== clientInfo?.clientSecret) {
|
||||||
throw new JacksonError('clientSecret mismatch', 400);
|
throw new JacksonError('clientSecret mismatch', 400);
|
||||||
|
@ -123,6 +124,7 @@ const oidc = {
|
||||||
let oidcProvider;
|
let oidcProvider;
|
||||||
if (_savedConnection && typeof _savedConnection.oidcProvider === 'object') {
|
if (_savedConnection && typeof _savedConnection.oidcProvider === 'object') {
|
||||||
oidcProvider = { ..._savedConnection.oidcProvider };
|
oidcProvider = { ..._savedConnection.oidcProvider };
|
||||||
|
|
||||||
if (oidcClientId && typeof oidcClientId === 'string') {
|
if (oidcClientId && typeof oidcClientId === 'string') {
|
||||||
const clientID = dbutils.keyDigest(
|
const clientID = dbutils.keyDigest(
|
||||||
dbutils.keyFromParts(clientInfo.tenant, clientInfo.product, oidcClientId)
|
dbutils.keyFromParts(clientInfo.tenant, clientInfo.product, oidcClientId)
|
||||||
|
@ -131,9 +133,11 @@ const oidc = {
|
||||||
throw new JacksonError('Tenant/Product config mismatch with OIDC Provider metadata', 400);
|
throw new JacksonError('Tenant/Product config mismatch with OIDC Provider metadata', 400);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (oidcClientSecret && typeof oidcClientSecret === 'string') {
|
if (oidcClientSecret && typeof oidcClientSecret === 'string') {
|
||||||
oidcProvider.clientSecret = oidcClientSecret;
|
oidcProvider.clientSecret = oidcClientSecret;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (oidcDiscoveryUrl && typeof oidcDiscoveryUrl === 'string') {
|
if (oidcDiscoveryUrl && typeof oidcDiscoveryUrl === 'string') {
|
||||||
oidcProvider.discoveryUrl = oidcDiscoveryUrl;
|
oidcProvider.discoveryUrl = oidcDiscoveryUrl;
|
||||||
const providerName = extractHostName(oidcDiscoveryUrl);
|
const providerName = extractHostName(oidcDiscoveryUrl);
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import {
|
import {
|
||||||
IConnectionAPIController,
|
IConnectionAPIController,
|
||||||
SAMLSSOConnection,
|
|
||||||
SAMLSSOConnectionWithEncodedMetadata,
|
SAMLSSOConnectionWithEncodedMetadata,
|
||||||
SAMLSSOConnectionWithRawMetadata,
|
SAMLSSOConnectionWithRawMetadata,
|
||||||
|
SAMLSSORecord,
|
||||||
Storable,
|
Storable,
|
||||||
} from '../../typings';
|
} from '../../typings';
|
||||||
import * as dbutils from '../../db/utils';
|
import * as dbutils from '../../db/utils';
|
||||||
|
@ -35,18 +35,15 @@ const saml = {
|
||||||
} = body;
|
} = body;
|
||||||
const forceAuthn = body.forceAuthn == 'true' || body.forceAuthn == true;
|
const forceAuthn = body.forceAuthn == 'true' || body.forceAuthn == true;
|
||||||
|
|
||||||
let connectionClientSecret;
|
let connectionClientSecret: string;
|
||||||
|
|
||||||
validateSSOConnection(body, 'saml');
|
validateSSOConnection(body, 'saml');
|
||||||
|
|
||||||
const redirectUrlList = extractRedirectUrls(redirectUrl);
|
const redirectUrlList = extractRedirectUrls(redirectUrl);
|
||||||
|
|
||||||
validateRedirectUrl({ defaultRedirectUrl, redirectUrlList });
|
validateRedirectUrl({ defaultRedirectUrl, redirectUrlList });
|
||||||
|
|
||||||
const record: Partial<SAMLSSOConnection> & {
|
const record: Partial<SAMLSSORecord> = {
|
||||||
clientID: string; // set by Jackson
|
|
||||||
clientSecret: string; // set by Jackson
|
|
||||||
idpMetadata?: Record<string, any>;
|
|
||||||
certs?: Record<'publicKey' | 'privateKey', string>;
|
|
||||||
} = {
|
|
||||||
defaultRedirectUrl,
|
defaultRedirectUrl,
|
||||||
redirectUrl: redirectUrlList,
|
redirectUrl: redirectUrlList,
|
||||||
tenant,
|
tenant,
|
||||||
|
@ -58,17 +55,21 @@ const saml = {
|
||||||
forceAuthn,
|
forceAuthn,
|
||||||
};
|
};
|
||||||
|
|
||||||
let metaData = rawMetadata;
|
let metaData = rawMetadata as string;
|
||||||
if (encodedRawMetadata) {
|
if (encodedRawMetadata) {
|
||||||
metaData = Buffer.from(encodedRawMetadata, 'base64').toString();
|
metaData = Buffer.from(encodedRawMetadata, 'base64').toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
const idpMetadata = await saml20.parseMetadata(metaData!, {});
|
const idpMetadata = (await saml20.parseMetadata(metaData, {})) as SAMLSSORecord['idpMetadata'];
|
||||||
|
|
||||||
|
if (!idpMetadata.entityID) {
|
||||||
|
throw new JacksonError("Couldn't parse EntityID from SAML metadata", 400);
|
||||||
|
}
|
||||||
|
|
||||||
// extract provider
|
// extract provider
|
||||||
let providerName = extractHostName(idpMetadata.entityID);
|
let providerName = extractHostName(idpMetadata.entityID);
|
||||||
if (!providerName) {
|
if (!providerName) {
|
||||||
providerName = extractHostName(idpMetadata.sso.redirectUrl || idpMetadata.sso.postUrl);
|
providerName = extractHostName(idpMetadata.sso.redirectUrl || idpMetadata.sso.postUrl || '');
|
||||||
}
|
}
|
||||||
|
|
||||||
idpMetadata.provider = providerName ? providerName : 'Unknown';
|
idpMetadata.provider = providerName ? providerName : 'Unknown';
|
||||||
|
@ -78,7 +79,7 @@ const saml = {
|
||||||
const certs = await x509.generate();
|
const certs = await x509.generate();
|
||||||
|
|
||||||
if (!certs) {
|
if (!certs) {
|
||||||
throw new Error('Error generating x509 certs');
|
throw new JacksonError('Error generating x509 certs');
|
||||||
}
|
}
|
||||||
|
|
||||||
record.idpMetadata = idpMetadata;
|
record.idpMetadata = idpMetadata;
|
||||||
|
@ -108,8 +109,9 @@ const saml = {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return record;
|
return record as SAMLSSORecord;
|
||||||
},
|
},
|
||||||
|
|
||||||
update: async (
|
update: async (
|
||||||
body: (SAMLSSOConnectionWithRawMetadata | SAMLSSOConnectionWithEncodedMetadata) & {
|
body: (SAMLSSOConnectionWithRawMetadata | SAMLSSOConnectionWithEncodedMetadata) & {
|
||||||
clientID: string;
|
clientID: string;
|
||||||
|
@ -128,25 +130,31 @@ const saml = {
|
||||||
forceAuthn = false,
|
forceAuthn = false,
|
||||||
...clientInfo
|
...clientInfo
|
||||||
} = body;
|
} = body;
|
||||||
|
|
||||||
if (!clientInfo?.clientID) {
|
if (!clientInfo?.clientID) {
|
||||||
throw new JacksonError('Please provide clientID', 400);
|
throw new JacksonError('Please provide clientID', 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!clientInfo?.clientSecret) {
|
if (!clientInfo?.clientSecret) {
|
||||||
throw new JacksonError('Please provide clientSecret', 400);
|
throw new JacksonError('Please provide clientSecret', 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!clientInfo?.tenant) {
|
if (!clientInfo?.tenant) {
|
||||||
throw new JacksonError('Please provide tenant', 400);
|
throw new JacksonError('Please provide tenant', 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!clientInfo?.product) {
|
if (!clientInfo?.product) {
|
||||||
throw new JacksonError('Please provide product', 400);
|
throw new JacksonError('Please provide product', 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (description && description.length > 100) {
|
if (description && description.length > 100) {
|
||||||
throw new JacksonError('Description should not exceed 100 characters', 400);
|
throw new JacksonError('Description should not exceed 100 characters', 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
const redirectUrlList = redirectUrl ? extractRedirectUrls(redirectUrl) : null;
|
const redirectUrlList = redirectUrl ? extractRedirectUrls(redirectUrl) : null;
|
||||||
validateRedirectUrl({ defaultRedirectUrl, redirectUrlList });
|
validateRedirectUrl({ defaultRedirectUrl, redirectUrlList });
|
||||||
|
|
||||||
const _savedConnection = (await connectionsGetter(clientInfo))[0];
|
const _savedConnection = (await connectionsGetter(clientInfo))[0] as SAMLSSORecord;
|
||||||
|
|
||||||
if (_savedConnection.clientSecret !== clientInfo?.clientSecret) {
|
if (_savedConnection.clientSecret !== clientInfo?.clientSecret) {
|
||||||
throw new JacksonError('clientSecret mismatch', 400);
|
throw new JacksonError('clientSecret mismatch', 400);
|
||||||
|
@ -156,10 +164,14 @@ const saml = {
|
||||||
if (encodedRawMetadata) {
|
if (encodedRawMetadata) {
|
||||||
metaData = Buffer.from(encodedRawMetadata, 'base64').toString();
|
metaData = Buffer.from(encodedRawMetadata, 'base64').toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
let newMetadata;
|
let newMetadata;
|
||||||
if (metaData) {
|
if (metaData) {
|
||||||
newMetadata = await saml20.parseMetadata(metaData, {});
|
newMetadata = await saml20.parseMetadata(metaData, {});
|
||||||
|
|
||||||
|
if (!newMetadata.entityID) {
|
||||||
|
throw new JacksonError("Couldn't parse EntityID from SAML metadata", 400);
|
||||||
|
}
|
||||||
// extract provider
|
// extract provider
|
||||||
let providerName = extractHostName(newMetadata.entityID);
|
let providerName = extractHostName(newMetadata.entityID);
|
||||||
if (!providerName) {
|
if (!providerName) {
|
||||||
|
|
|
@ -288,7 +288,7 @@ export class OAuthController implements IOAuthController {
|
||||||
|
|
||||||
if (
|
if (
|
||||||
requestedOIDCFlow &&
|
requestedOIDCFlow &&
|
||||||
(!this.opts.openid.jwtSigningKeys || !isJWSKeyPairLoaded(this.opts.openid.jwtSigningKeys))
|
(!this.opts.openid?.jwtSigningKeys || !isJWSKeyPairLoaded(this.opts.openid.jwtSigningKeys))
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
redirect_url: OAuthErrorResponse({
|
redirect_url: OAuthErrorResponse({
|
||||||
|
@ -404,6 +404,16 @@ export class OAuthController implements IOAuthController {
|
||||||
// OIDC Connection: Issuer discovery, openid-client init and extraction of authorization endpoint happens here
|
// OIDC Connection: Issuer discovery, openid-client init and extraction of authorization endpoint happens here
|
||||||
let oidcCodeVerifier: string | undefined;
|
let oidcCodeVerifier: string | undefined;
|
||||||
if (connectionIsOIDC) {
|
if (connectionIsOIDC) {
|
||||||
|
if (!this.opts.oidcPath) {
|
||||||
|
return {
|
||||||
|
redirect_url: OAuthErrorResponse({
|
||||||
|
error: 'server_error',
|
||||||
|
error_description: 'OpenID response handler path (oidcPath) is not set',
|
||||||
|
redirect_uri,
|
||||||
|
state,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
const { discoveryUrl, clientId, clientSecret } = connection.oidcProvider;
|
const { discoveryUrl, clientId, clientSecret } = connection.oidcProvider;
|
||||||
try {
|
try {
|
||||||
const oidcIssuer = await Issuer.discover(discoveryUrl);
|
const oidcIssuer = await Issuer.discover(discoveryUrl);
|
||||||
|
@ -955,7 +965,7 @@ export class OAuthController implements IOAuthController {
|
||||||
const requestedOIDCFlow = !!codeVal.requested?.oidc;
|
const requestedOIDCFlow = !!codeVal.requested?.oidc;
|
||||||
const requestHasNonce = !!codeVal.requested?.nonce;
|
const requestHasNonce = !!codeVal.requested?.nonce;
|
||||||
if (requestedOIDCFlow) {
|
if (requestedOIDCFlow) {
|
||||||
const { jwtSigningKeys, jwsAlg } = this.opts.openid;
|
const { jwtSigningKeys, jwsAlg } = this.opts.openid ?? {};
|
||||||
if (!jwtSigningKeys || !isJWSKeyPairLoaded(jwtSigningKeys)) {
|
if (!jwtSigningKeys || !isJWSKeyPairLoaded(jwtSigningKeys)) {
|
||||||
throw new JacksonError('JWT signing keys are not loaded', 500);
|
throw new JacksonError('JWT signing keys are not loaded', 500);
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,7 @@ export class OidcDiscoveryController implements IOidcDiscoveryController {
|
||||||
}
|
}
|
||||||
|
|
||||||
async jwks() {
|
async jwks() {
|
||||||
const { jwtSigningKeys, jwsAlg } = this.opts.openid;
|
const { jwtSigningKeys, jwsAlg } = this.opts.openid ?? {};
|
||||||
if (!jwtSigningKeys || !isJWSKeyPairLoaded(jwtSigningKeys)) {
|
if (!jwtSigningKeys || !isJWSKeyPairLoaded(jwtSigningKeys)) {
|
||||||
throw new JacksonError('JWT signing keys are not loaded', 501);
|
throw new JacksonError('JWT signing keys are not loaded', 501);
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,10 +28,6 @@ const defaultOpts = (opts: JacksonOption): JacksonOption => {
|
||||||
|
|
||||||
newOpts.scimPath = newOpts.scimPath || '/api/scim/v2.0';
|
newOpts.scimPath = newOpts.scimPath || '/api/scim/v2.0';
|
||||||
|
|
||||||
if (!newOpts.oidcPath) {
|
|
||||||
throw new Error('oidcPath is required');
|
|
||||||
}
|
|
||||||
|
|
||||||
newOpts.samlAudience = newOpts.samlAudience || 'https://saml.boxyhq.com';
|
newOpts.samlAudience = newOpts.samlAudience || 'https://saml.boxyhq.com';
|
||||||
// path to folder containing static IdP connections that will be preloaded. This is useful for self-hosted deployments that only have to support a single tenant (or small number of known tenants).
|
// path to folder containing static IdP connections that will be preloaded. This is useful for self-hosted deployments that only have to support a single tenant (or small number of known tenants).
|
||||||
newOpts.preLoadedConnection = newOpts.preLoadedConnection || '';
|
newOpts.preLoadedConnection = newOpts.preLoadedConnection || '';
|
||||||
|
@ -72,7 +68,7 @@ export const controllers = async (
|
||||||
const tokenStore = db.store('oauth:token', opts.db.ttl);
|
const tokenStore = db.store('oauth:token', opts.db.ttl);
|
||||||
const healthCheckStore = db.store('_health:check');
|
const healthCheckStore = db.store('_health:check');
|
||||||
|
|
||||||
const connectionAPIController = new ConnectionAPIController({ connectionStore });
|
const connectionAPIController = new ConnectionAPIController({ connectionStore, opts });
|
||||||
const adminController = new AdminController({ connectionStore });
|
const adminController = new AdminController({ connectionStore });
|
||||||
const healthCheckController = new HealthCheckController({ healthCheckStore });
|
const healthCheckController = new HealthCheckController({ healthCheckStore });
|
||||||
await healthCheckController.init();
|
await healthCheckController.init();
|
||||||
|
|
|
@ -29,16 +29,53 @@ export interface OIDCSSOConnection extends SSOConnection {
|
||||||
oidcClientSecret: string;
|
oidcClientSecret: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SAMLSSORecord extends SAMLSSOConnection {
|
||||||
|
clientID: string; // set by Jackson
|
||||||
|
clientSecret: string; // set by Jackson
|
||||||
|
idpMetadata: {
|
||||||
|
entityID: string;
|
||||||
|
loginType?: string;
|
||||||
|
provider: string | 'Unknown';
|
||||||
|
slo: {
|
||||||
|
postUrl?: string;
|
||||||
|
redirectUrl?: string;
|
||||||
|
};
|
||||||
|
sso: {
|
||||||
|
postUrl?: string;
|
||||||
|
redirectUrl?: string;
|
||||||
|
};
|
||||||
|
thumbprint?: string;
|
||||||
|
validTo?: string;
|
||||||
|
};
|
||||||
|
certs: {
|
||||||
|
privateKey: string;
|
||||||
|
publicKey: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OIDCSSORecord extends SSOConnection {
|
||||||
|
clientID: string; // set by Jackson
|
||||||
|
clientSecret: string; // set by Jackson
|
||||||
|
oidcProvider: {
|
||||||
|
provider?: string;
|
||||||
|
discoveryUrl?: string;
|
||||||
|
clientId?: string;
|
||||||
|
clientSecret?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export type ConnectionType = 'saml' | 'oidc';
|
export type ConnectionType = 'saml' | 'oidc';
|
||||||
|
|
||||||
type ClientIDQuery = {
|
type ClientIDQuery = {
|
||||||
clientID: string;
|
clientID: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TenantQuery = {
|
type TenantQuery = {
|
||||||
tenant: string;
|
tenant: string;
|
||||||
product: string;
|
product: string;
|
||||||
strategy?: ConnectionType;
|
strategy?: ConnectionType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GetConnectionsQuery = ClientIDQuery | TenantQuery;
|
export type GetConnectionsQuery = ClientIDQuery | TenantQuery;
|
||||||
export type DelConnectionsQuery = (ClientIDQuery & { clientSecret: string }) | TenantQuery;
|
export type DelConnectionsQuery = (ClientIDQuery & { clientSecret: string }) | TenantQuery;
|
||||||
|
|
||||||
|
@ -46,22 +83,34 @@ export type GetConfigQuery = ClientIDQuery | Omit<TenantQuery, 'strategy'>;
|
||||||
export type DelConfigQuery = (ClientIDQuery & { clientSecret: string }) | Omit<TenantQuery, 'strategy'>;
|
export type DelConfigQuery = (ClientIDQuery & { clientSecret: string }) | Omit<TenantQuery, 'strategy'>;
|
||||||
|
|
||||||
export interface IConnectionAPIController {
|
export interface IConnectionAPIController {
|
||||||
config(body: SAMLSSOConnection): Promise<any>;
|
/**
|
||||||
|
* @deprecated Use `createSAMLConnection` instead.
|
||||||
|
*/
|
||||||
|
config(body: SAMLSSOConnection): Promise<SAMLSSORecord>;
|
||||||
createSAMLConnection(
|
createSAMLConnection(
|
||||||
body: SAMLSSOConnectionWithRawMetadata | SAMLSSOConnectionWithEncodedMetadata
|
body: SAMLSSOConnectionWithRawMetadata | SAMLSSOConnectionWithEncodedMetadata
|
||||||
): Promise<any>;
|
): Promise<SAMLSSORecord>;
|
||||||
createOIDCConnection(body: OIDCSSOConnection): Promise<any>;
|
createOIDCConnection(body: OIDCSSOConnection): Promise<OIDCSSORecord>;
|
||||||
updateConfig(body: SAMLSSOConnection & { clientID: string; clientSecret: string }): Promise<any>;
|
/**
|
||||||
|
* @deprecated Use `updateSAMLConnection` instead.
|
||||||
|
*/
|
||||||
|
updateConfig(body: SAMLSSOConnection & { clientID: string; clientSecret: string }): Promise<void>;
|
||||||
updateSAMLConnection(
|
updateSAMLConnection(
|
||||||
body: (SAMLSSOConnectionWithRawMetadata | SAMLSSOConnectionWithEncodedMetadata) & {
|
body: (SAMLSSOConnectionWithRawMetadata | SAMLSSOConnectionWithEncodedMetadata) & {
|
||||||
clientID: string;
|
clientID: string;
|
||||||
clientSecret: string;
|
clientSecret: string;
|
||||||
}
|
}
|
||||||
): Promise<any>;
|
): Promise<void>;
|
||||||
updateOIDCConnection(body: OIDCSSOConnection & { clientID: string; clientSecret: string }): Promise<any>;
|
updateOIDCConnection(body: OIDCSSOConnection & { clientID: string; clientSecret: string }): Promise<void>;
|
||||||
getConnections(body: GetConnectionsQuery): Promise<Array<any>>;
|
getConnections(body: GetConnectionsQuery): Promise<Array<SAMLSSORecord | OIDCSSORecord>>;
|
||||||
getConfig(body: GetConfigQuery): Promise<any>;
|
/**
|
||||||
|
* @deprecated Use `getConnections` instead.
|
||||||
|
*/
|
||||||
|
getConfig(body: GetConfigQuery): Promise<SAMLSSORecord | Record<string, never>>;
|
||||||
deleteConnections(body: DelConnectionsQuery): Promise<void>;
|
deleteConnections(body: DelConnectionsQuery): Promise<void>;
|
||||||
|
/**
|
||||||
|
* @deprecated Use `deleteConnections` instead.
|
||||||
|
*/
|
||||||
deleteConfig(body: DelConfigQuery): Promise<void>;
|
deleteConfig(body: DelConfigQuery): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -123,11 +172,13 @@ export interface OAuthReqBody {
|
||||||
export interface OAuthReqBodyWithClientId extends OAuthReqBody {
|
export interface OAuthReqBodyWithClientId extends OAuthReqBody {
|
||||||
client_id: string;
|
client_id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OAuthReqBodyWithTenantProduct extends OAuthReqBody {
|
export interface OAuthReqBodyWithTenantProduct extends OAuthReqBody {
|
||||||
client_id: 'dummy';
|
client_id: 'dummy';
|
||||||
tenant: string;
|
tenant: string;
|
||||||
product: string;
|
product: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OAuthReqBodyWithAccessType extends OAuthReqBody {
|
export interface OAuthReqBodyWithAccessType extends OAuthReqBody {
|
||||||
client_id: 'dummy';
|
client_id: 'dummy';
|
||||||
access_type: string;
|
access_type: string;
|
||||||
|
@ -171,6 +222,7 @@ interface OAuthTokenReqBody {
|
||||||
grant_type: 'authorization_code';
|
grant_type: 'authorization_code';
|
||||||
redirect_uri: string;
|
redirect_uri: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OAuthTokenReqWithCodeVerifier extends OAuthTokenReqBody {
|
export interface OAuthTokenReqWithCodeVerifier extends OAuthTokenReqBody {
|
||||||
code_verifier: string;
|
code_verifier: string;
|
||||||
client_id?: never;
|
client_id?: never;
|
||||||
|
@ -252,7 +304,7 @@ export interface DatabaseOption {
|
||||||
export interface JacksonOption {
|
export interface JacksonOption {
|
||||||
externalUrl: string;
|
externalUrl: string;
|
||||||
samlPath: string;
|
samlPath: string;
|
||||||
oidcPath: string;
|
oidcPath?: string;
|
||||||
samlAudience?: string;
|
samlAudience?: string;
|
||||||
preLoadedConfig?: string;
|
preLoadedConfig?: string;
|
||||||
preLoadedConnection?: string;
|
preLoadedConnection?: string;
|
||||||
|
@ -261,7 +313,7 @@ export interface JacksonOption {
|
||||||
clientSecretVerifier?: string;
|
clientSecretVerifier?: string;
|
||||||
idpDiscoveryPath?: string;
|
idpDiscoveryPath?: string;
|
||||||
scimPath?: string;
|
scimPath?: string;
|
||||||
openid: {
|
openid?: {
|
||||||
jwsAlg?: string;
|
jwsAlg?: string;
|
||||||
jwtSigningKeys?: {
|
jwtSigningKeys?: {
|
||||||
private: string;
|
private: string;
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
// Without Entity ID in XML
|
||||||
|
module.exports = {
|
||||||
|
defaultRedirectUrl: 'http://localhost:3366/sso/oauth/completed',
|
||||||
|
redirectUrl: '["http://localhost:3366"]',
|
||||||
|
tenant: 'boxyhqnoentityID.com',
|
||||||
|
product: 'crm',
|
||||||
|
name: 'testConfig',
|
||||||
|
description: 'Just a test configuration',
|
||||||
|
};
|
|
@ -0,0 +1,33 @@
|
||||||
|
<!-- Without POST/Redirect bindings -->
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" validUntil="2026-06-22T18:39:53.000Z">
|
||||||
|
<IDPSSODescriptor WantAuthnRequestsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
|
||||||
|
<KeyDescriptor use="signing">
|
||||||
|
<KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
|
||||||
|
<X509Data>
|
||||||
|
<X509Certificate>
|
||||||
|
MIIDdDCCAlygAwIBAgIGAXo6K+u/MA0GCSqGSIb3DQEBCwUAMHsxFDASBgNVBAoTC0dvb2dsZSBJ
|
||||||
|
bmMuMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MQ8wDQYDVQQDEwZHb29nbGUxGDAWBgNVBAsTD0dv
|
||||||
|
b2dsZSBGb3IgV29yazELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWEwHhcNMjEwNjIz
|
||||||
|
MTgzOTUzWhcNMjYwNjIyMTgzOTUzWjB7MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEWMBQGA1UEBxMN
|
||||||
|
TW91bnRhaW4gVmlldzEPMA0GA1UEAxMGR29vZ2xlMRgwFgYDVQQLEw9Hb29nbGUgRm9yIFdvcmsx
|
||||||
|
CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
|
||||||
|
MIIBCgKCAQEA4qZcxwPiVka9GzGdQ9LVlgVkn3A7O3HtxR6RIm5AMaL4YZziEHt2HgxLdJZyXYJw
|
||||||
|
yfT1KB2IHt+XDQBkgEpQVXuuwSPI8vhI8Jr+nr8zia3MMoy9vJF8ZG7HuWeakaKEh7tJqjYu1Cl9
|
||||||
|
a81rkYdXAFUA+gl2q+stvK26xylAUwptCJSQo0NanWzCq+k5zvX0uLmh58+W5Yv11hDTtAoW+1dH
|
||||||
|
LWUTHXPfoZINPRy5NGKJ2Onq5/D5XJRimNnUa2iYi0Yv9txp1RRq4dpB9MaVttt3iKyDo4/+8fg/
|
||||||
|
bL8BLhguiOeqcP4DEIzMuExi3bZAOu2NC7k7Qf28nA81LzP9DQIDAQABMA0GCSqGSIb3DQEBCwUA
|
||||||
|
A4IBAQARBNB3+MfmKr5WXNXXE9YwUzUGmpfbqUPXh2y2dOAkj6TzoekAsOLWB0p8oyJ5d1bFlTsx
|
||||||
|
i1OY9RuFl0tc35Jbo+ae5GfUvJmbnYGi9z8sBL55HY6x3KQNmM/ehof7ttZwvB6nwuRxAiGYG497
|
||||||
|
3tSzrqMQzEskcgX1mlCW0vks/ztCaayprDXcCUxWdP9FaiSZDEXV6PHhFZgGlRNvERsgaMDJgOsq
|
||||||
|
v6hLX10Q9CtOWzqu18PI4DcfoZ7exWcC29yWvwZzDTfHGaSG1DtUFLwiQmhVUbfd7/fmLV+/iOxV
|
||||||
|
zI0b5xSYZOJ7Kena7gd5zGVrc2ygKAFKiffiI5GLmLkv
|
||||||
|
</X509Certificate>
|
||||||
|
</X509Data>
|
||||||
|
</KeyInfo>
|
||||||
|
</KeyDescriptor>
|
||||||
|
<NameIDFormat>
|
||||||
|
urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress
|
||||||
|
</NameIDFormat>
|
||||||
|
</IDPSSODescriptor>
|
||||||
|
</EntityDescriptor>
|
|
@ -66,8 +66,6 @@ tap.test('LogoutController -> createRequest', async (t) => {
|
||||||
t.equal(err.message, 'SAML connection not found.');
|
t.equal(err.message, 'SAML connection not found.');
|
||||||
t.equal(err.statusCode, 403);
|
t.equal(err.statusCode, 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
t.end();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
t.test("Should throw an error if metadata doesn't present SingleLogoutService URL", async (t) => {
|
t.test("Should throw an error if metadata doesn't present SingleLogoutService URL", async (t) => {
|
||||||
|
@ -79,8 +77,6 @@ tap.test('LogoutController -> createRequest', async (t) => {
|
||||||
t.equal(err.message, `accounts.google.com doesn't support SLO or disabled by IdP.`);
|
t.equal(err.message, `accounts.google.com doesn't support SLO or disabled by IdP.`);
|
||||||
t.equal(err.statusCode, 400);
|
t.equal(err.statusCode, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
t.end();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
t.test('Should return logoutUrl and logoutForm for a valid logout request', async (t) => {
|
t.test('Should return logoutUrl and logoutForm for a valid logout request', async (t) => {
|
||||||
|
@ -97,11 +93,7 @@ tap.test('LogoutController -> createRequest', async (t) => {
|
||||||
|
|
||||||
t.ok(params.has('SAMLRequest'));
|
t.ok(params.has('SAMLRequest'));
|
||||||
t.ok(params.has('RelayState'));
|
t.ok(params.has('RelayState'));
|
||||||
|
|
||||||
t.end();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
t.end();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
t.test('handleResponse', async (t) => {
|
t.test('handleResponse', async (t) => {
|
||||||
|
@ -136,8 +128,6 @@ tap.test('LogoutController -> createRequest', async (t) => {
|
||||||
t.equal(err.message, 'Unable to validate state from the origin request.');
|
t.equal(err.message, 'Unable to validate state from the origin request.');
|
||||||
t.equal(err.statusCode, 403);
|
t.equal(err.statusCode, 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
t.end();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
t.test('Should throw an error is logout request not success', async (t) => {
|
t.test('Should throw an error is logout request not success', async (t) => {
|
||||||
|
@ -150,8 +140,6 @@ tap.test('LogoutController -> createRequest', async (t) => {
|
||||||
t.equal(err.message, 'SLO failed with status urn:oasis:names:tc:SAML:2.0:status:AuthnFailed.');
|
t.equal(err.message, 'SLO failed with status urn:oasis:names:tc:SAML:2.0:status:AuthnFailed.');
|
||||||
t.equal(err.statusCode, 400);
|
t.equal(err.statusCode, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
t.end();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
t.test('Should throw an error when request ID mismatch', async (t) => {
|
t.test('Should throw an error when request ID mismatch', async (t) => {
|
||||||
|
@ -168,8 +156,6 @@ tap.test('LogoutController -> createRequest', async (t) => {
|
||||||
t.equal(err.message, 'SLO failed with mismatched request ID.');
|
t.equal(err.message, 'SLO failed with mismatched request ID.');
|
||||||
t.equal(err.statusCode, 400);
|
t.equal(err.statusCode, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
t.end();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
t.test('Return the redirectUrl after the post logout', async (t) => {
|
t.test('Return the redirectUrl after the post logout', async (t) => {
|
||||||
|
@ -180,12 +166,6 @@ tap.test('LogoutController -> createRequest', async (t) => {
|
||||||
|
|
||||||
t.ok('redirectUrl' in result);
|
t.ok('redirectUrl' in result);
|
||||||
t.match(result.redirectUrl, saml_connection.defaultRedirectUrl);
|
t.match(result.redirectUrl, saml_connection.defaultRedirectUrl);
|
||||||
|
|
||||||
t.end();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
t.end();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
t.end();
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,7 +2,7 @@ import sinon from 'sinon';
|
||||||
import tap from 'tap';
|
import tap from 'tap';
|
||||||
import * as dbutils from '../../src/db/utils';
|
import * as dbutils from '../../src/db/utils';
|
||||||
import controllers from '../../src/index';
|
import controllers from '../../src/index';
|
||||||
import { IConnectionAPIController, OIDCSSOConnection } from '../../src/typings';
|
import { IConnectionAPIController, OIDCSSOConnection, OIDCSSORecord } from '../../src/typings';
|
||||||
import { oidc_connection } from './fixture';
|
import { oidc_connection } from './fixture';
|
||||||
import { databaseOptions } from '../utils';
|
import { databaseOptions } from '../utils';
|
||||||
|
|
||||||
|
@ -30,6 +30,23 @@ tap.test('controller/api', async (t) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
t.test('Create the connection', async (t) => {
|
t.test('Create the connection', async (t) => {
|
||||||
|
t.test('should throw when `oidcPath` is not set', async (t) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
connectionAPIController.opts.oidcPath = undefined;
|
||||||
|
const body: OIDCSSOConnection = Object.assign({}, oidc_connection);
|
||||||
|
try {
|
||||||
|
await connectionAPIController.createOIDCConnection(body);
|
||||||
|
t.fail('Expecting JacksonError.');
|
||||||
|
} catch (err: any) {
|
||||||
|
t.equal(err.message, 'Please set OpenID response handler path (oidcPath) on Jackson');
|
||||||
|
t.equal(err.statusCode, 500);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
connectionAPIController.opts.oidcPath = databaseOptions.oidcPath;
|
||||||
|
});
|
||||||
|
|
||||||
t.test('when required fields are missing or invalid', async (t) => {
|
t.test('when required fields are missing or invalid', async (t) => {
|
||||||
t.test('missing discoveryUrl', async (t) => {
|
t.test('missing discoveryUrl', async (t) => {
|
||||||
const body: OIDCSSOConnection = Object.assign({}, oidc_connection);
|
const body: OIDCSSOConnection = Object.assign({}, oidc_connection);
|
||||||
|
@ -179,7 +196,7 @@ tap.test('controller/api', async (t) => {
|
||||||
await connectionAPIController.getConnections({
|
await connectionAPIController.getConnections({
|
||||||
clientID: CLIENT_ID_OIDC,
|
clientID: CLIENT_ID_OIDC,
|
||||||
})
|
})
|
||||||
)[0];
|
)[0] as OIDCSSORecord;
|
||||||
|
|
||||||
t.equal(savedConnection.name, oidc_connection.name);
|
t.equal(savedConnection.name, oidc_connection.name);
|
||||||
t.equal(savedConnection.oidcProvider.clientId, oidc_connection.oidcClientId);
|
t.equal(savedConnection.oidcProvider.clientId, oidc_connection.oidcClientId);
|
||||||
|
@ -191,6 +208,34 @@ tap.test('controller/api', async (t) => {
|
||||||
|
|
||||||
t.test('Update the connection', async (t) => {
|
t.test('Update the connection', async (t) => {
|
||||||
const body_oidc_provider: OIDCSSOConnection = Object.assign({}, oidc_connection);
|
const body_oidc_provider: OIDCSSOConnection = Object.assign({}, oidc_connection);
|
||||||
|
t.test('should throw when `oidcPath` is not set', async (t) => {
|
||||||
|
const { clientSecret, clientID } = await connectionAPIController.createOIDCConnection(
|
||||||
|
body_oidc_provider as OIDCSSOConnection
|
||||||
|
);
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
connectionAPIController.opts.oidcPath = undefined;
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
await connectionAPIController.updateOIDCConnection({
|
||||||
|
clientID,
|
||||||
|
clientSecret,
|
||||||
|
defaultRedirectUrl: oidc_connection.defaultRedirectUrl,
|
||||||
|
redirectUrl: oidc_connection.redirectUrl,
|
||||||
|
tenant: oidc_connection.tenant,
|
||||||
|
product: oidc_connection.product,
|
||||||
|
});
|
||||||
|
t.fail('Expecting JacksonError.');
|
||||||
|
} catch (err: any) {
|
||||||
|
t.equal(err.message, 'Please set OpenID response handler path (oidcPath) on Jackson');
|
||||||
|
t.equal(err.statusCode, 500);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
connectionAPIController.opts.oidcPath = databaseOptions.oidcPath;
|
||||||
|
});
|
||||||
|
|
||||||
t.test('When clientID is missing', async (t) => {
|
t.test('When clientID is missing', async (t) => {
|
||||||
const { clientSecret } = await connectionAPIController.createOIDCConnection(
|
const { clientSecret } = await connectionAPIController.createOIDCConnection(
|
||||||
body_oidc_provider as OIDCSSOConnection
|
body_oidc_provider as OIDCSSOConnection
|
||||||
|
@ -211,7 +256,6 @@ tap.test('controller/api', async (t) => {
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
t.equal(err.message, 'Please provide clientID');
|
t.equal(err.message, 'Please provide clientID');
|
||||||
t.equal(err.statusCode, 400);
|
t.equal(err.statusCode, 400);
|
||||||
t.end();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -235,7 +279,6 @@ tap.test('controller/api', async (t) => {
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
t.equal(err.message, 'Please provide clientSecret');
|
t.equal(err.message, 'Please provide clientSecret');
|
||||||
t.equal(err.statusCode, 400);
|
t.equal(err.statusCode, 400);
|
||||||
t.end();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -43,7 +43,32 @@ tap.test('[OIDCProvider]', async (t) => {
|
||||||
t.match(params.get('code_challenge'), codeChallenge, 'codeChallenge present');
|
t.match(params.get('code_challenge'), codeChallenge, 'codeChallenge present');
|
||||||
stubCodeVerifier.restore();
|
stubCodeVerifier.restore();
|
||||||
context.state = params.get('state');
|
context.state = params.get('state');
|
||||||
t.end();
|
});
|
||||||
|
|
||||||
|
t.test('[authorize] Should return error if `oidcPath` is not set', async (t) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
oauthController.opts.oidcPath = undefined;
|
||||||
|
const response = (await oauthController.authorize(<OAuthReq>authz_request_oidc_provider)) as {
|
||||||
|
redirect_url: string;
|
||||||
|
};
|
||||||
|
const response_params = new URLSearchParams(new URL(response.redirect_url!).search);
|
||||||
|
|
||||||
|
t.match(response_params.get('error'), 'server_error', 'got server_error when `oidcPath` is not set');
|
||||||
|
t.match(
|
||||||
|
response_params.get('error_description'),
|
||||||
|
'OpenID response handler path (oidcPath) is not set',
|
||||||
|
'matched error_description when `oidcPath` is not set'
|
||||||
|
);
|
||||||
|
t.match(
|
||||||
|
response_params.get('state'),
|
||||||
|
authz_request_oidc_provider.state,
|
||||||
|
'state present in error response'
|
||||||
|
);
|
||||||
|
// Restore
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
oauthController.opts.oidcPath = databaseOptions.oidcPath;
|
||||||
});
|
});
|
||||||
|
|
||||||
t.test('[oidcAuthzResponse] Should throw an error if `state` is missing', async (t) => {
|
t.test('[oidcAuthzResponse] Should throw an error if `state` is missing', async (t) => {
|
||||||
|
@ -56,37 +81,29 @@ tap.test('[OIDCProvider]', async (t) => {
|
||||||
t.equal(message, 'State from original request is missing.', 'got expected error message');
|
t.equal(message, 'State from original request is missing.', 'got expected error message');
|
||||||
t.equal(statusCode, 403, 'got expected status code');
|
t.equal(statusCode, 403, 'got expected status code');
|
||||||
}
|
}
|
||||||
t.end();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
t.test('[oidcAuthzResponse] Should throw an error if `code` is missing', async (t) => {
|
t.test('[oidcAuthzResponse] Should throw an error if `code` is missing', async (t) => {
|
||||||
try {
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
//@ts-ignore
|
||||||
//@ts-ignore
|
const { redirect_url } = await oauthController.oidcAuthzResponse({ state: context.state });
|
||||||
const { redirect_url } = await oauthController.oidcAuthzResponse({ state: context.state });
|
const response_params = new URLSearchParams(new URL(redirect_url!).search);
|
||||||
const response_params = new URLSearchParams(new URL(redirect_url!).search);
|
|
||||||
|
|
||||||
t.match(
|
t.match(
|
||||||
response_params.get('error'),
|
response_params.get('error'),
|
||||||
'server_error',
|
'server_error',
|
||||||
'got server_error when unable to retrieve code from provider'
|
'got server_error when unable to retrieve code from provider'
|
||||||
);
|
);
|
||||||
t.match(
|
t.match(
|
||||||
response_params.get('error_description'),
|
response_params.get('error_description'),
|
||||||
'Authorization code could not be retrieved from OIDC Provider',
|
'Authorization code could not be retrieved from OIDC Provider',
|
||||||
'matched error_description when unable to retrieve code from provider'
|
'matched error_description when unable to retrieve code from provider'
|
||||||
);
|
);
|
||||||
t.match(
|
t.match(
|
||||||
response_params.get('state'),
|
response_params.get('state'),
|
||||||
authz_request_oidc_provider.state,
|
authz_request_oidc_provider.state,
|
||||||
'state present in error response'
|
'state present in error response'
|
||||||
);
|
);
|
||||||
} catch (err) {
|
|
||||||
const { message, statusCode } = err as JacksonError;
|
|
||||||
t.equal(message, 'Code is missing in AuthzResponse from IdP', 'got expected error message');
|
|
||||||
t.equal(statusCode, 403, 'got expected status code');
|
|
||||||
}
|
|
||||||
t.end();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
t.test('[oidcAuthzResponse] Should throw an error if `state` is invalid', async (t) => {
|
t.test('[oidcAuthzResponse] Should throw an error if `state` is invalid', async (t) => {
|
||||||
|
@ -97,7 +114,6 @@ tap.test('[OIDCProvider]', async (t) => {
|
||||||
t.equal(message, 'Unable to validate state from the original request.', 'got expected error message');
|
t.equal(message, 'Unable to validate state from the original request.', 'got expected error message');
|
||||||
t.equal(statusCode, 403, 'got expected status code');
|
t.equal(statusCode, 403, 'got expected status code');
|
||||||
}
|
}
|
||||||
t.end();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
t.test('[oidcAuthzResponse] Should forward any provider errors to redirect_uri', async (t) => {
|
t.test('[oidcAuthzResponse] Should forward any provider errors to redirect_uri', async (t) => {
|
||||||
|
@ -118,7 +134,6 @@ tap.test('[OIDCProvider]', async (t) => {
|
||||||
authz_request_oidc_provider.state,
|
authz_request_oidc_provider.state,
|
||||||
'state present in error response'
|
'state present in error response'
|
||||||
);
|
);
|
||||||
t.end();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
t.test(
|
t.test(
|
||||||
|
@ -170,9 +185,6 @@ tap.test('[OIDCProvider]', async (t) => {
|
||||||
t.ok(response_params.has('code'), 'redirect_url has code');
|
t.ok(response_params.has('code'), 'redirect_url has code');
|
||||||
t.match(response_params.get('state'), authz_request_oidc_provider.state);
|
t.match(response_params.get('state'), authz_request_oidc_provider.state);
|
||||||
sinon.restore();
|
sinon.restore();
|
||||||
t.end();
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
t.end();
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
import * as fs from 'fs';
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
import tap from 'tap';
|
import tap from 'tap';
|
||||||
import * as dbutils from '../../src/db/utils';
|
import * as dbutils from '../../src/db/utils';
|
||||||
|
@ -8,9 +9,11 @@ import {
|
||||||
IConnectionAPIController,
|
IConnectionAPIController,
|
||||||
SAMLSSOConnection,
|
SAMLSSOConnection,
|
||||||
SAMLSSOConnectionWithEncodedMetadata,
|
SAMLSSOConnectionWithEncodedMetadata,
|
||||||
|
SAMLSSORecord,
|
||||||
} from '../../src/typings';
|
} from '../../src/typings';
|
||||||
import { saml_connection } from './fixture';
|
import { saml_connection } from './fixture';
|
||||||
import { databaseOptions } from '../utils';
|
import { databaseOptions } from '../utils';
|
||||||
|
import boxyhqNoentityID from './data/metadata/noentityID/boxyhq-noentityID';
|
||||||
|
|
||||||
let connectionAPIController: IConnectionAPIController;
|
let connectionAPIController: IConnectionAPIController;
|
||||||
|
|
||||||
|
@ -165,6 +168,25 @@ tap.test('controller/api', async (t) => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
t.test('When metadata XML is malformed', async (t) => {
|
||||||
|
t.test('entityID missing in XML', async (t) => {
|
||||||
|
const body = Object.assign({}, boxyhqNoentityID);
|
||||||
|
const metadataPath = path.join(__dirname, '/data/metadata/noentityID');
|
||||||
|
const files = await fs.promises.readdir(metadataPath);
|
||||||
|
const rawMetadataFile = files.filter((f) => f.endsWith('.xml'))?.[0];
|
||||||
|
const rawMetadata = await fs.promises.readFile(path.join(metadataPath, rawMetadataFile), 'utf8');
|
||||||
|
body.encodedRawMetadata = Buffer.from(rawMetadata, 'utf8').toString('base64');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await connectionAPIController.createSAMLConnection(body as SAMLSSOConnectionWithEncodedMetadata);
|
||||||
|
t.fail('Expecting JacksonError.');
|
||||||
|
} catch (err: any) {
|
||||||
|
t.equal(err.message, "Couldn't parse EntityID from SAML metadata");
|
||||||
|
t.equal(err.statusCode, 400);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
t.test('when the request is good', async (t) => {
|
t.test('when the request is good', async (t) => {
|
||||||
const body = Object.assign({}, saml_connection);
|
const body = Object.assign({}, saml_connection);
|
||||||
|
|
||||||
|
@ -182,7 +204,7 @@ tap.test('controller/api', async (t) => {
|
||||||
await connectionAPIController.getConnections({
|
await connectionAPIController.getConnections({
|
||||||
clientID: CLIENT_ID_SAML,
|
clientID: CLIENT_ID_SAML,
|
||||||
})
|
})
|
||||||
)[0];
|
)[0] as SAMLSSORecord;
|
||||||
|
|
||||||
t.equal(savedConnection.name, 'testConfig');
|
t.equal(savedConnection.name, 'testConfig');
|
||||||
t.equal(savedConnection.forceAuthn, false);
|
t.equal(savedConnection.forceAuthn, false);
|
||||||
|
@ -207,7 +229,7 @@ tap.test('controller/api', async (t) => {
|
||||||
await connectionAPIController.getConnections({
|
await connectionAPIController.getConnections({
|
||||||
clientID: CLIENT_ID_SAML,
|
clientID: CLIENT_ID_SAML,
|
||||||
})
|
})
|
||||||
)[0];
|
)[0] as SAMLSSORecord;
|
||||||
|
|
||||||
t.equal(savedConnection.forceAuthn, true);
|
t.equal(savedConnection.forceAuthn, true);
|
||||||
|
|
||||||
|
@ -237,7 +259,6 @@ tap.test('controller/api', async (t) => {
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
t.equal(err.message, 'Please provide clientID');
|
t.equal(err.message, 'Please provide clientID');
|
||||||
t.equal(err.statusCode, 400);
|
t.equal(err.statusCode, 400);
|
||||||
t.end();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -261,7 +282,6 @@ tap.test('controller/api', async (t) => {
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
t.equal(err.message, 'Please provide clientSecret');
|
t.equal(err.message, 'Please provide clientSecret');
|
||||||
t.equal(err.statusCode, 400);
|
t.equal(err.statusCode, 400);
|
||||||
t.end();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -288,6 +308,35 @@ tap.test('controller/api', async (t) => {
|
||||||
t.equal(savedConnection.name, 'A new name');
|
t.equal(savedConnection.name, 'A new name');
|
||||||
t.equal(savedConnection.description, 'A new description');
|
t.equal(savedConnection.description, 'A new description');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
t.test('When metadata XML is malformed', async (t) => {
|
||||||
|
t.test('entityID missing in XML', async (t) => {
|
||||||
|
const { clientID, clientSecret } = await connectionAPIController.createSAMLConnection(
|
||||||
|
body_saml_provider as SAMLSSOConnectionWithEncodedMetadata
|
||||||
|
);
|
||||||
|
const metadataPath = path.join(__dirname, '/data/metadata/noentityID');
|
||||||
|
const files = await fs.promises.readdir(metadataPath);
|
||||||
|
const rawMetadataFile = files.filter((f) => f.endsWith('.xml'))?.[0];
|
||||||
|
const rawMetadata = await fs.promises.readFile(path.join(metadataPath, rawMetadataFile), 'utf8');
|
||||||
|
const encodedRawMetadata = Buffer.from(rawMetadata, 'utf8').toString('base64');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await connectionAPIController.updateSAMLConnection({
|
||||||
|
clientID,
|
||||||
|
clientSecret,
|
||||||
|
tenant: body_saml_provider.tenant,
|
||||||
|
product: body_saml_provider.product,
|
||||||
|
redirectUrl: saml_connection.redirectUrl,
|
||||||
|
defaultRedirectUrl: saml_connection.defaultRedirectUrl,
|
||||||
|
encodedRawMetadata,
|
||||||
|
});
|
||||||
|
t.fail('Expecting JacksonError.');
|
||||||
|
} catch (err: any) {
|
||||||
|
t.equal(err.message, "Couldn't parse EntityID from SAML metadata");
|
||||||
|
t.equal(err.statusCode, 400);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
t.test('Get the connection', async (t) => {
|
t.test('Get the connection', async (t) => {
|
||||||
|
|
|
@ -92,8 +92,6 @@ tap.test('authorize()', async (t) => {
|
||||||
t.equal(message, 'Please specify a redirect URL.', 'got expected error message');
|
t.equal(message, 'Please specify a redirect URL.', 'got expected error message');
|
||||||
t.equal(statusCode, 400, 'got expected status code');
|
t.equal(statusCode, 400, 'got expected status code');
|
||||||
}
|
}
|
||||||
|
|
||||||
t.end();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
t.test('Should return OAuth Error response if `state` is not set', async (t) => {
|
t.test('Should return OAuth Error response if `state` is not set', async (t) => {
|
||||||
|
@ -108,8 +106,6 @@ tap.test('authorize()', async (t) => {
|
||||||
`${body.redirect_uri}?error=invalid_request&error_description=Please+specify+a+state+to+safeguard+against+XSRF+attacks`,
|
`${body.redirect_uri}?error=invalid_request&error_description=Please+specify+a+state+to+safeguard+against+XSRF+attacks`,
|
||||||
'got OAuth error'
|
'got OAuth error'
|
||||||
);
|
);
|
||||||
|
|
||||||
t.end();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
t.test('Should return OAuth Error response if `response_type` is not `code`', async (t) => {
|
t.test('Should return OAuth Error response if `response_type` is not `code`', async (t) => {
|
||||||
|
@ -124,8 +120,6 @@ tap.test('authorize()', async (t) => {
|
||||||
`${body.redirect_uri}?error=unsupported_response_type&error_description=Only+Authorization+Code+grant+is+supported&state=${body.state}`,
|
`${body.redirect_uri}?error=unsupported_response_type&error_description=Only+Authorization+Code+grant+is+supported&state=${body.state}`,
|
||||||
'got OAuth error'
|
'got OAuth error'
|
||||||
);
|
);
|
||||||
|
|
||||||
t.end();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
t.test('Should return OAuth Error response if saml binding could not be retrieved', async (t) => {
|
t.test('Should return OAuth Error response if saml binding could not be retrieved', async (t) => {
|
||||||
|
@ -140,8 +134,6 @@ tap.test('authorize()', async (t) => {
|
||||||
`${body.redirect_uri}?error=invalid_request&error_description=SAML+binding+could+not+be+retrieved&state=${body.state}`,
|
`${body.redirect_uri}?error=invalid_request&error_description=SAML+binding+could+not+be+retrieved&state=${body.state}`,
|
||||||
'got OAuth error'
|
'got OAuth error'
|
||||||
);
|
);
|
||||||
|
|
||||||
t.end();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
t.test('Should return OAuth Error response if request creation fails', async (t) => {
|
t.test('Should return OAuth Error response if request creation fails', async (t) => {
|
||||||
|
@ -156,7 +148,6 @@ tap.test('authorize()', async (t) => {
|
||||||
'got OAuth error'
|
'got OAuth error'
|
||||||
);
|
);
|
||||||
stubSamlRequest.restore();
|
stubSamlRequest.restore();
|
||||||
t.end();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
t.test('Should throw an error if `client_id` is invalid', async (t) => {
|
t.test('Should throw an error if `client_id` is invalid', async (t) => {
|
||||||
|
@ -171,8 +162,6 @@ tap.test('authorize()', async (t) => {
|
||||||
t.equal(message, 'IdP connection not found.', 'got expected error message');
|
t.equal(message, 'IdP connection not found.', 'got expected error message');
|
||||||
t.equal(statusCode, 403, 'got expected status code');
|
t.equal(statusCode, 403, 'got expected status code');
|
||||||
}
|
}
|
||||||
|
|
||||||
t.end();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
t.test('Should throw an error if `redirect_uri` is not allowed', async (t) => {
|
t.test('Should throw an error if `redirect_uri` is not allowed', async (t) => {
|
||||||
|
@ -187,8 +176,6 @@ tap.test('authorize()', async (t) => {
|
||||||
t.equal(message, 'Redirect URL is not allowed.', 'got expected error message');
|
t.equal(message, 'Redirect URL is not allowed.', 'got expected error message');
|
||||||
t.equal(statusCode, 403, 'got expected status code');
|
t.equal(statusCode, 403, 'got expected status code');
|
||||||
}
|
}
|
||||||
|
|
||||||
t.end();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
t.test('Should return the Idp SSO URL', async (t) => {
|
t.test('Should return the Idp SSO URL', async (t) => {
|
||||||
|
@ -203,8 +190,6 @@ tap.test('authorize()', async (t) => {
|
||||||
t.ok('redirect_url' in response, 'got the Idp authorize URL');
|
t.ok('redirect_url' in response, 'got the Idp authorize URL');
|
||||||
t.ok(params.has('RelayState'), 'RelayState present in the query string');
|
t.ok(params.has('RelayState'), 'RelayState present in the query string');
|
||||||
t.ok(params.has('SAMLRequest'), 'SAMLRequest present in the query string');
|
t.ok(params.has('SAMLRequest'), 'SAMLRequest present in the query string');
|
||||||
|
|
||||||
t.end();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
t.test('accepts single value in prompt', async (t) => {
|
t.test('accepts single value in prompt', async (t) => {
|
||||||
|
@ -216,8 +201,6 @@ tap.test('authorize()', async (t) => {
|
||||||
t.ok('redirect_url' in response, 'got the Idp authorize URL');
|
t.ok('redirect_url' in response, 'got the Idp authorize URL');
|
||||||
t.ok(params.has('RelayState'), 'RelayState present in the query string');
|
t.ok(params.has('RelayState'), 'RelayState present in the query string');
|
||||||
t.ok(params.has('SAMLRequest'), 'SAMLRequest present in the query string');
|
t.ok(params.has('SAMLRequest'), 'SAMLRequest present in the query string');
|
||||||
|
|
||||||
t.end();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
t.test('accepts multiple values in prompt', async (t) => {
|
t.test('accepts multiple values in prompt', async (t) => {
|
||||||
|
@ -229,8 +212,6 @@ tap.test('authorize()', async (t) => {
|
||||||
t.ok('redirect_url' in response, 'got the Idp authorize URL');
|
t.ok('redirect_url' in response, 'got the Idp authorize URL');
|
||||||
t.ok(params.has('RelayState'), 'RelayState present in the query string');
|
t.ok(params.has('RelayState'), 'RelayState present in the query string');
|
||||||
t.ok(params.has('SAMLRequest'), 'SAMLRequest present in the query string');
|
t.ok(params.has('SAMLRequest'), 'SAMLRequest present in the query string');
|
||||||
|
|
||||||
t.end();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
t.test('accepts access_type', async (t) => {
|
t.test('accepts access_type', async (t) => {
|
||||||
|
@ -244,8 +225,6 @@ tap.test('authorize()', async (t) => {
|
||||||
t.ok('redirect_url' in response, 'got the Idp authorize URL');
|
t.ok('redirect_url' in response, 'got the Idp authorize URL');
|
||||||
t.ok(params.has('RelayState'), 'RelayState present in the query string');
|
t.ok(params.has('RelayState'), 'RelayState present in the query string');
|
||||||
t.ok(params.has('SAMLRequest'), 'SAMLRequest present in the query string');
|
t.ok(params.has('SAMLRequest'), 'SAMLRequest present in the query string');
|
||||||
|
|
||||||
t.end();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
t.test('accepts resource', async (t) => {
|
t.test('accepts resource', async (t) => {
|
||||||
|
@ -259,8 +238,6 @@ tap.test('authorize()', async (t) => {
|
||||||
t.ok('redirect_url' in response, 'got the Idp authorize URL');
|
t.ok('redirect_url' in response, 'got the Idp authorize URL');
|
||||||
t.ok(params.has('RelayState'), 'RelayState present in the query string');
|
t.ok(params.has('RelayState'), 'RelayState present in the query string');
|
||||||
t.ok(params.has('SAMLRequest'), 'SAMLRequest present in the query string');
|
t.ok(params.has('SAMLRequest'), 'SAMLRequest present in the query string');
|
||||||
|
|
||||||
t.end();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
t.test('accepts scope', async (t) => {
|
t.test('accepts scope', async (t) => {
|
||||||
|
@ -274,12 +251,8 @@ tap.test('authorize()', async (t) => {
|
||||||
t.ok('redirect_url' in response, 'got the Idp authorize URL');
|
t.ok('redirect_url' in response, 'got the Idp authorize URL');
|
||||||
t.ok(params.has('RelayState'), 'RelayState present in the query string');
|
t.ok(params.has('RelayState'), 'RelayState present in the query string');
|
||||||
t.ok(params.has('SAMLRequest'), 'SAMLRequest present in the query string');
|
t.ok(params.has('SAMLRequest'), 'SAMLRequest present in the query string');
|
||||||
|
|
||||||
t.end();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
t.end();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('samlResponse()', async (t) => {
|
tap.test('samlResponse()', async (t) => {
|
||||||
|
@ -312,8 +285,6 @@ tap.test('samlResponse()', async (t) => {
|
||||||
|
|
||||||
t.equal(statusCode, 403, 'got expected status code');
|
t.equal(statusCode, 403, 'got expected status code');
|
||||||
}
|
}
|
||||||
|
|
||||||
t.end();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
t.test('Should return OAuth Error response if response validation fails', async (t) => {
|
t.test('Should return OAuth Error response if response validation fails', async (t) => {
|
||||||
|
@ -331,8 +302,6 @@ tap.test('samlResponse()', async (t) => {
|
||||||
t.match(params.get('error_description'), 'Internal error: Fatal');
|
t.match(params.get('error_description'), 'Internal error: Fatal');
|
||||||
|
|
||||||
stubValidate.restore();
|
stubValidate.restore();
|
||||||
|
|
||||||
t.end();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
t.test('Should return a URL with code and state as query params', async (t) => {
|
t.test('Should return a URL with code and state as query params', async (t) => {
|
||||||
|
@ -362,14 +331,10 @@ tap.test('samlResponse()', async (t) => {
|
||||||
|
|
||||||
stubRandomBytes.restore();
|
stubRandomBytes.restore();
|
||||||
stubValidate.restore();
|
stubValidate.restore();
|
||||||
|
|
||||||
t.end();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
t.end();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('token()', (t) => {
|
tap.test('token()', async (t) => {
|
||||||
t.test('Should throw an error if `grant_type` is not `authorization_code`', async (t) => {
|
t.test('Should throw an error if `grant_type` is not `authorization_code`', async (t) => {
|
||||||
const body = {
|
const body = {
|
||||||
grant_type: 'authorization_code_1',
|
grant_type: 'authorization_code_1',
|
||||||
|
@ -384,8 +349,6 @@ tap.test('token()', (t) => {
|
||||||
t.equal(message, 'Unsupported grant_type', 'got expected error message');
|
t.equal(message, 'Unsupported grant_type', 'got expected error message');
|
||||||
t.equal(statusCode, 400, 'got expected status code');
|
t.equal(statusCode, 400, 'got expected status code');
|
||||||
}
|
}
|
||||||
|
|
||||||
t.end();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
t.test('Should throw an error if `tenant` is invalid', async (t) => {
|
t.test('Should throw an error if `tenant` is invalid', async (t) => {
|
||||||
|
@ -400,8 +363,6 @@ tap.test('token()', (t) => {
|
||||||
t.equal(message, 'Invalid tenant or product');
|
t.equal(message, 'Invalid tenant or product');
|
||||||
t.equal(statusCode, 401, 'got expected status code');
|
t.equal(statusCode, 401, 'got expected status code');
|
||||||
}
|
}
|
||||||
|
|
||||||
t.end();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
t.test('Should throw an error if `product` is invalid', async (t) => {
|
t.test('Should throw an error if `product` is invalid', async (t) => {
|
||||||
|
@ -416,8 +377,6 @@ tap.test('token()', (t) => {
|
||||||
t.equal(message, 'Invalid tenant or product');
|
t.equal(message, 'Invalid tenant or product');
|
||||||
t.equal(statusCode, 401, 'got expected status code');
|
t.equal(statusCode, 401, 'got expected status code');
|
||||||
}
|
}
|
||||||
|
|
||||||
t.end();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
t.test('Should throw an error if `tenant` and `product` is invalid', async (t) => {
|
t.test('Should throw an error if `tenant` and `product` is invalid', async (t) => {
|
||||||
|
@ -432,8 +391,6 @@ tap.test('token()', (t) => {
|
||||||
t.equal(message, 'Invalid tenant or product');
|
t.equal(message, 'Invalid tenant or product');
|
||||||
t.equal(statusCode, 401, 'got expected status code');
|
t.equal(statusCode, 401, 'got expected status code');
|
||||||
}
|
}
|
||||||
|
|
||||||
t.end();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
t.test('Should throw an error if `code` is missing', async (t) => {
|
t.test('Should throw an error if `code` is missing', async (t) => {
|
||||||
|
@ -450,8 +407,6 @@ tap.test('token()', (t) => {
|
||||||
t.equal(message, 'Please specify code', 'got expected error message');
|
t.equal(message, 'Please specify code', 'got expected error message');
|
||||||
t.equal(statusCode, 400, 'got expected status code');
|
t.equal(statusCode, 400, 'got expected status code');
|
||||||
}
|
}
|
||||||
|
|
||||||
t.end();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
t.test('Should throw an error if `code` or `client_secret` is invalid', async (t) => {
|
t.test('Should throw an error if `code` or `client_secret` is invalid', async (t) => {
|
||||||
|
@ -516,8 +471,6 @@ tap.test('token()', (t) => {
|
||||||
t.equal(message, 'Invalid client_secret', 'got expected error message');
|
t.equal(message, 'Invalid client_secret', 'got expected error message');
|
||||||
t.equal(statusCode, 401, 'got expected status code');
|
t.equal(statusCode, 401, 'got expected status code');
|
||||||
}
|
}
|
||||||
|
|
||||||
t.end();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
t.test(
|
t.test(
|
||||||
|
@ -544,8 +497,6 @@ tap.test('token()', (t) => {
|
||||||
t.match(response.expires_in, 300);
|
t.match(response.expires_in, 300);
|
||||||
|
|
||||||
stubRandomBytes.restore();
|
stubRandomBytes.restore();
|
||||||
|
|
||||||
t.end();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
t.test('unencoded client_id', async (t) => {
|
t.test('unencoded client_id', async (t) => {
|
||||||
|
@ -599,8 +550,6 @@ tap.test('token()', (t) => {
|
||||||
|
|
||||||
stubRandomBytes.restore();
|
stubRandomBytes.restore();
|
||||||
stubValidate.restore();
|
stubValidate.restore();
|
||||||
|
|
||||||
t.end();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
t.test('openid flow', async (t) => {
|
t.test('openid flow', async (t) => {
|
||||||
|
@ -643,7 +592,7 @@ tap.test('token()', (t) => {
|
||||||
if (tokenRes.id_token) {
|
if (tokenRes.id_token) {
|
||||||
const claims = jose.decodeJwt(tokenRes.id_token);
|
const claims = jose.decodeJwt(tokenRes.id_token);
|
||||||
const { protectedHeader } = await jose.jwtVerify(tokenRes.id_token, keyPair.publicKey);
|
const { protectedHeader } = await jose.jwtVerify(tokenRes.id_token, keyPair.publicKey);
|
||||||
t.match(protectedHeader.alg, databaseOptions.openid.jwsAlg);
|
t.match(protectedHeader.alg, databaseOptions.openid?.jwsAlg);
|
||||||
t.match(claims.aud, authz_request_normal_oidc_flow.client_id);
|
t.match(claims.aud, authz_request_normal_oidc_flow.client_id);
|
||||||
t.match(claims.iss, databaseOptions.samlAudience);
|
t.match(claims.iss, databaseOptions.samlAudience);
|
||||||
}
|
}
|
||||||
|
@ -668,8 +617,6 @@ tap.test('token()', (t) => {
|
||||||
stubRandomBytes.restore();
|
stubRandomBytes.restore();
|
||||||
stubValidate.restore();
|
stubValidate.restore();
|
||||||
stubLoadJWSPrivateKey.restore();
|
stubLoadJWSPrivateKey.restore();
|
||||||
|
|
||||||
t.end();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
t.test('PKCE check', async (t) => {
|
t.test('PKCE check', async (t) => {
|
||||||
|
@ -733,13 +680,9 @@ tap.test('token()', (t) => {
|
||||||
|
|
||||||
stubRandomBytes.restore();
|
stubRandomBytes.restore();
|
||||||
stubValidate.restore();
|
stubValidate.restore();
|
||||||
|
|
||||||
t.end();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
t.end();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('IdP initiated flow should return token and profile', async (t) => {
|
tap.test('IdP initiated flow should return token and profile', async (t) => {
|
||||||
|
@ -774,5 +717,4 @@ tap.test('IdP initiated flow should return token and profile', async (t) => {
|
||||||
t.equal(profile.id, 'id');
|
t.equal(profile.id, 'id');
|
||||||
stubRandomBytes.restore();
|
stubRandomBytes.restore();
|
||||||
stubValidate.restore();
|
stubValidate.restore();
|
||||||
t.end();
|
|
||||||
});
|
});
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "jackson",
|
"name": "jackson",
|
||||||
"version": "1.3.0",
|
"version": "1.3.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "SAML 2.0 service",
|
"description": "SAML 2.0 service",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"info": {
|
"info": {
|
||||||
"title": "SAML Jackson API",
|
"title": "SAML Jackson API",
|
||||||
"version": "1.3.0",
|
"version": "1.3.1",
|
||||||
"description": "This is the API documentation for SAML Jackson service.",
|
"description": "This is the API documentation for SAML Jackson service.",
|
||||||
"termsOfService": "https://boxyhq.com/terms.html",
|
"termsOfService": "https://boxyhq.com/terms.html",
|
||||||
"contact": {
|
"contact": {
|
||||||
|
@ -87,6 +87,9 @@
|
||||||
},
|
},
|
||||||
"401": {
|
"401": {
|
||||||
"description": "Unauthorized"
|
"description": "Unauthorized"
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Please set OpenID response handler path (oidcPath) on Jackson"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -366,6 +369,9 @@
|
||||||
},
|
},
|
||||||
"401": {
|
"401": {
|
||||||
"description": "Unauthorized"
|
"description": "Unauthorized"
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Please set OpenID response handler path (oidcPath) on Jackson"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -380,6 +386,9 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"$ref": "#/parameters/clientIDParamGet"
|
"$ref": "#/parameters/clientIDParamGet"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/parameters/strategyParamGet"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"operationId": "get-connections",
|
"operationId": "get-connections",
|
||||||
|
@ -835,6 +844,12 @@
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Client ID"
|
"description": "Client ID"
|
||||||
},
|
},
|
||||||
|
"strategyParamGet": {
|
||||||
|
"in": "query",
|
||||||
|
"name": "strategy",
|
||||||
|
"type": "string",
|
||||||
|
"description": "Strategy which can help to filter connections with tenant/product query"
|
||||||
|
},
|
||||||
"clientIDDel": {
|
"clientIDDel": {
|
||||||
"name": "clientID",
|
"name": "clientID",
|
||||||
"in": "formData",
|
"in": "formData",
|
||||||
|
@ -863,7 +878,7 @@
|
||||||
"name": "strategy",
|
"name": "strategy",
|
||||||
"in": "formData",
|
"in": "formData",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Strategy"
|
"description": "Strategy which can help to filter connections with tenant/product query"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tags": []
|
"tags": []
|
||||||
|
|
Loading…
Reference in New Issue