Skip to main contentArrow Right

Table of Contents

User authentication is a critical component of any mobile application. A smooth and intuitive login experience plays a crucial role in reducing user friction and abandonment rates. Traditional authentication flows often rely on browser-based redirections, which can disrupt the user experience and introduce inconsistencies in branding and session management.

Descope Native Flows provide a solution by embedding authentication screens directly within an application’s interface. While the authentication logic remains securely hosted, the experience feels fully native, allowing for seamless integration with the app’s UI and branding. 

This guide explores the benefits of using Native Flows, implementation details using the Descope Kotlin SDK, and best practices for deep linking and session management.

Why use native auth experiences?

Many mobile applications rely on browser-based authentication, which involves redirecting users to an external browser or an in-app webview for login. While functional, this approach has limitations:

1. Context switching and user friction

Redirecting users to an external browser forces them to leave the app, which can lead to:

  • Increased drop-off rates as users hesitate to complete the login process.

  • Loss of app context, requiring additional steps to navigate back.

2. Limited control over session management

When authentication occurs in an external browser, session tokens are typically stored in third-party cookies or browser sessions, leading to:

  • Difficulties in maintaining persistent login sessions across app launches.

  • Inconsistent logout behavior, as users may remain logged in within the browser but be logged out of the app.

  • Limited ability to refresh tokens without browser intervention, impacting seamless authentication renewal.

3. Dependency on browser settings

  • Cookies and session storage are subject to browser settings that can impact authentication behavior.

  • Private browsing modes may prevent session persistence, requiring frequent logins.

Native auth experiences address these issues by embedding hosted authentication pages inside a webview that mimics a native UI. This approach ensures:

  • A seamless user experience, keeping users within the app throughout the login process.

  • Consistent branding, as the authentication branding and styling aligns with the rest of the application.

  • Greater flexibility, enabling different authentication methods based on device type or user preferences.

  • Session persistence, allowing applications to manage authentication sessions effectively.

Why use Descope Native Flows?

Fig: An example of Descope Native Flows in action
Fig: An example of Descope Native Flows in action

Descope Native Flows ensure that authentication is embedded in the mobile app, providing several key advantages:

1. Fully integrated, seamless authentication experience

Unlike browser-based solutions, Native Flows keep users inside the app, ensuring:

  • A smooth, uninterrupted login process without external redirects.

  • A more natural, native-like experience, improving engagement and reducing friction.

2. Enhanced session management and token persistence

Descope handles session management entirely within the app, allowing developers to:

  • Keep users logged in across app launches without relying on browser session storage.

  • Refresh authentication tokens automatically, avoiding frequent reauthentication prompts.

  • Implement fine-grained session controls, ensuring security without sacrificing user experience.

For example, when the app restarts, Descope’s session manager can check if the user’s session is valid and refresh it automatically:

val session = Descope.sessionManager.session
if (session != null && !session.refreshToken.isExpired) {
    startMainActivity() // Keep user logged in
} else {
    showLoginScreen() // Prompt login only if necessary
}

This ensures that users don’t have to re-enter credentials unnecessarily, providing a frictionless experience.

3. No-code authentication updates without app redeployment

One of the most significant advantages of Descope Native Flows is the ability to update your flow without requiring:

  • New app versions or deployments.

  • App store approvals, which can delay rollout.

  • Custom UI updates for authentication screens.

All changes are managed via Descope Flows, a visual, drag & drop interface that allows teams to:

  • Modify authentication flows instantly, adding new methods like passkeys, social login, or OTP.

  • Adjust branding and UI elements, such as colors, logos, and messages.

  • Enforce security policies dynamically, like requiring multi-factor authentication for high-risk users.

This eliminates the need for frequent mobile app updates, making authentication easier to maintain.

Setting up Native Flows in a Kotlin application

This section provides a step-by-step guide to integrating Native Flows in a Kotlin-based Android application. 

For more information about this, check out the Kotlin Getting Started guide on our docs site and bookmark our Native Flows sample app. You can also watch the supplementary video below to learn how the app works with our Kotlin SDK.

1. Adding the Descope Kotlin SDK

Begin by adding the Descope Kotlin SDK to the application’s dependencies:

dependencies {
    implementation 'com.descope:descope-kotlin:0.13.3'
}

This SDK provides the necessary tools to authenticate users, manage sessions, and integrate authentication flows.

2. Initializing Descope in the application

Before using authentication flows, initialize the Descope SDK in the Application class:

import com.descope.Descope

class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()

        Descope.setup(this, projectId = "<YOUR_PROJECT_ID>") {
            baseUrl = "https://my.auth.com"  // Optional custom auth domain
            if (BuildConfig.DEBUG) {
                logger = DescopeLogger() // Enable logging for debugging
            }
        }
    }
}

This ensures that Descope is configured when the application starts.

3. Running an authentication flow

