This document describes how to integrate with the Nuri Passkey Server from any client application (Android, iOS, Web, etc.).
- Base URL:
https://passkey.nuri.com - RP ID:
nuri.com - Supported Origins:
https://passkey.nuri.com,https://nuri.com
- Registration: Create a new passkey for a user
- Authentication: Sign in with an existing passkey
- Data Storage: Store encrypted data associated with the user
Get the challenge and options needed to create a new passkey.
Endpoint: GET /generate-registration-options
Query Parameters:
username(optional): Username for the account. If not provided, creates anonymous user.
Example Request:
# Named user
GET https://passkey.nuri.com/generate-registration-options?username=john_doe
# Anonymous user
GET https://passkey.nuri.com/generate-registration-optionsResponse:
{
"challenge": "uMgkqqp2bLRJZHSqR6D4...",
"rp": {
"id": "nuri.com",
"name": "Nuri Passkey Server"
},
"user": {
"id": "base64url-encoded-user-id",
"name": "john_doe",
"displayName": "john_doe"
},
"pubKeyCredParams": [
{ "alg": -8, "type": "public-key" },
{ "alg": -7, "type": "public-key" },
{ "alg": -257, "type": "public-key" }
],
"timeout": 60000,
"attestation": "none",
"excludeCredentials": [],
"authenticatorSelection": {
"residentKey": "required",
"userVerification": "preferred",
"requireResidentKey": true
},
"extensions": { "credProps": true },
"challengeKey": "john_doe" // or "anon_1234567890" for anonymous
}Submit the credential created by the authenticator for verification and storage.
Endpoint: POST /verify-registration
Headers:
Content-Type: application/json
Request Body:
{
"username": "john_doe", // or "Anonymous" for anonymous users
"challengeKey": "john_doe", // from registration options response
"cred": {
"id": "base64url-encoded-credential-id",
"rawId": "base64url-encoded-credential-id",
"type": "public-key",
"response": {
"attestationObject": "base64url-encoded-attestation",
"clientDataJSON": "base64url-encoded-client-data",
"transports": ["usb", "nfc", "ble", "internal"] // optional
}
}
}Response (Success):
{
"verified": true,
"userId": 123,
"username": "john_doe"
}Response (Error):
{
"error": "Challenge not found"
}Get the challenge needed to authenticate with an existing passkey.
Endpoint: GET /generate-authentication-options
Example Request:
GET https://passkey.nuri.com/generate-authentication-optionsResponse:
{
"challenge": "X2JqnWMrEgrRyFq6PRsu3JBP...",
"timeout": 60000,
"rpId": "nuri.com",
"userVerification": "preferred",
"allowCredentials": [] // Empty array allows any credential
}Submit the signed challenge to authenticate the user.
Endpoint: POST /verify-authentication
Headers:
Content-Type: application/json
Request Body:
{
"cred": {
"id": "base64url-encoded-credential-id",
"rawId": "base64url-encoded-credential-id",
"type": "public-key",
"response": {
"authenticatorData": "base64url-encoded-auth-data",
"clientDataJSON": "base64url-encoded-client-data",
"signature": "base64url-encoded-signature",
"userHandle": "base64url-encoded-user-handle" // optional
}
}
}Response (Success):
{
"verified": true,
"userId": 123,
"username": "john_doe"
}Response (Error):
{
"error": "Authenticator not found"
}Store encrypted data associated with the authenticated user.
Endpoint: POST /store-data
Headers:
Content-Type: application/json
Authorization: Bearer <session-token> // Received from verify-authentication
Request Body:
{
"data": {
"encrypted": "your-encrypted-data",
"anyField": "any-value"
}
}Response:
{
"success": true
}Retrieve stored data for the authenticated user.
Endpoint: GET /get-data
Headers:
Authorization: Bearer <session-token> // Received from verify-authentication
Response:
{
"data": {
"encrypted": "your-encrypted-data",
"anyField": "any-value"
},
"lastUpdated": "2025-07-31T10:30:00Z"
}dependencies {
implementation 'androidx.credentials:credentials:1.3.0'
implementation 'androidx.credentials:credentials-play-services-auth:1.3.0'
implementation 'com.google.android.gms:play-services-fido:21.1.0'
}class PasskeyManager(private val context: Context) {
private val credentialManager = CredentialManager.create(context)
suspend fun registerPasskey(username: String? = null) {
// Step 1: Get registration options from server
val optionsResponse = api.getRegistrationOptions(username)
// Step 2: Create credential
val createRequest = CreatePublicKeyCredentialRequest(
requestJson = buildRegistrationJson(optionsResponse)
)
try {
val result = credentialManager.createCredential(
request = createRequest,
context = context
)
// Step 3: Verify with server
val credential = result as CreatePublicKeyCredentialResponse
api.verifyRegistration(
username = username ?: "Anonymous",
challengeKey = optionsResponse.challengeKey,
credential = parseCredentialResponse(credential)
)
} catch (e: Exception) {
// Handle errors
}
}
private fun buildRegistrationJson(options: RegistrationOptions): String {
return JSONObject().apply {
put("challenge", options.challenge)
put("rp", JSONObject().apply {
put("name", options.rp.name)
put("id", options.rp.id)
})
put("user", JSONObject().apply {
put("id", options.user.id)
put("name", options.user.name)
put("displayName", options.user.displayName)
})
put("pubKeyCredParams", JSONArray().apply {
options.pubKeyCredParams.forEach { param ->
put(JSONObject().apply {
put("type", param.type)
put("alg", param.alg)
})
}
})
put("timeout", options.timeout)
put("attestation", options.attestation)
put("authenticatorSelection", JSONObject().apply {
put("residentKey", options.authenticatorSelection.residentKey)
put("userVerification", options.authenticatorSelection.userVerification)
})
}.toString()
}
}suspend fun authenticateWithPasskey() {
// Step 1: Get authentication options
val optionsResponse = api.getAuthenticationOptions()
// Step 2: Get credential
val getRequest = GetPublicKeyCredentialOption(
requestJson = buildAuthenticationJson(optionsResponse)
)
val getCredRequest = GetCredentialRequest(
listOf(getRequest)
)
try {
val result = credentialManager.getCredential(
request = getCredRequest,
context = context
)
// Step 3: Verify with server
val credential = result.credential as GetPublicKeyCredentialResponse
val authResult = api.verifyAuthentication(
credential = parseAuthCredentialResponse(credential)
)
// Save session token
sessionToken = authResult.sessionToken
} catch (e: Exception) {
// Handle errors
}
}All binary data (credential IDs, challenges, etc.) must be encoded using Base64URL format:
- Replace
+with- - Replace
/with_ - Remove padding
=
The server accepts both:
- Platform authenticators (fingerprint, Face ID): User verification performed
- Hardware security keys (YubiKey): User verification optional (touch only)
- Don't provide a username during registration
- Server creates anonymous user with ID like
anon_123456 - Credential ID becomes the user identifier
Common errors:
400 Bad Request: Invalid request format404 Not Found: Challenge expired or credential not found500 Internal Server Error: Server issue
After successful authentication:
- Server returns a session token
- Include in Authorization header for subsequent requests
- Token expires after inactivity
The server includes a web dashboard for testing:
- Registration:
https://passkey.nuri.com/dashboard.html - Click "Register Platform Authenticator" or "Register Security Key"
For debugging, the server logs:
- Credential IDs being searched
- Available credentials in database
- Encoding formats for comparison
- HTTPS Required: WebAuthn only works over secure connections
- Origin Validation: Server validates requests come from allowed origins
- Challenge Expiration: Challenges expire after 60 seconds
- Replay Protection: Each challenge can only be used once
For issues or questions:
- GitHub: https://github.com/nuri-com/passkey-server
- Server logs: Check PM2 logs for detailed error messages