Add a namespace page to get unique entity ids for multi tenant use (#472)

* allow a unique entity id per org

* updated metadata url to support org

* org specific login

* org -> namespace

* updated node and alpine

* spacing tweak
This commit is contained in:
Deepak Prabhakara 2024-01-20 23:20:37 +00:00 committed by GitHub
parent d7178eef3d
commit 6fae8857b1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 45 additions and 14 deletions

View File

@ -1,4 +1,4 @@
ARG NODEJS_IMAGE=node:20.10.0-alpine3.18 ARG NODEJS_IMAGE=node:20.11.0-alpine3.19
FROM --platform=$BUILDPLATFORM $NODEJS_IMAGE AS base FROM --platform=$BUILDPLATFORM $NODEJS_IMAGE AS base
FROM base AS deps FROM base AS deps

View File

@ -5,7 +5,7 @@
Mock SAML is a free SAML 2.0 Identity Provider for testing SAML SSO integrations. Mock SAML is a free SAML 2.0 Identity Provider for testing SAML SSO integrations.
Try [Mock SAML](https://mocksaml.com/), our free hosted service. Try [Mock SAML](https://mocksaml.com/), our free hosted service. Whilst we use the root domain for our own testing you can create your own unique namespace by navigating to https://mocksaml.com/namespace/<any name of your choice>.
## Install ## Install

5
lib/entity-id.ts Normal file
View File

@ -0,0 +1,5 @@
const getEntityId = (entityId: string, namespace: string | undefined) => {
return namespace ? `${entityId}/${namespace}` : entityId;
};
export { getEntityId };

View File

@ -0,0 +1,3 @@
import handler from 'pages/api/saml/auth';
export default handler;

View File

@ -0,0 +1,3 @@
import handler from 'pages/api/saml/metadata';
export default handler;

View File

@ -4,6 +4,7 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import type { User } from 'types'; import type { User } from 'types';
import { createResponseXML, signResponseXML } from 'utils'; import { createResponseXML, signResponseXML } from 'utils';
import saml from '@boxyhq/saml20'; import saml from '@boxyhq/saml20';
import { getEntityId } from 'lib/entity-id';
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === 'POST') { if (req.method === 'POST') {
@ -24,7 +25,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}; };
const xml = await createResponseXML({ const xml = await createResponseXML({
idpIdentityId: config.entityId, idpIdentityId: getEntityId(config.entityId, req.query.namespace as any),
audience, audience,
acsUrl, acsUrl,
samlReqId: id, samlReqId: id,

View File

@ -6,6 +6,7 @@ import type { IdPMetadata } from 'types';
import { createIdPMetadataXML } from 'utils'; import { createIdPMetadataXML } from 'utils';
import stream from 'stream'; import stream from 'stream';
import { promisify } from 'util'; import { promisify } from 'util';
import { getEntityId } from 'lib/entity-id';
const pipeline = promisify(stream.pipeline); const pipeline = promisify(stream.pipeline);
@ -21,8 +22,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
async function MetadataUrl() { async function MetadataUrl() {
const { download } = req.query as { download: any }; const { download } = req.query as { download: any };
const filename = 'mock-saml-metadata' + (req.query.namespace ? `-${req.query.namespace}` : '') + '.xml';
const xml = await createIdPMetadataXML({ const xml = await createIdPMetadataXML({
idpEntityId: config.entityId, idpEntityId: getEntityId(config.entityId, req.query.namespace as any),
idpSsoUrl: config.ssoUrl, idpSsoUrl: config.ssoUrl,
certificate: saml.stripCertHeaderAndFooter(config.publicKey), certificate: saml.stripCertHeaderAndFooter(config.publicKey),
}); });
@ -30,7 +33,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
res.setHeader('Content-type', 'text/xml'); res.setHeader('Content-type', 'text/xml');
if (download || download === '') { if (download || download === '') {
res.setHeader('Content-Disposition', 'attachment; filename=mock-saml-metadata.xml'); res.setHeader('Content-Disposition', `attachment; filename=${filename}`);
await pipeline(xml, res); await pipeline(xml, res);
return; return;

View File

@ -3,19 +3,25 @@ import Link from 'next/link';
import React from 'react'; import React from 'react';
import config from '../lib/env'; import config from '../lib/env';
import { IdPMetadata } from '../types'; import { IdPMetadata } from '../types';
import { getEntityId } from 'lib/entity-id';
const Home: React.FC<{ metadata: IdPMetadata; params: any }> = ({ metadata, params }) => {
const namespace = params.namespace;
const Home: React.FC<{ metadata: IdPMetadata }> = ({ metadata }) => {
const { ssoUrl, entityId, certificate } = metadata; const { ssoUrl, entityId, certificate } = metadata;
const namespaceEntityId = getEntityId(entityId, namespace);
const metadataDownloadUrl =
'/api' + (namespace ? `/namespace/${namespace}` : '') + '/saml/metadata?download=true';
const metadataUrl = '/api' + (namespace ? `/namespace/${namespace}` : '') + '/saml/metadata';
return ( return (
<div className='flex items-center justify-center md:py-10'> <div className='flex items-center justify-center'>
<div className='flex w-full max-w-4xl flex-col space-y-5 px-2'> <div className='flex w-full max-w-4xl flex-col space-y-5 px-2'>
<h1 className='text-center text-xl font-extrabold text-gray-900 md:text-2xl'> <h1 className='text-center text-xl font-extrabold text-gray-900 md:text-2xl'>
A free SAML 2.0 Identity Provider for testing SAML SSO integrations. A free SAML 2.0 Identity Provider for testing SAML SSO integrations.
</h1> </h1>
<div className='flex flex-col justify-between space-y-5 md:flex-row md:space-y-0'> <div className='flex flex-col justify-between space-y-5 md:flex-row md:space-y-0'>
<div className='flex flex-col space-y-5 md:flex-row md:space-x-5 md:space-y-0'> <div className='flex flex-col space-y-5 md:flex-row md:space-x-5 md:space-y-0'>
<Link href='/api/saml/metadata?download=true' className='btn-primary btn-active btn'> <Link href={metadataDownloadUrl} className='btn-primary btn-active btn'>
<svg <svg
className='mr-1 inline-block h-6 w-6' className='mr-1 inline-block h-6 w-6'
fill='none' fill='none'
@ -31,7 +37,7 @@ const Home: React.FC<{ metadata: IdPMetadata }> = ({ metadata }) => {
</svg> </svg>
Download Metadata Download Metadata
</Link> </Link>
<Link href='/api/saml/metadata' className='btn-outline btn-primary btn' target='_blank'> <Link href={metadataUrl} className='btn-outline btn-primary btn' target='_blank'>
Metadata URL Metadata URL
</Link> </Link>
</div> </div>
@ -52,7 +58,7 @@ const Home: React.FC<{ metadata: IdPMetadata }> = ({ metadata }) => {
<label className='label'> <label className='label'>
<span className='label-text font-bold'>Entity ID</span> <span className='label-text font-bold'>Entity ID</span>
</label> </label>
<input type='text' defaultValue={entityId} className='input-bordered input' disabled /> <input type='text' defaultValue={namespaceEntityId} className='input-bordered input' disabled />
</div> </div>
<div className='form-control col-span-2 w-full'> <div className='form-control col-span-2 w-full'>
<label className='label'> <label className='label'>
@ -75,7 +81,7 @@ const Home: React.FC<{ metadata: IdPMetadata }> = ({ metadata }) => {
); );
}; };
export const getServerSideProps: GetServerSideProps = async () => { export const getServerSideProps: GetServerSideProps = async ({ params }) => {
const metadata: IdPMetadata = { const metadata: IdPMetadata = {
ssoUrl: config.ssoUrl, ssoUrl: config.ssoUrl,
entityId: config.entityId, entityId: config.entityId,
@ -85,6 +91,7 @@ export const getServerSideProps: GetServerSideProps = async () => {
return { return {
props: { props: {
metadata, metadata,
params: params ? params : {},
}, },
}; };
}; };

View File

@ -0,0 +1,5 @@
import Home, { getServerSideProps as _getServerSideProps } from '../../index';
export const getServerSideProps = _getServerSideProps;
export default Home;

View File

@ -0,0 +1,3 @@
import Login from '../../../saml/login';
export default Login;

View File

@ -5,8 +5,9 @@ import { useEffect, useRef, useState } from 'react';
export default function Login() { export default function Login() {
const router = useRouter(); const router = useRouter();
const { id, audience, acsUrl, providerName, relayState } = router.query; const { id, audience, acsUrl, providerName, relayState, namespace } = router.query;
const authUrl = namespace ? `/api/namespace/${namespace}/saml/auth` : '/api/saml/auth';
const [state, setState] = useState({ const [state, setState] = useState({
username: 'jackson', username: 'jackson',
domain: 'example.com', domain: 'example.com',
@ -41,7 +42,7 @@ export default function Login() {
const { username, domain } = state; const { username, domain } = state;
const response = await fetch(`/api/saml/auth`, { const response = await fetch(authUrl, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',