mirror of https://github.com/boxyhq/jackson.git
Trace OIDC response path (#2179)
* [typings] OIDC provider clientId/secret is non optional * try/catch and trace the errors ... * Fix error message inside `resolveConnection` * Default for error_description, trace error should be either error or fallback to description * Attach traceId to OAuth error response * Add more context to the traces * [fed-saml] Add relayState to trace context * Tenant/product can be traced from session.request in case connection is not resolved * Minor change * [npm] Rename `saml-tracer` -> `sso-tracer` * [Admin UI/API] Rename `saml-tracer` -> `sso-tracer` * [v1 API] Rename `saml-traces` -> `sso-traces` with alias to old path * Fix assertion type display with fallback to `-` * Update swagger spec * Scroll in case text overflows
This commit is contained in:
parent
227a146419
commit
b81e9218f1
|
@ -59,9 +59,9 @@ export const Sidebar = ({ isOpen, setIsOpen }: SidebarProps) => {
|
|||
active: asPath.includes('/admin/federated-saml'),
|
||||
},
|
||||
{
|
||||
href: '/admin/saml-tracer',
|
||||
text: t('saml_tracer'),
|
||||
active: asPath.includes('/admin/saml-tracer'),
|
||||
href: '/admin/sso-tracer',
|
||||
text: t('sso_tracer'),
|
||||
active: asPath.includes('/admin/sso-tracer'),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -109,8 +109,8 @@
|
|||
"setup_link_sso_description": "Create a unique Setup Link to share with your customer so they can set up SSO Connection for your app.",
|
||||
"setup_link_dsync_description": "Create a unique Setup Link to share with your customer so they can set up Directory Sync for your app.",
|
||||
"saml_federation": "SAML Federation",
|
||||
"saml_tracer": "SAML Tracer",
|
||||
"no_saml_traces_found": "No SAML Traces recorded yet.",
|
||||
"sso_tracer": "SSO Tracer",
|
||||
"no_sso_traces_found": "No SSO Traces recorded yet.",
|
||||
"trace_details": "Trace details",
|
||||
"trace_id": "Trace ID",
|
||||
"timestamp": "Timestamp",
|
||||
|
|
|
@ -92,6 +92,10 @@ module.exports = {
|
|||
source: '/api/v1/directory-sync/:path*',
|
||||
destination: '/api/v1/dsync/:path*',
|
||||
},
|
||||
{
|
||||
source: '/api/v1/saml-traces/:path*',
|
||||
destination: '/api/v1/sso-traces/:path*',
|
||||
},
|
||||
];
|
||||
},
|
||||
images: {
|
||||
|
|
|
@ -3,7 +3,7 @@ import {
|
|||
Storable,
|
||||
SAMLSSORecord,
|
||||
OIDCSSORecord,
|
||||
SAMLTracerInstance,
|
||||
SSOTracerInstance,
|
||||
Records,
|
||||
Trace,
|
||||
} from '../typings';
|
||||
|
@ -12,11 +12,11 @@ import { transformConnections } from './utils';
|
|||
|
||||
export class AdminController implements IAdminController {
|
||||
private connectionStore: Storable;
|
||||
private samlTracer: SAMLTracerInstance;
|
||||
private ssoTracer: SSOTracerInstance;
|
||||
|
||||
constructor({ connectionStore, samlTracer }) {
|
||||
constructor({ connectionStore, ssoTracer }) {
|
||||
this.connectionStore = connectionStore;
|
||||
this.samlTracer = samlTracer;
|
||||
this.ssoTracer = ssoTracer;
|
||||
}
|
||||
|
||||
public async getAllConnection(pageOffset?: number, pageLimit?: number, pageToken?: string) {
|
||||
|
@ -33,8 +33,8 @@ export class AdminController implements IAdminController {
|
|||
return { data: transformConnections(connectionList), pageToken: nextPageToken };
|
||||
}
|
||||
|
||||
public async getAllSAMLTraces(pageOffset: number, pageLimit: number, pageToken?: string) {
|
||||
const { data: traces, pageToken: nextPageToken } = (await this.samlTracer.getAllTraces(
|
||||
public async getAllSSOTraces(pageOffset: number, pageLimit: number, pageToken?: string) {
|
||||
const { data: traces, pageToken: nextPageToken } = (await this.ssoTracer.getAllTraces(
|
||||
pageOffset,
|
||||
pageLimit,
|
||||
pageToken
|
||||
|
@ -47,8 +47,8 @@ export class AdminController implements IAdminController {
|
|||
return { data: traces, pageToken: nextPageToken };
|
||||
}
|
||||
|
||||
public async getSAMLTraceById(traceId: string) {
|
||||
const trace = await this.samlTracer.getByTraceId(traceId);
|
||||
public async getSSOTraceById(traceId: string) {
|
||||
const trace = await this.ssoTracer.getByTraceId(traceId);
|
||||
|
||||
if (!trace) {
|
||||
throw new JacksonError(`Trace with id ${traceId} not found`, 404);
|
||||
|
@ -63,6 +63,6 @@ export class AdminController implements IAdminController {
|
|||
pageLimit: number,
|
||||
pageToken?: string
|
||||
) {
|
||||
return await this.samlTracer.getTracesByProduct({ product, pageOffset, pageLimit, pageToken });
|
||||
return await this.ssoTracer.getTracesByProduct({ product, pageOffset, pageLimit, pageToken });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ import type {
|
|||
Storable,
|
||||
SAMLSSORecord,
|
||||
OIDCSSORecord,
|
||||
SAMLTracerInstance,
|
||||
SSOTracerInstance,
|
||||
OAuthErrorHandlerParams,
|
||||
OIDCAuthzResponsePayload,
|
||||
} from '../typings';
|
||||
|
@ -51,16 +51,16 @@ export class OAuthController implements IOAuthController {
|
|||
private sessionStore: Storable;
|
||||
private codeStore: Storable;
|
||||
private tokenStore: Storable;
|
||||
private samlTracer: SAMLTracerInstance;
|
||||
private ssoTracer: SSOTracerInstance;
|
||||
private opts: JacksonOption;
|
||||
private ssoHandler: SSOHandler;
|
||||
|
||||
constructor({ connectionStore, sessionStore, codeStore, tokenStore, samlTracer, opts }) {
|
||||
constructor({ connectionStore, sessionStore, codeStore, tokenStore, ssoTracer, opts }) {
|
||||
this.connectionStore = connectionStore;
|
||||
this.sessionStore = sessionStore;
|
||||
this.codeStore = codeStore;
|
||||
this.tokenStore = tokenStore;
|
||||
this.samlTracer = samlTracer;
|
||||
this.ssoTracer = ssoTracer;
|
||||
this.opts = opts;
|
||||
|
||||
this.ssoHandler = new SSOHandler({
|
||||
|
@ -187,7 +187,7 @@ export class OAuthController implements IOAuthController {
|
|||
} catch (err: unknown) {
|
||||
const error_description = getErrorMessage(err);
|
||||
// Save the error trace
|
||||
await this.samlTracer.saveTrace({
|
||||
await this.ssoTracer.saveTrace({
|
||||
error: error_description,
|
||||
context: {
|
||||
tenant: requestedTenant || '',
|
||||
|
@ -237,7 +237,7 @@ export class OAuthController implements IOAuthController {
|
|||
}
|
||||
|
||||
// Save the error trace
|
||||
const traceId = await this.samlTracer.saveTrace({
|
||||
const traceId = await this.ssoTracer.saveTrace({
|
||||
error: error_description,
|
||||
context: {
|
||||
tenant: requestedTenant,
|
||||
|
@ -281,7 +281,7 @@ export class OAuthController implements IOAuthController {
|
|||
// This code here is kept for backward compatibility. We now have validation while adding the SSO connection to ensure binding is present.
|
||||
const error_description = 'SAML binding could not be retrieved';
|
||||
// Save the error trace
|
||||
const traceId = await this.samlTracer.saveTrace({
|
||||
const traceId = await this.ssoTracer.saveTrace({
|
||||
error: error_description,
|
||||
context: {
|
||||
tenant: requestedTenant as string,
|
||||
|
@ -317,7 +317,7 @@ export class OAuthController implements IOAuthController {
|
|||
} catch (err: unknown) {
|
||||
const error_description = getErrorMessage(err);
|
||||
// Save the error trace
|
||||
const traceId = await this.samlTracer.saveTrace({
|
||||
const traceId = await this.ssoTracer.saveTrace({
|
||||
error: error_description,
|
||||
context: {
|
||||
tenant: requestedTenant,
|
||||
|
@ -463,7 +463,7 @@ export class OAuthController implements IOAuthController {
|
|||
} catch (err: unknown) {
|
||||
const error_description = getErrorMessage(err);
|
||||
// Save the error trace
|
||||
const traceId = await this.samlTracer.saveTrace({
|
||||
const traceId = await this.ssoTracer.saveTrace({
|
||||
error: error_description,
|
||||
context: {
|
||||
tenant: requestedTenant as string,
|
||||
|
@ -605,17 +605,21 @@ export class OAuthController implements IOAuthController {
|
|||
redirect_uri = ((session && session.redirect_uri) as string) || connection.defaultRedirectUrl;
|
||||
} catch (err: unknown) {
|
||||
// Save the error trace
|
||||
await this.samlTracer.saveTrace({
|
||||
await this.ssoTracer.saveTrace({
|
||||
error: getErrorMessage(err),
|
||||
context: {
|
||||
samlResponse: rawResponse,
|
||||
tenant: connection?.tenant || '',
|
||||
product: connection?.product || '',
|
||||
clientID: connection?.clientID || '',
|
||||
tenant: session?.requested?.tenant || connection?.tenant,
|
||||
product: session?.requested?.product || connection?.product,
|
||||
clientID: session?.requested?.client_id || connection?.clientID,
|
||||
providerName: connection?.idpMetadata?.provider,
|
||||
redirectUri: isIdPFlow ? connection?.defaultRedirectUrl : session?.redirect_uri,
|
||||
issuer: issuer || '',
|
||||
issuer,
|
||||
isSAMLFederated: !!isSAMLFederated,
|
||||
isIdPFlow: !!isIdPFlow,
|
||||
requestedOIDCFlow: !!session?.requested?.oidc,
|
||||
acsUrl: session?.requested?.acsUrl,
|
||||
entityId: session?.requested?.entityId,
|
||||
relayState: RelayState,
|
||||
},
|
||||
});
|
||||
|
@ -651,16 +655,20 @@ export class OAuthController implements IOAuthController {
|
|||
} catch (err: unknown) {
|
||||
const error_description = getErrorMessage(err);
|
||||
// Trace the error
|
||||
const traceId = await this.samlTracer.saveTrace({
|
||||
const traceId = await this.ssoTracer.saveTrace({
|
||||
error: error_description,
|
||||
context: {
|
||||
samlResponse: rawResponse,
|
||||
tenant: connection.tenant,
|
||||
product: connection.product,
|
||||
clientID: connection.clientID,
|
||||
providerName: connection?.idpMetadata?.provider,
|
||||
redirectUri: isIdPFlow ? connection?.defaultRedirectUrl : session?.redirect_uri,
|
||||
isSAMLFederated,
|
||||
isIdPFlow,
|
||||
acsUrl: session.requested.acsUrl,
|
||||
entityId: session.requested.entityId,
|
||||
requestedOIDCFlow: !!session.requested.oidc,
|
||||
relayState: RelayState,
|
||||
issuer,
|
||||
profile,
|
||||
|
@ -685,29 +693,66 @@ export class OAuthController implements IOAuthController {
|
|||
public async oidcAuthzResponse(
|
||||
body: OIDCAuthzResponsePayload
|
||||
): Promise<{ redirect_url?: string; response_form?: string }> {
|
||||
let oidcConnection: OIDCSSORecord | undefined;
|
||||
let session: any;
|
||||
let isSAMLFederated: boolean | undefined;
|
||||
let redirect_uri: string | undefined;
|
||||
let profile;
|
||||
|
||||
const callbackParams = body;
|
||||
|
||||
let RelayState = callbackParams.state || '';
|
||||
if (!RelayState) {
|
||||
throw new JacksonError('State from original request is missing.', 403);
|
||||
try {
|
||||
if (!RelayState) {
|
||||
throw new JacksonError('State from original request is missing.', 403);
|
||||
}
|
||||
|
||||
RelayState = RelayState.replace(relayStatePrefix, '');
|
||||
session = await this.sessionStore.get(RelayState);
|
||||
if (!session) {
|
||||
throw new JacksonError('Unable to validate state from the original request.', 403);
|
||||
}
|
||||
|
||||
isSAMLFederated = session && 'samlFederated' in session;
|
||||
|
||||
oidcConnection = await this.connectionStore.get(session.id);
|
||||
|
||||
if (!oidcConnection) {
|
||||
throw new JacksonError('OIDC connection not found.', 403);
|
||||
}
|
||||
|
||||
if (!isSAMLFederated) {
|
||||
redirect_uri = session && session.redirect_uri;
|
||||
if (!redirect_uri) {
|
||||
throw new JacksonError('Redirect URL from the authorization request could not be retrieved', 403);
|
||||
}
|
||||
|
||||
if (redirect_uri && !allowed.redirect(redirect_uri, oidcConnection.redirectUrl as string[])) {
|
||||
throw new JacksonError('Redirect URL is not allowed.', 403);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
await this.ssoTracer.saveTrace({
|
||||
error: getErrorMessage(err),
|
||||
context: {
|
||||
tenant: session?.requested?.tenant || oidcConnection?.tenant,
|
||||
product: session?.requested?.product || oidcConnection?.product,
|
||||
clientID: session?.requested?.client_id || oidcConnection?.clientID,
|
||||
providerName: oidcConnection?.oidcProvider?.provider,
|
||||
acsUrl: session?.requested?.acsUrl,
|
||||
entityId: session?.requested?.entityId,
|
||||
redirectUri: redirect_uri,
|
||||
relayState: RelayState,
|
||||
isSAMLFederated: !!isSAMLFederated,
|
||||
requestedOIDCFlow: !!session?.requested?.oidc,
|
||||
},
|
||||
});
|
||||
// Rethrow err and redirect to Jackson error page
|
||||
throw err;
|
||||
}
|
||||
|
||||
RelayState = RelayState.replace(relayStatePrefix, '');
|
||||
const session = await this.sessionStore.get(RelayState);
|
||||
if (!session) {
|
||||
throw new JacksonError('Unable to validate state from the original request.', 403);
|
||||
}
|
||||
|
||||
const oidcConnection = await this.connectionStore.get(session.id);
|
||||
|
||||
if (session.redirect_uri && !allowed.redirect(session.redirect_uri, oidcConnection.redirectUrl)) {
|
||||
throw new JacksonError('Redirect URL is not allowed.', 403);
|
||||
}
|
||||
const redirect_uri = (session && session.redirect_uri) || oidcConnection.defaultRedirectUrl;
|
||||
|
||||
// Reconstruct the oidcClient
|
||||
// Reconstruct the oidcClient, code exchange for token and user profile happens here
|
||||
const { discoveryUrl, metadata, clientId, clientSecret } = oidcConnection.oidcProvider;
|
||||
let profile;
|
||||
try {
|
||||
const oidcIssuer = await oidcIssuerInstance(discoveryUrl, metadata);
|
||||
const oidcClient = new oidcIssuer.Client({
|
||||
|
@ -722,30 +767,15 @@ export class OAuthController implements IOAuthController {
|
|||
state: callbackParams.state,
|
||||
});
|
||||
profile = await extractOIDCUserProfile(tokenSet, oidcClient);
|
||||
} catch (err: unknown) {
|
||||
if (err) {
|
||||
const { error, error_description } = err as Pick<
|
||||
OAuthErrorHandlerParams,
|
||||
'error' | 'error_description'
|
||||
>;
|
||||
return {
|
||||
redirect_url: OAuthErrorResponse({
|
||||
error: error || 'server_error',
|
||||
error_description: error_description || getErrorMessage(err),
|
||||
redirect_uri,
|
||||
state: session.state,
|
||||
}),
|
||||
};
|
||||
|
||||
if (isSAMLFederated) {
|
||||
const { responseForm } = await this.ssoHandler.createSAMLResponse({ profile, session });
|
||||
|
||||
await this.sessionStore.delete(RelayState);
|
||||
|
||||
return { response_form: responseForm };
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare the response
|
||||
let redirectUrl: string | undefined;
|
||||
let responseForm: string | undefined;
|
||||
|
||||
const isSAMLFederated = session && 'samlFederated' in session;
|
||||
|
||||
if (!isSAMLFederated) {
|
||||
const code = await this._buildAuthorizationCode(oidcConnection, profile, session, false);
|
||||
|
||||
const params = {
|
||||
|
@ -756,19 +786,43 @@ export class OAuthController implements IOAuthController {
|
|||
params['state'] = session.state;
|
||||
}
|
||||
|
||||
redirectUrl = redirect.success(redirect_uri, params);
|
||||
} else {
|
||||
const response = await this.ssoHandler.createSAMLResponse({ profile, session });
|
||||
await this.sessionStore.delete(RelayState);
|
||||
|
||||
responseForm = response.responseForm;
|
||||
return { redirect_url: redirect.success(redirect_uri!, params) };
|
||||
} catch (err: unknown) {
|
||||
const { error, error_description = getErrorMessage(err) } = err as Pick<
|
||||
OAuthErrorHandlerParams,
|
||||
'error' | 'error_description'
|
||||
>;
|
||||
|
||||
const traceId = await this.ssoTracer.saveTrace({
|
||||
error: error || error_description,
|
||||
context: {
|
||||
tenant: oidcConnection.tenant,
|
||||
product: oidcConnection.product,
|
||||
clientID: oidcConnection.clientID,
|
||||
providerName: oidcConnection.oidcProvider.provider,
|
||||
redirectUri: redirect_uri,
|
||||
relayState: RelayState,
|
||||
isSAMLFederated: !!isSAMLFederated,
|
||||
acsUrl: session.requested.acsUrl,
|
||||
entityId: session.requested.entityId,
|
||||
requestedOIDCFlow: !!session.requested.oidc,
|
||||
profile,
|
||||
},
|
||||
});
|
||||
if (isSAMLFederated) {
|
||||
throw err;
|
||||
}
|
||||
return {
|
||||
redirect_url: OAuthErrorResponse({
|
||||
error: error || 'server_error',
|
||||
error_description: traceId ? `${traceId}: ${error_description}` : error_description,
|
||||
redirect_uri: redirect_uri!,
|
||||
state: session.state,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
await this.sessionStore.delete(RelayState);
|
||||
|
||||
return {
|
||||
redirect_url: redirectUrl,
|
||||
response_form: responseForm,
|
||||
};
|
||||
}
|
||||
|
||||
// Build the authorization code for the session
|
||||
|
|
|
@ -81,8 +81,7 @@ export class SSOHandler {
|
|||
).data;
|
||||
}
|
||||
|
||||
const noSSOConnectionErrMessage =
|
||||
authFlow === 'oauth' ? 'No SSO connection found.' : 'No SAML connection found.';
|
||||
const noSSOConnectionErrMessage = 'No SSO connection found.';
|
||||
|
||||
if (!connections || connections.length === 0) {
|
||||
throw new JacksonError(noSSOConnectionErrMessage, 404);
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
import { SSO } from './sso';
|
||||
import { App } from './app';
|
||||
import type { JacksonOption, SAMLTracerInstance } from '../../typings';
|
||||
import type { JacksonOption, SSOTracerInstance } from '../../typings';
|
||||
import { SSOHandler } from '../../controller/sso-handler';
|
||||
|
||||
// This is the main entry point for the SAML Federation module
|
||||
const SAMLFederation = async ({
|
||||
db,
|
||||
opts,
|
||||
samlTracer,
|
||||
ssoTracer,
|
||||
}: {
|
||||
db;
|
||||
opts: JacksonOption;
|
||||
samlTracer: SAMLTracerInstance;
|
||||
ssoTracer: SSOTracerInstance;
|
||||
}) => {
|
||||
const appStore = db.store('samlfed:apps');
|
||||
const sessionStore = db.store('oauth:session', opts.db.ttl);
|
||||
|
@ -24,7 +24,7 @@ const SAMLFederation = async ({
|
|||
});
|
||||
|
||||
const app = new App({ store: appStore, opts });
|
||||
const sso = new SSO({ app, ssoHandler, samlTracer, opts });
|
||||
const sso = new SSO({ app, ssoHandler, ssoTracer, opts });
|
||||
|
||||
const response = {
|
||||
app,
|
||||
|
|
|
@ -3,7 +3,7 @@ import saml from '@boxyhq/saml20';
|
|||
import { App } from './app';
|
||||
import { JacksonError } from '../../controller/error';
|
||||
import { SSOHandler } from '../../controller/sso-handler';
|
||||
import type { JacksonOption, OIDCSSORecord, SAMLSSORecord, SAMLTracerInstance } from '../../typings';
|
||||
import type { JacksonOption, OIDCSSORecord, SAMLSSORecord, SSOTracerInstance } from '../../typings';
|
||||
import { extractSAMLRequestAttributes } from '../../saml/lib';
|
||||
import { getErrorMessage, isConnectionActive } from '../../controller/utils';
|
||||
import { throwIfInvalidLicense } from '../common/checkLicense';
|
||||
|
@ -15,23 +15,23 @@ const isSAMLConnection = (connection: SAMLSSORecord | OIDCSSORecord): connection
|
|||
export class SSO {
|
||||
private app: App;
|
||||
private ssoHandler: SSOHandler;
|
||||
private samlTracer: SAMLTracerInstance;
|
||||
private ssoTracer: SSOTracerInstance;
|
||||
private opts: JacksonOption;
|
||||
|
||||
constructor({
|
||||
app,
|
||||
ssoHandler,
|
||||
samlTracer,
|
||||
ssoTracer,
|
||||
opts,
|
||||
}: {
|
||||
app: App;
|
||||
ssoHandler: SSOHandler;
|
||||
samlTracer: SAMLTracerInstance;
|
||||
ssoTracer: SSOTracerInstance;
|
||||
opts: JacksonOption;
|
||||
}) {
|
||||
this.app = app;
|
||||
this.ssoHandler = ssoHandler;
|
||||
this.samlTracer = samlTracer;
|
||||
this.ssoTracer = ssoTracer;
|
||||
this.opts = opts;
|
||||
}
|
||||
|
||||
|
@ -127,13 +127,14 @@ export class SSO {
|
|||
} catch (err: unknown) {
|
||||
const error_description = getErrorMessage(err);
|
||||
|
||||
this.samlTracer.saveTrace({
|
||||
this.ssoTracer.saveTrace({
|
||||
error: error_description,
|
||||
context: {
|
||||
tenant: app?.tenant || '',
|
||||
product: app?.product || '',
|
||||
clientID: connection?.clientID || '',
|
||||
isSAMLFederated: true,
|
||||
relayState,
|
||||
providerName,
|
||||
acsUrl,
|
||||
entityId,
|
||||
|
|
|
@ -16,7 +16,7 @@ import * as x509 from './saml/x509';
|
|||
import initFederatedSAML, { type ISAMLFederationController } from './ee/federated-saml';
|
||||
import checkLicense from './ee/common/checkLicense';
|
||||
import { BrandingController } from './ee/branding';
|
||||
import SAMLTracer from './saml-tracer';
|
||||
import SSOTracer from './sso-tracer';
|
||||
import EventController from './event';
|
||||
import { ProductController } from './ee/product';
|
||||
|
||||
|
@ -87,11 +87,11 @@ export const controllers = async (
|
|||
const settingsStore = db.store('portal:settings');
|
||||
const productStore = db.store('product:config');
|
||||
|
||||
const samlTracer = new SAMLTracer({ db });
|
||||
const ssoTracer = new SSOTracer({ db });
|
||||
const eventController = new EventController({ opts });
|
||||
|
||||
const connectionAPIController = new ConnectionAPIController({ connectionStore, opts, eventController });
|
||||
const adminController = new AdminController({ connectionStore, samlTracer });
|
||||
const adminController = new AdminController({ connectionStore, ssoTracer });
|
||||
const healthCheckController = new HealthCheckController({ healthCheckStore });
|
||||
await healthCheckController.init();
|
||||
const setupLinkController = new SetupLinkController({ setupLinkStore, opts });
|
||||
|
@ -105,7 +105,7 @@ export const controllers = async (
|
|||
sessionStore,
|
||||
codeStore,
|
||||
tokenStore,
|
||||
samlTracer,
|
||||
ssoTracer,
|
||||
opts,
|
||||
});
|
||||
|
||||
|
@ -120,7 +120,7 @@ export const controllers = async (
|
|||
const directorySyncController = await initDirectorySync({ db, opts, eventController });
|
||||
|
||||
// Enterprise Features
|
||||
const samlFederatedController = await initFederatedSAML({ db, opts, samlTracer });
|
||||
const samlFederatedController = await initFederatedSAML({ db, opts, ssoTracer });
|
||||
const brandingController = new BrandingController({ store: settingsStore, opts });
|
||||
|
||||
// write pre-loaded connections if present
|
||||
|
|
|
@ -2,7 +2,7 @@ import { GetByProductParams, Records, Storable } from '../typings';
|
|||
import { generateMnemonic } from '@boxyhq/error-code-mnemonic';
|
||||
import { IndexNames } from '../controller/utils';
|
||||
import { keyFromParts } from '../db/utils';
|
||||
import type { SAMLTrace, Trace } from './types';
|
||||
import type { SSOTrace, Trace } from './types';
|
||||
import { JacksonError } from '../controller/error';
|
||||
|
||||
const INTERVAL_1_WEEK_MS = 7 * 24 * 60 * 60 * 1000;
|
||||
|
@ -11,7 +11,7 @@ const INTERVAL_1_DAY_MS = 24 * 60 * 60 * 1000;
|
|||
/**
|
||||
* @swagger
|
||||
* definitions:
|
||||
* SAMLTrace:
|
||||
* SSOTrace:
|
||||
* type: object
|
||||
* properties:
|
||||
* traceId:
|
||||
|
@ -51,7 +51,7 @@ const INTERVAL_1_DAY_MS = 24 * 60 * 60 * 1000;
|
|||
* type: boolean
|
||||
* description: Indicates if request is from IdP
|
||||
*/
|
||||
class SAMLTracer {
|
||||
class SSOTracer {
|
||||
tracerStore: Storable;
|
||||
|
||||
constructor({ db }) {
|
||||
|
@ -64,7 +64,7 @@ class SAMLTracer {
|
|||
}, INTERVAL_1_DAY_MS);
|
||||
}
|
||||
|
||||
public async saveTrace(payload: SAMLTrace) {
|
||||
public async saveTrace(payload: SSOTrace) {
|
||||
try {
|
||||
const { context } = payload;
|
||||
// Friendly trace id
|
||||
|
@ -103,7 +103,7 @@ class SAMLTracer {
|
|||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/v1/saml-traces:
|
||||
* /api/v1/sso-traces:
|
||||
* get:
|
||||
* summary: Get trace by ID
|
||||
* parameters:
|
||||
|
@ -120,7 +120,7 @@ class SAMLTracer {
|
|||
* '200':
|
||||
* description: Success
|
||||
* schema:
|
||||
* $ref: '#/definitions/SAMLTrace'
|
||||
* $ref: '#/definitions/SSOTrace'
|
||||
*/
|
||||
public async getByTraceId(traceId: string) {
|
||||
return (await this.tracerStore.get(traceId)) as Trace;
|
||||
|
@ -159,7 +159,7 @@ class SAMLTracer {
|
|||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/v1/saml-traces/product:
|
||||
* /api/v1/sso-traces/product:
|
||||
* get:
|
||||
* summary: Get all traces for a product
|
||||
* parameters:
|
||||
|
@ -174,7 +174,7 @@ class SAMLTracer {
|
|||
* schema:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/definitions/SAMLTrace'
|
||||
* $ref: '#/definitions/SSOTrace'
|
||||
*/
|
||||
public async getTracesByProduct(params: GetByProductParams) {
|
||||
const { product, pageOffset, pageLimit, pageToken } = params;
|
||||
|
@ -197,4 +197,4 @@ class SAMLTracer {
|
|||
}
|
||||
}
|
||||
|
||||
export default SAMLTracer;
|
||||
export default SSOTracer;
|
|
@ -1,5 +1,5 @@
|
|||
import { SAMLProfile } from '@boxyhq/saml20/dist/typings';
|
||||
import SAMLTracer from '.';
|
||||
import SSOTracer from '.';
|
||||
|
||||
export interface Trace {
|
||||
traceId: string;
|
||||
|
@ -10,7 +10,7 @@ export interface Trace {
|
|||
};
|
||||
}
|
||||
|
||||
export interface SAMLTrace extends Omit<Trace, 'traceId' | 'timestamp'> {
|
||||
export interface SSOTrace extends Omit<Trace, 'traceId' | 'timestamp'> {
|
||||
timestamp?: number /** Can be passed in from outside else will be set to Date.now() */;
|
||||
context: Trace['context'] & {
|
||||
tenant: string;
|
||||
|
@ -31,4 +31,4 @@ export interface SAMLTrace extends Omit<Trace, 'traceId' | 'timestamp'> {
|
|||
};
|
||||
}
|
||||
|
||||
export type SAMLTracerInstance = InstanceType<typeof SAMLTracer>;
|
||||
export type SSOTracerInstance = InstanceType<typeof SSOTracer>;
|
|
@ -2,7 +2,7 @@ import type { JWK } from 'jose';
|
|||
import type { CallbackParamsType, IssuerMetadata } from 'openid-client';
|
||||
|
||||
export * from './ee/federated-saml/types';
|
||||
export * from './saml-tracer/types';
|
||||
export * from './sso-tracer/types';
|
||||
export * from './directory-sync/types';
|
||||
export * from './event/types';
|
||||
|
||||
|
@ -83,8 +83,8 @@ export interface OIDCSSORecord extends SSOConnection {
|
|||
friendlyProviderName: string | null;
|
||||
discoveryUrl?: string;
|
||||
metadata?: IssuerMetadata;
|
||||
clientId?: string;
|
||||
clientSecret?: string;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
};
|
||||
deactivated?: boolean;
|
||||
}
|
||||
|
@ -177,15 +177,17 @@ export interface IOAuthController {
|
|||
samlResponse(
|
||||
body: SAMLResponsePayload
|
||||
): Promise<{ redirect_url?: string; app_select_form?: string; response_form?: string }>;
|
||||
oidcAuthzResponse(body: OIDCAuthzResponsePayload): Promise<{ redirect_url?: string }>;
|
||||
oidcAuthzResponse(
|
||||
body: OIDCAuthzResponsePayload
|
||||
): Promise<{ redirect_url?: string; response_form?: string }>;
|
||||
token(body: OAuthTokenReq): Promise<OAuthTokenRes>;
|
||||
userInfo(token: string): Promise<Profile>;
|
||||
}
|
||||
|
||||
export interface IAdminController {
|
||||
getAllConnection(pageOffset?: number, pageLimit?: number, pageToken?: string);
|
||||
getAllSAMLTraces(pageOffset: number, pageLimit: number, pageToken?: string);
|
||||
getSAMLTraceById(traceId: string);
|
||||
getAllSSOTraces(pageOffset: number, pageLimit: number, pageToken?: string);
|
||||
getSSOTraceById(traceId: string);
|
||||
getTracesByProduct(product: string, pageOffset: number, pageLimit: number, pageToken?: string);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,27 +1,27 @@
|
|||
import tap from 'tap';
|
||||
import SAMLTracer from '../../src/saml-tracer';
|
||||
import SSOTracer from '../../src/sso-tracer';
|
||||
import { jacksonOptions } from '../utils';
|
||||
import DB from '../../src/db/db';
|
||||
|
||||
let samlTracer: SAMLTracer;
|
||||
let ssoTracer: SSOTracer;
|
||||
const INTERVAL_1_WEEK_MS = 7 * 24 * 60 * 60 * 1000;
|
||||
|
||||
tap.before(async () => {
|
||||
const { db: dbOptions } = jacksonOptions;
|
||||
const db = await DB.new(dbOptions);
|
||||
samlTracer = new SAMLTracer({ db });
|
||||
ssoTracer = new SSOTracer({ db });
|
||||
});
|
||||
|
||||
tap.test('SAMLTracer', async () => {
|
||||
tap.test('SSOTracer', async () => {
|
||||
tap.test('able to save a trace in db', async (t) => {
|
||||
const test_trace = {
|
||||
error: 'Something wrong happened',
|
||||
context: { tenant: 'boxyhq.com', product: 'saml-demo.boxyhq.com', clientID: 'random-clientID' },
|
||||
};
|
||||
//save
|
||||
const traceId = await samlTracer.saveTrace(test_trace);
|
||||
const traceId = await ssoTracer.saveTrace(test_trace);
|
||||
// retrieve
|
||||
const { data: traces } = await samlTracer.getAllTraces(0, 50);
|
||||
const { data: traces } = await ssoTracer.getAllTraces(0, 50);
|
||||
// check if found trace has all the members of the test_trace saved
|
||||
t.hasStrict(traces[0], test_trace);
|
||||
// check if traceId follows the pattern expected from mnemonic
|
||||
|
@ -29,7 +29,7 @@ tap.test('SAMLTracer', async () => {
|
|||
// check if returned traceId from save operation is same as the one in the retrieved record
|
||||
t.equal(traces[0].traceId, traceId);
|
||||
//cleanup
|
||||
traceId && (await samlTracer.tracerStore.delete(traceId));
|
||||
traceId && (await ssoTracer.tracerStore.delete(traceId));
|
||||
});
|
||||
|
||||
tap.test('calling cleanUpStaleTraces cleans traces older than 1 week', async (t) => {
|
||||
|
@ -42,15 +42,15 @@ tap.test('SAMLTracer', async () => {
|
|||
Date.now() - INTERVAL_1_WEEK_MS - 2000,
|
||||
];
|
||||
for (let i = 0; i < STALE_TIMESTAMPS.length; i++) {
|
||||
await samlTracer.saveTrace({
|
||||
await ssoTracer.saveTrace({
|
||||
timestamp: STALE_TIMESTAMPS[i],
|
||||
error: 'Something wrong happened',
|
||||
context: { tenant: 'boxyhq.com', product: 'saml-demo.boxyhq.com', clientID: 'random-clientID' },
|
||||
});
|
||||
}
|
||||
// run cleanUpStaleTraces
|
||||
await samlTracer.cleanUpStaleTraces();
|
||||
const { data: traces } = await samlTracer.getAllTraces(0, 50);
|
||||
await ssoTracer.cleanUpStaleTraces();
|
||||
const { data: traces } = await ssoTracer.getAllTraces(0, 50);
|
||||
// should be empty
|
||||
t.equal(traces.length, 0);
|
||||
});
|
|
@ -1,7 +1,7 @@
|
|||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||
import { NextPage } from 'next';
|
||||
import { useRouter } from 'next/router';
|
||||
import type { SAMLTrace } from '@boxyhq/saml-jackson';
|
||||
import type { SSOTrace } from '@boxyhq/saml-jackson';
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import { materialOceanic } from 'react-syntax-highlighter/dist/cjs/styles/prism';
|
||||
import useSWR from 'swr';
|
||||
|
@ -17,19 +17,19 @@ import { CopyToClipboardButton } from '@components/ClipboardButton';
|
|||
const DescriptionListItem = ({ term, value }: { term: string; value: string | JSX.Element }) => (
|
||||
<div className='px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6'>
|
||||
<dt className='text-sm font-medium text-gray-500'>{term}</dt>
|
||||
<dd className='mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0'>{value}</dd>
|
||||
<dd className='mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0 overflow-auto'>{value}</dd>
|
||||
</div>
|
||||
);
|
||||
|
||||
const SAMLTraceInspector: NextPage = () => {
|
||||
const SSOTraceInspector: NextPage = () => {
|
||||
const { t } = useTranslation('common');
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const { traceId } = router.query as { traceId: string };
|
||||
|
||||
const { data, error, isLoading } = useSWR<ApiSuccess<SAMLTrace>, ApiError>(
|
||||
`/api/admin/saml-tracer/${traceId}`,
|
||||
const { data, error, isLoading } = useSWR<ApiSuccess<SSOTrace>, ApiError>(
|
||||
`/api/admin/sso-tracer/${traceId}`,
|
||||
fetcher,
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
|
@ -48,7 +48,7 @@ const SAMLTraceInspector: NextPage = () => {
|
|||
if (!data) return null;
|
||||
|
||||
const trace = data.data;
|
||||
const assertionType = trace.context.samlResponse ? 'Response' : 'Request';
|
||||
const assertionType = trace.context.samlResponse ? 'Response' : trace.context.samlRequest ? 'Request' : '-';
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -157,7 +157,7 @@ const SAMLTraceInspector: NextPage = () => {
|
|||
);
|
||||
};
|
||||
|
||||
export default SAMLTraceInspector;
|
||||
export default SSOTraceInspector;
|
||||
|
||||
export async function getServerSideProps({ locale }) {
|
||||
return {
|
|
@ -13,17 +13,17 @@ import { useTranslation } from 'next-i18next';
|
|||
import EmptyState from '@components/EmptyState';
|
||||
import Link from 'next/link';
|
||||
|
||||
const SAMLTraceViewer: NextPage = () => {
|
||||
const SSOTraceViewer: NextPage = () => {
|
||||
const { t } = useTranslation('common');
|
||||
const { paginate, setPaginate, pageTokenMap, setPageTokenMap } = usePaginate();
|
||||
|
||||
let getSamlTracesUrl = `/api/admin/saml-tracer?offset=${paginate.offset}&limit=${pageLimit}`;
|
||||
let getSSOTracesUrl = `/api/admin/sso-tracer?offset=${paginate.offset}&limit=${pageLimit}`;
|
||||
// Use the (next)pageToken mapped to the previous page offset to get the current page
|
||||
if (paginate.offset > 0 && pageTokenMap[paginate.offset - pageLimit]) {
|
||||
getSamlTracesUrl += `&pageToken=${pageTokenMap[paginate.offset - pageLimit]}`;
|
||||
getSSOTracesUrl += `&pageToken=${pageTokenMap[paginate.offset - pageLimit]}`;
|
||||
}
|
||||
|
||||
const { data, error, isLoading } = useSWR<ApiSuccess<Trace[]>, ApiError>(getSamlTracesUrl, fetcher);
|
||||
const { data, error, isLoading } = useSWR<ApiSuccess<Trace[]>, ApiError>(getSSOTracesUrl, fetcher);
|
||||
|
||||
const nextPageToken = data?.pageToken;
|
||||
// store the nextPageToken against the pageOffset
|
||||
|
@ -50,11 +50,11 @@ const SAMLTraceViewer: NextPage = () => {
|
|||
return (
|
||||
<>
|
||||
<div className='mb-5 flex items-center justify-between'>
|
||||
<h2 className='font-bold text-gray-700 dark:text-white md:text-xl'>{t('saml_tracer')}</h2>
|
||||
<h2 className='font-bold text-gray-700 dark:text-white md:text-xl'>{t('sso_tracer')}</h2>
|
||||
</div>
|
||||
{noTraces ? (
|
||||
<>
|
||||
<EmptyState title={t('no_saml_traces_found')} />
|
||||
<EmptyState title={t('no_sso_traces_found')} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
@ -76,13 +76,15 @@ const SAMLTraceViewer: NextPage = () => {
|
|||
className='border-b bg-white last:border-b-0 hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-800'>
|
||||
<td className='px-6 py-3'>
|
||||
<Link
|
||||
href={`/admin/saml-tracer/${traceId}/inspect`}
|
||||
href={`/admin/sso-tracer/${traceId}/inspect`}
|
||||
className='link-primary link flex'>
|
||||
{traceId}
|
||||
</Link>
|
||||
</td>
|
||||
<td className='whitespace-nowrap px-6 py-3'>{new Date(timestamp).toLocaleString()}</td>
|
||||
<td className='px-6 py-3'>{context?.samlResponse ? 'Response' : 'Request'}</td>
|
||||
<td className='px-6 py-3'>
|
||||
{context?.samlResponse ? 'Response' : context?.samlRequest ? 'Request' : '-'}
|
||||
</td>
|
||||
<td className='px-6'>{error}</td>
|
||||
</tr>
|
||||
);
|
||||
|
@ -111,7 +113,7 @@ const SAMLTraceViewer: NextPage = () => {
|
|||
);
|
||||
};
|
||||
|
||||
export default SAMLTraceViewer;
|
||||
export default SSOTraceViewer;
|
||||
|
||||
export async function getServerSideProps({ locale }) {
|
||||
return {
|
|
@ -25,7 +25,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||
const handleGET = async (req: NextApiRequest, res: NextApiResponse, adminController: IAdminController) => {
|
||||
const { traceId } = req.query as { traceId: string };
|
||||
|
||||
const trace = await adminController.getSAMLTraceById(traceId);
|
||||
const trace = await adminController.getSSOTraceById(traceId);
|
||||
|
||||
if (!trace) {
|
||||
return res.status(404).json({ error: { message: 'Trace not found.' } });
|
|
@ -28,7 +28,7 @@ const handleGET = async (req: NextApiRequest, res: NextApiResponse, adminControl
|
|||
const pageOffset = parseInt(offset);
|
||||
const pageLimit = parseInt(limit);
|
||||
|
||||
const tracesPaginated = await adminController.getAllSAMLTraces(pageOffset, pageLimit, pageToken);
|
||||
const tracesPaginated = await adminController.getAllSSOTraces(pageOffset, pageLimit, pageToken);
|
||||
|
||||
if (tracesPaginated.pageToken) {
|
||||
res.setHeader('jackson-pagetoken', tracesPaginated.pageToken);
|
|
@ -23,7 +23,7 @@ const handleGET = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||
|
||||
const { id } = req.query as { id: string };
|
||||
|
||||
const trace = await adminController.getSAMLTraceById(id);
|
||||
const trace = await adminController.getSSOTraceById(id);
|
||||
|
||||
res.json({ data: trace });
|
||||
};
|
|
@ -2,7 +2,7 @@
|
|||
"openapi": "3.1.0",
|
||||
"info": {
|
||||
"title": "Enterprise SSO & Directory Sync",
|
||||
"version": "1.15.4",
|
||||
"version": "1.17.0",
|
||||
"description": "This is the API documentation for SAML Jackson service.",
|
||||
"termsOfService": "https://boxyhq.com/terms.html",
|
||||
"contact": {
|
||||
|
@ -649,7 +649,7 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/saml-traces": {
|
||||
"/api/v1/sso-traces": {
|
||||
"get": {
|
||||
"summary": "Get trace by ID",
|
||||
"parameters": [
|
||||
|
@ -671,13 +671,13 @@
|
|||
"200": {
|
||||
"description": "Success",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/SAMLTrace"
|
||||
"$ref": "#/definitions/SSOTrace"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/saml-traces/product": {
|
||||
"/api/v1/sso-traces/product": {
|
||||
"get": {
|
||||
"summary": "Get all traces for a product",
|
||||
"parameters": [
|
||||
|
@ -697,7 +697,7 @@
|
|||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/SAMLTrace"
|
||||
"$ref": "#/definitions/SSOTrace"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1490,7 +1490,7 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"SAMLTrace": {
|
||||
"SSOTrace": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"traceId": {
|
||||
|
|
Loading…
Reference in New Issue