Add options for requesting Token and ID Token responses

Fix handling in OIDCCore for cases where token/id_token responses are requested and response data is encoded in fragment instead of query string.
This commit is contained in:
Radek Goláň jr. 2025-01-30 09:31:42 +01:00
parent b23dc252b9
commit d08c9ac36e
Signed by: shield
GPG Key ID: D86423BFC31F3591
3 changed files with 88 additions and 24 deletions

View File

@ -9,17 +9,17 @@ import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Checkbox
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@ -71,7 +71,7 @@ fun IssuerForm(modifier: Modifier = Modifier) {
val context = LocalContext.current
var client: OIDCCore? = null;
if (context.fileList().contains("issuerConfig")) {
if (context.fileList()?.contains("issuerConfig") == true) {
val ifo = context.openFileInput("issuerConfig")
try {
client = Json.decodeFromStream<OIDCCore>(ifo);
@ -89,6 +89,10 @@ fun IssuerForm(modifier: Modifier = Modifier) {
var clientSecret by remember { mutableStateOf(client?.clientSecret ?: "") }
var scopes by remember { mutableStateOf(client?.scope ?: listOf("")) }
var authCodeResponse by remember { mutableStateOf(client?.responseType?.contains(OIDCResponseType.CODE) ?: true) }
var tokenResponse by remember { mutableStateOf(client?.responseType?.contains(OIDCResponseType.TOKEN) ?: false) }
var idTokenResponse by remember { mutableStateOf(client?.responseType?.contains(OIDCResponseType.ID_TOKEN) ?: false) }
Column (modifier = modifier
.padding(10.dp)
.fillMaxWidth(),
@ -111,10 +115,40 @@ fun IssuerForm(modifier: Modifier = Modifier) {
modifier = Modifier.fillMaxWidth())
TextField(scopes.joinToString(" "), { scopes = it.split(" ")},
label = { Text("Scopes")},
modifier = Modifier.fillMaxWidth())
modifier = Modifier.fillMaxWidth()
)
Row(
Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(authCodeResponse, {authCodeResponse = it})
Text("Authorization Code")
}
Row(
Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(tokenResponse, {tokenResponse = it})
Text("Token")
}
Row(
Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(idTokenResponse, {idTokenResponse = it})
Text("ID Token")
}
Button({
val responseTypes = mutableListOf<OIDCResponseType>()
if (authCodeResponse)
responseTypes.add(OIDCResponseType.CODE)
if (tokenResponse)
responseTypes.add(OIDCResponseType.TOKEN)
if (idTokenResponse)
responseTypes.add(OIDCResponseType.ID_TOKEN)
val oidcClient = OIDCCore(
listOf(OIDCResponseType.CODE),
responseTypes,
scopes,
clientId,
context.resources.getString(R.string.redirect_uri),
@ -136,7 +170,7 @@ fun IssuerForm(modifier: Modifier = Modifier) {
val intent = Intent(Intent.ACTION_VIEW, authUri)
context.startActivity(intent)
}) {
}, enabled = authCodeResponse || tokenResponse || idTokenResponse) {
Text("Authorize")
}
}

View File

@ -1,5 +1,6 @@
package com.shielddagger.auth.oidc_debugger
import android.media.session.MediaSession.Token
import android.net.Uri
import android.os.Bundle
import android.util.Log
@ -17,11 +18,9 @@ import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@ -148,7 +147,7 @@ fun Analysis(data: Uri?, modifier: Modifier = Modifier) {
modifier = Modifier
.padding(32.dp, 0.dp, 0.dp),
color = if (it.success) Color(context.resources.getColor(R.color.success))
else Color(context.resources.getColor(R.color.failure))
else Color.Red
)
}
@ -293,10 +292,12 @@ fun Analysis(data: Uri?, modifier: Modifier = Modifier) {
}, {
tokenData.value = TokenResponse(OIDCTokenErrorResponse.INVALID_REQUEST)
tokenExchange.value = false
userinfoError.value = "Token Exchange Failed"
userinfoExchange.value = false
})
queue.add(tokenRequest)
}
@Preview(showBackground = true)

View File

@ -6,6 +6,7 @@ import com.android.volley.Request
import com.android.volley.Response
import com.android.volley.toolbox.JsonObjectRequest
import org.json.JSONObject
import java.net.URLDecoder
import java.util.ArrayList
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
@ -55,7 +56,8 @@ enum class OIDCAuthState(private val type: String, val message: String, val succ
INVALID_REQUEST_OBJECT("invalid_request_object", "Invalid Request Object", false),
REQUEST_NOT_SUPPORTED("request_not_supported", "Request Not Supported", false),
REQUEST_URI_NOT_SUPPORTED("request_uri_not_supported", "Request URI Not Supported", false),
REGISTRATION_NOT_SUPPORTED("registration_not_supported", "Registration Not Supported", false);
REGISTRATION_NOT_SUPPORTED("registration_not_supported", "Registration Not Supported", false),
BAD_RESPONSE("bad_response", "Bad Response from IdP", false);
override fun toString(): String {
return this.type
@ -117,9 +119,26 @@ data class TokenResponse(
val scope: List<String>? = null
)
private fun parseQueryString(query:String): Map<String, List<String>> {
return query.split("&")
.mapNotNull { kvItem ->
val kvList = kvItem.split("=")
kvList.takeIf { it.size == 2 }
}
.mapNotNull { kvList ->
val (k, v) = kvList
(k to URLDecoder.decode(v.trim(), "UTF-8")).takeIf { v.isNotBlank() }
}
.groupBy { (key, _) -> key }
.map { mapEntry ->
mapEntry.key to mapEntry.value.map { it.second }
}
.toMap()
}
@kotlinx.serialization.Serializable
class OIDCCore(
private val responseType: List<OIDCResponseType>,
val responseType: List<OIDCResponseType>,
val scope: List<String>,
val clientId: String,
val redirectUri: String,
@ -127,7 +146,7 @@ class OIDCCore(
val tokenUri: String,
val userinfoUri: String,
val clientSecret: String = "",
private val clientAuth: ClientAuthType = ClientAuthType.BASIC
private val clientAuth: ClientAuthType = ClientAuthType.POST
) {
private var nonce:String = ""
private var state:String = ""
@ -152,18 +171,31 @@ class OIDCCore(
}
fun validateAuthResponse(returnUri:Uri): List<OIDCAuthState>{
if (returnUri.getQueryParameter("state") != state){
Log.d("oidccore", "validateAuthResponse: query: ${returnUri.query}")
Log.d("oidccore", "validateAuthResponse: fragment: ${returnUri.fragment}")
val data = returnUri.encodedQuery ?: returnUri.encodedFragment
if (data == null){
return listOf(OIDCAuthState.BAD_RESPONSE)
}
val params = parseQueryString(data)
Log.d("oidccore", "validateAuthResponse: params: $params")
if (params["state"]!![0] != state){
Log.e("oidccore", "validateAuthResponse: expected: $state")
Log.e("oidccore", "validateAuthResponse: received: ${params["state"]!![0]}")
return listOf(OIDCAuthState.INVALID_STATE)
}
if (returnUri.getQueryParameter("error") != null){
if (params["error"]?.isNotEmpty() == true){
return listOf(OIDCAuthState.fromString(returnUri.getQueryParameter("error")!!))
}
val stateList = ArrayList<OIDCAuthState>(3)
if (responseType.contains(OIDCResponseType.CODE)) {
if (returnUri.getQueryParameter("code") != null){
if (params["code"]?.get(0) != null){
stateList.add(OIDCAuthState.CODE_OK)
} else {
stateList.add(OIDCAuthState.CODE_FAIL)
@ -171,7 +203,7 @@ class OIDCCore(
}
if (responseType.contains(OIDCResponseType.TOKEN)) {
if (returnUri.getQueryParameter("token") != null){
if (params["token"]?.get(0) != null){
stateList.add(OIDCAuthState.TOKEN_OK)
} else {
stateList.add(OIDCAuthState.TOKEN_FAIL)
@ -179,7 +211,7 @@ class OIDCCore(
}
if (responseType.contains(OIDCResponseType.ID_TOKEN)) {
if (returnUri.getQueryParameter("id_token") != null){
if (params["id_token"]?.get(0) != null){
stateList.add(OIDCAuthState.ID_TOKEN_OK)
} else {
stateList.add(OIDCAuthState.ID_TOKEN_FAIL)
@ -192,16 +224,13 @@ class OIDCCore(
fun getTokenFromCode(returnUri: Uri,
responseHandler: Response.Listener<JSONObject> = Response.Listener {},
errorHandler: Response.ErrorListener = Response.ErrorListener {}): JsonFormRequest {
if (!responseType.contains(OIDCResponseType.CODE)){
throw RuntimeException("Can't get token from code for a client not requesting a code response type")
}
if (returnUri.getQueryParameter("code") == null){
throw RuntimeException("Getting token from code impossible - no code in return URL")
}
val authData = returnUri.query ?: returnUri.fragment
val params = parseQueryString(authData ?: "")
val data = mutableMapOf<String,String>()
data["grant_type"] = "authorization_code"
data["code"] = returnUri.getQueryParameter("code")!!
data["code"] = params["code"]?.get(0) ?: ""
data["client_id"] = clientId
data["client_secret"] = clientSecret
data["redirect_uri"] = redirectUri