Docs
Organization Metadata
Experimental
Organization Metadata
Displays and edits organization metadata with schema validation.
Preferences
Installation
Install the following dependencies:
npx shadcn-ui@latest add card button form input select toast label separator
pnpm install zod react-hook-form @hookform/resolvers
Copy and paste the following code into your project.
"use client";
import { useCallback, useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Separator } from "@/components/ui/separator";
import { Toaster } from "@/components/ui/toaster";
import { useToast } from "@/components/ui/use-toast";
import { zodResolver } from "@hookform/resolvers/zod";
interface KeyValueMap {
[key: string]: any;
}
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="mr-2 animate-spin"
>
<path d="M21 12a9 9 0 1 1-6.219-8.56"></path>
</svg>
);
}
export default function OrganizationMetadataForm({
orgId,
schema,
metadata,
onSave,
onFetch,
}: {
orgId: string;
schema: any;
metadata?: KeyValueMap;
onFetch: (id: string) => Promise<KeyValueMap>;
onSave?: (
id: string,
values: KeyValueMap
) => Promise<{
organization?: KeyValueMap;
status: number;
}>;
}) {
const [fetching, setFetching] = useState(false);
const [defaultValues, setDefaultValues] = useState<KeyValueMap | undefined>(
metadata
);
const { toast } = useToast();
const [working, setWorking] = useState<boolean>(false);
const form = useForm<z.infer<typeof schema>>({
resolver: zodResolver(schema),
defaultValues,
});
async function onSubmit(values: z.infer<typeof schema>) {
setWorking(true);
if (typeof onSave === "function") {
const response = await onSave(orgId, { metadata: values });
if (response.status !== 200) {
toast({
title: "Info",
description:
"There was a problem updating preferences. Try again later.",
});
}
}
setWorking(false);
}
const handleFetching = useCallback(
async function handleFetching() {
setFetching(true);
const response = await onFetch(orgId);
if (response.status !== 200) {
return setFetching(false);
}
setDefaultValues(response.organization.metadata);
form.reset(response.organization.metadata, { keepValues: false });
setFetching(false);
},
[form, onFetch, orgId]
);
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">Preferences</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">
<Spinner />
<span className="text-sm text-muted-foreground">
Fetching Preferences...
</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 preferences. Try again later.
</p>
</Label>
</div>
</div>
</CardContent>
)}
{defaultValues && !fetching && (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<CardContent className="p-4 pt-0 md:p-6 md:pt-0">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{Object.keys(schema.shape).map((key: any) => {
// @ts-ignore
const type = schema.shape[key]._def;
const formLabel = (
<FormLabel className="capitalize">
{key.replace("_", " ")}
</FormLabel>
);
if (type.typeName === "ZodEnum") {
return (
<FormField
key={key}
control={form.control}
name={key}
render={({ field }) => (
<FormItem>
{formLabel}
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
{type.values.map((value: any) => (
<SelectItem key={value} value={value}>
{value}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
);
}
return (
<FormField
key={key}
control={form.control}
name={key}
render={({ field }) => (
<FormItem>
{formLabel}
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
);
})}
</div>
</CardContent>
<CardFooter className="p-4 pt-0 md:p-6 md:pt-0">
<Button
type="submit"
disabled={working}
className="disabled:opacity-50 ml-auto"
>
{working && <Spinner />}
Save Preferences
</Button>
</CardFooter>
</form>
</Form>
)}
</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 }
);
}
}
)
);
}