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:
Aswin V 2024-01-24 04:05:17 +05:30 committed by GitHub
parent 227a146419
commit b81e9218f1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 211 additions and 149 deletions

View File

@ -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'),
},
],
},

View File

@ -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",

View File

@ -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: {

View File

@ -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 });
}
}

View File

@ -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

View File

@ -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);

View File

@ -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,

View File

@ -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,

View File

@ -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

View File

@ -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;

View File

@ -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>;

View File

@ -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);
}

View File

@ -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);
});

View File

@ -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 {

View File

@ -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 {

View File

@ -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.' } });

View File

@ -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);

View File

@ -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 });
};

View File

@ -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": {