Docs
Organization Single sign-on
Experimental
Organization Single sign-on
Displays and edits organization Single sign-on configuration.
Important
Self-Service SSO is currently in Limited Early Access and subject to change. When integrating with the API, please exercise caution and expect potential updates.
Single sign-on
Installation
Install the following dependencies:
npx shadcn-ui@latest add badge button card form input label separator toaster
pnpm install zod react-hook-form @hookform/resolvers lucide-react
Copy and paste the following code into your project.
"use client";
import { Trash2 } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator";
import { Toaster } from "@/components/ui/toaster";
import { useToast } from "@/components/ui/use-toast";
interface KeyValueMap {
[key: string]: any;
}
interface IPopupWindow {
width: number;
height: number;
title: string;
url: string;
focus: boolean;
scrollbars: boolean;
}
function Spinner() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="17"
height="17"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="animate-spin"
>
<path d="M21 12a9 9 0 1 1-6.219-8.56"></path>
</svg>
);
}
function openPopupWindow(popupOptions: IPopupWindow): Window | null {
{
const dualScreenLeft =
window.screenLeft !== undefined ? window.screenLeft : window.screenX;
const dualScreenTop =
window.screenTop !== undefined ? window.screenTop : window.screenY;
const width = window.innerWidth
? window.innerWidth
: document.documentElement.clientWidth
? document.documentElement.clientWidth
: screen.width;
const height = window.innerHeight
? window.innerHeight
: document.documentElement.clientHeight
? document.documentElement.clientHeight
: screen.height;
const systemZoom = width / window.screen.availWidth;
const left = (width - popupOptions.width) / 2 / systemZoom + dualScreenLeft;
const top = (height - popupOptions.height) / 2 / systemZoom + dualScreenTop;
const newWindow = window.open(
popupOptions.url,
popupOptions.title,
`scrollbars=${popupOptions.scrollbars ? "yes" : "no"},
width=${popupOptions.width / systemZoom},
height=${popupOptions.height / systemZoom},
top=${top},
left=${left}
`
);
newWindow!.opener = null;
if (popupOptions.focus) {
newWindow!.focus();
}
return newWindow;
}
}
export default function OrganizationSSO({
orgId,
connections,
onConfigure,
onUpdateConfiguration,
onFetch,
onDelete,
}: {
orgId: string;
connections?: KeyValueMap[];
onFetch: (id: string) => Promise<KeyValueMap>;
onConfigure?: (values: KeyValueMap) => Promise<{
selfService?: KeyValueMap;
status: number;
}>;
onUpdateConfiguration: (values: KeyValueMap) => Promise<{
selfService?: KeyValueMap;
status: number;
}>;
onDelete: (id: string, connection_id: string) => Promise<KeyValueMap>;
}) {
const [fetching, setFetching] = useState(false);
const [defaultValues, setDefaultValues] = useState<KeyValueMap[] | undefined>(
connections
);
const { toast } = useToast();
const [working, setWorking] = useState<boolean>(false);
const [isConfiguring, setIsConfiguring] = useState<string | null>(null);
const [isRemoving, setIsRemoving] = useState<string | null>(null);
async function onStartSelfService(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setWorking(true);
if (typeof onConfigure === "function") {
const response = await onConfigure({
organizations_to_enable: [orgId],
});
if (response.status !== 200) {
toast({
title: "Info",
description:
"There was a problem creating connection. Try again later.",
});
}
if (response.selfService) {
const enrollmentPopupWindow = openPopupWindow({
url: response.selfService.ticket,
title: "",
width: 1080,
height: 768,
scrollbars: true,
focus: true,
});
const timer = setInterval(async () => {
if (enrollmentPopupWindow && enrollmentPopupWindow.closed) {
setWorking(false);
clearInterval(timer);
await handleFetching();
}
}, 0);
}
}
}
function onStartSelfServiceConnectionUpdate(connection_id: string) {
return async () => {
setIsConfiguring(connection_id);
if (typeof onConfigure === "function") {
const response = await onUpdateConfiguration({
connection_id,
organizations_to_enable: [orgId],
});
if (response.status !== 200) {
toast({
title: "Info",
description:
"There was a problem updating connection. Try again later.",
});
}
if (response.selfService) {
const enrollmentPopupWindow = openPopupWindow({
url: response.selfService.ticket,
title: "",
width: 1080,
height: 768,
scrollbars: true,
focus: true,
});
const timer = setInterval(async () => {
if (enrollmentPopupWindow && enrollmentPopupWindow.closed) {
setIsConfiguring(null);
clearInterval(timer);
await handleFetching();
}
}, 0);
}
}
};
}
const handleFetching = useCallback(
async function handleFetching() {
setFetching(true);
const response = await onFetch(orgId);
if (response.status !== 200) {
return setFetching(false);
}
setDefaultValues(response.connections);
setFetching(false);
},
[onFetch, orgId]
);
function handleOnDelete(connection_id: string) {
return async () => {
setIsRemoving(connection_id);
const response = await onDelete(orgId, connection_id);
if (response.status !== 200) {
return setIsRemoving(null);
}
setIsRemoving(null);
await handleFetching();
};
}
useEffect(() => {
(async () => {
if (!defaultValues) {
await handleFetching();
}
})();
}, [defaultValues, handleFetching]);
return (
<>
<Toaster />
<Card className="w-full">
<CardHeader className="p-4 md:p-6">
<CardTitle className="text-lg font-normal">Single sign-on </CardTitle>
<CardDescription></CardDescription>
</CardHeader>
{fetching && (
<CardContent className="p-4 pt-0 md:p-6 md:pt-0">
<div className="flex w-full items-center justify-left gap-2">
<Spinner />
<span className="text-sm text-muted-foreground">
Fetching Connections...
</span>
</div>
</CardContent>
)}
{!defaultValues && !fetching && (
<CardContent className="p-4 pt-0 md:p-6 md:pt-0">
<div className="flex flex-col gap-6">
<Separator />
<div className="flex items-center justify-between space-x-2">
<Label className="flex flex-col space-y-2">
<p className="font-normal leading-snug text-muted-foreground max-w-fit">
There was a problem retrieving connections. Try again later.
</p>
</Label>
</div>
</div>
</CardContent>
)}
{defaultValues && !fetching && (
<CardContent className="grid gap-6 p-4 pt-0 md:p-6 md:pt-0">
{defaultValues.map((value: any) => {
return (
<div key={value.connection_id} className="flex flex-col gap-6">
<div
key={value.connection.name}
className="flex flex-col md:flex-row items-center justify-between md:space-x-2 space-y-6 md:space-y-0"
>
<Label className="flex flex-row items-center">
<Badge
variant="default"
className="min-h-[38px] px-6 bg-transparent border-neutral-200 rounded-sm font-medium text-black hover:bg-transparent uppercase"
>
{value.connection.strategy}
</Badge>
</Label>
<div className="flex gap-2 items-center justify-end md:min-w-72">
<Button
className="h-fit min-w-24 border-black flex gap-2"
variant="outline"
onClick={onStartSelfServiceConnectionUpdate(
value.connection_id
)}
disabled={isConfiguring === value.connection_id}
>
{isConfiguring === value.connection_id && <Spinner />}
Configure
</Button>
<Button
className="h-fit min-h-[38px] pl-2 pr-2 border-red-500 text-red-500 hover:bg-red-500 hover:text-white justify-center"
variant="outline"
onClick={handleOnDelete(value.connection_id)}
disabled={isRemoving === value.connection_id}
>
{isRemoving === value.connection_id ? (
<Spinner />
) : (
<Trash2 className="w-4 h-4" />
)}
</Button>
</div>
</div>
<Separator />
</div>
);
})}
</CardContent>
)}
<CardFooter className="p-4 pt-0 md:p-6 md:pt-0">
<form onSubmit={onStartSelfService} className="space-y-8 ml-auto">
<Button
type="submit"
disabled={working}
className="ml-auto flex gap-2"
>
{working && <Spinner />}
Setup SSO
</Button>
</form>
</CardFooter>
</Card>
</>
);
}
Component behavior
By design, our components provide basic behavior without making any requests to the Auth0 Management API. To help you implement the full feature, we've also included React Hooks and NextJS routers for calling and proxying the Auth0 Management API.
Update the import paths to match your project setup.
React Hooks
useOrganizations
The hook can be used to create or update an organization.
"use client";
import { useCallback } from "react";
interface KeyValueMap {
[key: string]: any;
}
type OrganizationCreationResponse = {
name: string;
display_name?: string;
branding?: {
logo_url?: string;
colors?: {
primary: string;
page_background: string;
};
};
metadata?: {
[key: string]: any;
};
enabled_connections?: Array<{
connection_id: string;
assign_membership_on_login?: boolean;
show_as_button?: boolean;
}>;
id: string;
};
export default function useOrganizations() {
const createOrganization = useCallback(
async (
values: any
): Promise<{
organization?: OrganizationCreationResponse;
status: number;
}> => {
try {
/**
* '/api/auth/orgs' is a custom endpoint which will proxy
* the request to the Auth0 Management API.
*
* Proxy sample at: https://components.lab.auth0.com/docs/components/organization-creator#nextjs-routers
*/
const response = await fetch("/api/auth/orgs", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ name: values.organization_name }),
});
// TODO: Better handling rate limits
if (response.status === 429) {
return { status: 429 };
}
const organization: OrganizationCreationResponse =
await response.json();
return {
organization,
status: response.status,
};
} catch (error) {
console.error(error);
return { status: 500 };
}
},
[]
);
const fetchOrganization = useCallback(
async (id: string): Promise<KeyValueMap> => {
try {
/**
* '/api/auth/orgs/{id}' is a custom endpoint which will proxy
* the request to the Auth0 Management API.
*
* Proxy sample at: https://components.lab.auth0.com/docs/components/organization-metadata#nextjs-routers
*/
const response = await fetch(`/api/auth/orgs/${id}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
// TODO: Better handling rate limits
if (response.status === 429) {
return { status: 429 };
}
const organization: KeyValueMap = await response.json();
return {
organization,
status: response.status,
};
} catch (error) {
console.error(error);
return { status: 500 };
}
},
[]
);
const updateOrganization = useCallback(
async (
id: string,
values: KeyValueMap
): Promise<{
organization?: KeyValueMap;
status: number;
}> => {
try {
/**
* '/api/auth/orgs/{id}' is a custom endpoint which will proxy
* the request to the Auth0 Management API.
*
* Proxy sample at: https://components.lab.auth0.com/docs/components/organization-metadata#nextjs-routers
*/
const response = await fetch(`/api/auth/orgs/${id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(values),
});
// TODO: Better handling rate limits
if (response.status === 429) {
return { status: 429 };
}
const organization: KeyValueMap = await response.json();
return { organization, status: response.status };
} catch (e) {
console.error(e);
return { status: 500 };
}
},
[]
);
const startSelfServiceConfiguration = useCallback(
async (
values: KeyValueMap
): Promise<{
selfService?: KeyValueMap;
status: number;
}> => {
try {
/**
* '/api/auth/orgs/sso' is a custom endpoint which will proxy
* the request to the Auth0 Management API.
*
* Proxy sample at: https://components.lab.auth0.com/docs/components/organization-sso#nextjs-routers
*/
const response = await fetch("/api/auth/orgs/sso", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
organizations_to_enable: values.organizations_to_enable,
}),
});
// TODO: Better handling rate limits
if (response.status === 429) {
return { status: 429 };
}
const selfService = await response.json();
return {
selfService,
status: response.status,
};
} catch (error) {
console.error(error);
return { status: 500 };
}
},
[]
);
const fetchOrganizationConnections = useCallback(
async (id: string): Promise<KeyValueMap> => {
try {
/**
* '/api/auth/orgs/{id}/connections' is a custom endpoint which will proxy
* the request to the Auth0 Management API.
*
* Proxy sample at: https://components.lab.auth0.com/docs/components/organization-sso#nextjs-routers
*/
const response = await fetch(`/api/auth/orgs/${id}/connections`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
// TODO: Better handling rate limits
if (response.status === 429) {
return { status: 429 };
}
const connections: KeyValueMap[] = await response.json();
return {
connections,
status: response.status,
};
} catch (error) {
console.error(error);
return { status: 500 };
}
},
[]
);
const startSelfServiceConnectionUpdate = useCallback(
async (
values: KeyValueMap
): Promise<{
selfService?: KeyValueMap;
status: number;
}> => {
try {
/**
* '/api/auth/orgs/sso' is a custom endpoint which will proxy
* the request to the Auth0 Management API.
*
* Proxy sample at: https://components.lab.auth0.com/docs/components/organization-metadata#nextjs-routers
*/
const response = await fetch(`/api/auth/orgs/sso`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(values),
});
// TODO: Better handling rate limits
if (response.status === 429) {
return { status: 429 };
}
const selfService: KeyValueMap = await response.json();
return { selfService, status: response.status };
} catch (e) {
console.error(e);
return { status: 500 };
}
},
[]
);
const deleteOrganizationConnection = useCallback(
async (id: string, connection_id: string): Promise<KeyValueMap> => {
try {
/**
* '/api/auth/orgs/{id}/connections/{connection_id}' is a custom endpoint which will proxy
* the request to the Auth0 Management API.
*
* Proxy sample at: https://components.lab.auth0.com/docs/components/organization-sso#nextjs-routers
*/
const response = await fetch(
`/api/auth/orgs/${id}/connections/${connection_id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
// TODO: Better handling rate limits
if (response.status === 429) {
return { status: 429 };
}
const connection: KeyValueMap[] = await response.json();
return {
connection,
status: response.status,
};
} catch (error) {
console.error(error);
return { status: 500 };
}
},
[]
);
return {
createOrganization,
fetchOrganization,
updateOrganization,
fetchOrganizationConnections,
deleteOrganizationConnection,
startSelfServiceConfiguration,
startSelfServiceConnectionUpdate,
};
}
NextJS routers
Organizations router
The route can be used to create or update an organization.
import {
AuthenticationClient,
ManagementClient,
PostOrganizationsRequest,
} from "auth0";
import { NextRequest, NextResponse } from "next/server";
import { getSession, withApiAuthRequired } from "@auth0/nextjs-auth0";
/**
* Make sure to install the withRateLimit from:
* - https://components.lab.auth0.com/docs/rate-limit#helpers
*/
import { withRateLimit } from "./helpers/rate-limit";
const client = new ManagementClient({
domain: new URL(process.env.AUTH0_ISSUER_BASE_URL!).host,
clientId: process.env.AUTH0_CLIENT_ID_MGMT!,
clientSecret: process.env.AUTH0_CLIENT_SECRET_MGMT!,
});
const authClient = new AuthenticationClient({
domain: new URL(process.env.AUTH0_ISSUER_BASE_URL!).host,
clientId: process.env.AUTH0_CLIENT_ID_MGMT!,
clientSecret: process.env.AUTH0_CLIENT_SECRET_MGMT!,
});
type HandleOrganizationCreationParams = Pick<
PostOrganizationsRequest,
"enabled_connections"
>;
type SelfServiceParams = {
clients_to_enable: string[];
};
/**
* @example
*
* export const POST = handleOrganizationCreation({
* enabled_connections: [{
* connection_id: process.env.ORGANIZATIONS_ENABLED_CONNECTION!,
* assign_membership_on_login: false,
* }]
* });
*/
// TODO: better error handling
export function handleOrganizationCreation(
params?: HandleOrganizationCreationParams
) {
return withRateLimit(
withApiAuthRequired(async (request: Request): Promise<NextResponse> => {
try {
const session = await getSession();
const userId = session?.user.sub;
const body: PostOrganizationsRequest = await request.json();
const postOrganization: PostOrganizationsRequest = {
name: body.name,
};
if (params && params.enabled_connections) {
postOrganization.enabled_connections = params.enabled_connections;
}
// Create organization
const { data: organization } = await client.organizations.create(
postOrganization
);
// Add current user to new organization
await client.organizations.addMembers(
{ id: organization.id },
{ members: [userId] }
);
return NextResponse.json(
{
id: organization.id,
name: organization.name,
display_name: organization.display_name,
},
{
status: 200,
}
);
} catch (error) {
console.error(error);
return NextResponse.json(
{ error: "Error creating organization" },
{ status: 500 }
);
}
})
);
}
/**
* @example export const GET = handleFetchOrganization();
*/
export function handleFetchOrganization() {
return withRateLimit(
withApiAuthRequired(
async (request: NextRequest, { params }): Promise<NextResponse> => {
try {
const org_id = params?.id as string;
const response = await client.organizations.get({ id: org_id });
const { data } = response;
const org = {
id: data.id,
name: data.name,
display_name: data.display_name,
branding: data.branding || {
logo_url: `https://cdn.auth0.com/avatars/c.png`,
colors: {},
},
metadata: data.metadata || {},
};
return NextResponse.json(org, {
status: response.status,
});
} catch (error) {
console.error(error);
return NextResponse.json(
{ error: "Error fetching user metadata" },
{ status: 500 }
);
}
}
)
);
}
/**
* @example export const PUT = handleOrganizationUpdate();
*/
// TODO: better error handling
export function handleOrganizationUpdate() {
return withRateLimit(
withApiAuthRequired(
async (request: Request, { params }): Promise<NextResponse> => {
try {
const org_id = params?.id as string;
const org = await request.json();
await client.organizations.update({ id: org_id }, org);
return NextResponse.json(org, {
status: 200,
});
} catch (error) {
console.error(error);
return NextResponse.json(
{ error: "Error updating organization" },
{ status: 500 }
);
}
}
)
);
}
/**
* @example export const POST = handleSelfService();
*/
export function handleSelfService(params: SelfServiceParams) {
return withRateLimit(
withApiAuthRequired(async (request: NextRequest): Promise<NextResponse> => {
try {
const body = await request.json();
const { data: tokens } = await authClient.oauth.clientCredentialsGrant({
audience: `${process.env.AUTH0_ISSUER_BASE_URL}/api/v2/`,
});
const $selfServiceProfile = await fetch(
`${process.env.AUTH0_ISSUER_BASE_URL}/api/v2/self-service-profiles`,
{
headers: {
Authorization: `Bearer ${tokens.access_token}`,
},
}
);
const profile = (await $selfServiceProfile.json()).pop();
const randomId = crypto.getRandomValues(new Uint32Array(4)).join("-");
const $ticket = await fetch(
`${process.env.AUTH0_ISSUER_BASE_URL}/api/v2/self-service-profiles/${profile.id}/sso-ticket`,
{
method: "POST",
headers: {
Authorization: `Bearer ${tokens.access_token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
connection_config: {
name: `${randomId}`,
},
enabled_clients: params.clients_to_enable,
enabled_organizations: body.organizations_to_enable.map(
(organization_id: string) => ({ organization_id })
),
}),
}
);
const data = await $ticket.json();
return NextResponse.json(data, {
status: 200,
});
} catch (error) {
console.error(error);
return NextResponse.json(
{ error: "Error fetching user metadata" },
{ status: 500 }
);
}
})
);
}
/**
* @example export const PUT = handleSelfService();
*/
export function handleConnectionUpdate(params: SelfServiceParams) {
return withRateLimit(
withApiAuthRequired(async (request: NextRequest): Promise<NextResponse> => {
try {
const body = await request.json();
const { data: tokens } = await authClient.oauth.clientCredentialsGrant({
audience: `${process.env.AUTH0_ISSUER_BASE_URL}/api/v2/`,
});
const $selfServiceProfile = await fetch(
`${process.env.AUTH0_ISSUER_BASE_URL}/api/v2/self-service-profiles`,
{
headers: {
Authorization: `Bearer ${tokens.access_token}`,
},
}
);
const profile = (await $selfServiceProfile.json()).pop();
const $ticket = await fetch(
`${process.env.AUTH0_ISSUER_BASE_URL}/api/v2/self-service-profiles/${profile.id}/sso-ticket`,
{
method: "POST",
headers: {
Authorization: `Bearer ${tokens.access_token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
connection_id: body.connection_id,
}),
}
);
const data = await $ticket.json();
return NextResponse.json(data, {
status: 200,
});
} catch (error) {
console.error(error);
return NextResponse.json(
{ error: "Error fetching user metadata" },
{ status: 500 }
);
}
})
);
}
/**
* @example export const GET = handleFetchEnabledConnections();
*/
export function handleFetchEnabledConnections() {
return withRateLimit(
withApiAuthRequired(
async (request: NextRequest, { params }): Promise<NextResponse> => {
try {
const org_id = params?.id as string;
const response = await client.organizations.getEnabledConnections({
id: org_id,
});
const { data } = response;
return NextResponse.json(
data.filter((d) =>
// Note: Only strategies support on self-service.
["oidc", "okta", "samlp", "waad", "google-apps", "adfs"].includes(
d.connection.strategy
)
),
{
status: response.status,
}
);
} catch (error) {
console.error(error);
return NextResponse.json(
{ error: "Error fetching user metadata" },
{ status: 500 }
);
}
}
)
);
}
/**
* @example export const DELETE = handleDeleteConnection();
*/
export function handleDeleteConnection() {
return withRateLimit(
withApiAuthRequired(
async (request: NextRequest, { params }): Promise<NextResponse> => {
try {
const org_id = params?.id as string;
const connection_id = params?.connection_id as string;
await client.organizations.deleteEnabledConnection({
id: org_id,
connectionId: connection_id,
});
await client.connections.delete({ id: connection_id });
return NextResponse.json(
{},
{
status: 200,
}
);
} catch (error) {
console.error(error);
return NextResponse.json(
{ error: "Error deleting connection" },
{ status: 500 }
);
}
}
)
);
}