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 base AS deps

View File

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

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

View File

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

View File

@ -3,19 +3,25 @@ import Link from 'next/link';
import React from 'react';
import config from '../lib/env';
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 namespaceEntityId = getEntityId(entityId, namespace);
const metadataDownloadUrl =
'/api' + (namespace ? `/namespace/${namespace}` : '') + '/saml/metadata?download=true';
const metadataUrl = '/api' + (namespace ? `/namespace/${namespace}` : '') + '/saml/metadata';
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'>
<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.
</h1>
<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'>
<Link href='/api/saml/metadata?download=true' className='btn-primary btn-active btn'>
<Link href={metadataDownloadUrl} className='btn-primary btn-active btn'>
<svg
className='mr-1 inline-block h-6 w-6'
fill='none'
@ -31,7 +37,7 @@ const Home: React.FC<{ metadata: IdPMetadata }> = ({ metadata }) => {
</svg>
Download Metadata
</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
</Link>
</div>
@ -52,7 +58,7 @@ const Home: React.FC<{ metadata: IdPMetadata }> = ({ metadata }) => {
<label className='label'>
<span className='label-text font-bold'>Entity ID</span>
</label>
<input type='text' defaultValue={entityId} className='input-bordered input' disabled />
<input type='text' defaultValue={namespaceEntityId} className='input-bordered input' disabled />
</div>
<div className='form-control col-span-2 w-full'>
<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 = {
ssoUrl: config.ssoUrl,
entityId: config.entityId,
@ -85,6 +91,7 @@ export const getServerSideProps: GetServerSideProps = async () => {
return {
props: {
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() {
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({
username: 'jackson',
domain: 'example.com',
@ -41,7 +42,7 @@ export default function Login() {
const { username, domain } = state;
const response = await fetch(`/api/saml/auth`, {
const response = await fetch(authUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',