diff --git a/apps/studio/components/interfaces/JwtSecrets/jwt-secret-keys-table/create-key-dialog.tsx b/apps/studio/components/interfaces/JwtSecrets/jwt-secret-keys-table/create-key-dialog.tsx index 0a64ac99786..08c7495de9e 100644 --- a/apps/studio/components/interfaces/JwtSecrets/jwt-secret-keys-table/create-key-dialog.tsx +++ b/apps/studio/components/interfaces/JwtSecrets/jwt-secret-keys-table/create-key-dialog.tsx @@ -1,12 +1,15 @@ import dayjs from 'dayjs' import relativeTime from 'dayjs/plugin/relativeTime' -import { useState } from 'react' +import { useMemo, useState } from 'react' import { toast } from 'sonner' import { useJWTSigningKeyCreateMutation } from 'data/jwt-signing-keys/jwt-signing-key-create-mutation' import { JWTAlgorithm } from 'data/jwt-signing-keys/jwt-signing-keys-query' +import { stringToBase64URL } from 'lib/base64url' import { + Badge, Button, + Checkbox_Shadcn_, DialogFooter, DialogHeader, DialogSection, @@ -18,11 +21,18 @@ import { SelectTrigger_Shadcn_, SelectValue_Shadcn_, Select_Shadcn_, + Textarea, } from 'ui' -import { algorithmDescriptions } from '../algorithm-details' dayjs.extend(relativeTime) +const RSA_JWK_REQUIRED_PROPERTIES = ['kty', 'n', 'e', 'p', 'q', 'd', 'dq', 'dp', 'qi'] +const EC_JWK_REQUIRED_PROPERTIES = ['kty', 'crv', 'x', 'y', 'd'] +const ALLOWED_JWK_PROPERTIES = new Set([ + ...RSA_JWK_REQUIRED_PROPERTIES, + ...EC_JWK_REQUIRED_PROPERTIES, +]) + export const CreateKeyDialog = ({ projectRef, onClose, @@ -30,7 +40,73 @@ export const CreateKeyDialog = ({ projectRef: string onClose: () => void }) => { - const [newKeyAlgorithm, setNewKeyAlgorithm] = useState('RS256') + const [newKeyAlgorithm, setNewKeyAlgorithm] = useState('ES256') + const [isBYOK, setBYOK] = useState(false) + const [privateKey, setPrivateKey] = useState('') + const [isBase64, setBase64] = useState(false) + + const privateKeyMessage = useMemo(() => { + const plain = privateKey.replace(/\s+/g, '') + + if (!plain) { + return null + } + + if (newKeyAlgorithm === 'HS256') { + if (privateKey.length < 16) { + return 'Secret must be at least 16 letters long' + } + + return null + } + + let jwk + try { + jwk = JSON.parse(privateKey) + } catch (e: any) { + return 'Private key is not valid JSON' + } + + if (typeof jwk !== 'object' || !jwk) { + return 'Private key must be a JSON object' + } + + if (typeof jwk.kty !== 'string' || !jwk.kty) { + return 'Private key must have a kty property' + } + + if (newKeyAlgorithm === 'RS256') { + if (jwk.kty !== 'RSA') { + return 'Private key must be of RSA type' + } + + if (jwk.e !== 'AQAB') { + return 'RSA private keys must use the 65537 (AQAB) public exponent' + } + + for (let prop of RSA_JWK_REQUIRED_PROPERTIES) { + if (typeof jwk[prop] !== 'string' || !jwk[prop]) { + return `Incomplete RSA private key, required properties are: ${RSA_JWK_REQUIRED_PROPERTIES.join(', ')}` + } + } + } else if (newKeyAlgorithm === 'ES256') { + if (jwk.kty !== 'EC') { + return 'Private key must be of EC type' + } + + if (jwk.crv !== 'P-256') { + return 'EC private keys must use P-256 curve' + } + + for (let prop of EC_JWK_REQUIRED_PROPERTIES) { + if (typeof jwk[prop] !== 'string' || !jwk[prop]) { + return `Incomplete EC private key, required properties are: ${EC_JWK_REQUIRED_PROPERTIES.join(', ')}` + } + } + } + + return null + }, [privateKey, newKeyAlgorithm]) const { mutate, isLoading: isLoadingMutation } = useJWTSigningKeyCreateMutation({ onSuccess: () => { @@ -60,6 +136,24 @@ export const CreateKeyDialog = ({ projectRef: projectRef!, algorithm: newKeyAlgorithm, status: 'standby', + private_jwk: isBYOK + ? newKeyAlgorithm === 'HS256' + ? { + kty: 'oct', + k: isBase64 + ? privateKey + .replace(/\s+/g, '') + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=/g, '') + : stringToBase64URL(privateKey), + } + : Object.fromEntries( + Object.entries(JSON.parse(privateKey)).filter(([prop]) => + ALLOWED_JWK_PROPERTIES.has(prop) + ) + ) + : null, }) } @@ -70,9 +164,18 @@ export const CreateKeyDialog = ({ -
- Choose the key type to use: +

+ This will create a new JWT signing key in standby mode. The key will be available for your + application to use, but won't be used for signing new JWTs until you activate it. Once + you've updated your application to trust this key, you can switch to it for new JWTs. +

+ + + +
+ Choose signing algorithm: setNewKeyAlgorithm(value)} > @@ -82,21 +185,65 @@ export const CreateKeyDialog = ({ HS256 (Symmetric) RS256 (RSA) - ES256 (ECC) - - EdDSA (Ed25519) + + ES256 (ECC) + + Recommended + -

- {algorithmDescriptions[newKeyAlgorithm]} -

+
+
+ + setBYOK(!!value)} + /> + {newKeyAlgorithm === 'HS256' + ? 'Import an existing secret' + : 'Import an existing private key'} + + {isBYOK && ( +
+