feat: migrate auth to EncryptID SDK client
Replace duplicated WebAuthn ceremony code with SDK EncryptIDClient. Add cookie persistence for server-side token access. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
6411604852
commit
57f3caf8aa
108
lib/auth.ts
108
lib/auth.ts
|
|
@ -2,13 +2,15 @@
|
|||
* EncryptID Auth Store for rfunds-online
|
||||
*
|
||||
* Provides passkey-based identity for space creation and editing.
|
||||
* Uses Zustand with localStorage persistence.
|
||||
* Uses Zustand with localStorage persistence, delegates WebAuthn ceremony to @encryptid/sdk.
|
||||
*/
|
||||
|
||||
import { create } from 'zustand'
|
||||
import { persist } from 'zustand/middleware'
|
||||
import { EncryptIDClient } from '@encryptid/sdk/client'
|
||||
|
||||
const ENCRYPTID_SERVER = process.env.NEXT_PUBLIC_ENCRYPTID_SERVER_URL || 'https://encryptid.jeffemmett.com'
|
||||
const client = new EncryptIDClient(ENCRYPTID_SERVER)
|
||||
|
||||
interface AuthState {
|
||||
isAuthenticated: boolean
|
||||
|
|
@ -22,17 +24,6 @@ interface AuthState {
|
|||
logout: () => void
|
||||
}
|
||||
|
||||
function toBase64url(buffer: ArrayBuffer): string {
|
||||
return btoa(String.fromCharCode(...new Uint8Array(buffer)))
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/=+$/, '')
|
||||
}
|
||||
|
||||
function fromBase64url(str: string): Uint8Array {
|
||||
return Uint8Array.from(atob(str.replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0))
|
||||
}
|
||||
|
||||
export const useAuthStore = create<AuthState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
|
|
@ -45,48 +36,8 @@ export const useAuthStore = create<AuthState>()(
|
|||
login: async () => {
|
||||
set({ loading: true })
|
||||
try {
|
||||
const startRes = await fetch(`${ENCRYPTID_SERVER}/api/auth/start`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({}),
|
||||
})
|
||||
const { options } = await startRes.json()
|
||||
|
||||
const publicKeyOptions: PublicKeyCredentialRequestOptions = {
|
||||
challenge: fromBase64url(options.challenge).buffer as ArrayBuffer,
|
||||
rpId: options.rpId,
|
||||
userVerification: options.userVerification as UserVerificationRequirement,
|
||||
timeout: options.timeout,
|
||||
allowCredentials: options.allowCredentials?.map((c: { type: string; id: string; transports: string[] }) => ({
|
||||
type: c.type as PublicKeyCredentialType,
|
||||
id: fromBase64url(c.id).buffer as ArrayBuffer,
|
||||
transports: c.transports as AuthenticatorTransport[],
|
||||
})),
|
||||
}
|
||||
|
||||
const assertion = await navigator.credentials.get({ publicKey: publicKeyOptions }) as PublicKeyCredential
|
||||
if (!assertion) throw new Error('Authentication cancelled')
|
||||
|
||||
const response = assertion.response as AuthenticatorAssertionResponse
|
||||
|
||||
const completeRes = await fetch(`${ENCRYPTID_SERVER}/api/auth/complete`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
challenge: options.challenge,
|
||||
credential: {
|
||||
credentialId: assertion.id,
|
||||
authenticatorData: toBase64url(response.authenticatorData),
|
||||
clientDataJSON: toBase64url(response.clientDataJSON),
|
||||
signature: toBase64url(response.signature),
|
||||
userHandle: response.userHandle ? toBase64url(response.userHandle) : null,
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
const result = await completeRes.json()
|
||||
if (!result.success) throw new Error(result.error || 'Authentication failed')
|
||||
|
||||
const result = await client.authenticate()
|
||||
document.cookie = `encryptid_token=${result.token};path=/;max-age=900;SameSite=Lax`
|
||||
set({
|
||||
isAuthenticated: true,
|
||||
token: result.token,
|
||||
|
|
@ -103,52 +54,8 @@ export const useAuthStore = create<AuthState>()(
|
|||
register: async (username: string) => {
|
||||
set({ loading: true })
|
||||
try {
|
||||
const startRes = await fetch(`${ENCRYPTID_SERVER}/api/register/start`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, displayName: username }),
|
||||
})
|
||||
const { options, userId } = await startRes.json()
|
||||
|
||||
const publicKeyOptions: PublicKeyCredentialCreationOptions = {
|
||||
challenge: fromBase64url(options.challenge).buffer as ArrayBuffer,
|
||||
rp: options.rp,
|
||||
user: {
|
||||
id: fromBase64url(options.user.id).buffer as ArrayBuffer,
|
||||
name: options.user.name,
|
||||
displayName: options.user.displayName,
|
||||
},
|
||||
pubKeyCredParams: options.pubKeyCredParams,
|
||||
authenticatorSelection: options.authenticatorSelection,
|
||||
timeout: options.timeout,
|
||||
attestation: options.attestation as AttestationConveyancePreference,
|
||||
}
|
||||
|
||||
const credential = await navigator.credentials.create({ publicKey: publicKeyOptions }) as PublicKeyCredential
|
||||
if (!credential) throw new Error('Registration cancelled')
|
||||
|
||||
const response = credential.response as AuthenticatorAttestationResponse
|
||||
|
||||
const completeRes = await fetch(`${ENCRYPTID_SERVER}/api/register/complete`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
challenge: options.challenge,
|
||||
userId,
|
||||
username,
|
||||
credential: {
|
||||
credentialId: credential.id,
|
||||
publicKey: toBase64url(response.getPublicKey?.() || response.attestationObject),
|
||||
attestationObject: toBase64url(response.attestationObject),
|
||||
clientDataJSON: toBase64url(response.clientDataJSON),
|
||||
transports: (response as AuthenticatorAttestationResponse & { getTransports?: () => string[] }).getTransports?.() || [],
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
const result = await completeRes.json()
|
||||
if (!result.success) throw new Error(result.error || 'Registration failed')
|
||||
|
||||
const result = await client.register(username)
|
||||
document.cookie = `encryptid_token=${result.token};path=/;max-age=900;SameSite=Lax`
|
||||
set({
|
||||
isAuthenticated: true,
|
||||
token: result.token,
|
||||
|
|
@ -163,6 +70,7 @@ export const useAuthStore = create<AuthState>()(
|
|||
},
|
||||
|
||||
logout: () => {
|
||||
document.cookie = 'encryptid_token=;path=/;max-age=0;SameSite=Lax'
|
||||
set({
|
||||
isAuthenticated: false,
|
||||
token: null,
|
||||
|
|
|
|||
Loading…
Reference in New Issue