This document describes the IPification Android SDK and its usage. The main purpose of the SDK is to provide network-based authentication for mobile users.
| Item | Details |
|---|---|
| Minimum OS | Android 5.0 (API 21) or higher |
| Device prerequisite | Mobile / cellular data must be enabled. All IPification requests (and redirects) are forced over the cellular interface. |
| Required manifest permissions | INTERNET, ACCESS_NETWORK_STATE, CHANGE_NETWORK_STATE, ACCESS_WIFI_STATE |
| Networking library | IPification ships with OkHttp 5 (socket binding + custom DNS). |
This configuration is required only for the following markets and providers:
| Market | Provider or platform | Cleartext-enabled domains |
|---|---|---|
| Indonesia | Tri, XL, Smartfren | ipification.com, xl.co.id, smartfren.com |
| Canada | EnStream | enstreamidentity.com |
| Mexico | Telcel | ipification.com |
| United Kingdom | O2 (SmartDigits/TRUID), Vodafone | smartdigits.io, vodafone.com |
| Argentina | OpenXpand | openxpand.com |
| Sri Lanka | Ideabiz | ideabiz.lk |
| India | Vodafone Idea, Airtel, Jio | ipification.com, airtel.in, jio.com, jiolabs.com |
| Malaysia | CelcomDigi | celcomdigi.com |
To authenticate with these providers, enable cleartext traffic only for the listed domains.
Use the maintained
ipification_network_security_config.xml
as the source of truth.
Need the MSISDN for coverage checking?
Use Android’s Phone Number Hint API as shown in our snippet:
Android Phone Number Hint guide
- Check Coverage
- Call the Coverage API with the client's
phonenumber (GET) through the Cellular Network . - Receive a response with:
is_available-true: supported |false: not supported
- Start Authentication
- Prepare the
authorization requestwith required parameters - Call Authorization API with
authorization request( GET ) throughCellular Network. - Receive a
coderesponse with:- result directly via
redirect_uri(1) or - redirection url (
301or302) (2)
- result directly via
- (1) -> Parser the response then return the result to client
- (2) -> Perform all url(s) redirection until receive the result with
redirect_uri(throughCellular Network)
Note: The mobile Coverage, Authorization, and required redirect requests must use the cellular interface. The server-to-server token exchange is performed by your backend and does not use the device's cellular connection.
- Complete verification through your backend (server to server)
- The final
redirect_uricontains an authorizationcodeand the returnedstate. - Before accepting the result, verify that the returned
statematches the value created when the authentication flow started. Reject mismatched or missing state values. - Send the short-lived authorization
codefrom the mobile app to your own backend over HTTPS. Include the sameredirect_uriand any internal transaction/session identifier your backend needs to correlate the request. - Your backend sends the
codeto the configured IPification token endpoint using the confidential client credentials supplied during onboarding and the sameredirect_uriused in the Authorization request. - Your backend validates the token response and verification result, then creates or updates the application's authenticated session and returns only the required result to the app.
Security requirements:
- Never embed the client secret in the Android application or perform the confidential token exchange directly from the device.
- Treat the authorization code as single-use and short-lived. Do not log, cache, or persist it.
- Do not log tokens or return confidential client credentials to the mobile app.
GET https://{api-server}/auth/realms/ipification/protocol/openid-connect/auth?
response_type=code&
client_id={client-id}&
redirect_uri={client-callback-uri}&
scope=openid ip:phone_verify&
state={state}&
login_hint={login_hint}
-------
Response:
(1) 200 - redirect_uri?code=abcxyz&state={state}
(2) 302 - url redirection (ex: https://mnv.telco.com/webhook/api/webhook/auth?state=xyzabc)
| Name | Description |
|---|---|
| api-server | API host for the configured environment or client deployment. |
| client_id | unique identifier of the client that is generated by IPification and provided to client during the onboarding process. |
| redirect_uri | is used when redirecting a user back to the client application. During the onboarding process, the client's redirect_uri will be provided, this value can represent wildcard uri and will be used to validate provided redirect_uri in the request. Redirect URI must be same accross all requestst in a flow. |
| scope | use openid ip:phone_verify for phone number verifying |
| login_hint | end-user phone number (MSISDN).Phone number should be specified according to the E.164 number formatting (http://en.wikipedia.org/wiki/E.164) without leading + sign. |
| consent_id (optional) | Unique ID for the consent that is traceable if consent audit is required. Value will be provided if needed in integration process. |
| consent_timestamp (optional) | The time stamp when consent was accepted by end user. Accepted format is UNIX time stamp in seconds. |
| mcc (optional) | Mobile Country Code |
| mnc (optional) | Mobile Network Code |
Choose the routing scope that matches the integration:
| Option | Routing scope | Cleanup |
|---|---|---|
| 5.1 Force cellular for each request | Only the IPification HTTP client/request uses cellular. Recommended. | Keep the NetworkCallback registered for the complete cellular sequence, then unregister it after the final required cellular request finishes. |
| 5.2 Force cellular for the whole app process | All future sockets and DNS lookups in the app process use cellular. Android API 23+. | After all requests finish, restore the default network and unregister the shared NetworkCallback. |
Request a cellular Network and bind only the IPification OkHttp client to its socket factory
and DNS resolver. Other application traffic continues using Android's default network. Keep
the same cellular network request active while required requests and redirects are still in
progress.
import android.annotation.TargetApi
import android.content.Context
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.os.Build
import android.util.Log
import com.ipification.mobile.sdk.android.interceptor.HandleRedirectInterceptor
import com.ipification.mobile.sdk.android.request.AuthRequest
import com.ipification.mobile.sdk.android.utils.NetworkUtils
import okhttp3.Call
import okhttp3.Callback
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import java.io.IOException
import java.util.*
import java.util.concurrent.atomic.AtomicBoolean
// Manifest.xml: required permission: INTERNET, ACCESS_WIFI_STATE, ACCESS_NETWORK_STATE, CHANGE_NETWORK_STATE, android:usesCleartextTraffic="true"
// external library: OkHttp : com.squareup.okhttp3:okhttp 5.x
class CellularConnection {
companion object {
private const val TAG = "CellularConnection"
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
fun performRequest(
context: Context,
authRequest: AuthRequest,
onSuccess: (responseBody: String) -> Unit,
onFailure: (error: Throwable) -> Unit,
) {
// wifi is OFF, DATA is ON -> request with current network interface
if (NetworkUtils.isMobileDataEnabled(context) && !NetworkUtils.isWifiEnabled(context)) {
processRequest(
context,
null,
authRequest,
onSuccess = onSuccess,
onFailure = { error -> onFailure(error) },
)
} else {
requestCellularNetwork(context, authRequest, onSuccess, onFailure)
}
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private fun requestCellularNetwork(
context: Context,
authRequest: AuthRequest,
onSuccess: (responseBody: String) -> Unit,
onFailure: (error: Throwable) -> Unit,
) {
// 1. force network connection via cellular interface
// If your app supports Android 21+, you need to implement handling timeout manually.
// Android 21++ support requestNetwork (NetworkRequest request,
// ConnectivityManager.NetworkCallback networkCallback)
// Android 26++ support requestNetwork(NetworkRequest request,
// ConnectivityManager.NetworkCallback networkCallback,
// int timeoutMs)
// https://developer.android.com/reference/android/net/ConnectivityManager#requestNetwork(android.net.NetworkRequest,%20android.net.ConnectivityManager.NetworkCallback)
val connectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val request = NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET).build()
val requestStarted = AtomicBoolean(false)
val isCompleted = AtomicBoolean(false)
var timeoutTimer: Timer? = null
lateinit var networkCallback: ConnectivityManager.NetworkCallback
// Every terminal path supplies a Result here. This function cancels the timeout,
// unregisters the requested cellular Network, and delivers one public callback.
// compareAndSet prevents duplicate cleanup if timeout and HTTP callbacks race.
fun finishCellularRequest(result: Result<String>) {
if (!isCompleted.compareAndSet(false, true)) return
timeoutTimer?.cancel()
try {
connectivityManager.unregisterNetworkCallback(networkCallback)
} catch (_: IllegalArgumentException) {
// Android may already have released a timed-out network request.
}
result.exceptionOrNull()?.let { error ->
Log.e(TAG, "Cellular sequence failed", error)
}
result.fold(
onSuccess = onSuccess,
onFailure = onFailure,
)
}
networkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
// Ignore duplicate availability callbacks for this cellular sequence.
if (!requestStarted.compareAndSet(false, true)) return
processRequest(
context,
network,
authRequest,
onSuccess = { responseBody ->
// OkHttp has followed all required redirects. Cleanup, then forward the
// response so the caller can validate `state` and extract `code`.
finishCellularRequest(Result.success(responseBody))
},
onFailure = { error ->
finishCellularRequest(Result.failure(error))
},
)
}
override fun onUnavailable() {
finishCellularRequest(
Result.failure(IOException("Cellular network is unavailable"))
)
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
connectivityManager.requestNetwork(request, networkCallback, 5000)
} else {
timeoutTimer = Timer("cellular-network-timeout", true).apply {
schedule(object : TimerTask() {
override fun run() {
finishCellularRequest(
Result.failure(IOException("Timed out waiting for cellular network"))
)
}
}, 5000)
}
connectivityManager.requestNetwork(request, networkCallback)
}
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private fun processRequest(
context: Context,
network: Network?,
authRequest: AuthRequest,
onSuccess: (responseBody: String) -> Unit,
onFailure: (error: IOException) -> Unit,
) {
// using OkHTTP library to make the connection
val httpBuilder =OkHttpClient.Builder()
if (network != null) {
// enable socket for network
httpBuilder.socketFactory(network.socketFactory)
// Add network-specific DNS resolution when a cellular Network is selected.
val dns = NetworkDns.instance
dns.setNetwork(network)
httpBuilder.dns(dns)
}
// Keep intermediate HTTP redirects on this cellular-bound OkHttp client.
// HandleRedirectInterceptor allows normal telco redirects to continue, but when a
// Location header starts with the registered redirect_uri, it treats that URL as the
// terminal callback and returns it in the response body. The caller can then parse
// the authorization code and state without opening the redirect URI in a browser.
httpBuilder.addNetworkInterceptor(
HandleRedirectInterceptor(authRequest.mRedirectUri.toString())
)
// Optional Singtel compatibility: Singtel may inspect or reject requests containing a
// User-Agent header. Enable this only when required for the Singtel integration.
// A network interceptor is required because OkHttp may add its default User-Agent
// after application interceptors have already run.
//
// httpBuilder.addNetworkInterceptor { chain ->
// val requestWithoutUserAgent = chain.request()
// .newBuilder()
// .removeHeader("User-Agent")
// .build()
// chain.proceed(requestWithoutUserAgent)
// }
val httpClient = httpBuilder.build()
val okHttpRequestBuilder = Request.Builder()
//url
okHttpRequestBuilder.url(authRequest.getUrl())
val okHttpRequest: Request = okHttpRequestBuilder
.build()
httpClient.newCall(okHttpRequest).enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) {
// Runs asynchronously on an OkHttp dispatcher thread, not the main thread.
// This callback is delivered after OkHttp follows intermediate redirects. If
// HandleRedirectInterceptor detects the registered redirect_uri, the response
// body contains that terminal callback URL for parsing `code` and `state`.
try {
// body.string() consumes the response body and can be called only once.
// Parse/store the value here, then post UI updates to the main thread.
val responseBody = response.body?.string().orEmpty()
// OkHttp delivers HTTP error status codes here as well. Report only 2xx
// responses as success; transport and HTTP failures share onFailure.
if (response.isSuccessful) {
// The caller handles parsing, state validation, and cellular cleanup.
// Avoid logging the authorization response in production.
onSuccess(responseBody)
} else {
onFailure(IOException("HTTP request failed with ${response.code}"))
}
} finally {
// Always close the response to release the socket back to OkHttp.
response.close()
}
}
override fun onFailure(call: Call, e: IOException) {
// Called for transport failures, cancellation, DNS errors, or timeouts; HTTP
// error status codes are still delivered to onResponse(). This also runs on an
// OkHttp dispatcher thread, so post any UI work to the main thread.
// The caller decides whether to retry on the same cellular Network, fall back,
// or complete the sequence. Do not expose the raw exception to users.
onFailure(e)
}
})
}
}--------------------------
NetworkDns.kt
Use this class to resolve the hostname through the selected cellular network when Wi-Fi is also active.
--------------------------
import android.net.Network
import android.os.Build
import android.os.Build.VERSION_CODES
import okhttp3.Dns
import java.net.InetAddress
import java.net.UnknownHostException
class NetworkDns private constructor() : Dns {
private var mNetwork: Network? = null
fun setNetwork(network: Network?) {
mNetwork = network
}
@Throws(UnknownHostException::class)
override fun lookup(hostname: String): List<InetAddress> {
return if (mNetwork != null && Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
try {
// Preserve the DNS resolver's original IPv4/IPv6 address order.
mNetwork!!.getAllByName(hostname).toList()
} catch (ex: NullPointerException) {
try {
Dns.SYSTEM.lookup(hostname)
} catch (e: UnknownHostException) {
InetAddress.getAllByName(hostname).toList()
}
} catch (ex: UnknownHostException) {
try {
Dns.SYSTEM.lookup(hostname)
} catch (e: UnknownHostException) {
InetAddress.getAllByName(hostname).toList()
}
}
} else Dns.SYSTEM.lookup(hostname)
}
companion object {
private var sInstance: NetworkDns? = null
val instance: NetworkDns
get() {
if (sInstance == null) {
sInstance = NetworkDns()
}
return sInstance!!
}
}
}OkHttp normally follows HTTP redirects automatically. Add this as a network interceptor so
it can inspect each redirect response before OkHttp follows it. Intermediate telco redirects
are returned unchanged and continue normally. A redirect matching the registered client
redirect_uri is captured as the terminal authentication result.
import android.net.Uri
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
class HandleRedirectInterceptor(redirectUri: String) : Interceptor {
private val expectedRedirectUri = Uri.parse(redirectUri).also { uri ->
require(!uri.scheme.isNullOrBlank()) { "redirectUri must include a URI scheme" }
}
override fun intercept(chain: Interceptor.Chain): Response {
val response = chain.proceed(chain.request())
// OkHttp header lookup is case-insensitive, so one lookup handles Location/location.
val location = response.header("Location")
if (!response.isRedirect || location == null || !isTerminalRedirect(location)) {
// Keep intermediate redirects unchanged so OkHttp can follow them using the same
// cellular-bound client, socket factory, DNS resolver, cookies, and headers.
return response
}
// Replace only the terminal redirect body and preserve the original request, protocol,
// headers, timestamps, TLS handshake, and other response metadata.
val terminalResponse = response.newBuilder()
.code(200)
.message("success")
.removeHeader("Location")
.body(location.toResponseBody("text/plain; charset=utf-8".toMediaType()))
.build()
// The original body is no longer returned and must be closed to avoid leaking a socket.
response.close()
return terminalResponse
}
private fun isTerminalRedirect(location: String): Boolean {
val actualUri = Uri.parse(location)
// Compare URI components instead of using startsWith(), which can incorrectly accept
// a different callback such as "myapp://callback.attacker". Query parameters are not
// compared because the terminal URI adds dynamic values such as `code` and `state`.
if (actualUri.isOpaque || expectedRedirectUri.isOpaque) {
return actualUri.isOpaque == expectedRedirectUri.isOpaque &&
actualUri.scheme.equals(expectedRedirectUri.scheme, ignoreCase = true) &&
actualUri.schemeSpecificPart.substringBefore('?') ==
expectedRedirectUri.schemeSpecificPart.substringBefore('?')
}
return actualUri.scheme.equals(expectedRedirectUri.scheme, ignoreCase = true) &&
actualUri.authority.equals(expectedRedirectUri.authority, ignoreCase = true) &&
actualUri.path.orEmpty() == expectedRedirectUri.path.orEmpty()
}
}Register it with addNetworkInterceptor(), as shown above. After receiving the captured URL,
parse its query parameters and verify that state matches the value generated at the start of
the flow before accepting the authorization code. If onboarding permits wildcard redirect
URIs, replace isTerminalRedirect() with an explicit matcher for the approved wildcard pattern;
do not fall back to unrestricted string-prefix matching.
Singtel compatibility — optional
User-Agentremoval: Singtel may inspect theUser-Agentheader and block requests that include it. If IPification confirms this behavior for the Singtel integration, use the network-interceptor example above to remove the header from every request and redirect in the cellular flow. Do not remove it globally or by default, because unrelated endpoints and SDKs may rely on normal HTTP headers. Test this behavior on the Singtel network before release.
More detail: IPificationService.kt
After the final required cellular request reaches a terminal success or failure callback,
call ConnectivityManager.unregisterNetworkCallback() with the same callback passed to
requestNetwork(). Do not unregister between redirects or while another required cellular
request still needs the selected network. In the sample,
finishCellularRequest() is the only terminal result and cleanup path. Its
AtomicBoolean.compareAndSet(false, true) guard ensures that timeout, unavailability, success,
and failure cannot publish a result or unregister the network more than once. Cellular
acquisition failures—including onUnavailable() and the manual timeout—are delivered through
the same public onFailure callback as HTTP transport failures.
Use this option only when every new network connection created by the application process
must use cellular. Call bind() once before starting the batch. Create new HTTP clients and
connections only after onBound is called.
These are the two essential operations used by the complete helper below:
// Call only after requestNetwork() returns a cellular Network in onAvailable().
private fun forceWholeAppToCellular(cellularNetwork: Network): Boolean {
// Core forcing call: all NEW sockets and DNS lookups created by this app process
// use the supplied cellular network. Existing connections do not move.
return connectivityManager.bindProcessToNetwork(cellularNetwork)
}
// Call after the FINAL required cellular request succeeds, fails, or is cancelled.
private fun stopForcingWholeAppToCellular(
networkCallback: ConnectivityManager.NetworkCallback,
) {
// Restore Android's normal default-network selection, usually Wi-Fi when available.
connectivityManager.bindProcessToNetwork(null)
// Release the cellular Network requested for this batch.
try {
connectivityManager.unregisterNetworkCallback(networkCallback)
} catch (_: IllegalArgumentException) {
// The callback may already have been released after timeout or unavailability.
}
}The line that forces the whole app process to cellular is
connectivityManager.bindProcessToNetwork(cellularNetwork). It affects new connections created after the call succeeds. To stop forcing cellular, first callbindProcessToNetwork(null), then unregister the sameNetworkCallbackoriginally passed torequestNetwork().
We provide a sample helper named
ProcessCellularBinding. Copy
this class into the client application. It requests the cellular network, binds the app process,
handles the shared NetworkCallback, and restores normal routing during cleanup. The client only
needs to call its bind() and unbind() functions:
- Call
bind()and wait foronBound. - Create a new HTTP client and perform all required cellular requests.
- After the final success, failure, or cancellation, stop the client and call
unbind(). - New connections can then use Android's normal default network again.
private val cellularBinding by lazy {
ProcessCellularBinding(applicationContext)
}
private val cellularBatchStarted = AtomicBoolean(false)
fun performAllRequestsOnCellular() {
cellularBinding.bind(
timeoutMillis = 10_000,
onBound = {
// onBound may run again if Android replaces a lost cellular network.
if (!cellularBatchStarted.compareAndSet(false, true)) return@bind
// Create clients after the process is bound. Existing sockets do not move.
val httpClient = OkHttpClient.Builder().build()
performCoverageRequest(httpClient) { coverageResult ->
if (coverageResult.isFailure) {
finishCellularWork()
return@performCoverageRequest
}
performAuthenticationRequest(httpClient) {
// All requests in this cellular-only batch are now complete.
finishCellularWork()
}
}
},
onUnavailable = {
// The helper has already released its network request.
cellularBatchStarted.set(false)
handleCellularUnavailable()
},
onLost = {
// Pause or fail pending work while Android looks for a replacement network.
},
)
}
private fun finishCellularWork() {
// Restores normal routing with bindProcessToNetwork(null), then calls
// unregisterNetworkCallback() for the shared cellular network request.
cellularBinding.unbind()
cellularBatchStarted.set(false)
}A runnable project is available at
samples/android/process-binding-ip-check.
Copyright 2022 IPification, Inc.
Licensed to the Apache Software Foundation (ASF) under one or more contributor
license agreements. See the NOTICE file distributed with this work for
additional information regarding copyright ownership. The ASF licenses this
file to you under the Apache License, Version 2.0 (the "License"); you may not
use this file except in compliance with the License. You may obtain a copy of
the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations under
the License.