Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
564f163
feat: add passkeys registration, auth, verify routes
matteyu Jan 22, 2025
edc5e1e
add auth methods for passkeys
matteyu Jan 24, 2025
4655f75
handle merge conflicts
matteyu Feb 25, 2025
91b3475
add missing userprofile fields
matteyu Feb 25, 2025
1746dbd
fix imports
matteyu Feb 26, 2025
f11d5f0
add deps for erd
matteyu Feb 26, 2025
d1d19ce
skip erd generate on vercel
matteyu Feb 26, 2025
f7ba311
swtich to npx
matteyu Feb 26, 2025
5caa432
comment out prisma erd generator
matteyu Feb 26, 2025
a49eaff
refactor passkeys
matteyu Feb 26, 2025
fde81bd
change to buffer.from
matteyu Feb 26, 2025
2951926
add exported types from prisma
matteyu Feb 26, 2025
88d195f
dry functions for challenge validations
matteyu Feb 26, 2025
1e08ccf
fix merge conflicts
matteyu Feb 27, 2025
e11ff0b
add challenge purpose authentication
matteyu Feb 27, 2025
7f49d71
fix merge conflicts
matteyu Mar 4, 2025
47adcc9
merge conflicts
matteyu Mar 5, 2025
6e8a885
fix merge conflicts
matteyu Mar 10, 2025
f284f19
add apple callback
matteyu Mar 10, 2025
b0e5316
fix merge conflict
matteyu Mar 12, 2025
19f1040
refactor passkeys
matteyu Mar 12, 2025
bf147c6
remove unused auth
matteyu Mar 13, 2025
b6530a9
modify test page
matteyu Mar 20, 2025
df6ed2f
switch to otp
matteyu Mar 24, 2025
e184777
add refresh token test
matteyu Mar 24, 2025
c26f941
Merge branch 'development' of github.com:arconnectio/embed-api into a…
matteyu Mar 25, 2025
2c461d1
add back readme
matteyu Mar 25, 2025
d253050
revert back context session update
matteyu Mar 25, 2025
73cbec6
fix input errors
matteyu Mar 25, 2025
995ee31
remake init migration
matteyu Mar 25, 2025
0bcf1a6
resolve merge confilicts
matteyu Mar 25, 2025
e152969
add back custom access token migration
matteyu Mar 25, 2025
9a7ee95
rename init migration
matteyu Mar 25, 2025
4a6a618
remove countryCode from context
matteyu Mar 25, 2025
8cd440f
fix context errors
matteyu Mar 25, 2025
72cf369
Merge branch 'development' of github.com:arconnectio/embed-api into a…
matteyu Apr 1, 2025
8c3a390
add index
matteyu Apr 2, 2025
845aa21
fix passkey logic
matteyu Apr 8, 2025
bdfc880
implement authentication verification
matteyu Apr 9, 2025
ba37c16
add session metadata
matteyu Apr 9, 2025
fe976e0
modifications for UI integration
matteyu Apr 20, 2025
bb53dfc
fix migration files
matteyu Apr 21, 2025
a3dff5e
fix reuse body in req for custom token
matteyu Apr 21, 2025
d4cdabe
enable for iframe
matteyu Apr 22, 2025
c1f0fe2
fix merge conflicts
matteyu Apr 30, 2025
224925c
use supabase auth for token generation
matteyu May 1, 2025
ac6f774
fix user trigger
matteyu May 2, 2025
70153a5
cleanup crossauth
matteyu May 2, 2025
cfad026
fix session data structure
matteyu May 6, 2025
0c4438b
restore x-custom-auth
matteyu May 6, 2025
626ecf3
fix merge conflicts
matteyu May 6, 2025
34b3e8d
modify passkey appraoch
matteyu May 7, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,10 @@ MAX_ACTIVATIONS_PER_WALLET=""
MAX_WORK_SHARES_PER_WALLET=""
MAX_RECOVERY_SHARES_PER_WALLET=""
MAX_RECOVERIES_PER_WALLET=""
MAX_EXPORTS_PER_WALLET=""
MAX_EXPORTS_PER_WALLET=""


