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:
parent
b23dc252b9
commit
d08c9ac36e
@ -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")
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user