Implementing Android OAuth with AppAuth library

Maxim Myalkin
Lonto
Published in
8 min readDec 6, 2022

Hey! My name is Maxim Myalkin, I am a mobile lead at Lonto.

In this series of article, I am going to explain OAuth nuances in mobile applications in a structured way: which points should be noticed, which methods of implementation to choose. I am also going to share my experience in configuring OAuth in an Android app using the AppAuth library.

In a previous article we reviewed OAuth mobile implementation details, how Authorization Code Flow with PKCE works, implementation options.

  1. OAuth mobile implementation details
  2. Implementing Android OAuth with AppAuth library (you are here)
  3. Implementing iOS OAuth in 30 minutes

Сontent of this article:

Implementation in the Android application
\
General setup
\ Implementation in Android
\ Authorization
\ Logout
\ Token refreshing
Conclusion

Implementation in the Android application 📱

Let’s see how we can implement OAuth in your Android app using the AppAuth library. The code is available on GitHub.

The application is simple: displaying information about my GitHub profile.

To do this, each time you launch the app, we will open the GitHub authorization page. After successful authorization, we navigate the user to the main page, on the main page user can get information about the current profile.

We need to pay attention to 3 key points:

  • user authorization
  • updating the token
  • user logout

General setup 🛠

The first step is to register the OAuth application in GitHub.

Set the CALLBACK_URL for your app on the service on the registration page. User will be redirected to this URL after successful authorization, and your app will intercept it.

We will use com.kts.oauth://github.com/callback as the CALLBACK_URL

You should use the custom scheme com.kts.oauth so that only your app can intercept the redirect.

After registration, you should have client_id and client_secret (it should be generated on OAuth app registration). Save them.

Next, you need to find the authorization URL on the GitHub service, and URL to exchange the authorization code for a token. The answer can be found in the GitHub OAuth documentation.

URL for authorization: https://github.com/login/oauth/authorize
URL for token exchange:
https://github.com/login/oauth/access_token

For OAuth process, we need to identify the scopes that github will grant access to after user authorization. Our app needs access to the user’s information and repositories in the app, so the scopes are: user, repo.

The general parameters have been defined. Let’s move on to Android implementation.

implementation 'net.openid:appauth:0.9.1'

Let’s write all OAuth settings to Kotlin object AuthConfig to make it easy to work with:

object AuthConfig {
const val AUTH_URI = "https://github.com/login/oauth/authorize"
const val TOKEN_URI = "https://github.com/login/oauth/access_token"
const val END_SESSION_URI = "https://github.com/logout"
const val RESPONSE_TYPE = ResponseTypeValues.CODE
const val SCOPE = "user,repo"
const val CLIENT_ID = "..."
const val CLIENT_SECRET = "..."
const val CALLBACK_URL = "ru.kts.oauth://github.com/callback"
const val LOGOUT_CALLBACK_URL = "ru.kts.oauth://github.com/logout_callback"
}

Compared to the general setting, the following parameters has been added:

  • RESPONSE_TYPE. Use the “code” constant from the AppAuth library. This constant is responsible for returned value to the client after successful authorization in the browser. Options: code, token, id_token.

According to the OAuth Authorization Code Flow, we need a code.

In fact, GitHub API does not require the response_type parameter to be passed and always returns only the code. However, this parameter may be required for other services.

END_SESSION_URI, LOGOUT_CALLBACK_URL. Settings required for the logout.

Authorization 🗣

Now let’s open the authorization page using Chrome Custom Tabs.

The AppAuth library provides the AuthorizationService class to work with CCT and perform automatic operations of code exchange for a token. AuthorizationService instance should be created when you enter the screen. When you exit the screen, it should be cleared. In the example, this is done inside the ViewModel authorization screen.

Creating in ViewModel init block:

private val authService: AuthorizationService = AuthorizationService(getApplication())

Clear in ViewModel.onCleared:

authService.dispose()

To open the authorization page in CCT, you need an android intent. To get intent, we are creating the AuthorizationRequest based on the AuthConfig data:

private val serviceConfiguration = AuthorizationServiceConfiguration(
Uri.parse(AuthConfig.AUTH_URI),
Uri.parse(AuthConfig.TOKEN_URI),
null, // registration endpoint
Uri.parse(AuthConfig.END_SESSION_URI)
)

fun getAuthRequest(): AuthorizationRequest {
val redirectUri = AuthConfig.CALLBACK_URL.toUri()
return AuthorizationRequest.Builder(
serviceConfiguration,
AuthConfig.CLIENT_ID,
AuthConfig.RESPONSE_TYPE,
redirectUri
)
.setScope(AuthConfig.SCOPE)
.build()
}

