Using OIDC for Easy Android Login
This tutorial will walk you through the simple steps to set up the Okta OIDC SDK on Android, leaving you with a working app that allows for secure login.
Join the DZone community and get the full member experience.
Join For FreeIn modern mobile apps, a dedicated auth page is a must. Users want the apps they love to be personalized and expect to be able to use services on different devices and platforms without a hassle. More important than this, they want to know that their data is secure.
When working on Android, the implementation process for secure login can be complex, as many different parts need to align correctly for it to work. On the Android side, you need to create an easy and engaging login UI, communicate with a back-end service, securely persist the user data, and maintain that data, as the user expects a single sign-on experience. In addition to that, you will need to have a back-end app for supporting the login feature in a secure and efficient way.
Here is where the Okta OIDC SDK comes in! As a mobile SDK developed by industry-leading security experts, the Okta OIDC SDK is designed to provide a simple alternative for your users to log into your app. By using OAuth 2.0 and OpenID Connect (OIDC), this is made easy, as these are the security industry’s standards for authorization and authentication. To learn more, consider reading An Illustrated Guide to OAuth and OpenID Connect.
This tutorial will walk you through the simple steps to set up the Okta OIDC SDK on Android, leaving you with a working app that allows for secure login.
Create an Android Login Application
To begin, create a new Android app. It will consist of three screens:
- Splash screen where you figure out if the user is already logged in or not.
- Login screen.
- Home screen, which is only accessible to the logged-in users.
First, you’ll need to download and install the latest version of Android Studio (v4.1.1 at the time of this writing).
Next, launch the app and navigate to File → New… → New Project…. Then, create an "Empty Activity" for "Phone and Tablet." You should now see a screen similar to this:
Change your application name and add the right package name, set the minimum SDK to API 23, and click Finish to create the project.
Add the Okta Android OIDC SDK
Android apps use Gradle as their build tool. To add the Okta OIDC SDK as a dependency to your project, you will need to modify the app module’s build.gradle
file:
You need to add the right auth redirect scheme for your Okta app and add the Okta library as a dependency. Make sure the applicationId
matches what you used when creating your app.
plugins {
id 'com.android.application'
id 'kotlin-android'
}
android {
compileSdkVersion 29
buildToolsVersion "30.0.2"
defaultConfig {
applicationId "dev.dbikic.oktaloginexample"
minSdkVersion 23
targetSdkVersion 29
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
manifestPlaceholders = [
"appAuthRedirectScheme": "com.okta.dev-123456"
]
}
buildFeatures {
viewBinding true
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.2.1'
implementation "androidx.constraintlayout:constraintlayout:2.0.4"
implementation 'com.okta.android:oidc-androidx:1.0.17'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}
- The redirect URI for the application you created in your Okta Developer Console.
- We are using a view binding feature to interact with our views. More information can be found here.
- Okta OIDC libraries require Java 1.8 compatibility.
- Add the dependency required for the Okta OIDC library.
Sync the project with Gradle files by clicking the File → Sync Project with Gradle Files, so the Okta dependency gets downloaded.
Install the Okta CLI
Getting started with Okta is made quite straightforward with the help of the Okta CLI. It’s a tool that makes creating an Okta account and an Okta application a breeze!
The Okta CLI is available for macOS, Linux, and Windows.
MacOS (via Homebrew)
xxxxxxxxxx
brew install --cask oktadeveloper/tap/okta
Linux (via Flatpak)
xxxxxxxxxx
flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
flatpak install com.okta.developer.CLI
alias okta="flatpak run com.okta.developer.CLI"
Windows (via Chocolatey)
xxxxxxxxxx
choco install okta --version=0.7.1
If you don’t want to use a package manager, you can install the Okta CLI with curl and bash.
xxxxxxxxxx
curl https://raw.githubusercontent.com/oktadeveloper/okta-cli/master/cli/src/main/scripts/install.sh | bash
Register Your Android Application
To create an Okta developer account, simply run the following command:
xxxxxxxxxx
okta register
You will be prompted with a few questions:
xxxxxxxxxx
➜ okta register
First name: Dino
Last name: Bikic
Email address: ******@*****.***
Company: Okta
Creating new Okta Organization, this may take a minute:
OrgUrl: https://dev-123456.okta.com (1)
An email has been sent to you with a verification code.
Check your email
Verification code: 123456 (2)
Your Okta Domain: https://dev-123456.okta.com
To set your password open this link:
https://dev-123456.okta.com/welcome/drp7UBGB_GVjeHp_5Jbs (3)
(1) This is your Okta domain; you will need it later
(2) Enter the code you receive in your email.
(3) Click on this link to change your password. You will need these credentials for this example
Create an Okta OIDC App
To create a new Okta OIDC app, run the following command:
xxxxxxxxxx
okta apps create
You will be prompted with a few questions:
xxxxxxxxxx
➜ okta apps create
Application name [dbikic]: Android Login(1)
Type of Application
(The Okta CLI only supports a subset of application types and properties):
> 1: Web
> 2: Single Page App
> 3: Native App (mobile)(2)
> 4: Service (Machine-to-Machine)
Enter your choice [Web]: 3
Redirect URI
Common defaults:
Reverse Domain name - com.okta.dev-123456:/callback
Enter your Redirect URI [com.okta.dev-123456:/callback]:(3)
Enter your Post Logout Redirect URI [com.okta.dev-123456:/]:(4)
Configuring a new OIDC Application, almost done:
Created OIDC application, client-id: ********************
Okta application configuration:
okta.oauth2.issuer: https://dev-123456.okta.com/oauth2/default
okta.oauth2.client-id: ******************** (5)
(1) Name your Okta app
(2) Native app (mobile) is the option to select for Android applications.
(3) Press enter for the default value. You will need this value for the Okta library setup.
(4) Press enter for the default value. You will need this value for the Okta library setup.
(5) You will need this ID for the initialization of the Okta library in your application
Create an Android Application Class
In the root folder of your app’s package (in the provided example, that’s the folder app/src/main/java/dev/dbikic/oktaloginexample
) create a Kotlin application class named`OktaLoginApplication`.
For now, just create the class and make it extend the Application
class from the Android framework.
xxxxxxxxxx
package dev.dbikic.oktaloginexample (1)
import android.app.Application
class OktaLoginApplication : Application()
(1) Update the package to match the one you set when creating the project.
After creating the application class, you need to reference it in your app/src/main/AndroidManifest.xml
file:
xxxxxxxxxx
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="dev.dbikic.oktaloginexample">
<uses-permission android:name="android.permission.INTERNET" /> (1)
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:name=".OktaLoginApplication" (2)
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.OktaLoginExample" />
</manifest>
(1) Okta OIDC SDK
needs internet permission to communicate with the back-end
(2) Reference the application class you created in the previous step.
Manage Authentication With a Manager Class
When adding third-party libraries to your codebase, it’s usually a good idea to create a wrapper class that will hide the actual usage. Reasons for this include:
Reusing of the common interaction with the libraries.
You can define all the library interactions in an interface and provide the actual implementation with dependency injection throughout your app.
Everything related to that library is in one place. Replacing the library with a different one is easy as you only need to change the wrapper class.
Because the above, create a class called OktaManager
in the root package. This class will be used in all the screens you create:
xxxxxxxxxx
package dev.dbikic.oktaloginexample
import android.app.Activity
import android.content.Context
import com.okta.oidc.*
import com.okta.oidc.clients.sessions.SessionClient
import com.okta.oidc.clients.web.WebAuthClient
import com.okta.oidc.net.response.UserInfo
import com.okta.oidc.storage.security.DefaultEncryptionManager
import com.okta.oidc.util.AuthorizationException
class OktaManager(applicationContext: Context) {
/**
* Authorization client using chrome custom tab as a user agent.
*/
private var webAuth: WebAuthClient (1)
/**
* The authorized client to interact with Okta's endpoints.
*/
private var sessionClient: SessionClient (2)
init {
val config = OIDCConfig.Builder()
.clientId("********************") (3)
.discoveryUri("https://dev-123456.okta.com") (4)
.redirectUri("com.okta.dev-123456:/callback") (5)
.endSessionRedirectUri("com.okta.dev-123456:/") (6)
.scopes("openid", "profile", "offline_access")
.create()
webAuth = Okta.WebAuthBuilder()
.withConfig(config)
.withContext(applicationContext)
.withCallbackExecutor(null)
.withEncryptionManager(DefaultEncryptionManager(applicationContext))
.setRequireHardwareBackedKeyStore(true) (7)
.create()
sessionClient = webAuth.sessionClient
}
fun isAuthenticated(): Boolean {
return sessionClient.isAuthenticated
}
fun registerWebAuthCallback(callback: ResultCallback<AuthorizationStatus, AuthorizationException>, activity: Activity) {
webAuth.registerCallback(callback, activity)
}
fun registerUserProfileCallback(callback: RequestCallback<UserInfo, AuthorizationException>) {
sessionClient.getUserProfile(callback)
}
fun signIn(activity: Activity, payload: AuthenticationPayload) {
webAuth.signIn(activity, payload)
}
fun signOut(activity: Activity, callback: RequestCallback<Int, AuthorizationException>) {
webAuth.signOut(activity, callback)
}
fun clearUserData() {
sessionClient.clear()
}
}
(1) private lateinit var webAuth: WebAuthClient
is a reference to the web client you will invoke to log in.
(2) private lateinit var sessionClient: SessionClient
refers to the session you can use to conduct multiple operations after logging in, such as getting the user’s profile, revoking the authentication token, refreshing the authentication token, etc.
(3) Replace with your client ID.
(4) Replace with your discovery URL.
(5) Replace with your redirect URL.
(6) Replace with your end session redirect URL.
(7) setRequireHardwareBackedKeyStore(true)
forces the app to require a device with encryption capabilities. This is the default configuration for Okta OIDC, and it’s considered the best practice. If you want to run this code in an emulator, though, you can temporarily set it to false
.
The last step of the setup stage will be to initialize the OktaManager
. Remember the empty OktaLoginApplication
class? Now you need to modify it to initialize the manager when the app is created.
xxxxxxxxxx
package dev.dbikic.oktaloginexample
import android.app.Application
class OktaLoginApplication : Application() {
lateinit var oktaManager: OktaManager
override fun onCreate() {
super.onCreate()
oktaManager = OktaManager(this)
}
}
That’s it! Now, let’s create the screens.
Add an Android Splash Screen
The purpose of a splash screen is to initialize all the applications' dependencies and prepare the app for usage. You’ll use it to figure out if the user is authenticated and decide which screen to show next: the login screen or the home screen.
Create a SplashActivity
class in the root package.
xxxxxxxxxx
package dev.dbikic.oktaloginexample
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import dev.dbikic.oktaloginexample.ui.LoginActivity
class SplashActivity : AppCompatActivity() {
(1)
private val oktaManager: OktaManager by lazy { (application as OktaLoginApplication).oktaManager }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (oktaManager.isAuthenticated()) {
navigateToHome()
} else {
navigateToLogin()
}
}
private fun navigateToHome() {
// todo implement
}
private fun navigateToLogin() {
startActivity(Intent(this, LoginActivity::class.java)) (2)
finish()
}
}
(1) For simplicity, the instance of the OktaManager
class is in the application class so that it can be easily accessed from all the activities. The real-world solution here would be to use dependency injection and inject the instance class
(2) Ignore the unresolved reference error for now because we will add the missing class in the next step.
Register the activity in the AndroidManifest.xml
file:
xxxxxxxxxx
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="dev.dbikic.oktaloginexample">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:name=".OktaLoginApplication"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.OktaLoginExample">
<activity
android:name=".SplashActivity">
<intent-filter> (1)
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
(1) This intent filter specifies that the SplashActivity
is the first activity that will be shown when the app is launched.
This class won’t compile just yet. You’ll need to create HomeActivity
and LoginActivity
classes before it does.
Build an Android Login Screen
Now, let’s do the LoginActivity
! First, create a simple layout with a button in app/src/main/res/layout/activity_login.xml
:
xxxxxxxxxx
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
android:orientation="vertical"
tools:context=".LoginActivity">
<Button
android:id="@+id/signInButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="40dp"
android:text="Sign in"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Then, create the LoginActivity
class in a new ui
package:
xxxxxxxxxx
package dev.dbikic.oktaloginexample.ui
import android.content.Intent
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import com.okta.oidc.*
import com.okta.oidc.AuthorizationStatus.*
import com.okta.oidc.util.AuthorizationException
import dev.dbikic.oktaloginexample.OktaLoginApplication
import dev.dbikic.oktaloginexample.OktaManager
import dev.dbikic.oktaloginexample.databinding.ActivityLoginBinding
class LoginActivity : AppCompatActivity() {
private val oktaManager: OktaManager by lazy { (application as OktaLoginApplication).oktaManager }
private lateinit var binding: ActivityLoginBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityLoginBinding.inflate(layoutInflater)
setContentView(binding.root)
setupOktaCallback()
setupViews()
}
private fun setupOktaCallback() {
oktaManager.registerWebAuthCallback(getAuthCallback(), this)
}
private fun setupViews() {
binding.signInButton.setOnClickListener {
val payload = AuthenticationPayload.Builder().build()
oktaManager.signIn(this, payload)
}
}
private fun getAuthCallback(): ResultCallback<AuthorizationStatus, AuthorizationException> {
return object : ResultCallback<AuthorizationStatus, AuthorizationException> {
override fun onSuccess(result: AuthorizationStatus) {
when (result) {
AUTHORIZED -> navigateToHome()
SIGNED_OUT -> Log.d("LoginActivity", "Signed out")
CANCELED -> Log.d("LoginActivity", "Canceled")
ERROR -> Log.d("LoginActivity", "Error")
EMAIL_VERIFICATION_AUTHENTICATED -> Log.d("LoginActivity", "Email verification authenticated")
EMAIL_VERIFICATION_UNAUTHENTICATED -> Log.d("LoginActivity", "Email verification unauthenticated")
}
}
override fun onCancel() {
Log.d("LoginActivity", "Canceled")
}
override fun onError(msg: String?, exception: AuthorizationException?) {
Log.d("LoginActivity", "Error: $msg")
}
}
}
private fun navigateToHome() {
// todo implement
}
}
(1) Register the auth callback with the OktaManager
.
(2) Call the sign-in method when the button is clicked.
(3) The result is an AuthorizationStatus
object. With a simple when
expression we can quickly figure out the status type and access its members if needed.
And register it in the AndroidManifest.xml
:
xxxxxxxxxx
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="dev.dbikic.oktaloginexample">
...
<application
... >
...
<activity
android:name=".ui.LoginActivity"
android:theme="@style/Theme.MaterialComponents.Light.NoActionBar" />
</application>
</manifest>
The purpose of the LoginActivity
is to try to authenticate the user with Okta when the login button is pressed. To achieve that, you need to register the web auth callback with the Okta OIDC SDK, and call the signIn()
method.
This is enough for the SDK to open a custom Chrome tab with the login screen of the Okta application. Users input their credentials into the form, and when the process is finished, the appropriate method of your auth callback will be called. This allows you to gracefully handle the possible errors or handle the login success, which is, in this case, navigating to the HomeActivity
.
Create an Android Home Screen
HomeActivity
is the part of your app which can be accessed only by authorized users. In this example, you can fetch the user details, display the user name on the UI, and sign the user out of the app. First, create the layout file in res/layout/activity_home.xml
:
xxxxxxxxxx
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".HomeActivity">
<TextView
android:id="@+id/userLabel"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:gravity="center"
android:textSize="22sp"
tools:ignore="HardcodedText"
tools:text="Hello, user!" />
<Button
android:id="@+id/signOutButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="40dp"
android:text="Log out"
tools:ignore="HardcodedText" />
</LinearLayout>
Then, create the HomeActivity
:
xxxxxxxxxx
package dev.dbikic.oktaloginexample.ui
import android.content.Intent
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import com.okta.oidc.RequestCallback
import com.okta.oidc.net.response.UserInfo
import com.okta.oidc.util.AuthorizationException
import dev.dbikic.oktaloginexample.OktaLoginApplication
import dev.dbikic.oktaloginexample.OktaManager
import dev.dbikic.oktaloginexample.databinding.ActivityHomeBinding
class HomeActivity : AppCompatActivity() {
private val oktaManager: OktaManager by lazy { (application as OktaLoginApplication).oktaManager }
private lateinit var binding: ActivityHomeBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityHomeBinding.inflate(layoutInflater)
setContentView(binding.root)
oktaManager.registerUserProfileCallback(getUserProfileCallback())
binding.signOutButton.setOnClickListener {
oktaManager.signOut(this, getSignOutCallback())
}
}
private fun getSignOutCallback(): RequestCallback<Int, AuthorizationException> {
return object : RequestCallback<Int, AuthorizationException> {
override fun onSuccess(result: Int) {
oktaManager.clearUserData()
val intent = Intent(this@HomeActivity, LoginActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP
startActivity(intent)
finish()
}
override fun onError(msg: String?, exception: AuthorizationException?) {
Log.d("HomeActivity", "Error: $msg")
}
}
}
private fun getUserProfileCallback(): RequestCallback<UserInfo, AuthorizationException> {
return object : RequestCallback<UserInfo, AuthorizationException> {
override fun onSuccess(result: UserInfo) {
binding.userLabel.text = "Hello, ${result["preferred_username"]}!"
}
override fun onError(msg: String?, exception: AuthorizationException?) {
Log.d("HomeActivity", "Error: $msg")
}
}
}
}
(1) Register the user profile callback with the OktaManager
.
(2) Sign out from the app on the sign out button.
(3) After the user is successfully logged out from Okta, clear the user’s data.
(4) Navigate the user back to the LoginActivity
after they sign out.
(5) This flag makes sure that all the back stack activities are cleared and that the LoginActivity
will be the only activity in the memory.
(6) You have fetched the user info successfully! You can check which fields you received here.
And register it in the AndroidManifest.xml
:
xxxxxxxxxx
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="dev.dbikic.oktaloginexample">
...
<application
... >
...
<activity
android:name=".ui.HomeActivity"
android:theme="@style/Theme.MaterialComponents.Light.NoActionBar" />
</application>
</manifest>
You can now implement the empty method navigateToHome()
in both SplashActivity
and LoginActivity
.
xxxxxxxxxx
import dev.dbikic.oktaloginexample.ui.HomeActivity
...
private fun navigateToHome() {
startActivity(Intent(this, HomeActivity::class.java))
finish()
}
Run Your Android Application
Now it’s time to run the application on an emulator or on a physical device, by pressing the play icon in the top right part of Android Studio. Your app and its login process should look similar to the video below:
What’s cool about the Okta OIDC SDK is that it also securely stores the user session to the app’s local storage and maintains its state for you. Instead of creating a custom user management system and handling multiple edge-cases that can happen in the real world, you can concentrate on spending your time building app features for your users.
You also implemented the logout flow, which is triggered by the user clicking the Log Out button:
Learn More About Android and OIDC
This post showcased how easy it is to set up and use the Okta OIDC SDK for an Android app. The functionalities which the SDK brings to your app, like the OAuth 2.0 authorization and OpenID Connect authentication, are essential for a modern-day app with challenges like security and data privacy.
Creating a custom solution for security and privacy is challenging and time-consuming since the code on the mobile part is not enough, and you also need to have a back-end app that supports those features. Maintaining two applications can cause a lot of long-term work.
You can find the source code for this example on GitHub, in the oktadeveloper/okta-android-login-example repository.
Published at DZone with permission of Dino Bikić. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments