"use client"; import { zodResolver } from "@hookform/resolvers/zod"; import { useState, useTransition } from "react"; import { useForm } from "react-hook-form"; import * as z from "zod"; import { KJUR } from "jsrsasign"; import { Button } from "@/components/ui/button"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; // Zod schema for validation const appleJwtSchema = z.object({ teamId: z.string().min(1, { message: "Team ID is required." }), clientId: z .string() .min(1, { message: "Client ID (Service ID) is required." }), keyId: z.string().min(1, { message: "Key ID is required." }), privateKey: z .string() .min(1, { message: "Private Key content is required." }) .refine( (key) => key.startsWith("-----BEGIN PRIVATE KEY-----"), "Private key must be in PKCS#8 PEM format (starting with -----BEGIN PRIVATE KEY-----)", ) .refine( (key) => key.includes("-----END PRIVATE KEY-----"), "Private key must be in PKCS#8 PEM format (ending with -----END PRIVATE KEY-----)", ), }); type AppleJwtFormValues = z.infer; export const GenerateAppleJwt = () => { const [generatedJwt, setGeneratedJwt] = useState(null); const [error, setError] = useState(null); const [isLoading, startTransition] = useTransition(); const form = useForm({ resolver: zodResolver(appleJwtSchema), defaultValues: { teamId: "", clientId: "", keyId: "", privateKey: "", }, }); const onSubmit = async (data: AppleJwtFormValues) => { setGeneratedJwt(null); setError(null); startTransition(() => { try { //normalize the private key by replacing \r\n with \n and trimming whitespace just incase lol const normalizedKey = data.privateKey.replace(/\r\n/g, "\n").trim(); //since jose is not working with safari, we are using jsrsasign const header = { alg: "ES256", kid: data.keyId, typ: "JWT", }; const issuedAtSeconds = Math.floor(Date.now() / 1000); const expirationSeconds = issuedAtSeconds + 180 * 24 * 60 * 60; // 180 days. Should we let the user choose this ? MAX is 6 months const payload = { iss: data.teamId, // Issuer (Team ID) aud: "https://appleid.apple.com", // Audience sub: data.clientId, // Subject (Client ID -> Service ID) iat: issuedAtSeconds, // Issued At timestamp exp: expirationSeconds, // Expiration timestamp }; const sHeader = JSON.stringify(header); const sPayload = JSON.stringify(payload); const jwt = KJUR.jws.JWS.sign( "ES256", sHeader, sPayload, normalizedKey, ); setGeneratedJwt(jwt); } catch (err: any) { console.error("JWT Generation Error:", err); setError( `Failed to generate JWT: ${ err.message || "Unknown error" }. Check key format and details.`, ); } }); }; const copyToClipboard = () => { if (generatedJwt) { navigator.clipboard.writeText(generatedJwt); } }; return (
( Apple Team ID )} /> ( Client ID (Service ID) The identifier for the service you created in Apple Developer. )} /> ( Key ID The ID associated with your private key (.p8 file). )} /> ( Private Key Content (.p8 file content)