import { PermissionAction } from '@supabase/shared-types/out/constants' import { useParams } from 'common' import { AnimatePresence } from 'framer-motion' import { AlertCircle, RotateCw, Timer } from 'lucide-react' import { useMemo, useState } from 'react' import { toast } from 'sonner' import { AlertDialog, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, Button, Card, CardContent, Dialog, DialogContent, DialogFooter, DialogHeader, DialogSection, DialogSectionSeparator, DialogTitle, Table, TableBody, TableHead, TableHeader, TableRow, } from 'ui' import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import { StartUsingJwtSigningKeysBanner } from '../start-using-keys-banner' import { ActionPanel } from './action-panel' import { CreateKeyDialog } from './create-key-dialog' import { KeyDetailsDialog } from './key-details-dialog' import { RotateKeyDialog } from './rotate-key-dialog' import { SigningKeyRow } from './signing-key-row' import { TextConfirmModal } from '@/components/ui/TextConfirmModalWrapper' import { useLegacyAPIKeysStatusQuery } from '@/data/api-keys/legacy-api-keys-status-query' import { useJWTSigningKeyDeleteMutation } from '@/data/jwt-signing-keys/jwt-signing-key-delete-mutation' import { useJWTSigningKeyUpdateMutation } from '@/data/jwt-signing-keys/jwt-signing-key-update-mutation' import { JWTSigningKey, useJWTSigningKeysQuery, } from '@/data/jwt-signing-keys/jwt-signing-keys-query' import { useLegacyJWTSigningKeyCreateMutation } from '@/data/jwt-signing-keys/legacy-jwt-signing-key-create-mutation' import { useLegacyJWTSigningKeyQuery } from '@/data/jwt-signing-keys/legacy-jwt-signing-key-query' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' type DialogType = 'legacy' | 'create' | 'rotate' | 'key-details' | 'revoke' | 'delete' export const JWTSecretKeysTable = () => { const { ref: projectRef } = useParams() const { data: project, isPending: isProjectLoading } = useSelectedProjectQuery() const [selectedKey, setSelectedKey] = useState() const [selectedKeyToUpdate, setSelectedKeyToUpdate] = useState() const [shownDialog, setShownDialog] = useState() const { can: canReadAPIKeys, isLoading: isLoadingCanReadAPIKeys } = useAsyncCheckPermissions( PermissionAction.SECRETS_READ, '*' ) const { data: signingKeys, isPending: isLoadingSigningKeys } = useJWTSigningKeysQuery( { projectRef, }, { enabled: canReadAPIKeys } ) const { data: legacyKey, isPending: isLoadingLegacyKey } = useLegacyJWTSigningKeyQuery( { projectRef, }, { enabled: canReadAPIKeys } ) const { data: legacyAPIKeysStatus, isPending: isLoadingLegacyAPIKeysStatus } = useLegacyAPIKeysStatusQuery({ projectRef }, { enabled: canReadAPIKeys }) const { mutate: migrateJWTSecret, isPending: isMigrating } = useLegacyJWTSigningKeyCreateMutation( { onSuccess: () => { setShownDialog(undefined) toast.success('Successfully migrated JWT secret!') }, } ) const { mutate: updateJWTSigningKey, isPending: isUpdatingJWTSigningKey } = useJWTSigningKeyUpdateMutation({ onSuccess: () => { resetDialog() setSelectedKeyToUpdate(undefined) }, }) const { mutate: deleteJWTSigningKey, isPending: isDeletingJWTSigningKey } = useJWTSigningKeyDeleteMutation({ onSuccess: () => resetDialog(), onError: () => resetDialog() }) const isPendingMutation = isUpdatingJWTSigningKey || isDeletingJWTSigningKey || isMigrating const isLoading = isProjectLoading || isLoadingSigningKeys || isLoadingLegacyKey || isLoadingLegacyAPIKeysStatus const sortedKeys = useMemo(() => { if (!signingKeys || !Array.isArray(signingKeys.keys)) return [] return signingKeys.keys.sort((a: JWTSigningKey, b: JWTSigningKey) => { const order: Record = { standby: 0, in_use: 1, previously_used: 2, revoked: 3, } return ( order[a.status] - order[b.status] || new Date(b.created_at).getTime() - new Date(a.created_at).getTime() ) }) }, [signingKeys]) const standbyKey = useMemo(() => sortedKeys.find((key) => key.status === 'standby'), [sortedKeys]) const inUseKey = useMemo(() => sortedKeys.find((key) => key.status === 'in_use'), [sortedKeys]) const previouslyUsedKeys = useMemo( () => sortedKeys.filter((key) => key.status === 'previously_used'), [sortedKeys] ) const revokedKeys = useMemo( () => sortedKeys.filter((key) => key.status === 'revoked'), [sortedKeys] ) const resetDialog = () => { setSelectedKey(undefined) setShownDialog(undefined) } const handlePreviouslyUsedKey = async (keyId: string) => { setSelectedKeyToUpdate(keyId) updateJWTSigningKey( { projectRef, keyId, status: 'previously_used' }, { onSuccess: () => toast.success('Successfully moved key to previously used') } ) } const handleStandbyKey = (keyId: string) => { setSelectedKeyToUpdate(keyId) updateJWTSigningKey( { projectRef: projectRef!, keyId, status: 'standby' }, { onSuccess: () => toast.success('Successfully moved key to standby') } ) } const handleRevokeKey = (keyId: string) => { updateJWTSigningKey( { projectRef: projectRef!, keyId, status: 'revoked' }, { onSuccess: () => toast.success('Successfully revoked key') } ) } const handleDeleteKey = (keyId: string) => { deleteJWTSigningKey( { projectRef: projectRef!, keyId }, { onSuccess: () => toast.success('Successfully deleted key') } ) } if (!canReadAPIKeys && !isLoadingCanReadAPIKeys) { return (

