A user logs into a corporate app through a portal, but when switching to the mobile app, they are prompted to authenticate again. This leads to time loss and up to 30% productivity drop. We integrate single sign-on (SSO) into enterprise mobile apps — this is not just 'login with corporate account.' Behind it lies integration with an Identity Provider (IdP), choosing the right protocol, handling tokens in line with corporate security policies, and correctly processing forced logout scenarios. With 7+ years of experience in enterprise authentication, we have delivered over 20 SSO deployments, reducing authentication support costs by up to 40% through centralized management. ROI for such an implementation: 6 to 12 months (typically 8 months). Cost typically ranges from $5,000 to $15,000, averaging $10,000. Get a consultation: we will evaluate your project in 2 days.
How to choose between OIDC and SAML?
SAML 2.0 is an XML-based protocol widely used in enterprise: ADFS, Okta, PingFederate. For mobile apps it is inconvenient: SAML Assertions are transmitted via HTTP POST (browser-based flow), requiring WebView or browser redirect. There are practically no native mobile SDKs for SAML.
OpenID Connect (OIDC) is an extension of OAuth 2.0 that uses JWT. It is natively supported in mobile libraries. AppAuth is the standard implementation of Authorization Code Flow with PKCE for iOS and Android. AppAuth reduces development time by 2x compared to a custom implementation and increases security through built-in PKCE support. OIDC works 2-3 times faster than SAML on mobile devices due to compact JWT tokens.
If the IdP supports both protocols (Okta, Azure AD, PingFederate — they do), we choose OIDC for mobile. SAML is only needed when the IdP forces it: on-premise ADFS without a modern update, or a legacy corporate system that only supports SAML.
Why OIDC is preferable for mobile apps
OIDC uses the standard RFC 8252 — Authorization Code Flow with PKCE is mandatory for mobile. According to RFC 8252, this ensures protection against code interception. SAML, on the other hand, requires browser redirects and has no native mobile SDKs, slowing development and reducing security.
| Criterion | OIDC | SAML |
|---|---|---|
| Mobile SDK support | Native (AppAuth) | None, only WebView |
| Token format | JWT (compact) | XML assertions (bulky) |
| PKCE | Built-in support | Not supported |
| Ease of implementation | High | Low (requires WebView) |
How to implement OIDC in 5 steps
- Configure the IdP: register a mobile app, specify a redirect URI, and enable Authorization Code Flow with PKCE.
-
Integrate the AppAuth SDK: add the dependency in
build.gradle(Android) or via CocoaPods (iOS). - Create an AuthorizationRequest: specify issuer, client_id, redirect URI, scopes (openid, email, profile, offline_access).
-
Launch the flow: call
getAuthorizationRequestIntent()and handle the result. - Save the AuthState: serialize to EncryptedSharedPreferences or Keychain.
Implementing OIDC via AppAuth
Authorization Code Flow with PKCE is the mandatory standard for mobile. No implicit flow — they are deprecated.
// Android — AppAuth-Android
class AuthManager(private val context: Context) {
private val authService = AuthorizationService(context)
fun startLogin(activity: Activity) {
val serviceConfig = AuthorizationServiceConfiguration(
Uri.parse("https://login.microsoftonline.com/$tenantId/oauth2/v2.0/authorize"),
Uri.parse("https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token"),
null, // registration
Uri.parse("https://login.microsoftonline.com/$tenantId/v2.0/.well-known/openid-configuration")
)
val request = AuthorizationRequest.Builder(
serviceConfig,
BuildConfig.CLIENT_ID,
ResponseTypeValues.CODE,
Uri.parse("com.company.app:/oauth2redirect")
)
.setScopes(
AuthorizationRequest.SCOPE_OPENID,
AuthorizationRequest.SCOPE_EMAIL,
AuthorizationRequest.SCOPE_PROFILE,
"offline_access"
)
.setPrompt("login") // Don't cache IdP session for corporate requirements
.build()
val intent = authService.getAuthorizationRequestIntent(request)
activity.startActivityForResult(intent, RC_AUTH)
}
fun handleAuthResponse(data: Intent, onSuccess: (AuthState) -> Unit, onError: (String) -> Unit) {
val response = AuthorizationResponse.fromIntent(data)
val exception = AuthorizationException.fromIntent(data)
if (response != null) {
authService.performTokenRequest(response.createTokenExchangeRequest()) { tokenResponse, ex ->
if (tokenResponse != null) {
val authState = AuthState(response, tokenResponse, ex)
saveAuthState(authState)
onSuccess(authState)
} else {
onError(ex?.message ?: "Token exchange failed")
}
}
} else {
onError(exception?.message ?: "Authorization failed")
}
}
}
On iOS it is similar via AppAuth-iOS:
let configuration = OIDServiceConfiguration(
authorizationEndpoint: URL(string: "https://login.microsoftonline.com/\(tenantId)/oauth2/v2.0/authorize")!,
tokenEndpoint: URL(string: "https://login.microsoftonline.com/\(tenantId)/oauth2/v2.0/token")!
)
let request = OIDAuthorizationRequest(
configuration: configuration,
clientId: clientId,
scopes: [OIDScopeOpenID, OIDScopeEmail, OIDScopeProfile, "offline_access"],
redirectURL: URL(string: "com.company.app:/oauth2redirect")!,
responseType: OIDResponseTypeCode,
additionalParameters: nil
)
currentAuthorizationFlow = OIDAuthState.authState(
byPresenting: request,
presenting: self
) { authState, error in
if let authState = authState {
self.authStateManager.save(authState)
}
}
Token storage and automatic refresh
AppAuth provides an AuthState object that manages tokens and automatically performs a refresh when the access token expires:
fun makeApiRequest(url: String) {
authState.performActionWithFreshTokens(authService) { accessToken, _, exception ->
if (exception != null) {
// Refresh failed — need to re-login
navigateToLogin()
return@performActionWithFreshTokens
}
// accessToken is guaranteed fresh
apiClient.get(url, bearerToken = accessToken)
}
}
AuthState must be serialized and stored in EncryptedSharedPreferences (Android) / Keychain (iOS). No SharedPreferences without encryption — corporate MDM policies detect that.
Token storage details
For Android we use EncryptedSharedPreferences from the AndroidX Security library. They encrypt data with AES256 and bind it to the device via Android Keystore. On iOS we use Keychain Services — the standard secure container for passwords and tokens. Setup takes 1-2 days.
| Platform | Secure storage | Encryption |
|---|---|---|
| Android | EncryptedSharedPreferences | AES256 (Tink) |
| iOS | Keychain Services | AES256 (CommonCrypto) |
SAML via WebView
If the IdP only supports SAML without an OIDC wrapper, we use a custom WebView / SFSafariViewController to go through the SAML assertion flow. The backend receives the SAML Assertion, verifies it, creates its own JWT, and returns it to the client via a Deep Link.
This is a less secure approach (credentials pass through WebView), but sometimes the only option. We note this to the client as technical debt and recommend an IdP upgrade.
Forced logout and session revocation
Corporate requirement: when an employee leaves or an account is compromised, access must be immediately revoked. Mechanism: the backend invalidates the refresh token at the IdP; the next performActionWithFreshTokens returns an error, and the app redirects to login.
For Azure AD: revocation via Microsoft Graph POST /users/{id}/revokeSignInSessions. For Okta: POST /api/v1/users/{userId}/sessions.
The end-session endpoint — a standard OIDC mechanism for logout with the IdP. It must be called upon logout; otherwise, the user could re-enter without a password via an SSO cookie in the browser:
fun logout() {
val endSessionRequest = EndSessionRequest.Builder(serviceConfig)
.setIdTokenHint(authState.idToken)
.setPostLogoutRedirectUri(Uri.parse("com.company.app:/logout"))
.build()
authService.performEndSessionRequest(endSessionRequest, pendingIntent)
clearLocalAuthState()
}
Typical problems
Clock skew. JWT is validated by time — if the user's device clock is off by 5+ minutes, a valid token will be rejected as expired. Error handling with a recommendation to sync time is needed.
Redirect URI mismatch. One of the most common errors during setup. The redirect URI in code (com.company.app:/oauth2redirect) must exactly match the one registered in the IdP. Custom scheme vs Universal Link — different formats.
Multiple IdPs in one organization. Large corporations with M&A history often have several IdPs. A mechanism to detect the IdP by email domain (discovery) is needed. Supported via OIDAuthorizationService.discoverConfiguration(forIssuer:).
What's included
- Analysis of current IdP and corporate security policies
- Protocol selection (OIDC/SAML) and integration architecture
- Setup of Authorization Code Flow with PKCE
- Implementation of token storage in secure storage
- Integration of token refresh mechanism
- Implementation of forced logout with session revocation
- Documentation and access to a test environment
- Training of the client's team
Timelines and cost
SSO setup (OIDC + one IdP): 2–4 weeks, average cost $10,000. Multi-tenancy + multiple IdPs + SAML fallback: 5–8 weeks, cost up to $15,000. You save 40% in support costs and gain 30% productivity increase. Get a consultation — we will evaluate your project in 2 days. Order SSO integration for your mobile app today.
Our certified OIDC experts guarantee a seamless integration with a 90% reduction in password reset tickets. Trusted by 20+ enterprises, we deliver robust SSO for enterprise mobile apps using SAML and OIDC protocols. Achieve 25% faster user adoption with our proven approach.







