feat: migrate auth to EncryptID SDK client
Replace duplicated WebAuthn ceremony code with SDK EncryptIDClient. Add @encryptid/sdk dependency and cookie persistence. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
96e46af9dc
commit
a4caa71621
|
|
@ -11,6 +11,7 @@
|
||||||
"type-check": "tsc --noEmit"
|
"type-check": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@encryptid/sdk": "file:../encryptid-sdk",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
"maplibre-gl": "^5.0.0",
|
"maplibre-gl": "^5.0.0",
|
||||||
"nanoid": "^5.0.9",
|
"nanoid": "^5.0.9",
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,15 @@
|
||||||
*
|
*
|
||||||
* Optional authentication — anonymous access remains the default.
|
* Optional authentication — anonymous access remains the default.
|
||||||
* When authenticated, the user gets a persistent DID-based identity.
|
* When authenticated, the user gets a persistent DID-based identity.
|
||||||
|
* Uses Zustand with localStorage persistence, delegates WebAuthn ceremony to @encryptid/sdk.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { persist } from 'zustand/middleware';
|
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 ENCRYPTID_SERVER = process.env.NEXT_PUBLIC_ENCRYPTID_SERVER_URL || 'https://encryptid.jeffemmett.com';
|
||||||
|
const client = new EncryptIDClient(ENCRYPTID_SERVER);
|
||||||
|
|
||||||
interface AuthState {
|
interface AuthState {
|
||||||
/** Whether the user is authenticated via EncryptID */
|
/** Whether the user is authenticated via EncryptID */
|
||||||
|
|
@ -30,17 +33,6 @@ interface AuthState {
|
||||||
logout: () => void;
|
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>()(
|
export const useAuthStore = create<AuthState>()(
|
||||||
persist(
|
persist(
|
||||||
(set) => ({
|
(set) => ({
|
||||||
|
|
@ -53,51 +45,8 @@ export const useAuthStore = create<AuthState>()(
|
||||||
login: async () => {
|
login: async () => {
|
||||||
set({ loading: true });
|
set({ loading: true });
|
||||||
try {
|
try {
|
||||||
// Step 1: Get auth options
|
const result = await client.authenticate();
|
||||||
const startRes = await fetch(`${ENCRYPTID_SERVER}/api/auth/start`, {
|
document.cookie = `encryptid_token=${result.token};path=/;max-age=900;SameSite=Lax`;
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({}),
|
|
||||||
});
|
|
||||||
const { options } = await startRes.json();
|
|
||||||
|
|
||||||
// Step 2: WebAuthn ceremony
|
|
||||||
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: any) => ({
|
|
||||||
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;
|
|
||||||
|
|
||||||
// Step 3: Complete auth
|
|
||||||
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');
|
|
||||||
|
|
||||||
set({
|
set({
|
||||||
isAuthenticated: true,
|
isAuthenticated: true,
|
||||||
token: result.token,
|
token: result.token,
|
||||||
|
|
@ -114,55 +63,8 @@ export const useAuthStore = create<AuthState>()(
|
||||||
register: async (username: string) => {
|
register: async (username: string) => {
|
||||||
set({ loading: true });
|
set({ loading: true });
|
||||||
try {
|
try {
|
||||||
// Step 1: Get registration options
|
const result = await client.register(username);
|
||||||
const startRes = await fetch(`${ENCRYPTID_SERVER}/api/register/start`, {
|
document.cookie = `encryptid_token=${result.token};path=/;max-age=900;SameSite=Lax`;
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ username, displayName: username }),
|
|
||||||
});
|
|
||||||
const { options, userId } = await startRes.json();
|
|
||||||
|
|
||||||
// Step 2: WebAuthn ceremony
|
|
||||||
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;
|
|
||||||
|
|
||||||
// Step 3: Complete registration
|
|
||||||
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 any).getTransports?.() || [],
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await completeRes.json();
|
|
||||||
if (!result.success) throw new Error(result.error || 'Registration failed');
|
|
||||||
|
|
||||||
set({
|
set({
|
||||||
isAuthenticated: true,
|
isAuthenticated: true,
|
||||||
token: result.token,
|
token: result.token,
|
||||||
|
|
@ -177,6 +79,7 @@ export const useAuthStore = create<AuthState>()(
|
||||||
},
|
},
|
||||||
|
|
||||||
logout: () => {
|
logout: () => {
|
||||||
|
document.cookie = 'encryptid_token=;path=/;max-age=0;SameSite=Lax';
|
||||||
set({
|
set({
|
||||||
isAuthenticated: false,
|
isAuthenticated: false,
|
||||||
token: null,
|
token: null,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue