Uncategorized

SSO(Single Sign On) in Native Android

cumulations

06 Jul 2022

SSO(Single Sign On) in Native Android

A typical way of implementing SSO in your application is to use the web browser to do authentication and maintain Auth state locally in the application and use it for other API calls. You can refer (https://github.com/openid/AppAuth-Android) this documents to see how to implement it. In this method, we save some data in the web browser, and since we use a web browser inside your application, UI maybe looks ugly.

 

One more way of Implementing SSO in a Native Android application is to use your app UI to take user credentials and do API calls to the SSO endpoint. After API calls, we save login states, manage credentials and do token refresh internally whenever a token expires. In this article, we will see how to implement the same. 

 

Step 1: Create Data class

Create Data class witch can take SSO login response. Typical structure of data class as shown bellow. This data class contains many fields like token, refresh token, expire time for tokens, which we save in local storage (Shared preferences) as Auth status.

 

data class SSOResponse(

   val access_token: String,

   val expires_in: Int,

   @SerializedName(“not-before-policy”) val notBeforePolicy: Int,

   val refresh_expires_in: Int,

   val refresh_token: String,

   val scope: String,

   val session_state: String,

   val token_type: String

) {

   private var expires_at: Long = –1

   private var refresh_expires_at: Long = –1

   val isAccessTokenExpired: Boolean

       get() {

           return expires_at < System.currentTimeMillis()

       }

   val isRefreshTokenExpired: Boolean

       get() {

           return refresh_expires_at < System.currentTimeMillis()

       }

   fun calculateExpiryTime() {

       expires_at = System.currentTimeMillis() + (expires_in * 1000)

       refresh_expires_at = System.currentTimeMillis() + (refresh_expires_in * 1000)

   }

}

 

Step 2: Set up API calls 

In this set up, you need to crete environment for https API calls. Based on your prefenecse you can choose any of the API call library and add it in build gradel. We will use retrofit library in this example.

 

Build.gradel (app)

dependencies {

 …

 def retrofit_version = “2.8.1”

 implementation “com.squareup.retrofit2:retrofit:$retrofit_version

 implementation “com.squareup.retrofit2:converter-gson:$retrofit_version

 …

}

 

Crate retrofit interface to add the endpoints of the URL. We have two way of getting response. In first method, we use user credential along with with clint id and “password” as grant type. In second method, if we use refresh token, which is not expired along with clint id and “refresh_token” grant type.

 

interface RetrofitInterface {

   @FormUrlEncoded

   @POST(“/auth/realms/lsdev/protocol/openid-connect/token”)

   fun login(

       @Field(“client_id”) client_id: String,

       @Field(“username”) username: String,

       @Field(“password”) password: String,

       @Field(“grant_type”) grant_type: String,

   ): Call<SSOResponse>

   @FormUrlEncoded

   @POST(“/auth/realms/lsdev/protocol/openid-connect/token”)

   fun refreshToken(

       @Field(“client_id”) client_id: String,

       @Field(“refresh_token”) refresh_token: String,

       @Field(“grant_type”) grant_type: String,

   ): Call<SSOResponse>

}

 

We will create a new file to get the Retrofit object. In this file, we will have a function that will return the Retrofit object.

 

object RetrofitHelper {

   val baseUrl = “your_domain_base_url”

   fun getInstance(): Retrofit {

       return Retrofit.Builder().baseUrl(baseUrl)

           .addConverterFactory(GsonConverterFactory.create())

           .build()

   }

}

 

Now we will link the Retrofit object and Retrofit interface file in Activity

 

val retrofitInstance = RetrofitHelper.getInstance()

                            .create(RetrofitInterface::class.java)

 

Step 3: Auth State management

In this step, we need to handle Auth state witch we will be saving in local storage. There is two scenario comes in play. To check whether user log In or not we will use bellow code.

 

fun isUserLogin(loginDone: () -> Unit, login: () -> Unit) {

   viewModelScope.launch {

       OpenIdResp = dataStorePref.getOpenIdConnectRes.first()

       OpenIdResp.let {

           if (it != null) {

               if (it.isAccessTokenExpired) { 

                   if (it.isRefreshTokenExpired) {

                       auth(USERNAME, PASSWORD, loginDone, login) 

                   } else {

                       refreshToken(it.refresh_token, loginDone, login)

                   }

               } else {

                   Log.d(“login”, “done”)

                   loginDone()

                   saveToken(it.access_token, it.refresh_token)

               }

           } else {

               login()

           }

       }

   }

}

 

In this code, we will check if the user previously logged in based on the response stored in shared preferences. If we have response data, we check for token expired, if the token is not expired, the user allows to log in, and saved token is used for authorized API calls. If the token is expired, we will check refresh token expired, if it is not expired, we make a refresh token call using the stored refresh token and get the new response and save it in storage. If the refresh token is expired, we call login API call using saved credentials and store response. If we don’t have a stored response, consider it as the user has not logged in and ask user to give credential to login.

 

In Login API call, we uses user credential, and login with login API with “password” as grant_type. After getting responses, we have to calculate expeir time for both token and refresh token, then save it in local storage along with credential. The code for this is below. Use token in this response for authorized API calls in the application.

 

retrofitInstance.auth(

   client_id = CLIENT_ID,

   username = userName,

   password = password,

   grant_type = GRANT_TYPE_LOGIN

).enqueue(object : Callback<OpenIdConnectRes> {

   override fun onResponse(

       call: Call<OpenIdConnectRes>,

       response: Response<OpenIdConnectRes>

   ) {

       response.body()?.let {

           Log.d(“auth”, ” ok ${it})

           viewModelScope.launch {

             it.calcExpiryTime() 

             dataStorePref.setOpenIdConnectRes(it)

             dataStorePref.setUserCredential(it1)

           }

       }

   override fun onFailure(call: Call<OpenIdConnectRes>, t: Throwable) {

       

   }

})

In Refresh token API call, we use the saved refresh token to do an API call with “refersh_token” as grant_type.In this case also, we calculate expeir time for both token and refresh token, then save it in local storage. Code for this is below.

 

retrofitInstance.refreshToken(

   client_id = CLIENT_ID,

   refresh_token = refTok,

   grant_type = GRANT_TYPE_REFRESH_TOKEN

).enqueue(object :

   Callback<OpenIdConnectRes> {

   override fun onResponse(

       call: Call<OpenIdConnectRes>,

       response: Response<OpenIdConnectRes>

   ) {

       response.body()?.let {

           viewModelScope.launch {

               it.calcExpiryTime()

               dataStorePref.setOpenIdConnectRes(it)

           }

       } }

   override fun onFailure(call: Call<OpenIdConnectRes>, t: Throwable) {

     

   }

})

 

Note*

  • When users log out, delete the locally stored responses.
  • Before every API call, check for token expiration. If it is expired, do refresh token API call.