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 } ); } } ) ); }