I have a simple Azure app registration and I have an android app where I use AppAuth to implement OAuth Microsoft/Azure login. Login works fine, I get the access token. Redirect from chrome tab back to app works fine. What doesn't work is the logout. I'm not redirected back to the app. Is the user supposed to close chrome tab himself?
Demo
https://i.ibb.co/1JFyYQ5/Hnet-image-3.gif
app/build.gradle
dependencies {
implementation 'net.openid:appauth:0.9.1'
implementation group: 'com.google.code.gson', name: 'gson', version: '2.9.0'
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<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:animateLayoutChanges="true"
android:orientation="vertical"
android:padding="16dp"
tools:context=".MainActivity">
<TextView
android:id="@+id/text_email"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center" />
<ProgressBar
android:id="@+id/progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone" />
<Button
android:id="@+id/btn_login"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Login" />
<Button
android:id="@+id/btn_logout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:backgroundTint="#D22B2B"
android:text="Logout"
android:visibility="gone" />
</LinearLayout>
MainActivity.kt
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.util.Base64
import android.view.View
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.google.gson.Gson
import com.google.gson.annotations.SerializedName
import net.openid.appauth.*
class MainActivity : AppCompatActivity(), AuthorizationService.TokenResponseCallback {
private val authorizationEndpoint = "https://login.microsoftonline.com/5445bade-1c6c-45cf-b87a-f21276046af6/oauth2/v2.0/authorize"
private val tokenEndpoint = "https://login.microsoftonline.com/5445bade-1c6c-45cf-b87a-f21276046af6/oauth2/v2.0/token"
private val endSessionEndpoint = "https://login.microsoftonline.com/common/oauth2/v2.0/logout"
private val clientId = "041cb3c5-8096-454a-b3c2-c18bb783cde0"
private val clientSecret = ""
private val redirectUrl = "oauth.playground://callback"
private val scope = "openid"
private val authorizationServiceConfig = AuthorizationServiceConfiguration(
Uri.parse(authorizationEndpoint),
Uri.parse(tokenEndpoint),
null,
Uri.parse(endSessionEndpoint)
)
private lateinit var authService: AuthorizationService
private val authState = AuthState()
private val RC_LOGIN = 1
private val RC_LOGOUT = 2
private lateinit var textEmail: TextView
private lateinit var progress: View
private lateinit var btnLogin: View
private lateinit var btnLogout: View
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
textEmail = findViewById(R.id.text_email)
progress = findViewById(R.id.progress)
btnLogin = findViewById(R.id.btn_login)
btnLogout = findViewById(R.id.btn_logout)
btnLogin.setOnClickListener { login() }
btnLogout.setOnClickListener { logout() }
authService = AuthorizationService(this)
}
private fun login() {
val authRequest = AuthorizationRequest.Builder(authorizationServiceConfig, clientId, ResponseTypeValues.CODE, Uri.parse(redirectUrl))
.setScope(scope)
.setPrompt(AuthorizationRequest.Prompt.LOGIN)
.build()
try {
val authIntent: Intent = authService.getAuthorizationRequestIntent(authRequest)
startActivityForResult(authIntent, RC_LOGIN)
} catch (e: ActivityNotFoundException) {
Toast.makeText(this, "No suitable browser is available to perform the authorization flow", Toast.LENGTH_SHORT).show()
}
}
private fun logout() {
val endSessionRequest = EndSessionRequest.Builder(authState.authorizationServiceConfiguration!!)
.setIdTokenHint(authState.idToken)
.setPostLogoutRedirectUri(Uri.parse(redirectUrl))
.build()
val endSessionIntent: Intent = authService.getEndSessionRequestIntent(endSessionRequest)
startActivityForResult(endSessionIntent, RC_LOGOUT)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
RC_LOGIN -> {
if (data != null) {
val response = AuthorizationResponse.fromIntent(data)
val exception = AuthorizationException.fromIntent(data)
if (response != null) {
authState.update(response, exception)
progress.visibility = View.VISIBLE
authService.performTokenRequest(response.createTokenExchangeRequest(), ClientSecretPost(clientSecret), this)
}
}
}
RC_LOGOUT -> {
if (resultCode == Activity.RESULT_OK) {
textEmail.text = null
btnLogin.visibility = View.VISIBLE
btnLogout.visibility = View.GONE
Toast.makeText(this, "Logout successful", Toast.LENGTH_SHORT).show()
}
}
}
}
override fun onTokenRequestCompleted(response: TokenResponse?, exception: AuthorizationException?) {
progress.visibility = View.GONE
if (response != null) {
authState.update(response, exception)
val idToken = parseJwtToken(authState.accessToken!!)
textEmail.text = idToken.uniqueName
btnLogin.visibility = View.GONE
btnLogout.visibility = View.VISIBLE
}
}
private fun parseJwtToken(jwtIdToken: String): JwtPaylod {
val payload = jwtIdToken.split('.')[1]
val payloadJson = String(Base64.decode(payload, Base64.NO_WRAP))
return Gson().fromJson(payloadJson, JwtPaylod::class.java)
}
private data class JwtPaylod(
@SerializedName("unique_name") val uniqueName: String?
)
}