Start a passkey login
curl --request POST \
--url https://api.next.orenda.finance/v1/auth/passkey/login \
--header 'x-program-id: <x-program-id>'import requests
url = "https://api.next.orenda.finance/v1/auth/passkey/login"
headers = {"x-program-id": "<x-program-id>"}
response = requests.post(url, headers=headers)
print(response.text)const options = {method: 'POST', headers: {'x-program-id': '<x-program-id>'}};
fetch('https://api.next.orenda.finance/v1/auth/passkey/login', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.next.orenda.finance/v1/auth/passkey/login",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => [
"x-program-id: <x-program-id>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.next.orenda.finance/v1/auth/passkey/login"
req, _ := http.NewRequest("POST", url, nil)
req.Header.Add("x-program-id", "<x-program-id>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.next.orenda.finance/v1/auth/passkey/login")
.header("x-program-id", "<x-program-id>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.next.orenda.finance/v1/auth/passkey/login")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["x-program-id"] = '<x-program-id>'
response = http.request(request)
puts response.read_body{
"success": true,
"data": {
"authenticated": false,
"challenge": "PASSKEY",
"session_token": "3f1c2e8a-9b4d-4e6f-8a1b-2c3d4e5f6a7b",
"fido2options": {
"challenge": "q1n9bXJ0c2VjdXJlY2hhbGxlbmdl",
"timeout": 60000,
"rpId": "next.orenda.finance",
"userVerification": "preferred",
"allowCredentials": [
{
"type": "public-key",
"id": "AaIWjR2k8Qm3sV7tZ1pYxC"
}
]
}
}
}{
"success": false,
"code": "RESOURCE_NOT_FOUND",
"message": "Program issuer not found"
}{
"success": false,
"code": "VALIDATION_ERROR",
"message": "email is required"
}{
"success": false,
"code": "INTERNAL_SERVER_ERROR",
"message": "Internal Server Error"
}Passkey sign-in (recommended)
Passkey sign-in (start)
Begin a passkey sign-in: get the challenge and unlock on the device.
POST
/
v1
/
auth
/
passkey
/
login
Start a passkey login
curl --request POST \
--url https://api.next.orenda.finance/v1/auth/passkey/login \
--header 'x-program-id: <x-program-id>'import requests
url = "https://api.next.orenda.finance/v1/auth/passkey/login"
headers = {"x-program-id": "<x-program-id>"}
response = requests.post(url, headers=headers)
print(response.text)const options = {method: 'POST', headers: {'x-program-id': '<x-program-id>'}};
fetch('https://api.next.orenda.finance/v1/auth/passkey/login', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.next.orenda.finance/v1/auth/passkey/login",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => [
"x-program-id: <x-program-id>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.next.orenda.finance/v1/auth/passkey/login"
req, _ := http.NewRequest("POST", url, nil)
req.Header.Add("x-program-id", "<x-program-id>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.next.orenda.finance/v1/auth/passkey/login")
.header("x-program-id", "<x-program-id>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.next.orenda.finance/v1/auth/passkey/login")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["x-program-id"] = '<x-program-id>'
response = http.request(request)
puts response.read_body{
"success": true,
"data": {
"authenticated": false,
"challenge": "PASSKEY",
"session_token": "3f1c2e8a-9b4d-4e6f-8a1b-2c3d4e5f6a7b",
"fido2options": {
"challenge": "q1n9bXJ0c2VjdXJlY2hhbGxlbmdl",
"timeout": 60000,
"rpId": "next.orenda.finance",
"userVerification": "preferred",
"allowCredentials": [
{
"type": "public-key",
"id": "AaIWjR2k8Qm3sV7tZ1pYxC"
}
]
}
}
}{
"success": false,
"code": "RESOURCE_NOT_FOUND",
"message": "Program issuer not found"
}{
"success": false,
"code": "VALIDATION_ERROR",
"message": "email is required"
}{
"success": false,
"code": "INTERNAL_SERVER_ERROR",
"message": "Internal Server Error"
}Step 1 of signing in with a passkey. Call
Our API sends and expects binary values as base64url strings, while the browser works
with byte arrays. The web example below uses these two helpers to convert between them:
Encode the result into the assertion that Passkey sign-in
(finish) sends to
passkey/login (public — no bearer token) to get
a session_token and fido2options, then unlock the passkey on the device. You finish on
Passkey sign-in (finish).
Before you begin
Passkeys need a browser or app webview that supports WebAuthn. Check for support before you show a passkey option:const passkeysSupported =
window.PublicKeyCredential !== undefined &&
(await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable());
// bytes → base64url
const toB64url = (buf) =>
btoa(String.fromCharCode(...new Uint8Array(buf)))
.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
// base64url → bytes
const fromB64url = (s) =>
Uint8Array.from(atob(s.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0));
A WebAuthn helper library such as
@simplewebauthn/browser
does these conversions for you. The examples here show the raw approach so you can see
exactly what’s happening.The whole flow
This one function runs all the way through both calls to tokens. On Kotlin, Swift, and Flutter the device step uses the platform’s credential API — see Unlocking the passkey on the device.async function signInWithPasskey(programId) {
const base = "https://api.next.orenda.finance";
const headers = { "Content-Type": "application/json", "x-program-id": programId };
// 1. Start
const start = await fetch(`${base}/v1/auth/passkey/login`, {
method: "POST", headers,
}).then((r) => r.json());
const { session_token, fido2options } = start.data;
// 2. Device — unlock the passkey
fido2options.challenge = fromB64url(fido2options.challenge);
(fido2options.allowCredentials ?? []).forEach((c) => { c.id = fromB64url(c.id); });
const cred = await navigator.credentials.get({ publicKey: fido2options });
// 3. Finish
const result = await fetch(`${base}/v1/auth/passkey/verify`, {
method: "POST", headers,
body: JSON.stringify({
session_token,
assertion: {
credentialIdB64: toB64url(cred.rawId),
userHandleB64: toB64url(cred.response.userHandle),
clientDataJSON_B64: toB64url(cred.response.clientDataJSON),
authenticatorDataB64: toB64url(cred.response.authenticatorData),
signatureB64: toB64url(cred.response.signature),
},
}),
}).then((r) => r.json());
return result.data; // access_token, id_token, refresh_token, ...
}
suspend fun signInWithPasskey(programId: String): JSONObject {
val base = "https://api.next.orenda.finance"
val client = OkHttpClient()
fun post(path: String, body: String): JSONObject {
val req = Request.Builder().url("$base$path")
.addHeader("x-program-id", programId)
.post(body.toRequestBody("application/json".toMediaType())).build()
return JSONObject(client.newCall(req).execute().body!!.string())
}
// 1. Start
val data = post("/v1/auth/passkey/login", "{}").getJSONObject("data")
val sessionToken = data.getString("session_token")
val fido2options = data.getJSONObject("fido2options")
// 2. Device — unlock the passkey with Android Credential Manager (see below)
val cred = getPasskey(fido2options)
// 3. Finish
val assertion = JSONObject()
.put("credentialIdB64", cred.credentialIdB64)
.put("userHandleB64", cred.userHandleB64)
.put("clientDataJSON_B64", cred.clientDataJsonB64)
.put("authenticatorDataB64", cred.authenticatorDataB64)
.put("signatureB64", cred.signatureB64)
return post("/v1/auth/passkey/verify", JSONObject()
.put("session_token", sessionToken).put("assertion", assertion).toString())
.getJSONObject("data")
}
func signInWithPasskey(programId: String) async throws -> [String: Any] {
let base = "https://api.next.orenda.finance"
func post(_ path: String, _ body: [String: Any]) async throws -> [String: Any] {
var req = URLRequest(url: URL(string: base + path)!)
req.httpMethod = "POST"
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
req.setValue(programId, forHTTPHeaderField: "x-program-id")
req.httpBody = try JSONSerialization.data(withJSONObject: body)
let (data, _) = try await URLSession.shared.data(for: req)
return try JSONSerialization.jsonObject(with: data) as! [String: Any]
}
// 1. Start
let start = try await post("/v1/auth/passkey/login", [:])["data"] as! [String: Any]
let sessionToken = start["session_token"] as! String
let fido2options = start["fido2options"] as! [String: Any]
// 2. Device — unlock the passkey with AuthenticationServices (see below)
let cred = try await getPasskey(fido2options)
// 3. Finish
let result = try await post("/v1/auth/passkey/verify", [
"session_token": sessionToken,
"assertion": [
"credentialIdB64": cred.credentialIdB64,
"userHandleB64": cred.userHandleB64,
"clientDataJSON_B64": cred.clientDataJSONB64,
"authenticatorDataB64": cred.authenticatorDataB64,
"signatureB64": cred.signatureB64,
],
])
return result["data"] as! [String: Any]
}
Future<Map<String, dynamic>> signInWithPasskey(String programId) async {
const base = 'https://api.next.orenda.finance';
final headers = {'Content-Type': 'application/json', 'x-program-id': programId};
// 1. Start
final start = jsonDecode((await http.post(
Uri.parse('$base/v1/auth/passkey/login'), headers: headers,
)).body)['data'];
final sessionToken = start['session_token'];
final fido2options = start['fido2options'];
// 2. Device — unlock the passkey with a passkeys plugin (see below)
final cred = await getPasskey(fido2options);
// 3. Finish
final result = jsonDecode((await http.post(
Uri.parse('$base/v1/auth/passkey/verify'),
headers: headers,
body: jsonEncode({
'session_token': sessionToken,
'assertion': {
'credentialIdB64': cred.credentialIdB64,
'userHandleB64': cred.userHandleB64,
'clientDataJSON_B64': cred.clientDataJsonB64,
'authenticatorDataB64': cred.authenticatorDataB64,
'signatureB64': cred.signatureB64,
},
}),
)).body)['data'];
return result; // access_token, id_token, refresh_token, ...
}
Unlocking the passkey on the device
The device step turns thefido2options into a signed assertion. On the web that’s
navigator.credentials.get() (shown above). On native platforms, use the OS credential
API:
| Platform | Use |
|---|---|
| Android | Credential Manager — GetCredentialRequest with GetPublicKeyCredentialOption |
| iOS | AuthenticationServices — ASAuthorizationPlatformPublicKeyCredentialProvider assertion request |
| Flutter | A plugin such as passkeys |
passkey/verify.⌘I