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:
parent
d7178eef3d
commit
6fae8857b1
@ -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
|
||||
|
||||
@ -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
5
lib/entity-id.ts
Normal file
@ -0,0 +1,5 @@
|
||||
const getEntityId = (entityId: string, namespace: string | undefined) => {
|
||||
return namespace ? `${entityId}/${namespace}` : entityId;
|
||||
};
|
||||
|
||||
export { getEntityId };
|
||||
3
pages/api/namespace/[namespace]/saml/auth.ts
Normal file
3
pages/api/namespace/[namespace]/saml/auth.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import handler from 'pages/api/saml/auth';
|
||||
|
||||
export default handler;
|
||||
3
pages/api/namespace/[namespace]/saml/metadata.ts
Normal file
3
pages/api/namespace/[namespace]/saml/metadata.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import handler from 'pages/api/saml/metadata';
|
||||
|
||||
export default handler;
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
@ -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 : {},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
5
pages/namespace/[namespace]/index.tsx
Normal file
5
pages/namespace/[namespace]/index.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
import Home, { getServerSideProps as _getServerSideProps } from '../../index';
|
||||
|
||||
export const getServerSideProps = _getServerSideProps;
|
||||
|
||||
export default Home;
|
||||
3
pages/namespace/[namespace]/saml/login.tsx
Normal file
3
pages/namespace/[namespace]/saml/login.tsx
Normal file
@ -0,0 +1,3 @@
|
||||
import Login from '../../../saml/login';
|
||||
|
||||
export default Login;
|
||||
@ -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',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user