diff --git a/app/src/main/java/com/shielddagger/auth/oidc_debugger/MainActivity.kt b/app/src/main/java/com/shielddagger/auth/oidc_debugger/MainActivity.kt index 88416ee..2791220 100644 --- a/app/src/main/java/com/shielddagger/auth/oidc_debugger/MainActivity.kt +++ b/app/src/main/java/com/shielddagger/auth/oidc_debugger/MainActivity.kt @@ -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(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() + 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") } } diff --git a/app/src/main/java/com/shielddagger/auth/oidc_debugger/ValidateActivity.kt b/app/src/main/java/com/shielddagger/auth/oidc_debugger/ValidateActivity.kt index 8a965de..ba18572 100644 --- a/app/src/main/java/com/shielddagger/auth/oidc_debugger/ValidateActivity.kt +++ b/app/src/main/java/com/shielddagger/auth/oidc_debugger/ValidateActivity.kt @@ -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) diff --git a/app/src/main/java/com/shielddagger/auth/oidc_debugger/oidc/OIDCCore.kt b/app/src/main/java/com/shielddagger/auth/oidc_debugger/oidc/OIDCCore.kt index 81175f8..7787a0c 100644 --- a/app/src/main/java/com/shielddagger/auth/oidc_debugger/oidc/OIDCCore.kt +++ b/app/src/main/java/com/shielddagger/auth/oidc_debugger/oidc/OIDCCore.kt @@ -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? = null ) +private fun parseQueryString(query:String): Map> { + 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, + val responseType: List, val scope: List, 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{ - 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(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 = 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() 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