Appearance
Upgrade from the legacy Android SDK
This guide is for Android teams currently shipping the legacy EntryLite SDK (com.synapser:entry-lite-sdk) who are moving to EntryIdP. EntryIdP replaces that proprietary SDK with standard OpenID Connect — you drop the .aar and integrate with an off-the-shelf OIDC library instead.
It assumes you already know the legacy integration (the EntryLiteSDKProvider init + startUserIdentificationOrRegistration flow). For the full target-state Android setup it links to Integrate your app → Android rather than repeating it — read this page for what changes and why, then follow that section for the complete AppAuth-Android wiring.
Why upgrade — what you gain
This is not a like-for-like SDK swap; it's a move to a stronger model. What you get:
- Verifiable trust. The SDK handed your app an unsigned
CommonUserModelthat your backend had to take on faith. EntryIdP issues signed JWTs (RS256, signed in AWS KMS) that any resource server can verify independently against the published JWKS — see the claims reference and the validation examples in Secure your API. - No proprietary SDK to maintain. No
.aarpulled from a Drive link, no local Maven repo underapp/libs, noPackageConsumesSdkVersionpinning, no per-release SDK upgrades. You use a maintained, off-the-shelf library (AppAuth-Android) over a standard protocol — see How EntryIdP works. - Standards and portability. The exact same OIDC model works across web, iOS, Android, React Native, and Flutter — one mental model for every client. Endpoints come from discovery (
/.well-known/openid-configuration), so there are no hardcoded environment base URLs to keep in sync. - Stronger security posture. Biometric-only (no security questions to phish or reset), PKCE mandatory, short-lived access tokens, and single-use rotating refresh tokens whose whole family is revoked on replay. Rate limiting and WAF-based abuse protection sit in front of EntryIdP, so brute-force and bot defence for the auth flow is no longer your app's problem.
- Richer identity model. Multi-tenant claims (
tenant_id,roles) and a clean separation between liveness (face_liveness_verified) and bureau identity proofing (identity_verified) — see Liveness is not identity verification. - Self-service and compliance, handled for you. EntryIdP hosts a user account portal — profile, consents, activity log, active-session management, account deletion, and GDPR data export — plus centralised consent capture and audit logging. With the EntryLite SDK your app had to build all of that (or go without); with EntryIdP the obligations around biometric special-category data live at the IdP, not in your code.
What changes at a glance
| Aspect | Legacy EntryLite Android SDK | EntryIdP (OIDC) |
|---|---|---|
| Integration | Proprietary .aar (com.synapser:entry-lite-sdk) from a shared drive, local Maven repo | Standard net.openid:appauth — no custom SDK |
| App identity | Android package name registered server-side (no key or secret) | OIDC client_id + an exact-match custom-scheme redirect URI, registered by email |
| Auth call | One combined startUserIdentificationOrRegistration(listener, allowRegistration) | Two entry points: /authorize (login) and /authorize?prompt=create (enrol) |
| Liveness UI | SDK-owned full-screen LivenessCheckActivity inside your app | EntryIdP-hosted liveness UI in Chrome Custom Tabs — you never own it |
| Result | A plain CommonUserModel profile object — not verifiable | A signed ID token + access token (+ refresh token); PII fetched from /userinfo |
| User id to persist | entryUserId (GUID) | The sub claim — the same GUID for adopted legacy users (see User continuity) |
| Environment | Hardcoded entry-lite-api.{env}.entrymfa.com base URLs | Issuer + discovery; never hardcode endpoints |
Step 1 — Register an OIDC client
The legacy SDK authorised your app by the Android package name you sent to Synapser. EntryIdP instead identifies your app by a registered client_id with an explicit redirect URI.
There is no self-service registration — email entryidp@synapser.com with your app details (see Get a client for exactly what to send). For a native Android app you'll register a public client and choose a custom-scheme redirect URI, e.g. com.yourapp://callback. EntryIdP does not host assetlinks.json, so App Links are not available — custom schemes only.
You'll get back a client_id and the issuer URL for your environment. Note the mapping from the legacy environments:
| Legacy base URL | EntryIdP issuer |
|---|---|
entry-lite-api.test.uk.entrymfa.com | https://idp-test.entryidp.com |
entry-lite-api.demo.uk.entrymfa.com | https://idp-demo.entryidp.com |
entry-lite-api.live.uk.entrymfa.com | https://idp.entryidp.com |
Read the issuer from config — never hardcode it — and discover endpoints from {issuer}/.well-known/openid-configuration. See the full environments table.
Step 2 — Swap the dependency
Remove the local file-based Maven repo and the SDK dependency, and add AppAuth-Android.
Before — legacy SDK from app/libs:
groovy
// settings.gradle
dependencyResolutionManagement {
repositories {
maven { url uri('app/libs') } // ← remove
}
}
// build.gradle (app)
dependencies {
implementation 'com.synapser:entry-lite-sdk:2.5.x' // ← remove
}After — AppAuth-Android:
groovy
// build.gradle (app)
android {
defaultConfig {
// Required — AppAuth's bundled manifest uses this placeholder, and the
// manifest merger fails without it. Use your redirect URI's scheme.
manifestPlaceholders["appAuthRedirectScheme"] = "com.yourapp"
}
}
dependencies {
implementation 'net.openid:appauth:0.11.1'
}Manifest cleanup:
- The
CAMERApermission and theFileProviderdeclaration the SDK required are no longer needed — the liveness camera now runs inside EntryIdP's own UI in a Chrome Custom Tab, not in your app. KeepINTERNET. - Declare AppAuth's redirect receiver with an
<intent-filter>for your custom scheme. See Integrate your app → Android, step B for the exact manifest block.
Step 3 — Replace the auth call
This is the core change. The single combined SDK call becomes a standard OIDC Authorization Code + PKCE flow, with login and enrolment as two distinct entry points.
Before — one call does identify-or-register, returning a CommonUserModel:
kotlin
val entrySdk = EntryLiteSDKProvider.getInstance(applicationContext, EnvironmentEnum.TEST)
lifecycle.addObserver(entrySdk)
entrySdk.initAsync(object : EntryLiteSDKProvider.InitializationListener {
override fun onInitialized() {
entrySdk.startUserIdentificationOrRegistration(
object : UserIdentificationOrRegistrationResultListener {
override fun onSuccess(user: CommonUserModel) {
// Trust the object as-is; persist user.entryUserId.
entrySdk.clearActiveResultListener()
}
override fun onFailure(errorMessage: String) {
entrySdk.clearActiveResultListener()
}
},
allowRegistration = true,
)
}
override fun onInitializationFailed(error: Throwable) { /* handle */ }
})After — AppAuth builds the authorization request; EntryIdP runs liveness and returns a code you exchange for tokens:
kotlin
// 1. Discovery — do NOT use AuthorizationServiceConfiguration.fetchFromIssuer:
// AppAuth requires subject_types_supported in the document and throws
// MissingArgumentException when it is absent. Fetch the discovery doc
// yourself on a background thread and build the config directly.
val config = AuthorizationServiceConfiguration(
Uri.parse("$issuer/authorize"), // authorization_endpoint from discovery
Uri.parse("$issuer/token"), // token_endpoint from discovery
)
// 2. Build the request. "Sign in" (login) vs "Create account" (enrol):
val builder = AuthorizationRequest.Builder(
config,
clientId, // your registered client_id
ResponseTypeValues.CODE, // Authorization Code
Uri.parse("com.yourapp://callback"), // exact-match redirect URI
)
.setScope("openid profile offline_access")
// AppAuth does NOT add PKCE automatically — set the verifier explicitly.
.setCodeVerifier(CodeVerifierUtil.generateRandomCodeVerifier())
// Enrol path (was allowRegistration = true → first-time users):
builder.setAdditionalParameters(mapOf("prompt" to "create"))
// Login-only path (was allowRegistration = false): omit prompt=create.
val authRequest = builder.build()
// 3. Launch — AppAuth opens a Chrome Custom Tab; EntryIdP runs liveness.
val authService = AuthorizationService(context)
startActivityForResult(authService.getAuthorizationRequestIntent(authRequest), RC_AUTH)
// 4. In onActivityResult: exchange the code for tokens (public client → no secret).
val resp = AuthorizationResponse.fromIntent(data)
val ex = AuthorizationException.fromIntent(data)
if (resp != null) {
authService.performTokenRequest(
resp.createTokenExchangeRequest(),
NoClientAuthentication.INSTANCE,
) { tokenResp, tokenEx ->
// tokenResp.idToken / .accessToken / .refreshToken
authState.update(tokenResp, tokenEx)
// Persist authState in EncryptedSharedPreferences (Android Keystore-backed).
}
}Mapping the old allowRegistration flag:
allowRegistration = true(the default) → give users a "Sign in" button (plain/authorize) and a "Create account" button (prompt=create). Alternatively, attempt login first and, if the result iserror=access_deniedwitherror_description=user_not_registered, retry withprompt=create.allowRegistration = false→ login only: never sendprompt=create; treatuser_not_registeredas a hard failure.
See Integrate your app → Android for the full working setup (manifest, threading, token storage) and a copyable AI prompt.
Step 4 — Consume tokens, not CommonUserModel
The SDK's success callback gave you a CommonUserModel. EntryIdP gives you tokens: authentication facts live in the ID token, and profile/identity PII is fetched from GET /userinfo with the access token, gated by scope. Persist the sub claim in place of entryUserId.
Field-by-field mapping:
CommonUserModel field | EntryIdP equivalent | Where it comes from |
|---|---|---|
entryUserId | sub | ID token / access token — persist this as your stable user key |
firstName / lastName | given_name / family_name | /userinfo (scope profile) |
emailAddress | email | /userinfo (scope profile) — email_verified is always false |
mobileNumber | phone_number | /userinfo (scope profile) — phone_number_verified is always false |
gender | gender | /userinfo (scope profile) |
dateOfBirth | birthdate | /userinfo (scope identity:verified) |
photoIdentityDocumentNumber | id_number | /userinfo (scope identity:verified) |
photoIdentityDocumentType | (no equivalent) | id_number carries no document-type claim — don't infer type from it |
securityQuestion / securityAnswer | dropped | Biometric-only auth forbids security questions |
isRegistrationComplete | implicit | A successful token exchange means the user is enrolled |
Don't trust an unsigned object anymore. Where the SDK asked you to take
CommonUserModelon faith, you must now validate the ID token: RS256 signature via the JWKSkid,issequals your issuer,audcontains yourclient_id,expin the future,noncematches. Use an established OIDC library — don't hand-parse JWTs. See the claims tables for exactly which claims land where, and the security checklist.
Use an AI prompt to do the cutover
Prefer to hand the mechanical part to an agent? Paste the prompt below into Claude, Cursor, or any coding agent working in your Android project. Unlike the greenfield Android prompt, this one is written for a migration — it finds and removes the legacy SDK before wiring up OIDC. Review the diff before shipping.
Migrate this native Android app (Kotlin) from the legacy Synapser EntryLite SDK to
EntryIdP, using AppAuth-Android. EntryIdP is a standard OpenID Connect provider; users
authenticate ONLY with a face liveness check — no typed credentials, OTPs, security
questions, or social logins. Do not build any sign-in form or credential-entry UI.
Issuer: https://idp-test.entryidp.com (use the issuer from my client registration; read
from config — never hardcode in logic). My client_id is a PUBLIC client.
First, find the existing legacy integration so you can remove it:
1. In settings.gradle / build.gradle, locate the local Maven repo (maven { url uri('app/libs') })
and the dependency implementation 'com.synapser:entry-lite-sdk:...'.
2. In Kotlin, locate EntryLiteSDKProvider usage: getInstance(...)/initAsync(...),
startUserIdentificationOrRegistration(..., allowRegistration=...), the
UserIdentificationOrRegistrationResultListener, CommonUserModel handling, and
dispose()/clearActiveResultListener() lifecycle calls.
3. In AndroidManifest.xml, note the CAMERA permission and FileProvider that existed only
for the SDK's on-device liveness capture.
Remove:
- The app/libs Maven repo and the entry-lite-sdk dependency.
- All EntryLiteSDKProvider code, its listeners, and CommonUserModel usage.
- The CAMERA permission and the SDK's FileProvider IF nothing else in the app uses them
(EntryIdP runs the camera inside its own UI in a Chrome Custom Tab). Keep INTERNET.
Add and implement with AppAuth-Android:
- build.gradle: implementation 'net.openid:appauth', and inside defaultConfig add
manifestPlaceholders["appAuthRedirectScheme"] = "<your-custom-scheme>"
(required — the manifest merger fails without it).
- AndroidManifest.xml: declare net.openid.appauth.RedirectUriReceiverActivity with
tools:node="merge" and an <intent-filter> for the custom scheme.
- Authorization Code + PKCE only. Never implicit flow or response_type=token, never
grant_type=client_credentials.
- PKCE: AppAuth does NOT add PKCE automatically. Call
setCodeVerifier(CodeVerifierUtil.generateRandomCodeVerifier()) on the request builder.
- Discovery: do NOT use AuthorizationServiceConfiguration.fetchFromIssuer — EntryIdP's
discovery document omits subject_types_supported and AppAuth throws
MissingArgumentException. Fetch {issuer}/.well-known/openid-configuration manually on a
background thread, read authorization_endpoint and token_endpoint, and construct
AuthorizationServiceConfiguration(authEndpointUri, tokenEndpointUri) directly.
- Use NoClientAuthentication.INSTANCE for performTokenRequest (public client, no secret).
- Use Chrome Custom Tabs (AppAuth default). Do NOT use a WebView for auth.
- Redirect URI is a CUSTOM URL SCHEME (e.g. com.yourapp://callback). EntryIdP does NOT
support App Links — do not configure assetlinks.json.
- Persist AppAuth AuthState in EncryptedSharedPreferences (Android Keystore-backed).
Replace the result handling (this is the key behavioural change):
- The SDK returned an unsigned CommonUserModel you trusted directly. Now VALIDATE the ID
token (RS256 via JWKS kid, iss, aud, exp, nonce) with an OIDC/JWT library — never
hand-parse or blindly trust it.
- Persist the `sub` claim in place of entryUserId as the stable user key (for adopted
legacy users it is the same GUID).
- Fetch profile/identity data from GET /userinfo with the access token, gated by scope,
instead of reading CommonUserModel fields:
firstName/lastName -> given_name/family_name (scope profile)
emailAddress/mobileNumber -> email/phone_number (scope profile; *_verified always false)
dateOfBirth -> birthdate, photoIdentityDocumentNumber -> id_number (scope identity:verified)
Drop securityQuestion/securityAnswer entirely (biometric-only; no equivalent).
Login vs enrol (EntryIdP-specific — this replaces the allowRegistration flag):
- "Sign in" -> standard AuthorizationRequest (login of an existing face). This is
allowRegistration=false behaviour.
- "Create account" -> setAdditionalParameters(mapOf("prompt" to "create")) (first-time
face enrolment). Offering both buttons is the allowRegistration=true equivalent.
- If the result is error=access_denied with error_description=user_not_registered, route
the user into the prompt=create flow.
Guardrails:
- No sign-in form, OTP, security-question, or social-login UI.
- No client_secret anywhere.
- No WebView for authentication; no App Links.
- Custom URL scheme redirect only; endpoints from the manual discovery fetch, not hardcoded.
- Do not leave any EntryLiteSDK import, dependency, or dead code behind.Secure your own API with EntryIdP tokens
This is a capability the legacy SDK could not give you. Because you now hold a signed access token, your own backend API can protect its endpoints by validating that token (authentication) and checking scope, roles, and tenant_id (authorization) — with no shared secret and without ever calling back to Synapser at request time.
And because EntryIdP never issues client_credentials tokens, a valid EntryIdP access token always represents a real person who passed a live face check — there is no machine-to-machine path that could mint one.
See Secure your API for the full resource-server setup, including .NET (AddJwtBearer) and Node.js (jose) validation examples, the access-token claims table, and a worked authorization example. Don't reimplement JWT validation by hand.
What's dropped or has no equivalent
Some legacy SDK features have no client-facing OIDC equivalent today:
- Related users / dependants (
startRelatedUserIdentificationOrRegistration) — no client-facing equivalent. - Identify from a photo (
startUserIdentificationFromPhotoOnly) and image fetch (fetchUserIdDocImage,fetchUserSelfiePhoto) — these are admin-side capabilities in EntryIdP, not exposed to relying-party apps. - Test-environment user delete (
deleteUserForNonLiveEnvironment) — users delete their own accounts via the self-service portal, not via a client API call. - Security question (
requiresSecurityQuestion,securityQuestion/securityAnswer) — removed entirely; biometric liveness is the sole credential.
If your app depends on any of these, raise it with us at entryidp@synapser.com before you plan the cutover.
User continuity
Your existing enrolled users do not have to re-enrol. On a user's first EntryIdP login their legacy record is adopted automatically: the shared Rekognition face collection means their existing face resolves as before, and their legacy identifier is reused as the OIDC sub. So the sub you start persisting is the same GUID you knew as entryUserId — existing per-user data keyed on it stays valid.
Next steps
- Get a client — register your Android client and get a
client_id. - Integrate your app → Android — the full AppAuth-Android setup and the pre-launch checklist.
- Secure your API — validate EntryIdP access tokens in your backend.
- Reference — endpoints, scopes, claims, token lifetimes, and error codes.