Create an intent:

// here you can configure the chromeCustomTabs ui
val customTabsIntent = CustomTabsIntent.Builder().build()

val openAuthPageIntent = authService.getAuthorizationRequestIntent(
getAuthRequest(),
customTabsIntent
)

After that, open the Activity by intent. We need to process the activity result to get the code.

Therefore, use ActivityResultContracts. You can also use startActivityForResult.

private val getAuthResponse = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
val dataIntent = it.data ?: return
handleAuthResponseIntent(dataIntent)
}

getAuthResponse.launch(openAuthPageIntent)

Under the hood Activity from the AppAuth library will be opened (AuthorizationManagementActivity), which will take responsibility for opening the CCT and processing the redirect. And the result of the operation will be passed to the activity of your application (AppActivity).

Activity stack diagram

All the information that we previously specified in AuthConfig, as well as the generated code_challenge, will be passed to openAuthPageIntent.

AppAuth automatically generates an URL to open the login page:

https://github.com/login/oauth/authorize?redirect_uri=ru.kts.oauth%3A%2F%2Fgithub.com%2Fcallback&client_id=3fe9464f41fc4bd2788b&response_type=code&state=mrhOJm7ot4C1aE9ND3lWdA&nonce=4zVLkQrhQ4L46hfQ1jdTHw&scope=user%2Crepo&code_challenge=gs23wPEpmJYv3cdmTRWNSQLvvnPtHUhtSv4zhbfKS_o&code_challenge_method=S256

We should specify that our application can handle URLs with our custom scheme com.kts.oauth to process auth redirect.

To do this, specify manifest placeholder inside the defaultConfig of app build.gradle:

manifestPlaceholders = [
appAuthRedirectScheme: "ru.kts.oauth"
]

After that, activity RedirectUriReceiverActivity that handles URLs with custom scheme will be added to your app’s AndroidManifest.xml automatically. Merged manifest:

<activity 
android:name="net.openid.appauth.RedirectUriReceiverActivity"
android:exported="true" >
<intent-filter>
...
<data android:scheme="ru.kts.oauth" />
</intent-filter>
</activity>

You can also set up a redirect using standard schemes. You can read more in the AppAuth docs.

Next, you need to get an authorization code and exchange it for a token. In this case, an authorization error may occur, and it should be handled.

The AppAuth lets you get an error or request from the result intent to exchange the code for a token:

private fun handleAuthResponseIntent(intent: Intent) {
// пытаемся получить ошибку из ответа. null - если все ок
val exception = AuthorizationException.fromIntent(intent)
// пытаемся получить запрос для обмена кода на токен, null - если произошла ошибка
val tokenExchangeRequest = AuthorizationResponse.fromIntent(intent)
?.createTokenExchangeRequest()
when {
// авторизация завершались ошибкой
exception != null -> viewModel.onAuthCodeFailed(exception)
// авторизация прошла успешно, меняем код на токен
tokenExchangeRequest != null ->
viewModel.onAuthCodeReceived(tokenExchangeRequest)
}
}

The token request will be created automatically and the code_verifier will be added to it. Therefore, the saving code_verifier and code_verifier has already been done by AppAuth.

We are not going to consider the option with an authorization error. You can show Toast or Snackbar in this case.

We received a tokenExchangeRequest request that needs to be performed. To do this, use AuthService.performTokenRequest method.

Under the hood, the performTokenRequest method runs the legacy AsyncTask, so the library API uses callbacks.

fun performTokenRequest(
authService: AuthorizationService,
tokenRequest: TokenRequest,
onComplete: () -> Unit,
onError: () -> Unit
) {
authService.performTokenRequest(tokenRequest, getClientAuthentication()) { response, ex ->
when {
response != null -> {
//обмен кода на токен произошел успешно, сохраняем токены и завершаем авторизацию
TokenStorage.accessToken = response.accessToken.orEmpty()
TokenStorage.refreshToken = response.refreshToken
onComplete()
}
//обмен кода на токен произошел неуспешно, показываем ошибку авторизации
else -> onError()
}
}
}

The callback interface can be easily turned into a suspend call and used together with coroutines in your application. You can see an example in the project.

When exchanging a code for a token, we need to send client_secret according to the Github documentation. Therefore, when calling the performTokenRequest method, you need to pass the ClientAuthentication instance. The library has several implementations: ClientSecretBasic, ClientSecretPost, NoClientAuthentication. You need to choose based on server requirements.