You don't have permission to view JWT signing keys. These keys are restricted to users with higher access levels.

) } if (isLoading) { return } return ( <>
{!canReadAPIKeys ? null : legacyKey ? ( <> {standbyKey ? ( setShownDialog('rotate')} loading={isUpdatingJWTSigningKey} icon={} type="primary" /> ) : ( setShownDialog('create')} loading={isPendingMutation} type="primary" icon={} /> )} ) : ( setShownDialog('legacy')} isLoading={isMigrating} /> )}
{sortedKeys.length > 0 && ( <>
Status Key ID Type Actions {standbyKey && ( )} {inUseKey && ( )}

Previously used keys

These JWT signing keys are still used to{' '} verify tokens that are yet to expire. Revoke once all tokens have expired.

{previouslyUsedKeys.length > 0 ? ( Status Key ID Type Last rotated at Actions {previouslyUsedKeys.map((key) => ( ))}
) : (

No previously used keys

Rotated keys will appear here for verification of existing tokens

)}
)} {revokedKeys.length > 0 && (

Revoked keys

These keys are no longer used to verify or sign JWTs.

Status Key ID Type Last rotated at Actions {revokedKeys.map((key) => ( ))}
)} Start using new JWT signing keys

Your project today uses a legacy symmetric JWT secret to create JWTs. To be able to use an asymmetric JWT signing key you first have to migrate it to the new approach.

This change does not cause any downtime on your project.

{standbyKey && inUseKey && projectRef && ( )} {selectedKey && project && ( )} {selectedKey && selectedKey.status === 'previously_used' && (legacyKey?.id !== selectedKey.id || !(legacyAPIKeysStatus?.enabled ?? false)) && ( handleRevokeKey(selectedKey.id)} onCancel={resetDialog} title={`Revoke ${selectedKey.id}`} confirmString={selectedKey.id} confirmLabel="Yes, revoke this signing key" confirmPlaceholder="Type the ID of the key to confirm" variant="destructive" alert={{ title: 'This key will no longer be trusted!', description: 'By revoking a signing key, all applications trusting it will no longer do so. If there are JWTs (access tokens) that are valid at the time of revocation, they will no longer be trusted, causing users with such JWTs to be signed out.', }} /> )} {selectedKey && selectedKey.status === 'previously_used' && legacyKey?.id === selectedKey.id && (legacyAPIKeysStatus?.enabled ?? true) && ( resetDialog()}> Disable JWT-based legacy API keys first It's not possible to revoke the legacy JWT secret unless you have already disabled JWT-based legacy API keys. This is because revoking the JWT secret invalidates the JWT-based legacy API keys. OK )} {selectedKey && selectedKey.status === 'revoked' && ( handleDeleteKey(selectedKey.id)} onCancel={resetDialog} title={`Permanently delete ${selectedKey.id}`} confirmString={selectedKey.id} confirmLabel="Yes, permanently delete this key" confirmPlaceholder="Type the ID of the key to confirm" variant="destructive" alert={{ title: 'This key will be permanently deleted.', description: 'The private key and all information about this key will be permanently deleted from our records. This action cannot be undone.', }} /> )} ) }