# PASSKEYS
WEBAUTHN_RELYING_PARTY_NAME=""
WEBAUTHN_RELYING_PARTY_ID=""
WEBAUTHN_RELYING_PARTY_ORIGIN=""
6 changes: 3 additions & 3 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ export default function Login() {
const handleGoogleSignIn = async () => {
setIsLoading(true)
try {
const { url } = await loginMutation.mutateAsync({ authProviderType: "GOOGLE" })
if (url) {
const loginData = await loginMutation.mutateAsync({ authProviderType: "GOOGLE" })
if (loginData?.url) {
// Redirect to Google's OAuth page
window.location.href = url
window.location.href = loginData.url
} else {
console.error("No URL returned from authenticate")
}
Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
},
"dependencies": {
"@prisma/client": "6.2.1",
"@simplewebauthn/browser": "^13.1.0",
"@simplewebauthn/server": "^13.1.0",
"@supabase/supabase-js": "^2.47.15",
"@tanstack/react-query": "4.36.1",
"@trpc/client": "^10.45.2",
Expand All @@ -26,13 +28,15 @@
"zod": "^3.24.1"
},
"devDependencies": {
"@mermaid-js/mermaid-cli": "^11.4.2",
"@types/jsonwebtoken": "^9.0.7",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"eslint": "^8",
"eslint-config-next": "14.2.16",
"prisma": "6.2.1",
"prisma-erd-generator": "^1.11.2",
"ts-node": "^10.9.2",
"typescript": "^5"
}
Expand Down
1 change: 1 addition & 0 deletions prisma/ERD.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions prisma/migrations/20250122083115_add_passkeys/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
-- CreateEnum
CREATE TYPE "WebAuthnDeviceType" AS ENUM ('SINGLE_DEVICE', 'MULTI_DEVICE');

-- CreateEnum
CREATE TYPE "WebAuthnBackupState" AS ENUM ('NOT_BACKED_UP', 'BACKED_UP');

-- AlterTable
ALTER TABLE "AuthMethods" ADD COLUMN "aaguid" VARCHAR(255) DEFAULT '00000000-0000-0000-0000-000000000000',
ADD COLUMN "backupState" "WebAuthnBackupState" NOT NULL DEFAULT 'NOT_BACKED_UP',
ADD COLUMN "deviceType" "WebAuthnDeviceType" NOT NULL DEFAULT 'SINGLE_DEVICE',
ADD COLUMN "publicKey" BYTEA,
ADD COLUMN "signCount" INTEGER,
ADD COLUMN "transports" TEXT[];

-- AlterTable
ALTER TABLE "Challenges" ADD COLUMN "usedAt" TIMESTAMP(3);
28 changes: 23 additions & 5 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ generator client {
provider = "prisma-client-js"
}

generator erd {
provider = "prisma-erd-generator"
}

datasource db {
provider = "postgresql"
url = env("POSTGRES_PRISMA_URL") // uses connection pooling
Expand Down Expand Up @@ -70,6 +74,16 @@ enum ChallengePurpose {
ACCOUNT_RECOVERY_CONFIRMATION
}

enum WebAuthnDeviceType {
SINGLE_DEVICE
MULTI_DEVICE
}

enum WebAuthnBackupState {
NOT_BACKED_UP
BACKED_UP
}

// TODO: Indexes need to be added manually.

// TODO: Should we add triggers somewhere for cascade updates/deletions?
Expand All @@ -80,11 +94,15 @@ enum ChallengePurpose {

model AuthMethod {
id String @id @default(uuid())
// TODO: Not sure we need this separate ID. This would come from Auth0. Maybe it can just be the main id on this table.
providerId String @db.VarChar(255)
providerId String @db.VarChar(255) // Can store passkey credential ID for passkeys
providerType AuthProviderType
/// This can be an email for EMAIL, a profile handler for social networks or some kind of device ID for passkeys.
providerLabel String @db.VarChar(255)
providerLabel String @db.VarChar(255) // Friendly name for the passkey
publicKey Bytes? // Public key for passkeys
aaguid String? @db.VarChar(255) @default("00000000-0000-0000-0000-000000000000") // Authenticator ID
signCount Int? // Used for replay protection
transports String[] // List of transports (e.g., "usb", "ble")
deviceType WebAuthnDeviceType @default(SINGLE_DEVICE) // Type of device used
backupState WebAuthnBackupState @default(NOT_BACKED_UP) // Backup state
linkedAt DateTime @default(now())
lastUsedAt DateTime @default(now())
unlinkedAt DateTime?
Expand All @@ -93,7 +111,6 @@ model AuthMethod {
userId String

@@unique([userId, providerId], name: "userAuthentication")
// TODO: If we end up removing providerId, we can remove this index
@@index([providerId])
@@index([lastUsedAt])
@@map("AuthMethods")
Expand Down Expand Up @@ -372,6 +389,7 @@ model Challenge {
value String @db.VarChar(255)
version String @db.VarChar(50)
issuedAt DateTime @default(now())
usedAt DateTime? // New field to track when the challenge is used?

user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
userId String?
Expand Down
1 change: 0 additions & 1 deletion server/routers/_app.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { z } from "zod";
import { protectedProcedure, router } from "../trpc";
import { authenticateRouter } from "./authenticate";

Expand Down
56 changes: 0 additions & 56 deletions server/routers/authenticate.ts

This file was deleted.

10 changes: 10 additions & 0 deletions server/routers/authenticate/authProviders/google.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { publicProcedure } from "@/server/trpc"
import { handleGoogleCallback } from "@/services/auth"

export const googleRoutes = {
handleGoogleCallback: publicProcedure.query(async () => {
const user = await handleGoogleCallback()
return { user }
}),
// Add any other google auth related routes here
}
125 changes: 125 additions & 0 deletions server/routers/authenticate/authProviders/passkeys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// src/server/routers/passkeys.ts
import {
generateRegistrationOptions,
verifyRegistrationResponse,
} from "@simplewebauthn/server";
import { supabase } from "@/lib/supabaseClient";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { publicProcedure } from "@/server/trpc";
import {
relyingPartyID,
relyingPartyName,
relyingPartyOrigin,
} from "@/services/webauthnConfig";

function stringToUint8Array(str: string): Uint8Array {
return new TextEncoder().encode(str);
}

export const passkeysRoutes = {
startRegistration: publicProcedure
.input(
z.object({
userId: z.string(),
userEmail: z.string(),
})
)
.mutation(async ({ input }) => {
const { userId, userEmail } = input;

// Generate registration options
const options = await generateRegistrationOptions({
rpName: relyingPartyName,
rpID: relyingPartyID,
userID: stringToUint8Array(userId),
userName: userEmail,
attestationType: "direct",
authenticatorSelection: {
residentKey: "preferred",
userVerification: "preferred",
authenticatorAttachment: "platform",
},
});

// Store challenge in the database
const { error } = await supabase.from("Challenges").insert({
type: "SIGNATURE",
purpose: "ACCOUNT_RECOVERY",
value: options.challenge,
user_id: userId,
});

if (error) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: error.message,
});
}

return options;
}),
verifyRegistration: publicProcedure
.input(
z.object({
userId: z.string(),
attestationResponse: z.any(),
})
)
.mutation(async ({ input }) => {
const { userId, attestationResponse } = input;

// Retrieve the challenge
const { data: challenge, error: challengeError } = await supabase
.from("Challenges")
.select("*")
.eq("user_id", userId)
.order("created_at", { ascending: false })
.limit(1)
.single();

if (challengeError || !challenge) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Challenge not found",
});
}

// Verify the response
const verification = await verifyRegistrationResponse({
response: attestationResponse,
expectedChallenge: challenge.value,
expectedOrigin: relyingPartyOrigin,
expectedRPID: relyingPartyID,
});

if (!verification.verified) {
throw new TRPCError({
code: "FORBIDDEN",
message: "Verification failed",
});
}

// Save the credential
const { error: saveError } = await supabase.from("AuthMethods").insert({
user_id: userId,
provider_id: verification.registrationInfo?.credential.id,
public_key: verification.registrationInfo?.credential.publicKey,
sign_count: verification.registrationInfo?.credential.counter,
provider_label: `Passkey created ${new Date().toLocaleString()}`,
provider_type: "PASSKEYS",
});

if (saveError) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: saveError.message,
});
}

// Delete the challenge
await supabase.from("Challenges").delete().eq("id", challenge.id);

return { verified: true };
}),
};
Loading