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 --platform=$BUILDPLATFORM $NODEJS_IMAGE AS base
|
||||||
|
|
||||||
FROM base AS deps
|
FROM base AS deps
|
||||||
|
|||||||
@ -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
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 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,
|
||||||
|
|||||||
@ -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;
|
||||||
@ -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 : {},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
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() {
|
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',
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user