| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292 |
- import { ArrowRight, ExternalLink, Info, Key, Timer } from 'lucide-react'
- import { useState } from 'react'
- import { toast } from 'sonner'
- import {
- Badge,
- Button,
- Checkbox,
- cn,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogSection,
- DialogSectionSeparator,
- DialogTitle,
- Label,
- Skeleton,
- } from 'ui'
- import { algorithmLabels } from '../algorithm-details'
- import { statusColors } from '../jwt.constants'
- import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
- import { useEdgeFunctionsQuery } from '@/data/edge-functions/edge-functions-query'
- import { useJWTSigningKeyUpdateMutation } from '@/data/jwt-signing-keys/jwt-signing-key-update-mutation'
- import { JWTSigningKey } from '@/data/jwt-signing-keys/jwt-signing-keys-query'
- export function RotateKeyDialog({
- projectRef,
- standbyKey,
- inUseKey,
- onClose,
- }: {
- projectRef: string
- standbyKey: JWTSigningKey
- inUseKey: JWTSigningKey
- onClose: () => void
- }) {
- const [isStandbyUnderstood, setStandbyUnderstood] = useState(false)
- const [isPreviouslyUsedUnderstood, setPreviouslyUsedUnderstood] = useState(false)
- const [isEdgeFunctionsVerifyJWTUnderstood, setEdgeFunctionsVerifyJWTUnderstood] = useState(false)
- const { data: edgeFunctions, isPending: isLoadingEdgeFunctions } = useEdgeFunctionsQuery({
- projectRef,
- })
- const { mutate, isPending: isPendingMutation } = useJWTSigningKeyUpdateMutation({
- onSuccess: () => {
- toast.success('Signing key rotated successfully')
- onClose()
- },
- onError: (error) => {
- toast.error(`Failed to rotate signing key: ${error.message}`)
- },
- })
- const verifyJWTEdgeFunctions = edgeFunctions?.filter(({ verify_jwt }) => verify_jwt) ?? []
- return (
- <>
- <DialogHeader>
- <DialogTitle>Rotate JWT signing key</DialogTitle>
- <DialogDescription>
- Change the key used by Briven Auth to create new JSON Web Tokens. Non-expired tokens
- remain <span className="text-brand">valid and accepted</span>!
- </DialogDescription>
- </DialogHeader>
- <DialogSectionSeparator />
- <DialogSection className="bg">
- <div className="grid grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] items-center gap-x-4 gap-y-2">
- <Badge
- className={cn(
- statusColors['standby'],
- 'px-4 py-1 gap-2 flex flex-row items-center uppercase'
- )}
- >
- <Timer size={14} />
- Standby key
- </Badge>
- <div>
- <ArrowRight className="size-4 text-foreground-light" />
- </div>
- <div>
- <Badge
- className={cn(
- statusColors['in_use'],
- 'px-4 py-1 gap-2 flex flex-row items-center uppercase'
- )}
- >
- <Key size={14} />
- Current key
- </Badge>
- </div>
- <div className="text-xs text-foreground-light font-mono text-center">
- {algorithmLabels[standbyKey.algorithm]}
- </div>
- <div />
- <div />
- <div className="col-span-3" />
- <Badge
- className={cn(
- statusColors['in_use'],
- 'px-4 py-1 gap-2 flex flex-row items-center uppercase'
- )}
- >
- <Key size={14} />
- Current key
- </Badge>
- <div>
- <ArrowRight className="h-4 w-4 text-foreground-light" />
- </div>
- <Badge
- className={cn(
- statusColors['previously_used'],
- 'px-4 py-1 gap-2 flex flex-row items-center uppercase'
- )}
- >
- <Timer size={14} />
- Previous key
- </Badge>
- <div />
- <div />
- <div className="text-xs text-foreground-light font-mono text-center">
- {algorithmLabels[inUseKey.algorithm]}
- </div>
- </div>
- </DialogSection>
- <DialogSectionSeparator />
- <DialogSection className="flex flex-col gap-4">
- {isLoadingEdgeFunctions ? (
- <>
- <Skeleton className="h-4 w-full" />
- <Skeleton className="h-4 w-full" />
- <Skeleton className="h-4 w-full" />
- <Skeleton className="h-4 w-full" />
- </>
- ) : (
- <>
- <div className="text-sm">To proceed please confirm:</div>
- <Label
- htmlFor="understands-standby"
- className="flex items-top gap-4 text-sm leading-none"
- >
- <Checkbox
- id="understands-standby"
- className="mt-0.5"
- checked={isStandbyUnderstood}
- onCheckedChange={(value) => setStandbyUnderstood(!!value)}
- />
- <p className="text-sm text-foreground-light">
- All of my application's components have picked up the standby key.
- </p>
- <ButtonTooltip
- type="default"
- icon={<Info />}
- className="px-1.5 py-2 mt-0.5"
- tooltip={{
- content: {
- className: 'max-w-[320px] p-4',
- text: (
- <p>
- If your application verifies JWTs on its own in backend servers, functions,
- lambdas or other such components, ensure that they've picked up and are
- verifying tokens against the standby key.
- <br />
- <br />
- Recommendation: Periodically fetch the public keys from the project's{' '}
- <code>jwks.json</code> endpoint.
- </p>
- ),
- },
- }}
- />
- </Label>
- <Label
- htmlFor="understands-previously-used"
- className="flex items-top gap-4 text-sm leading-none"
- >
- <Checkbox
- className="mt-0.5"
- id="understands-previously-used"
- checked={isPreviouslyUsedUnderstood}
- onCheckedChange={(value) => setPreviouslyUsedUnderstood(!!value)}
- />
- <p className="text-sm text-foreground-light">
- To invalidate non-expired JWTs I need to explicitly revoke the currently used key.
- </p>
- <ButtonTooltip
- type="default"
- icon={<Info />}
- className="px-1.5 py-2 mt-0.5"
- tooltip={{
- content: {
- className: 'max-w-[320px] p-4',
- text: (
- <p>
- Rotating the signing key only changes what key is used by Briven Auth to
- issue <em className="text-brand not-italic">new tokens</em>
- .<br />
- <br />
- To prevent users from being prematurely signed out, you have to manually
- revoke the current in use key after rotation.
- <br />
- <br />
- Recommendation: If your JWT expiry time is 1 hour, wait at least 1 hour and
- 15 minutes before revoking the key.
- </p>
- ),
- },
- }}
- />
- </Label>
- {verifyJWTEdgeFunctions.length > 0 && (
- <Label htmlFor="edge-functions-verify-jwt" className="flex gap-4 text-sm">
- <Checkbox
- id="edge-functions-verify-jwt"
- className="mt-0.5"
- checked={isEdgeFunctionsVerifyJWTUnderstood}
- onCheckedChange={(value) => setEdgeFunctionsVerifyJWTUnderstood(!!value)}
- />
- <p className="text-sm text-foreground-light">
- The following Edge Functions may stop functioning for signed-in users as they
- verify the legacy JWT secret:{' '}
- {verifyJWTEdgeFunctions
- .map(({ name }) => (
- <a
- key={name}
- className=""
- href={`../../functions/${name}/details`}
- target="_blank"
- title={name}
- >
- <ExternalLink className="size-3 inline-block" /> <code>{name}</code>
- </a>
- ))
- .reduce<React.ReactNode[]>(
- (arr, v) => (arr.length > 0 ? [...arr, ', ', v] : [v]),
- []
- )}
- </p>
- <ButtonTooltip
- type="default"
- icon={<Info />}
- className="px-1.5 py-2 mt-0.5"
- tooltip={{
- content: {
- className: 'max-w-[320px] p-4',
- text: (
- <p>
- Some of your Edge Functions are set up to require a JWT in the{' '}
- <code>Authorization</code> header signed with the{' '}
- <em className="text-brand not-italic">legacy JWT secret</em>. Rotation
- causes{' '}
- <em className="text-brand not-italic">invocations by signed-in users</em>{' '}
- to fail with HTTP 401 Unauthorized, as the JWT no longer meets this
- requirement.
- <br />
- <br />
- Recommendation: Change all of your Edge Functions to no longer verify JWT
- and implement the verification logic in the function's code yourself by
- using the Briven client library or any other library for working with
- JWT.
- </p>
- ),
- },
- }}
- />
- </Label>
- )}
- </>
- )}
- </DialogSection>
- <DialogFooter>
- <Button
- onClick={() => mutate({ projectRef, keyId: standbyKey.id, status: 'in_use' })}
- disabled={
- isLoadingEdgeFunctions ||
- !isPreviouslyUsedUnderstood ||
- !isStandbyUnderstood ||
- (!!verifyJWTEdgeFunctions.length && !isEdgeFunctionsVerifyJWTUnderstood)
- }
- loading={isPendingMutation}
- >
- Rotate signing key
- </Button>
- </DialogFooter>
- </>
- )
- }
|