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.activity.enableEdgeToEdge
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.Checkbox
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextField
|
import androidx.compose.material3.TextField
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@ -71,7 +71,7 @@ fun IssuerForm(modifier: Modifier = Modifier) {
|
|||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
var client: OIDCCore? = null;
|
var client: OIDCCore? = null;
|
||||||
|
|
||||||
if (context.fileList().contains("issuerConfig")) {
|
if (context.fileList()?.contains("issuerConfig") == true) {
|
||||||
val ifo = context.openFileInput("issuerConfig")
|
val ifo = context.openFileInput("issuerConfig")
|
||||||
try {
|
try {
|
||||||
client = Json.decodeFromStream<OIDCCore>(ifo);
|
client = Json.decodeFromStream<OIDCCore>(ifo);
|
||||||
@ -89,6 +89,10 @@ fun IssuerForm(modifier: Modifier = Modifier) {
|
|||||||
var clientSecret by remember { mutableStateOf(client?.clientSecret ?: "") }
|
var clientSecret by remember { mutableStateOf(client?.clientSecret ?: "") }
|
||||||
var scopes by remember { mutableStateOf(client?.scope ?: listOf("")) }
|
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
|
Column (modifier = modifier
|
||||||
.padding(10.dp)
|
.padding(10.dp)
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
@ -111,10 +115,40 @@ fun IssuerForm(modifier: Modifier = Modifier) {
|
|||||||
modifier = Modifier.fillMaxWidth())
|
modifier = Modifier.fillMaxWidth())
|
||||||
TextField(scopes.joinToString(" "), { scopes = it.split(" ")},
|
TextField(scopes.joinToString(" "), { scopes = it.split(" ")},
|
||||||
label = { Text("Scopes")},
|
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({
|
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(
|
val oidcClient = OIDCCore(
|
||||||
listOf(OIDCResponseType.CODE),
|
responseTypes,
|
||||||
scopes,
|
scopes,
|
||||||
clientId,
|
clientId,
|
||||||
context.resources.getString(R.string.redirect_uri),
|
context.resources.getString(R.string.redirect_uri),
|
||||||
@ -136,7 +170,7 @@ fun IssuerForm(modifier: Modifier = Modifier) {
|
|||||||
|
|
||||||
val intent = Intent(Intent.ACTION_VIEW, authUri)
|
val intent = Intent(Intent.ACTION_VIEW, authUri)
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
}) {
|
}, enabled = authCodeResponse || tokenResponse || idTokenResponse) {
|
||||||
Text("Authorize")
|
Text("Authorize")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package com.shielddagger.auth.oidc_debugger
|
package com.shielddagger.auth.oidc_debugger
|
||||||
|
|
||||||
|
import android.media.session.MediaSession.Token
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@ -17,11 +18,9 @@ import androidx.compose.material3.CardDefaults
|
|||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.ElevatedCard
|
import androidx.compose.material3.ElevatedCard
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@ -148,7 +147,7 @@ fun Analysis(data: Uri?, modifier: Modifier = Modifier) {
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(32.dp, 0.dp, 0.dp),
|
.padding(32.dp, 0.dp, 0.dp),
|
||||||
color = if (it.success) Color(context.resources.getColor(R.color.success))
|
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)
|
tokenData.value = TokenResponse(OIDCTokenErrorResponse.INVALID_REQUEST)
|
||||||
tokenExchange.value = false
|
tokenExchange.value = false
|
||||||
|
userinfoError.value = "Token Exchange Failed"
|
||||||
userinfoExchange.value = false
|
userinfoExchange.value = false
|
||||||
})
|
})
|
||||||
queue.add(tokenRequest)
|
queue.add(tokenRequest)
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true)
|
||||||
|
@ -6,6 +6,7 @@ import com.android.volley.Request
|
|||||||
import com.android.volley.Response
|
import com.android.volley.Response
|
||||||
import com.android.volley.toolbox.JsonObjectRequest
|
import com.android.volley.toolbox.JsonObjectRequest
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
import java.net.URLDecoder
|
||||||
import java.util.ArrayList
|
import java.util.ArrayList
|
||||||
import kotlin.io.encoding.Base64
|
import kotlin.io.encoding.Base64
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
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),
|
INVALID_REQUEST_OBJECT("invalid_request_object", "Invalid Request Object", false),
|
||||||
REQUEST_NOT_SUPPORTED("request_not_supported", "Request Not Supported", false),
|
REQUEST_NOT_SUPPORTED("request_not_supported", "Request Not Supported", false),
|
||||||
REQUEST_URI_NOT_SUPPORTED("request_uri_not_supported", "Request URI 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 {
|
override fun toString(): String {
|
||||||
return this.type
|
return this.type
|
||||||
@ -117,9 +119,26 @@ data class TokenResponse(
|
|||||||
val scope: List<String>? = null
|
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
|
@kotlinx.serialization.Serializable
|
||||||
class OIDCCore(
|
class OIDCCore(
|
||||||
private val responseType: List<OIDCResponseType>,
|
val responseType: List<OIDCResponseType>,
|
||||||
val scope: List<String>,
|
val scope: List<String>,
|
||||||
val clientId: String,
|
val clientId: String,
|
||||||
val redirectUri: String,
|
val redirectUri: String,
|
||||||
@ -127,7 +146,7 @@ class OIDCCore(
|
|||||||
val tokenUri: String,
|
val tokenUri: String,
|
||||||
val userinfoUri: String,
|
val userinfoUri: String,
|
||||||
val clientSecret: String = "",
|
val clientSecret: String = "",
|
||||||
private val clientAuth: ClientAuthType = ClientAuthType.BASIC
|
private val clientAuth: ClientAuthType = ClientAuthType.POST
|
||||||
) {
|
) {
|
||||||
private var nonce:String = ""
|
private var nonce:String = ""
|
||||||
private var state:String = ""
|
private var state:String = ""
|
||||||
@ -152,18 +171,31 @@ class OIDCCore(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun validateAuthResponse(returnUri:Uri): List<OIDCAuthState>{
|
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)
|
return listOf(OIDCAuthState.INVALID_STATE)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (returnUri.getQueryParameter("error") != null){
|
if (params["error"]?.isNotEmpty() == true){
|
||||||
return listOf(OIDCAuthState.fromString(returnUri.getQueryParameter("error")!!))
|
return listOf(OIDCAuthState.fromString(returnUri.getQueryParameter("error")!!))
|
||||||
}
|
}
|
||||||
|
|
||||||
val stateList = ArrayList<OIDCAuthState>(3)
|
val stateList = ArrayList<OIDCAuthState>(3)
|
||||||
|
|
||||||
if (responseType.contains(OIDCResponseType.CODE)) {
|
if (responseType.contains(OIDCResponseType.CODE)) {
|
||||||
if (returnUri.getQueryParameter("code") != null){
|
if (params["code"]?.get(0) != null){
|
||||||
stateList.add(OIDCAuthState.CODE_OK)
|
stateList.add(OIDCAuthState.CODE_OK)
|
||||||
} else {
|
} else {
|
||||||
stateList.add(OIDCAuthState.CODE_FAIL)
|
stateList.add(OIDCAuthState.CODE_FAIL)
|
||||||
@ -171,7 +203,7 @@ class OIDCCore(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (responseType.contains(OIDCResponseType.TOKEN)) {
|
if (responseType.contains(OIDCResponseType.TOKEN)) {
|
||||||
if (returnUri.getQueryParameter("token") != null){
|
if (params["token"]?.get(0) != null){
|
||||||
stateList.add(OIDCAuthState.TOKEN_OK)
|
stateList.add(OIDCAuthState.TOKEN_OK)
|
||||||
} else {
|
} else {
|
||||||
stateList.add(OIDCAuthState.TOKEN_FAIL)
|
stateList.add(OIDCAuthState.TOKEN_FAIL)
|
||||||
@ -179,7 +211,7 @@ class OIDCCore(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (responseType.contains(OIDCResponseType.ID_TOKEN)) {
|
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)
|
stateList.add(OIDCAuthState.ID_TOKEN_OK)
|
||||||
} else {
|
} else {
|
||||||
stateList.add(OIDCAuthState.ID_TOKEN_FAIL)
|
stateList.add(OIDCAuthState.ID_TOKEN_FAIL)
|
||||||
@ -192,16 +224,13 @@ class OIDCCore(
|
|||||||
fun getTokenFromCode(returnUri: Uri,
|
fun getTokenFromCode(returnUri: Uri,
|
||||||
responseHandler: Response.Listener<JSONObject> = Response.Listener {},
|
responseHandler: Response.Listener<JSONObject> = Response.Listener {},
|
||||||
errorHandler: Response.ErrorListener = Response.ErrorListener {}): JsonFormRequest {
|
errorHandler: Response.ErrorListener = Response.ErrorListener {}): JsonFormRequest {
|
||||||
if (!responseType.contains(OIDCResponseType.CODE)){
|
val authData = returnUri.query ?: returnUri.fragment
|
||||||
throw RuntimeException("Can't get token from code for a client not requesting a code response type")
|
|
||||||
}
|
val params = parseQueryString(authData ?: "")
|
||||||
if (returnUri.getQueryParameter("code") == null){
|
|
||||||
throw RuntimeException("Getting token from code impossible - no code in return URL")
|
|
||||||
}
|
|
||||||
|
|
||||||
val data = mutableMapOf<String,String>()
|
val data = mutableMapOf<String,String>()
|
||||||
data["grant_type"] = "authorization_code"
|
data["grant_type"] = "authorization_code"
|
||||||
data["code"] = returnUri.getQueryParameter("code")!!
|
data["code"] = params["code"]?.get(0) ?: ""
|
||||||
data["client_id"] = clientId
|
data["client_id"] = clientId
|
||||||
data["client_secret"] = clientSecret
|
data["client_secret"] = clientSecret
|
||||||
data["redirect_uri"] = redirectUri
|
data["redirect_uri"] = redirectUri
|
||||||
|
Loading…
x
Reference in New Issue
Block a user