For GitHub you should send client_secret by ClientSecretPost:

private fun getClientAuthentication(): ClientAuthentication {
return ClientSecretPost(AuthConfig.CLIENT_SECRET)
}

If the service does not require client_secret, you can use ClientSecretBasic(“”).

And we finished the authorization implementation in GitHub using the AppAuth library.

Lets’s describe the steps briefly.

  1. Connect the AppAuth library.
  2. Create AuthConfig.
  3. Specify manifestPlaceholder appAuthRedirectScheme.
  4. Create AuthorizationService instance, for example, in ViewModel.
  5. Authorize user:
    → create an AuthorizationRequest
    → create intent;
    → launch activity with CCT.
  6. Exchange the code for a token:
    → get TokenExchangeRequest from activity result intent;
    → perform TokenExchangeRequest using authService.performTokenRequest;
    → save tokens in the success callback.

Logout 📤

For a logout, we need not only to clear the token inside the app, but also to clear browser cookies. You can’t do that from your application, because the browser is an external application. To do this, you need to open the web page that will clear browser cookies.

For GitHub webpage is https://github.com/logout. We previously specified this constant in AuthConfig.END_SESSION_URI.

The idea for opening the page is the same as for the login page:

  1. Create a logout request:
val endSessionRequest = EndSessionRequest.Builder(authServiceConfig)
//Требуется для некоторых сервисов, idToken получается при авторизации аналогично accessToken и refreshToken
.setIdTokenHint(idToken)
// uri на который произойдет редирект после успешного логаута, не везде поддерживается
.setPostLogoutRedirectUri(AuthConfig.LOGOUT_CALLBACK_URL.toUri())
.build()

2. Create a custom tabs intent:

val customTabsIntent = CustomTabsIntent.Builder().build()

3. Create a activity intent:

val endSessionIntent = authService.getEndSessionRequestIntent(
endSessionRequest,
customTabsIntent
)

4. Open the logout page:

private val logoutResponse = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {}

logoutResponse.launch(endSessionIntent)

The user goes to the logout page, where the browser session is cleared.

After successful logout, application should intercept the redirect to it. Not all services redirect to app after the successful logout (GitHub does not redirect). Therefore, the user will need to click on the close button in CCT.

After manual closing of CCT activity, app get activity result=cancelled, because there was no redirect to the application.

In our example with GitHub, we clear the session anyway and navigate to the application login page.

private val logoutResponse = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
// очищаем сессию и переходим на экран логина
viewModel.webLogoutComplete()
}

Token refreshing 🥦

When working with OAuth and the AppAuth library, it is important to keep access tokens up to date. access_token received from the server may be expired. In order to not navigate the user to login page, you need to refresh the token. This is done using refresh_token.

The refresh process is similar to tokenExchangeRequest:

  1. Create token refresh request
val refreshRequest = TokenRequest.Builder(
authServiceConfig,
AuthConfig.CLIENT_ID
)
.setGrantType(GrantTypeValues.REFRESH_TOKEN)
.setScopes(AuthConfig.SCOPE)
.setRefreshToken(TokenStorage.refreshToken)
.build()

It is important to take into account 2 lines:

.setGrantType(GrantTypeValues.REFRESH_TOKEN)
.setRefreshToken(TokenStorage.refreshToken)

As grantType, pass the “refreshToken” string, and pass the refresh_token received during authorization.

2. Execute the token refresh request:

authorizationService.performTokenRequest(refreshRequest) { response, ex ->
when {
response != null -> emitter.onSuccess(response)
ex != null -> emitter.tryOnError(ex)
else -> emitter.tryOnError(IllegalStateException("response and exception is null"))
}
}

This code can be used in the places where you are updating the token in the project. For example, in OkHttp interceptor. The full interceptor example can be found in the repository.

If the token refresh failed (for example, refresh_token is invalid), you should logout the user.

See the logout example 👆👆

Access tokens are never expired in the GitHub service, so the refresh example can be used in other services.

Conclusion 🧐

The full project code is in my GitHub repository.

In this article we reviewed OAuth implementation details in mobile applications, and an example of Android app OAuth implementation using the AppAuth library. This implementation will allow you to quickly setup OAuth in your app.

According to our experience, AppAuth makes it easier to work with OAuth in the mobile application, saving you from writing implementation details manually. However, it creates requirements for an authorization server. If the auth service does not comply with RFC-8252 (OAuth 2.0 for Native Apps), AppAuth may not provide required auth features.

Share in the comments, how did you implement OAuth in mobile apps? Did you have any problems? Did you use AppAuth?

--

--