Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
31 changes: 26 additions & 5 deletions samples/frontend/src/steps/embeddedWallet/RegisterPasskey.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,12 @@ interface Props {
disabled: boolean
}

// Two-phase: backend mints a WebAuthn challenge, client runs
// navigator.credentials.create(), client posts the attestation back, backend
// forwards to Grid as POST /auth/credentials.
const SANDBOX_MODE = (import.meta.env.VITE_SANDBOX_PASSKEY ?? '1') !== '0'
const SANDBOX_WALLET_SIGNATURE = 'sandbox-valid-signature'

// Backend mints a WebAuthn challenge, client runs navigator.credentials.create(),
// client posts the attestation back, and Grid may require a signed retry before
// returning the new auth method.
export default function RegisterPasskey({
walletAccountId,
customerId,
Expand Down Expand Up @@ -62,7 +65,7 @@ export default function RegisterPasskey({
(att as AuthenticatorAttestationResponse & { getTransports?: () => string[] })
.getTransports?.() ?? []

const data = await apiPost<Record<string, unknown>>('/api/auth/credentials', {
const createBody = {
accountId: walletAccountId,
challenge: reg.challenge,
nickname: 'Grid Global Account passkey',
Expand All @@ -72,7 +75,16 @@ export default function RegisterPasskey({
attestationObject: bytesToBase64url(new Uint8Array(att.attestationObject)),
transports,
},
})
}

let data = await apiPost<Record<string, unknown>>('/api/auth/credentials', createBody)
if (typeof data.payloadToSign === 'string' && typeof data.requestId === 'string') {
const signature = registrationSignature(data.payloadToSign)
data = await apiPost<Record<string, unknown>>('/api/auth/credentials', createBody, {
'Grid-Wallet-Signature': signature,
'Request-Id': data.requestId,
})
}

setResponse(JSON.stringify(data, null, 2))
const authMethodId = (data.id ?? data.authMethodId) as string | undefined
Expand Down Expand Up @@ -105,6 +117,15 @@ export default function RegisterPasskey({
)
}

function registrationSignature(payloadToSign: string): string {
const configured = import.meta.env.VITE_REGISTRATION_GRID_WALLET_SIGNATURE
if (configured) return configured
if (SANDBOX_MODE) return SANDBOX_WALLET_SIGNATURE
throw new Error(
`Passkey registration requires a signed retry for payloadToSign: ${payloadToSign}`,
)
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.

function base64urlToBytes(s: string): ArrayBuffer {
const pad = '='.repeat((4 - (s.length % 4)) % 4)
const b64 = (s + pad).replace(/-/g, '+').replace(/_/g, '/')
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.grid.sample.routes

import com.lightspark.grid.models.auth.credentials.CredentialCreateParams
import com.lightspark.grid.models.auth.credentials.CredentialCreateParams.AuthCredentialCreateRequest.PasskeyCredentialCreateRequest
import com.lightspark.grid.models.auth.credentials.CredentialResendChallengeParams
import com.lightspark.grid.models.auth.credentials.CredentialVerifyParams
Expand Down Expand Up @@ -34,6 +35,11 @@ private object RegistrationChallengeStore {
val expiresAt = store.remove(challenge) ?: return false
return System.currentTimeMillis() < expiresAt
}

fun isValid(challenge: String): Boolean {
val expiresAt = store[challenge] ?: return false
return System.currentTimeMillis() < expiresAt
}
}

fun Route.authCredentialRoutes() {
Expand Down Expand Up @@ -89,7 +95,7 @@ fun Route.authCredentialRoutes() {

val challenge = json.optText("challenge")
?: throw IllegalArgumentException("challenge is required")
if (!RegistrationChallengeStore.consume(challenge)) {
if (!RegistrationChallengeStore.isValid(challenge)) {
return@post call.respondText(
"""{"error": "challenge is invalid or expired"}""",
ContentType.Application.Json,
Expand All @@ -114,13 +120,34 @@ fun Route.authCredentialRoutes() {
json.optText("nickname")?.let { nickname(it) }
}
.build()
val gridWalletSignature = call.request.headers["Grid-Wallet-Signature"]
val requestId = call.request.headers["Request-Id"]
val params = CredentialCreateParams.builder()
.authCredentialCreateRequest(request)
.apply {
gridWalletSignature?.let { gridWalletSignature(it) }
requestId?.let { requestId(it) }
}
.build()

Log.gridRequest("auth.credentials.create", JsonUtils.prettyPrint(request))
val response = GridClientBuilder.client.auth().credentials().create(request)
val responseJson = JsonUtils.prettyPrint(response)
Log.gridRequest(
"auth.credentials.create",
"signedRetry=${gridWalletSignature != null} body=${JsonUtils.prettyPrint(request)}",
)
val response = GridClientBuilder.client.auth().credentials()
.withRawResponse()
.create(params)
val responseJson = response.body().bufferedReader().use { it.readText() }
Log.gridResponse("auth.credentials.create", responseJson)
if (response.statusCode() != HttpStatusCode.Accepted.value) {
RegistrationChallengeStore.consume(challenge)
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.

call.respondText(responseJson, ContentType.Application.Json, HttpStatusCode.Created)
call.respondText(
responseJson,
ContentType.Application.Json,
HttpStatusCode.fromValue(response.statusCode()),
)
} catch (e: Exception) {
Log.gridError("auth.credentials.create", e)
call.respondText(
Expand Down
Loading