import { PermissionAction } from '@supabase/shared-types/out/constants' import { JwtSecretUpdateError, JwtSecretUpdateProgress, JwtSecretUpdateStatus, } from '@supabase/shared-types/out/events' import { AlertCircle, ChevronDown, CloudOff, ExternalLink, Eye, EyeOff, Hourglass, Key, Lightbulb, Loader2, PenTool, Power, RefreshCw, TriangleAlert, } from 'lucide-react' import Link from 'next/link' import { Dispatch, SetStateAction, useState } from 'react' import { toast } from 'sonner' import { useFlag, useParams } from 'common' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { FormActions } from 'components/ui/Forms/FormActions' import Panel from 'components/ui/Panel' import { TextConfirmModal } from 'components/ui/TextConfirmModalWrapper' import { useLegacyAPIKeysStatusQuery } from 'data/api-keys/legacy-api-keys-status-query' import { useAuthConfigQuery } from 'data/auth/auth-config-query' import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation' import { useJwtSecretUpdateMutation } from 'data/config/jwt-secret-update-mutation' import { useJwtSecretUpdatingStatusQuery } from 'data/config/jwt-secret-updating-status-query' import { useProjectPostgrestConfigQuery } from 'data/config/project-postgrest-config-query' import { useLegacyJWTSigningKeyQuery } from 'data/jwt-signing-keys/legacy-jwt-signing-key-query' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { uuidv4 } from 'lib/helpers' import { Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, Form, Input, InputNumber, Modal, } from 'ui' import { Admonition } from 'ui-patterns/admonition' import { number, object } from 'yup' import { JWT_SECRET_UPDATE_ERROR_MESSAGES, JWT_SECRET_UPDATE_PROGRESS_MESSAGES, } from './jwt.constants' const schema = object({ JWT_EXP: number() .max(604800, 'Must be less than 604800') .required('Must have a JWT expiry value'), }) const JWTSettings = () => { const { ref: projectRef } = useParams() const newJwtSecrets = useFlag('newJwtSecrets') const [customToken, setCustomToken] = useState('') const [showCustomTokenInput, setShowCustomTokenInput] = useState(false) const [isCreatingKey, setIsCreatingKey] = useState(false) const [isRegeneratingKey, setIsGeneratingKey] = useState(false) const { can: canReadJWTSecret } = useAsyncCheckPermissions( PermissionAction.READ, 'field.jwt_secret' ) const { can: canGenerateNewJWTSecret } = useAsyncCheckPermissions( PermissionAction.INFRA_EXECUTE, 'queue_job.projects.update_jwt' ) const { can: canUpdateConfig } = useAsyncCheckPermissions( PermissionAction.UPDATE, 'custom_config_gotrue' ) const { data } = useJwtSecretUpdatingStatusQuery({ projectRef }) const { data: config, isError } = useProjectPostgrestConfigQuery({ projectRef }) const { mutateAsync: updateJwt, isPending: isSubmittingJwtSecretUpdateRequest } = useJwtSecretUpdateMutation() const { can: canReadAPIKeys } = useAsyncCheckPermissions(PermissionAction.SECRETS_READ, '*') const { data: legacyKey } = useLegacyJWTSigningKeyQuery( { projectRef, }, { enabled: canReadAPIKeys } ) const { data: legacyAPIKeysStatus } = useLegacyAPIKeysStatusQuery( { projectRef }, { enabled: canReadAPIKeys } ) const { data: authConfig, isPending: isLoadingAuthConfig } = useAuthConfigQuery({ projectRef }) const { mutate: updateAuthConfig, isPending: isUpdatingAuthConfig } = useAuthConfigUpdateMutation() const { Failed, Updated, Updating } = JwtSecretUpdateStatus const isJwtSecretUpdateFailed = data?.jwtSecretUpdateStatus === Failed const isNotUpdatingJwtSecret = data?.jwtSecretUpdateStatus === undefined || data?.jwtSecretUpdateStatus === Updated const isUpdatingJwtSecret = data?.jwtSecretUpdateStatus === Updating const jwtSecretUpdateErrorMessage = JWT_SECRET_UPDATE_ERROR_MESSAGES[data?.jwtSecretUpdateError as JwtSecretUpdateError] const jwtSecretUpdateProgressMessage = JWT_SECRET_UPDATE_PROGRESS_MESSAGES[data?.jwtSecretUpdateProgress as JwtSecretUpdateProgress] const INITIAL_VALUES = { JWT_EXP: authConfig?.JWT_EXP ?? 3600, } const onUpdateJwtExp = async (values: any, { resetForm }: any) => { if (!projectRef) return console.error('Project ref is required') updateAuthConfig( { projectRef, config: values }, { onError: (error) => { toast.error(`Failed to update JWT expiry: ${error?.message}`) }, onSuccess: () => { toast.success('Successfully updated JWT expiry') resetForm({ values, initialValues: values }) }, } ) } async function handleJwtSecretUpdate( jwt_secret: string, setModalVisibility: Dispatch> ) { if (!projectRef) return console.error('Project ref is required') const trackingId = uuidv4() try { await updateJwt({ projectRef, jwtSecret: jwt_secret, changeTrackingId: trackingId }) setModalVisibility(false) toast( 'Successfully submitted JWT secret update request. Please wait while your project is updated.' ) } catch (error: any) { toast.error(`Failed to update JWT secret: ${error.message}`) } } return ( <>
{({ handleReset, values, initialValues }: any) => { const hasChanges = JSON.stringify(values) !== JSON.stringify(initialValues) return ( } > {isError ? (

Failed to retrieve JWT settings

) : ( <> {legacyKey && legacyKey.status !== 'revoked' && (

Changing the legacy JWT secret can only be done by rotating to a standby key and then revoking it. It is used to{' '} {legacyKey.status === 'in_use' ? 'sign and verify' : 'only verify'} {' '} JSON Web Tokens by Supabase products.

{legacyAPIKeysStatus && legacyAPIKeysStatus.enabled && (

This includes the anon and service_role JWT based API keys. {' '} Consider switching to publishable and secret API keys to disable them.

)}
)} {legacyKey && legacyKey.status === 'revoked' && ( )} seconds} disabled={!canUpdateConfig || isLoadingAuthConfig} /> {newJwtSecrets && !legacyKey && ( <> {isUpdatingJwtSecret && (

Updating JWT secret: {jwtSecretUpdateProgressMessage}

)} {isJwtSecretUpdateFailed && ( Please try again. If the failures persist, please contact Supabase support with the following details:
Change tracking ID: {data?.changeTrackingId}
Error message: {jwtSecretUpdateErrorMessage}
)}

How to change your JWT secret?

Instead of changing the legacy JWT secret use a combination of the JWT Signing Keys and API keys features. Consider these advantages:

  • Zero-downtime, reversible change.
  • Users remain signed in and bad actors out.
  • Create multiple secret API keys that are immediately revocable and fully covered by audit logs.
  • Private keys and shared secrets are no longer visible by organization members, so they can't leak.
  • Maintain tighter alignment with SOC2 and other security compliance frameworks.
  • Improve app's performance by using public keys to verify JWTs instead of calling getUser().
} loading={isUpdatingJwtSecret} tooltip={{ content: { side: 'bottom', text: !canGenerateNewJWTSecret ? 'You need additional permissions to generate a new JWT secret' : undefined, }, }} > Change legacy JWT secret setIsGeneratingKey(true)} >

Generate a random secret

setIsCreatingKey(true)} >

Create my own secret

)} {!newJwtSecrets && (
{isUpdatingJwtSecret ? (

Updating JWT secret: {jwtSecretUpdateProgressMessage}

) : (

Generate a new JWT secret

A random secret will be created, or you can create your own.

{isUpdatingJwtSecret ? ( ) : !canGenerateNewJWTSecret ? ( } tooltip={{ content: { side: 'bottom', text: 'You need additional permissions to generate a new JWT secret', }, }} > Generate a new secret ) : ( setIsGeneratingKey(true)} >

Generate a random secret

setIsCreatingKey(true)} >

Create my own secret

)}
)}
{isJwtSecretUpdateFailed ? ( Please try again. If the failures persist, please contact Supabase support with the following details:
Change tracking ID: {data?.changeTrackingId}
Error message: {jwtSecretUpdateErrorMessage}
) : null}
)} )} ) }} { setIsGeneratingKey(false) setCustomToken('') }} onConfirm={() => handleJwtSecretUpdate(customToken || 'ROLL', setIsGeneratingKey)} >
  • Use new JWT Signing Keys and API Keys instead

    Consider using a combination of the JWT Signing Keys and API Keys features to achieve the same effect.{' '} Some or all of the warnings listed below might not apply when using these features .

  • Your application will experience significant downtime

    As new anon and service_role keys will be created and the existing ones permanently destroyed, your application will stop functioning for the duration it takes you to swap them.{' '} If you have a mobile, desktop, CLI or any offline-capable application the downtime may be more significant and dependent on app store reviews or user-initiated upgrades or downloads!

    Currently active users will be forcefully signed out (inactive users will keep their sessions).

    All long-lived Storage pre-signed URLs will be permanently invalidated.

  • Your project and database will be restarted

    This process restarts your project, terminating existing connections to your database. You may see API or other unusual errors for{' '} up to 2 minutes while the new secret is deployed.

  • 20-minute cooldown period

    Should you need to revert or repeat this operation, it will take at least 20 minutes before you're able to do so again.

  • Irreversible change! This cannot be undone!

    The old JWT secret will be permanently lost (unless you've saved it prior). Even if you use it again the anon and service_role API keys{' '} will not be restorable to their exact values.

{ setIsCreatingKey(false) setCustomToken('') }} loading={isSubmittingJwtSecretUpdateRequest} customFooter={
} >

Pick a new custom JWT secret. Make sure it is a strong combination of characters that cannot be guessed easily.

setCustomToken(e.target.value)} value={customToken} icon={} type={showCustomTokenInput ? 'text' : 'password'} className="w-full text-left" label="Custom JWT secret" descriptionText="Minimally 32 characters long, '@' and '$' are not allowed." actions={
} />
) } export default JWTSettings