Skip to content

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 CommonUserModel that 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 .aar pulled from a Drive link, no local Maven repo under app/libs, no PackageConsumesSdkVersion pinning, 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

AspectLegacy EntryLite Android SDKEntryIdP (OIDC)
IntegrationProprietary .aar (com.synapser:entry-lite-sdk) from a shared drive, local Maven repoStandard net.openid:appauthno custom SDK
App identityAndroid package name registered server-side (no key or secret)OIDC client_id + an exact-match custom-scheme redirect URI, registered by email
Auth callOne combined startUserIdentificationOrRegistration(listener, allowRegistration)Two entry points: /authorize (login) and /authorize?prompt=create (enrol)
Liveness UISDK-owned full-screen LivenessCheckActivity inside your appEntryIdP-hosted liveness UI in Chrome Custom Tabs — you never own it
ResultA plain CommonUserModel profile object — not verifiableA signed ID token + access token (+ refresh token); PII fetched from /userinfo
User id to persistentryUserId (GUID)The sub claim — the same GUID for adopted legacy users (see User continuity)
EnvironmentHardcoded entry-lite-api.{env}.entrymfa.com base URLsIssuer + 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 URLEntryIdP issuer
entry-lite-api.test.uk.entrymfa.comhttps://idp-test.entryidp.com
entry-lite-api.demo.uk.entrymfa.comhttps://idp-demo.entryidp.com
entry-lite-api.live.uk.entrymfa.comhttps://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 CAMERA permission and the FileProvider declaration 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. Keep INTERNET.
  • 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 is error=access_denied with error_description=user_not_registered, retry with prompt=create.
  • allowRegistration = false → login only: never send prompt=create; treat user_not_registered as 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 fieldEntryIdP equivalentWhere it comes from
entryUserIdsubID token / access token — persist this as your stable user key
firstName / lastNamegiven_name / family_name/userinfo (scope profile)
emailAddressemail/userinfo (scope profile) — email_verified is always false
mobileNumberphone_number/userinfo (scope profile) — phone_number_verified is always false
gendergender/userinfo (scope profile)
dateOfBirthbirthdate/userinfo (scope identity:verified)
photoIdentityDocumentNumberid_number/userinfo (scope identity:verified)
photoIdentityDocumentType(no equivalent)id_number carries no document-type claim — don't infer type from it
securityQuestion / securityAnswerdroppedBiometric-only auth forbids security questions
isRegistrationCompleteimplicitA successful token exchange means the user is enrolled

Don't trust an unsigned object anymore. Where the SDK asked you to take CommonUserModel on faith, you must now validate the ID token: RS256 signature via the JWKS kid, iss equals your issuer, aud contains your client_id, exp in the future, nonce matches. 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

  1. Get a client — register your Android client and get a client_id.
  2. Integrate your app → Android — the full AppAuth-Android setup and the pre-launch checklist.
  3. Secure your API — validate EntryIdP access tokens in your backend.
  4. Reference — endpoints, scopes, claims, token lifetimes, and error codes.

EntryIdP — Synapser