fix: additional fixes for api keys, jwt signing keys (#37069)

* fix: additional fixes for api keys, jwt signing keys

* Smol fix

* fix issue with screen

* Fix success handler for migrate JWT secret

* Tiny

* text fix

---------

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
This commit is contained in:
Stojan DimitrovskiandJoshen Lim authored and GitHub committed 2025-07-14 10:04:24 +02:00
1 parent 1cc7124213
commit e0a3ab2c74
7 files changed
+133 -141

No files matched your search

@@ -1,16 +1,7 @@
import { AnimatePresence, motion } from 'framer-motion'
import {
CircleArrowUp,
Eye,
FileKey,
Key,
MoreVertical,
RotateCw,
ShieldOff,
Timer,
Trash2,
} from 'lucide-react'
import { AnimatePresence } from 'framer-motion'
import { RotateCw, Timer } from 'lucide-react'
import { useMemo, useState } from 'react'
import { toast } from 'sonner'
import { useParams } from 'common'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
@@ -18,11 +9,7 @@ import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
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 {
JWTAlgorithm,
JWTSigningKey,
useJWTSigningKeysQuery,
} from 'data/jwt-signing-keys/jwt-signing-keys-query'
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 { useFlag } from 'hooks/ui/useFlag'
@@ -34,11 +21,9 @@ import {
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
Badge,
Button,
Card,
CardContent,
cn,
Dialog,
DialogContent,
DialogFooter,
@@ -46,29 +31,22 @@ import {
DialogSection,
DialogSectionSeparator,
DialogTitle,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from 'ui'
import TextConfirmModal from 'ui-patterns/Dialogs/TextConfirmModal'
import { AlgorithmHoverCard } from '../algorithm-hover-card'
import { statusColors, statusLabels } from '../jwt.constants'
import { SigningKeysComingSoonBanner } from '../signing-keys-coming-soon'
import { StartUsingJwtSigningKeysBanner } from '../start-using-keys-banner'
import { ActionPanel } from './action-panel'
import { CreateKeyDialog } from './create-key-dialog'
import { RotateKeyDialog } from './rotate-key-dialog'
import { KeyDetailsDialog } from './key-details-dialog'
import { RotateKeyDialog } from './rotate-key-dialog'
import { SigningKeyRow } from './signing-key-row'
const MotionTableRow = motion(TableRow)
type DialogType = 'legacy' | 'create' | 'rotate' | 'key-details' | 'revoke' | 'delete'
export default function JWTSecretKeysTable() {
const { ref: projectRef } = useParams()
@@ -76,17 +54,8 @@ export default function JWTSecretKeysTable() {
const newJwtSecrets = useFlag('newJwtSecrets')
const [selectedKey, setSelectedKey] = useState<JWTSigningKey | null>(null)
const [shownDialog, setShownDialog] = useState<
'legacy' | 'create' | 'rotate' | 'key-details' | 'revoke' | 'delete' | null
>(null)
const resetDialog = () => {
setSelectedKey(null)
setShownDialog(null)
}
const [newKeyAlgorithm, setNewKeyAlgorithm] = useState<JWTAlgorithm>('RS256')
const [selectedKey, setSelectedKey] = useState<JWTSigningKey>()
const [shownDialog, setShownDialog] = useState<DialogType>()
const { data: signingKeys, isLoading: isLoadingSigningKeys } = useJWTSigningKeysQuery({
projectRef,
@@ -97,13 +66,21 @@ export default function JWTSecretKeysTable() {
const { data: legacyAPIKeysStatus, isLoading: isLoadingLegacyAPIKeysStatus } =
useLegacyAPIKeysStatusQuery({ projectRef })
const legacyMutation = useLegacyJWTSigningKeyCreateMutation()
const { mutate: migrateJWTSecret, isLoading: isMigrating } = useLegacyJWTSigningKeyCreateMutation(
{
onSuccess: () => {
setShownDialog(undefined)
toast.success('Successfully migrated JWT secret!')
},
}
)
const updateMutation = useJWTSigningKeyUpdateMutation()
const deleteMutation = useJWTSigningKeyDeleteMutation()
const { mutate: updateJWTSigningKey, isLoading: isUpdatingJWTSigningKey } =
useJWTSigningKeyUpdateMutation({ onSuccess: () => resetDialog() })
const { mutate: deleteJWTSigningKey, isLoading: isDeletingJWTSigningKey } =
useJWTSigningKeyDeleteMutation({ onSuccess: () => resetDialog(), onError: () => resetDialog() })
const isLoadingMutation =
updateMutation.isLoading || deleteMutation.isLoading || legacyMutation.isLoading
const isLoadingMutation = isUpdatingJWTSigningKey || isDeletingJWTSigningKey || isMigrating
const isLoading =
isProjectLoading || isLoadingSigningKeys || isLoadingLegacyKey || isLoadingLegacyAPIKeysStatus
@@ -135,61 +112,25 @@ export default function JWTSecretKeysTable() {
[sortedKeys]
)
const handleLegacyMigration = async () => {
try {
await legacyMutation.mutateAsync({
projectRef: projectRef!,
})
} catch (error) {
console.error('Failed to migrate legacy JWT secret to new JWT signing keys', error)
}
const resetDialog = () => {
setSelectedKey(undefined)
setShownDialog(undefined)
}
const handlePreviouslyUsedKey = async (keyId: string) => {
updateMutation.mutate(
{ projectRef, keyId, status: 'previously_used' },
{
onSuccess: () => {
resetDialog()
},
}
)
updateJWTSigningKey({ projectRef, keyId, status: 'previously_used' })
}
const handleStandbyKey = (keyId: string) => {
updateMutation.mutate(
{ projectRef: projectRef!, keyId, status: 'standby' },
{
onSuccess: () => {
resetDialog()
},
}
)
updateJWTSigningKey({ projectRef: projectRef!, keyId, status: 'standby' })
}
const handleRevokeKey = (keyId: string) => {
updateMutation.mutate(
{ projectRef: projectRef!, keyId, status: 'revoked' },
{
onSuccess: () => {
resetDialog()
},
}
)
updateJWTSigningKey({ projectRef: projectRef!, keyId, status: 'revoked' })
}
const handleDeleteKey = (keyId: string) => {
deleteMutation.mutate(
{ projectRef: projectRef!, keyId },
{
onSuccess: () => {
resetDialog()
},
onError: () => {
resetDialog()
},
}
)
deleteJWTSigningKey({ projectRef: projectRef!, keyId })
}
if (isLoading) {
@@ -232,7 +173,7 @@ export default function JWTSecretKeysTable() {
) : (
<StartUsingJwtSigningKeysBanner
onClick={() => setShownDialog('legacy')}
isLoading={isLoadingMutation}
isLoading={isMigrating}
/>
)}
</div>
@@ -455,9 +396,8 @@ export default function JWTSecretKeysTable() {
</DialogSection>
<DialogFooter>
<Button
onClick={() => handleLegacyMigration()}
disabled={isLoadingMutation}
loading={isLoadingMutation}
loading={isMigrating}
onClick={() => migrateJWTSecret({ projectRef: projectRef! })}
>
Migrate JWT secret
</Button>
@@ -2,12 +2,12 @@ import { motion } from 'framer-motion'
import {
CircleArrowDown,
CircleArrowUp,
Trash2,
Eye,
Key,
MoreVertical,
ShieldOff,
Timer,
Trash2,
} from 'lucide-react'
import { components } from 'api-types'
@@ -28,8 +28,8 @@ import { statusColors, statusLabels } from '../jwt.constants'
interface SigningKeyRowProps {
signingKey: components['schemas']['SigningKeyResponse']
setSelectedKey: (key: JWTSigningKey | null) => void
setShownDialog: (dialog: 'key-details' | 'revoke' | 'delete' | null) => void
setSelectedKey: (key?: JWTSigningKey) => void
setShownDialog: (dialog?: 'key-details' | 'revoke' | 'delete') => void
handlePreviouslyUsedKey: (keyId: string) => void
handleStandbyKey: (keyId: string) => void
legacyKey?: JWTSigningKey | null
@@ -4,43 +4,40 @@ import {
JwtSecretUpdateProgress,
JwtSecretUpdateStatus,
} from '@supabase/shared-types/out/events'
import {
AlertCircle,
ChevronDown,
Eye,
EyeOff,
Hourglass,
Key,
CloudOff,
Loader2,
PenTool,
Power,
RefreshCw,
TriangleAlert,
ExternalLink,
Lightbulb,
} from 'lucide-react'
import { Dispatch, SetStateAction, useState } from 'react'
import { toast } from 'sonner'
import { number, object } from 'yup'
import Link from 'next/link'
import { useParams } from 'common'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { FormActions } from 'components/ui/Forms/FormActions'
import Panel from 'components/ui/Panel'
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 { useLegacyAPIKeysStatusQuery } from 'data/api-keys/legacy-api-keys-status-query'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useFlag } from 'hooks/ui/useFlag'
import { uuidv4 } from 'lib/helpers'
import {
AlertDescription_Shadcn_,
AlertTitle_Shadcn_,
Alert_Shadcn_,
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 {
Button,
DropdownMenu,
DropdownMenuContent,
@@ -51,12 +48,10 @@ import {
Input,
InputNumber,
Modal,
WarningIcon,
} from 'ui'
import { Admonition } from 'ui-patterns/admonition'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import TextConfirmModal from 'ui-patterns/Dialogs/TextConfirmModal'
import { useFlag } from 'hooks/ui/useFlag'
import { number, object } from 'yup'
import {
JWT_SECRET_UPDATE_ERROR_MESSAGES,
JWT_SECRET_UPDATE_PROGRESS_MESSAGES,
@@ -193,25 +188,29 @@ const JWTSettings = () => {
) : (
<>
{legacyKey && legacyKey.status !== 'revoked' && (
<Admonition type="warning">
You've successfully migrated your legacy JWT secret to the new JWT Signing
Keys feature. Changing the legacy JWT secret now can only be done by
rotating to a standby key and finally revoking it. Now used to{' '}
<em className="text-brand not-italic">
{legacyKey.status === 'in_use' ? 'sign and verify' : 'only verify'}
</em>{' '}
JSON Web Tokens by Supabase products.{' '}
<Admonition
type="warning"
title="Legacy JWT secret has been migrated to new JWT Signing Keys"
>
<p className="!leading-normal">
Changing the legacy JWT secret can only be done by rotating to a standby
key and then revoking it. It is used to{' '}
<em className="text-foreground not-italic">
{legacyKey.status === 'in_use' ? 'sign and verify' : 'only verify'}
</em>{' '}
JSON Web Tokens by Supabase products.
</p>
{legacyAPIKeysStatus && legacyAPIKeysStatus.enabled && (
<>
<p className="!leading-normal">
<em className="text-warning not-italic">
This includes the <code>anon</code> and <code>service_role</code> JWT
based API keys.
</em>{' '}
Consider switching to publishable and secret API keys to disable them.
</>
</p>
)}
<br />
<br />
<Button type="default" asChild icon={<ExternalLink className="size-4" />}>
<Link href={`/project/${projectRef}/settings/api-keys`}>
Go to API keys
@@ -220,10 +219,11 @@ const JWTSettings = () => {
</Admonition>
)}
{legacyKey && legacyKey.status === 'revoked' && (
<Admonition type="note">
Your project has revoked the legacy JWT secret. No new JSON Web Tokens are
issued nor verified with it by Supabase products.
</Admonition>
<Admonition
type="note"
title="Your project has revoked the legacy JWT secret"
description="No new JSON Web Tokens are issued nor verified with it by Supabase products."
/>
)}
<Input
label={
@@ -9,12 +9,12 @@ const ApiKeysLayout = ({ children }: PropsWithChildren) => {
const navigationItems = [
{
label: 'API Keys',
label: 'Legacy API Keys',
href: `/project/${projectRef}/settings/api-keys`,
id: 'legacy-keys',
},
{
label: 'New API Keys (Coming Soon)',
label: 'API Keys',
href: `/project/${projectRef}/settings/api-keys/new`,
id: 'new-keys',
},
@@ -7,13 +7,28 @@ import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { useToggleLegacyAPIKeysMutation } from 'data/api-keys/legacy-api-key-toggle-mutation'
import { useLegacyAPIKeysStatusQuery } from 'data/api-keys/legacy-api-keys-status-query'
import { useLegacyJWTSigningKeyQuery } from 'data/jwt-signing-keys/legacy-jwt-signing-key-query'
import { useAuthorizedAppsQuery } from 'data/oauth/authorized-apps-query'
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from 'ui'
import TextConfirmModal from 'ui-patterns/Dialogs/TextConfirmModal'
import Panel from '../Panel'
export const ToggleLegacyApiKeysPanel = () => {
const { ref: projectRef } = useParams()
const org = useSelectedOrganization()
const [isConfirmOpen, setIsConfirmOpen] = useState(false)
const [isAppsWarningOpen, setIsAppsWarningOpen] = useState(false)
const { data: legacyAPIKeysStatusData, isSuccess: isLegacyAPIKeysStatusSuccess } =
useLegacyAPIKeysStatusQuery({ projectRef })
@@ -23,9 +38,13 @@ export const ToggleLegacyApiKeysPanel = () => {
const { can: canUpdateAPIKeys, isSuccess: isPermissionsSuccess } =
useAsyncCheckProjectPermissions(PermissionAction.SECRETS_WRITE, '*')
const { data: authorizedApps = [], isSuccess: isAuthorizedAppsSuccess } = useAuthorizedAppsQuery({
slug: org?.slug,
})
const { enabled: isLegacyKeysEnabled } = legacyAPIKeysStatusData || {}
if (!(isLegacyAPIKeysStatusSuccess && isPermissionsSuccess)) {
if (!(isLegacyAPIKeysStatusSuccess && isPermissionsSuccess && isAuthorizedAppsSuccess)) {
return null
}
@@ -47,7 +66,11 @@ export const ToggleLegacyApiKeysPanel = () => {
<div className="flex items-center">
<ButtonTooltip
type="default"
onClick={() => setIsConfirmOpen(true)}
onClick={
authorizedApps?.length
? () => setIsAppsWarningOpen(true)
: () => setIsConfirmOpen(true)
}
disabled={
!canUpdateAPIKeys ||
(!isLegacyKeysEnabled && legacyJWTSecret?.status === 'revoked')
@@ -77,6 +100,25 @@ export const ToggleLegacyApiKeysPanel = () => {
onClose={() => setIsConfirmOpen(false)}
legacyAPIKeysStatusData={legacyAPIKeysStatusData}
/>
<AlertDialog open={isAppsWarningOpen} onOpenChange={(value) => setIsAppsWarningOpen(value)}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Apps using Supabase may break</AlertDialogTitle>
<AlertDialogDescription>
Your project uses apps that integrate with Supabase. Disabling the legacy API keys is
a brand new feature and the apps you're using may not have added support for this yet.
It can cause them to stop functioning. Check before continuing.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={() => setIsConfirmOpen(true)}>
Proceed to disable API keys
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</section>
)
}
@@ -136,6 +178,12 @@ const ToggleApiKeysModal = ({
use will cause downtime for your application. Ensure they are no longer in use
before proceeding. If you have not created a publishable and at least one secret
API key, some dashboard functionality may become unavailable.
<br />
<br />
<span className="text-danger">
This disables API keys when used in the <code>apikey</code> header. They remain
valid as a JWT.
</span>
</span>
),
}
@@ -47,6 +47,8 @@ export const useLegacyJWTSigningKeyCreateMutation = ({
const { projectRef } = variables
await queryClient.invalidateQueries(jwtSigningKeysKeys.legacy(projectRef))
// invalidate the keys as well since migration creates 2 new keys and the UI needs to be refreshed
await queryClient.invalidateQueries(jwtSigningKeysKeys.list(projectRef))
await onSuccess?.(data, variables, context)
},
@@ -37,6 +37,8 @@ export const useLegacyJWTSigningKeyQuery = <TData = LegacyJWTSigningKeyData>(
({ signal }) => getLegacyJWTSigningKey({ projectRef }, signal),
{
enabled: enabled && !!projectRef,
retry: false,
refetchOnWindowFocus: false,
...options,
}
)