To display an authentication flow, add a DescopeFlowView to the activity layout:

<com.descope.ui.DescopeFlowView
    android:id="@+id/descopeFlowView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>

Then, start the authentication flow in the activity:

val descopeFlowView = findViewById<DescopeFlowView>(R.id.descopeFlowView)
val descopeFlow = DescopeFlow("<YOUR_FLOW_URL>")
descopeFlowView.run(descopeFlow)

4. Handling authentication success and errors

To handle authentication events, implement the DescopeFlowView.Listener interface:

descopeFlowView.listener = object : DescopeFlowView.Listener {
    override fun onSuccess(response: AuthenticationResponse) {
        val session = DescopeSession(response)
        Descope.sessionManager.manageSession(session)
        navigateToMainScreen()
    }

    override fun onError(exception: DescopeException) {
        when (exception) {
            DescopeException.networkError -> showToast("Network issue. Please check your connection.")
            DescopeException.wrongOtpCode -> showToast("Incorrect OTP. Try again.")
            else -> showToast("Unexpected error: ${exception.message}")
        }
    }
}

This ensures that users receive appropriate feedback during authentication.

Certain authentication methods, such as OAuth-based social logins (Google, Apple, etc.) and magic links, require users to momentarily leave the app to authenticate. Deep linking ensures a seamless return by automatically redirecting users back to the app and completing authentication, preventing disruptions and improving user experience.

Additionally, Descope’s Kotlin SDK supports Native OAuth, eliminating the need for browser-based authentication by enabling fully in-app Google Sign-In, enhancing both speed and security.

When an authentication method requires external navigation (e.g., Google OAuth or a magic link email client), your app must be able to process the returning deep link and resume the authentication flow.

1. Creating an activity to handle deep links

Define a dedicated activity, FlowRedirectActivity, to capture and process deep links:

class FlowRedirectActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Resume authentication from deep link
        intent?.data?.let { deepLinkUri ->
            findViewById<DescopeFlowView>(R.id.descopeFlowView)
                ?.resumeFromDeepLink(deepLinkUri)
        }
    }

    override fun onNewIntent(intent: Intent?) {
        super.onNewIntent(intent)
        intent?.data?.let { deepLinkUri ->
            findViewById<DescopeFlowView>(R.id.descopeFlowView)
                ?.resumeFromDeepLink(deepLinkUri)
        }
    }
}

This ensures that users who authenticate externally seamlessly return to the app, where authentication is resumed automatically.

2. Configuring deep links in AndroidManifest.xml

To allow Android to handle authentication deep links, modify AndroidManifest.xml to define intent filters:

<activity android:name=".FlowRedirectActivity" android:exported="true">
    <intent-filter android:autoVerify="true">
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="https" android:host="<YOUR_AUTH_DOMAIN>" android:path="/done" />
    </intent-filter>
</activity>

Here are some key configurations to keep in mind:

  • android:autoVerify="true" enables automatic deep link verification.

  • android:host="<YOUR_AUTH_DOMAIN>" should match the authentication domain (e.g., auth.yourapp.com).

  • android:path="/done" specifies the deep link path that OAuth and magic links should use.

This setup ensures magic links and OAuth return URLs redirect directly into your app instead of opening a browser.

3. Hosting assetlinks.json to enable app links

For Android to recognize deep links without browser prompts, configure App Links by hosting a Digital Asset Links JSON file at:

https:///.well-known/assetlinks.json

Below are the contents of assetlinks.json:

[
  {
    "relation": ["delegate_permission/common.handle_all_urls"],
    "target": {
      "namespace": "android_app",
      "package_name": "com.yourapp.package",
      "sha256_cert_fingerprints": ["<YOUR_APP_SHA256_FINGERPRINT>"]
    }
  }
]
  • Replace com.yourapp.package with your Android package name.

  • Replace <YOUR_APP_SHA256_FINGERPRINT> with your app’s SHA-256 certificate fingerprint (keytool -list).

Run the following command to verify deep link handling in Android Studio:

adb shell am start -a android.intent.action.VIEW -d "https://<YOUR_AUTH_DOMAIN>/done"

A successful test confirms that clicking a magic link or OAuth return URL will open inside the app instead of a browser.

4. Enabling native OAuth for a fully in-app experience

By default, OAuth authentication redirects users to an external browser. However, Descope’s Kotlin SDK enables fully native OAuth, allowing Google Sign-In to happen inside the app via Google Play Services—eliminating context switching and improving security.

Here’s how to implement native OAuth in Kotlin:

val descopeFlow = DescopeFlow("<YOUR_FLOW_URL>")
descopeFlow.oauthNativeProvider = OAuthProvider.Google
descopeFlow.oauthRedirect = "<YOUR_APP_LINK_URL>"
descopeFlowView.run(descopeFlow)
  • oauthNativeProvider = OAuthProvider.Google triggers Google Sign-In natively instead of opening a browser.

  • oauthRedirect ensures users return to the correct deep link after authentication.

