How to build a native mobile app flow — Versola Docs
VersolaVersola/docs
Pre-1.00.7.1versola.kzGitHub

How to build a native mobile app flow

Register a native client in the admin console, let edge enroll its certificate, then walk the native start, complete, token and revoke calls and learn what to check when one fails.

This guide registers a mobile or desktop client on a fapi2 tenant and walks the calls your app makes to the edge native endpoints. At the end you hold a DPoP-bound access token, have called an API with it, refreshed it and revoked it. You need an edge with a native block and a dpop block, and an auth with a mutual-TLS listener. The concepts are on Native apps through edge.

The example uses https://edge.example.com for edge, https://id.example.com for auth, acme-mobile as the client ID and https://app.example.com/callback as the redirect URI.

Prerequisites

  • A tenant on the fapi2 security profile, linked to your edge. The default tenant is fapi2.
  • edge configured as in Edge configuration: native block, dpop block, EDGE_NATIVE_BLOB_KEY.
  • auth with a mutual-tls block whose trusted-certificates contains the CA central signs client certificates with, and central with a client-certificates backend (Edge client certificates). The CLI targets configure these.
  • A domain you control for the App Link (Android) or Universal Link (iOS), with the association file hosted, so that https://app.example.com/callback opens your app.
  • A resource behind edge and a role that grants it, as in How to protect an API with edge, if you want to call an API at the end.

1. Create the client

  1. Open Clients and start the create wizard.
  2. In step 1, choose Mobile or desktop app. The plan panel lists what the wizard fixes for this kind on your tenant: PKCE S256, mTLS by edge, DPoP-bound access tokens, pushed authorization requests, App Link redirects only, a 1 hour access token, refresh token rotation.
  3. In Basics, enter the client ID acme-mobile, a name and the redirect URI https://app.example.com/callback. A custom scheme such as com.example.app:// is refused on a fapi2 tenant.
  4. Give the client the scopes the app needs, in the wizard or later on the client’s edit page: openid, and offline_access if the app needs a refresh token.
  5. Review and create.

The credential is edge-provisioned: there is nothing to enter or store. In Central API terms the wizard registers applicationType: native, authMethod: tls_client_auth, dpopBoundAccessTokens: true, requirePushedAuthorizationRequests: true, certificateBoundAccessTokens: false and enrollEdgeClientCertificate: true. If you register by API, send those fields, since enrollEdgeClientCertificate and issueEdgeClientCertificate are required and never defaulted.

To narrow the DPoP proof algorithms for this client, open the client and set the DPoP algorithms. Leave it empty to accept what the server advertises (ES256 and PS256 in the default metadata).

If the app will call an API, add acme-mobile to the Audience of the resource and make sure the users have a role with the permission.

2. Let edge enroll the certificate

Nothing to do. Within configuration-cache-refresh-interval (generated: 5 minutes), edge loads the client, generates a key pair and asks central to sign a certificate for it (enrollment steps).

To check that edge serves the client, call start without a proof:

curl -i -X POST https://edge.example.com/native/start/acme-mobile
HTTP/1.1 400 Bad Request
{"error":"invalid_dpop_proof","error_description":"request must carry a DPoP proof from the device key"}

A 400 means edge has a certificate and treats the client as native. A 404 means it does not yet, or the client does not meet the requirements. Wait one interval and read the edge log for edge_certificate_unavailable.

3. Prepare the device key

On the device, generate an ES256 key pair in the keystore once and keep it for the life of the installation. For each call you build a proof, a JWT signed with the private key:

PartContent
Headertyp: dpop+jwt, alg: ES256, jwk: the public key
Payloadhtm: POST, htu: see the table below, iat: now, jti: a fresh random value, nonce when edge or auth asked for one

Any DPoP-capable library builds this (RFC 9449). The htu differs per call:

Callhtu
starthttps://edge.example.com/native/start/acme-mobile
complete, tokentoken_endpoint from the start response
revokerevocation_endpoint from the start response

4. Walk the calls

The DPoP values below are placeholders for the proofs from step 3.

Start. Push the authorization request through edge.

curl -X POST https://edge.example.com/native/start/acme-mobile \
  -H "DPoP: <proof for start>" \
  -d "scope=openid offline_access"
{"client_id":"acme-mobile","request_uri":"urn:ietf:params:oauth:request_uri:...","expires_in":600,
 "authorization_endpoint":"https://id.example.com/authorize","state":"...","blob":"...",
 "token_endpoint":"https://id.example.com:8083/token","revocation_endpoint":"https://id.example.com:8083/revoke"}

If the answer is 401 with DPoP-Nonce, build the proof again with that nonce and retry.

Browser. Open the system browser at authorization_endpoint with client_id and request_uri:

https://id.example.com/authorize?client_id=acme-mobile&request_uri=urn%3Aietf%3Aparams%3Aoauth%3Arequest_uri%3A...

The user signs in. auth redirects to https://app.example.com/callback?code=...&state=...&iss=..., which opens your app.

Complete. Within expires_in seconds, send the three values and the blob, with a proof for token_endpoint.

curl -X POST https://edge.example.com/native/complete/acme-mobile \
  -H "DPoP: <proof for token_endpoint>" \
  -d "code=<code>" -d "state=<state>" -d "iss=<iss>" -d "blob=<blob>"
{"access_token":"...","token_type":"DPoP","expires_in":3600,"refresh_token":"...","scope":"openid offline_access"}

Call the API. The proof for an API call is addressed to the edge URL and carries htm of the request method and the hash of the access token (ath).

curl https://edge.example.com/resources/orders-api/orders \
  -H "Authorization: DPoP <access token>" \
  -H "DPoP: <proof for this request>"

Refresh.

curl -X POST https://edge.example.com/native/token/acme-mobile \
  -H "DPoP: <proof for token_endpoint>" \
  -H "Idempotency-Key: <unique per rotation>" \
  -d "refresh_token=<refresh token>"

Revoke on sign-out.

curl -X POST https://edge.example.com/native/revoke/acme-mobile \
  -H "DPoP: <proof for revocation_endpoint>" \
  -d "token=<refresh token>" -d "token_type_hint=refresh_token"

Refreshing the same refresh token again now returns invalid_grant.

5. When it fails

AnswerLikely cause
404 from any /native/* callNo native block. The client is not native, or has no edge certificate yet. The client’s tenant is not linked to this edge. The client ID is wrong
400 invalid_dpop_proofMissing or second DPoP header. Wrong htu (use the URLs from the start response, not your own). iat outside the allowed window. The proof from start was reused, so build a new one for each call. The key algorithm is not accepted
401 with DPoP-NonceA nonce is required. Retry once with a proof that includes it
400 invalid_grant on completeThe blob expired (expires_in passed). state or iss does not match what the redirect returned. The proof was signed with a different key than the one that called start. If iss is the cause, check that native.auth-issuer equals auth’s jwt.issuer
400 invalid_requestredirect_uri is not registered, a parameter is missing or repeated, or the body is not form-encoded
invalid_client relayed from authauth does not trust the CA that signed edge’s certificate, or the certificate subject does not match mtlsAuth. See Edge client certificates
400 relayed from /par about the redirect URI or PKCEThe client registration and the request disagree. Check the redirect URI and that the tenant is fapi2
Authorization at auth fails right after the browser opensThe request_uri expired or was used already. Call start again
401 from the API with invalid_dpop_proofThe API proof lacks ath, uses the wrong htu (the edge URL of that call), or the token was sent as Bearer. A key-bound token is never accepted as Bearer

Next steps