5. Optimizing magic link handling for seamless authentication

For magic link authentication, users should be redirected back to the app and logged in automatically, without any extra steps.

Modify the deep link handler to resume authentication instantly:

val descopeFlowView = findViewById<DescopeFlowView>(R.id.descopeFlowView)
intent?.data?.let { deepLinkUri ->
    descopeFlowView.resumeFromDeepLink(deepLinkUri)
}

With this setup, users who click a Magic Link never see a browser page—they are taken directly to the app and logged in instantly.

Managing authentication sessions

Descope provides a built-in session manager to handle authentication persistence.

1. Checking for an active session

On application launch, verify if the user is already authenticated:

val session = Descope.sessionManager.session
if (session != null && !session.refreshToken.isExpired) {
    startMainActivity()
} else {
    showLoginScreen()
}

2. Using the session for API calls

To authenticate outgoing API requests, attach the session token to the request:

val connection = url.openConnection() as HttpsURLConnection
connection.setAuthorization(Descope.sessionManager)

3. Logging out

The snippet below logs out the user and revokes their session:

Descope.sessionManager.clearSession()

The snippet below revokes the session from the Descope backend:

Descope.sessionManager.session?.refreshJwt?.let { refreshJwt ->
    CoroutineScope(Dispatchers.IO).launch {
        try {
            Descope.auth.revokeSessions(RevokeType.CurrentSession, refreshJwt)
        } catch (ignored: Exception) { }
    }
}

Advanced Native Flow features

Integrating Descope Native Flows into a mobile application not only enhances the authentication experience but also introduces powerful capabilities that improve security, flexibility, and maintainability. This section explores how developers can dynamically adjust authentication flows, tailor the experience for different devices, and enforce adaptive security measures—all without modifying the app’s code or redeploying it.

1. Running multiple flows within an app

Authentication is more than just signing in. Many applications require additional authentication or user interaction at different stages of the user journey. With Descope’s Native Flows, you can run multiple authentication flows within an app, dynamically invoking the appropriate flow when needed—all without relying on browser-based logic. 

This ensures a seamless experience where users stay inside the app rather than being redirected to external web pages.

You may need to run different flows at different stages of the user lifecycle:

  • New users: Complete signup and email verification.

  • Returning users: Skip signup and go straight to login.

  • Performing sensitive actions: Require step-up authentication (e.g., MFA or biometric authentication).

  • Incomplete user profiles: Prompt users for missing information (e.g., phone number, company name).

With Descope, these flows can be dynamically invoked without modifying the app’s core logic or UI.

Here’s an example of switching between different flows based on context:

val descopeFlow = when {
    isReturningUser -> DescopeFlow("<LOGIN_FLOW>") // Returning user logs in
    isNewUser -> DescopeFlow("<SIGNUP_FLOW>") // New user signs up
    requiresStepUpAuth -> DescopeFlow("<STEP_UP_AUTH_FLOW>") // Extra authentication required
    needsProfileCompletion -> DescopeFlow("<PROGRESSIVE_PROFILE_FLOW>") // Prompt for additional details
    else -> DescopeFlow("<DEFAULT_FLOW>")
}

descopeFlowView.run(descopeFlow)

2. Adapting authentication for mobile and web

Authentication requirements can differ significantly between mobile and desktop users. For example:

  • Passkeys (biometric-based authentication) are ideal for mobile users.

  • Magic links or email-based authentication may be more convenient for desktop users.

Instead of maintaining separate authentication logic for each platform, developers can dynamically adjust the authentication method based on the user’s device using sourceUserAgent.mobile.

The image below shows a conditional step to check if the user is attempting signup / login from a mobile or web device and creates different user journey branches.

Fig: Flow condition with sourceUserAgent.mobile
Fig: Flow condition with sourceUserAgent.mobile

Next, we can see the branching paths in Descope flows. In this example, mobile users will be prompted to use passkeys, while desktop users will be directed to use an email one-time password (OTP).

Fig: Mobile vs. desktop flow logic
Fig: Mobile vs. desktop flow logic

Unifying web & mobile authentication with Native Flows

Descope Native Flows provide a streamlined authentication experience that eliminates unnecessary redirects while ensuring a secure and scalable authentication infrastructure. 

By integrating Native Flows into a Kotlin application, developers can enhance user experience, improve session management, and maintain flexibility in authentication methods.

For a working implementation, refer to the Descope Native Flows Kotlin sample app. As a supplement to the step-by-step guide in the blog above, you can follow along with a walkthrough video illustrating how this app works and how you can add our Kotlin SDK to your application.

Excited to build your own seamless mobile auth flows? Sign up for a free Descope account or a book a demo with our auth experts to learn more.