diff --git a/sdk/src/main/java/com/oursky/authgear/AuthenticateOptions.kt b/sdk/src/main/java/com/oursky/authgear/AuthenticateOptions.kt index 9ca5d4c9..1c76eb5f 100644 --- a/sdk/src/main/java/com/oursky/authgear/AuthenticateOptions.kt +++ b/sdk/src/main/java/com/oursky/authgear/AuthenticateOptions.kt @@ -76,7 +76,8 @@ data class AuthenticateOptions @JvmOverloads constructor( internal fun AuthenticateOptions.toRequest( isSsoEnabled: Boolean, - preAuthenticatedURLEnabled: Boolean + preAuthenticatedURLEnabled: Boolean, + dpopJKT: String? ): OidcAuthenticationRequest { return OidcAuthenticationRequest( redirectUri = this.redirectUri, @@ -93,6 +94,7 @@ internal fun AuthenticateOptions.toRequest( colorScheme = this.colorScheme, wechatRedirectURI = this.wechatRedirectURI, page = this.page, - authenticationFlowGroup = this.authenticationFlowGroup + authenticationFlowGroup = this.authenticationFlowGroup, + dpopJKT = dpopJKT ) } \ No newline at end of file diff --git a/sdk/src/main/java/com/oursky/authgear/Authgear.kt b/sdk/src/main/java/com/oursky/authgear/Authgear.kt index e3b50c59..caf2a0bc 100644 --- a/sdk/src/main/java/com/oursky/authgear/Authgear.kt +++ b/sdk/src/main/java/com/oursky/authgear/Authgear.kt @@ -15,6 +15,7 @@ import com.oursky.authgear.app2app.App2AppOptions import com.oursky.authgear.data.assetlink.AssetLinkRepoHttp import com.oursky.authgear.data.key.KeyRepoKeystore import com.oursky.authgear.data.oauth.OAuthRepoHttp +import com.oursky.authgear.dpop.DefaultDPoPProvider import kotlinx.coroutines.* import java.util.* @@ -38,6 +39,14 @@ constructor( internal val core: AuthgearCore init { + val name = name ?: "default" + val keyRepo = KeyRepoKeystore() + val sharedStorage = PersistentInterAppSharedStorage(application) + val dpopProvider = DefaultDPoPProvider( + namespace = name, + keyRepo = keyRepo, + sharedStorage = sharedStorage, + ) this.core = AuthgearCore( this, application, @@ -46,12 +55,13 @@ constructor( isSsoEnabled, preAuthenticatedURLEnabled, app2AppOptions, + dpopProvider, tokenStorage, uiImplementation, PersistentContainerStorage(application), - PersistentInterAppSharedStorage(application), - OAuthRepoHttp(), - KeyRepoKeystore(), + sharedStorage, + OAuthRepoHttp(dPoPProvider = dpopProvider), + keyRepo, AssetLinkRepoHttp(), name ) diff --git a/sdk/src/main/java/com/oursky/authgear/AuthgearCore.kt b/sdk/src/main/java/com/oursky/authgear/AuthgearCore.kt index 32d0b13a..81bc02f0 100644 --- a/sdk/src/main/java/com/oursky/authgear/AuthgearCore.kt +++ b/sdk/src/main/java/com/oursky/authgear/AuthgearCore.kt @@ -20,6 +20,7 @@ import com.oursky.authgear.app2app.App2AppOptions import com.oursky.authgear.data.assetlink.AssetLinkRepo import com.oursky.authgear.data.key.KeyRepo import com.oursky.authgear.data.oauth.OAuthRepo +import com.oursky.authgear.dpop.DPoPProvider import com.oursky.authgear.net.toQueryParameter import com.oursky.authgear.oauth.OidcAuthenticationRequest import com.oursky.authgear.oauth.OidcTokenRequest @@ -58,6 +59,7 @@ internal class AuthgearCore( private val isSsoEnabled: Boolean, private val preAuthenticatedURLEnabled: Boolean, private val app2AppOptions: App2AppOptions, + private val dPoPProvider: DPoPProvider, private val tokenStorage: TokenStorage, private val uiImplementation: UIImplementation, private val storage: ContainerStorage, @@ -65,7 +67,7 @@ internal class AuthgearCore( private val oauthRepo: OAuthRepo, private val keyRepo: KeyRepo, private val assetLinkRepo: AssetLinkRepo, - name: String? = null + private val name: String ) { companion object { @Suppress("unused") @@ -145,7 +147,6 @@ internal class AuthgearCore( val challenge: String ) - private val name = name ?: "default" private var isInitialized = false private var refreshToken: String? = null var accessToken: String? = null @@ -285,7 +286,10 @@ internal class AuthgearCore( verifier: Verifier = generateCodeVerifier() ): AuthenticationRequest { requireIsInitialized() - val request = options.toRequest(this.isSsoEnabled, this.preAuthenticatedURLEnabled) + val request = options.toRequest( + this.isSsoEnabled, + this.preAuthenticatedURLEnabled, + dpopJKT = dPoPProvider.computeJKT()) val authorizeUri = authorizeEndpoint(this.clientId, request, verifier) return AuthenticationRequest(authorizeUri, request.redirectUri, verifier) } diff --git a/sdk/src/main/java/com/oursky/authgear/InterAppSharedStorage.kt b/sdk/src/main/java/com/oursky/authgear/InterAppSharedStorage.kt index 357e56e0..3619b0bd 100644 --- a/sdk/src/main/java/com/oursky/authgear/InterAppSharedStorage.kt +++ b/sdk/src/main/java/com/oursky/authgear/InterAppSharedStorage.kt @@ -9,5 +9,9 @@ internal interface InterAppSharedStorage { fun getDeviceSecret(namespace: String): String? fun deleteDeviceSecret(namespace: String) + fun setDPoPKeyId(namespace: String, keyId: String) + fun getDPoPKeyId(namespace: String): String? + fun deleteDPoPKeyId(namespace: String) + fun onLogout(namespace: String) -} \ No newline at end of file +} diff --git a/sdk/src/main/java/com/oursky/authgear/JWK.kt b/sdk/src/main/java/com/oursky/authgear/JWK.kt index 75f6717e..09269390 100644 --- a/sdk/src/main/java/com/oursky/authgear/JWK.kt +++ b/sdk/src/main/java/com/oursky/authgear/JWK.kt @@ -1,10 +1,17 @@ package com.oursky.authgear +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive +import java.math.BigInteger +import java.security.MessageDigest import java.security.PublicKey import java.security.interfaces.RSAPublicKey +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi +import kotlin.math.ceil internal data class JWK( val kid: String, @@ -24,11 +31,44 @@ internal fun JWK.toJsonObject(): JsonObject { return JsonObject(m) } +@OptIn(ExperimentalEncodingApi::class) +internal fun JWK.toSHA256Thumbprint(): String { + val p = mutableMapOf() + when (kty) { + "RSA" -> { + // required members for an RSA public key are e, kty, n + // in lexicographic order + p["e"] = e + p["kty"] = kty + p["n"] = n + } + else -> { + throw NotImplementedError("unknown kty") + } + } + val jsonBytes = Json.encodeToString(p).toByteArray() + val digest: MessageDigest = MessageDigest.getInstance("SHA-256") + val hashBytes = digest.digest(jsonBytes) + + return Base64.UrlSafe.encode(hashBytes).removeSuffix("=") +} + +internal fun BigInteger.toUnsignedByteArray(): ByteArray { + // BigInteger always include a bit to represent the sign + // So the array length is ceil((this.bitLength() + 1)/8) + // This sign bit causes an extra byte to be added to the ByteArray when bitLength is just divisible by 8 + // We want to exclude that extra byte in some cases, such as sending the bytes in a JWK as Base64urlUInt + val expectedLength = ceil(this.bitLength() / 8.0).toInt() + val bytes = this.toByteArray() + val startIdx = bytes.size - expectedLength + return bytes.sliceArray(IntRange(startIdx, bytes.size - 1)) +} + internal fun publicKeyToJWK(kid: String, publicKey: PublicKey): JWK { val rsaPublicKey = publicKey as RSAPublicKey return JWK( kid = kid, - n = base64UrlEncode(rsaPublicKey.modulus.toByteArray()), - e = base64UrlEncode(rsaPublicKey.publicExponent.toByteArray()) + n = base64UrlEncode(rsaPublicKey.modulus.toUnsignedByteArray()), + e = base64UrlEncode(rsaPublicKey.publicExponent.toUnsignedByteArray()) ) } \ No newline at end of file diff --git a/sdk/src/main/java/com/oursky/authgear/JWT.kt b/sdk/src/main/java/com/oursky/authgear/JWT.kt index 448d4730..1ee5b97e 100644 --- a/sdk/src/main/java/com/oursky/authgear/JWT.kt +++ b/sdk/src/main/java/com/oursky/authgear/JWT.kt @@ -9,7 +9,8 @@ import java.time.Instant internal enum class JWTHeaderType(val value: String) { ANONYMOUS("vnd.authgear.anonymous-request"), BIOMETRIC("vnd.authgear.biometric-request"), - APP2APP("vnd.authgear.app2app-request") + APP2APP("vnd.authgear.app2app-request"), + DPOPJWT("dpop+jwt") } internal data class JWTHeader( @@ -33,9 +34,12 @@ internal fun JWTHeader.toJsonObject(): JsonObject { internal data class JWTPayload( val iat: Long, val exp: Long, - val challenge: String, - val action: String, - val deviceInfo: DeviceInfoRoot? + val jti: String? = null, + val htm: String? = null, + val htu: String? = null, + val challenge: String? = null, + val action: String? = null, + val deviceInfo: DeviceInfoRoot? = null ) { constructor(now: Instant, challenge: String, action: String, deviceInfo: DeviceInfoRoot? = null) : this( iat = now.epochSecond, @@ -44,17 +48,38 @@ internal data class JWTPayload( action = action, deviceInfo = deviceInfo ) + + constructor(now: Instant, jti: String, htu: String, htm: String) : this( + iat = now.epochSecond, + exp = now.epochSecond + 60, + jti = jti, + htu = htu, + htm = htm + ) } internal fun JWTPayload.toJsonObject(): JsonObject { val m = mutableMapOf() m["iat"] = JsonPrimitive(iat) m["exp"] = JsonPrimitive(exp) - m["challenge"] = JsonPrimitive(challenge) - m["action"] = JsonPrimitive(action) - if (deviceInfo != null) { + challenge?.let { + m["challenge"] = JsonPrimitive(challenge) + } + action?.let { + m["action"] = JsonPrimitive(action) + } + deviceInfo?.let { m["device_info"] = deviceInfo.toJsonObject() } + jti?.let { + m["jti"] = JsonPrimitive(jti) + } + htu?.let { + m["htu"] = JsonPrimitive(htu) + } + htm?.let { + m["htm"] = JsonPrimitive(htm) + } return JsonObject(m) } diff --git a/sdk/src/main/java/com/oursky/authgear/PersistentInterAppSharedStorage.kt b/sdk/src/main/java/com/oursky/authgear/PersistentInterAppSharedStorage.kt index ec40c27f..f5b7c7c1 100644 --- a/sdk/src/main/java/com/oursky/authgear/PersistentInterAppSharedStorage.kt +++ b/sdk/src/main/java/com/oursky/authgear/PersistentInterAppSharedStorage.kt @@ -16,6 +16,7 @@ internal class PersistentInterAppSharedStorage(val context: Context) : InterAppS private const val LOGTAG = "Authgear" private const val IDToken = "idToken" private const val DeviceSecret = "deviceSecret" + private const val DPoPKeyID = "dpopKeyId" } private val masterKey = MasterKey.Builder(context) @@ -84,9 +85,22 @@ internal class PersistentInterAppSharedStorage(val context: Context) : InterAppS deleteItem(namespace, DeviceSecret) } + override fun setDPoPKeyId(namespace: String, keyId: String) { + setItem(namespace, DPoPKeyID, keyId) + } + + override fun getDPoPKeyId(namespace: String): String? { + return getItem(namespace, DPoPKeyID) + } + + override fun deleteDPoPKeyId(namespace: String) { + deleteItem(namespace, DPoPKeyID) + } + override fun onLogout(namespace: String) { deleteDeviceSecret(namespace) deleteIDToken(namespace) + deleteDPoPKeyId(namespace) } private fun handleBackupProblem(e: Exception, namespace: String): Boolean { @@ -139,4 +153,4 @@ internal class PersistentInterAppSharedStorage(val context: Context) : InterAppS EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM ) } -} \ No newline at end of file +} diff --git a/sdk/src/main/java/com/oursky/authgear/data/key/KeyRepo.kt b/sdk/src/main/java/com/oursky/authgear/data/key/KeyRepo.kt index 47ae9cb1..ae319713 100644 --- a/sdk/src/main/java/com/oursky/authgear/data/key/KeyRepo.kt +++ b/sdk/src/main/java/com/oursky/authgear/data/key/KeyRepo.kt @@ -8,4 +8,7 @@ internal interface KeyRepo { fun generateApp2AppDeviceKey(kid: String): KeyPair fun getApp2AppDeviceKey(kid: String): KeyPair? + + fun generateDPoPKey(kid: String): KeyPair + fun getDPoPKey(kid: String): KeyPair? } diff --git a/sdk/src/main/java/com/oursky/authgear/data/key/KeyRepoKeystore.kt b/sdk/src/main/java/com/oursky/authgear/data/key/KeyRepoKeystore.kt index d4d472de..5332ab7c 100644 --- a/sdk/src/main/java/com/oursky/authgear/data/key/KeyRepoKeystore.kt +++ b/sdk/src/main/java/com/oursky/authgear/data/key/KeyRepoKeystore.kt @@ -77,4 +77,40 @@ internal class KeyRepoKeystore : KeyRepo { return KeyPair(entry.certificate.publicKey, entry.privateKey) } + + @RequiresApi(api = Build.VERSION_CODES.M) + override fun generateDPoPKey(kid: String): KeyPair { + val alias = formatApp2AppDeviceKeyAlias(kid) + val builder = KeyGenParameterSpec.Builder( + alias, + KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY + ) + .setDigests(KeyProperties.DIGEST_SHA256) + .setSignaturePaddings(KeyProperties.SIGNATURE_PADDING_RSA_PKCS1) + .setUserAuthenticationRequired(false) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + builder.setInvalidatedByBiometricEnrollment(false) + } + val spec = builder.build() + val kpg = + KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, "AndroidKeyStore") + kpg.initialize(spec) + return kpg.generateKeyPair() + } + + @RequiresApi(api = Build.VERSION_CODES.M) + override fun getDPoPKey(kid: String): KeyPair? { + val alias = formatApp2AppDeviceKeyAlias(kid) + + val ks = KeyStore.getInstance("AndroidKeyStore") + ks.load(null) + + val entry = ks.getEntry(alias, null) + if (entry !is KeyStore.PrivateKeyEntry) { + return null + } + + return KeyPair(entry.certificate.publicKey, entry.privateKey) + } } diff --git a/sdk/src/main/java/com/oursky/authgear/data/oauth/OAuthRepoHttp.kt b/sdk/src/main/java/com/oursky/authgear/data/oauth/OAuthRepoHttp.kt index 8dee1a0c..ef213b28 100644 --- a/sdk/src/main/java/com/oursky/authgear/data/oauth/OAuthRepoHttp.kt +++ b/sdk/src/main/java/com/oursky/authgear/data/oauth/OAuthRepoHttp.kt @@ -5,15 +5,18 @@ import com.oursky.authgear.AuthgearException import com.oursky.authgear.GrantType import com.oursky.authgear.UserInfo import com.oursky.authgear.data.HttpClient +import com.oursky.authgear.dpop.DPoPProvider import com.oursky.authgear.getOrigin import com.oursky.authgear.net.toFormData import com.oursky.authgear.oauth.* -import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString +import java.net.HttpURLConnection import java.net.URL import java.nio.charset.StandardCharsets -internal class OAuthRepoHttp : OAuthRepo { +internal class OAuthRepoHttp( + private val dPoPProvider: DPoPProvider +) : OAuthRepo { companion object { @Suppress("unused") private val TAG = OAuthRepoHttp::class.java.simpleName @@ -36,7 +39,7 @@ internal class OAuthRepoHttp : OAuthRepo { val configAfterAcquire = this.config if (configAfterAcquire != null) return configAfterAcquire val url = URL(URL(endpoint), "/.well-known/openid-configuration") - val newConfig: OidcConfiguration = HttpClient.fetch(url = url, method = "GET", headers = emptyMap()) { conn -> + val newConfig: OidcConfiguration = fetchWithDPoP(url = url, method = "GET", headers = emptyMap()) { conn -> conn.errorStream?.use { val responseString = String(it.readBytes(), StandardCharsets.UTF_8) HttpClient.throwErrorIfNeeded(conn, responseString) @@ -80,9 +83,18 @@ internal class OAuthRepoHttp : OAuthRepo { request.accessToken?.let { headers["authorization"] = "Bearer $it" } - return HttpClient.fetch( - url = URL(config.tokenEndpoint), - method = "POST", + val url = URL(config.tokenEndpoint) + val method = "POST" + val dpopProof = dPoPProvider.generateDPoPProof( + htm = method, + htu = url.toString() + ) + dpopProof?.let { + headers["DPoP"] = dpopProof + } + return fetchWithDPoP( + url = url, + method = method, headers = headers ) { conn -> conn.outputStream.use { @@ -106,7 +118,7 @@ internal class OAuthRepoHttp : OAuthRepo { body["client_id"] = clientId body["grant_type"] = GrantType.BIOMETRIC.raw body["jwt"] = jwt - return HttpClient.fetch( + return fetchWithDPoP( url = URL(config.tokenEndpoint), method = "POST", headers = mutableMapOf( @@ -132,7 +144,7 @@ internal class OAuthRepoHttp : OAuthRepo { val config = getOidcConfiguration() val body = mutableMapOf() body["token"] = refreshToken - HttpClient.fetch( + fetchWithDPoP( url = URL(config.revocationEndpoint), method = "POST", headers = mutableMapOf( @@ -155,7 +167,7 @@ internal class OAuthRepoHttp : OAuthRepo { override fun oidcUserInfoRequest(accessToken: String): UserInfo { val config = getOidcConfiguration() - return HttpClient.fetch( + return fetchWithDPoP( url = URL(config.userInfoEndpoint), method = "GET", headers = mutableMapOf( @@ -177,7 +189,7 @@ internal class OAuthRepoHttp : OAuthRepo { override fun oauthChallenge(purpose: String): ChallengeResponse { val body = mutableMapOf() body["purpose"] = purpose - val response: ChallengeResponseResult = HttpClient.fetch( + val response: ChallengeResponseResult = fetchWithDPoP( url = buildApiUrl("/oauth2/challenge"), method = "POST", headers = mutableMapOf( @@ -203,7 +215,7 @@ internal class OAuthRepoHttp : OAuthRepo { override fun oauthAppSessionToken(refreshToken: String): AppSessionTokenResponse { val body = mutableMapOf() body["refresh_token"] = refreshToken - val response: AppSessionTokenResponseResult = HttpClient.fetch( + val response: AppSessionTokenResponseResult = fetchWithDPoP( url = buildApiUrl("/oauth2/app_session_token"), method = "POST", headers = mutableMapOf( @@ -231,7 +243,7 @@ internal class OAuthRepoHttp : OAuthRepo { body["code"] = code body["state"] = state body["x_platform"] = "android" - HttpClient.fetch( + fetchWithDPoP( url = buildApiUrl("/sso/wechat/callback"), method = "POST", headers = mutableMapOf( @@ -252,6 +264,30 @@ internal class OAuthRepoHttp : OAuthRepo { } } + private fun fetchWithDPoP( + url: URL, + method: String, + headers: Map, + followRedirect: Boolean = true, + callback: (conn: HttpURLConnection) -> T + ): T { + val h = headers.toMutableMap() + val dpopProof = dPoPProvider.generateDPoPProof( + htm = method, + htu = url.toString() + ) + dpopProof?.let { + h["DPoP"] = dpopProof + } + return HttpClient.fetch( + url = url, + method = method, + headers = h, + followRedirect = followRedirect, + callback = callback + ) + } + private fun buildApiUrl(path: String): URL { val config = getOidcConfiguration() val builder = Uri.parse(config.authorizationEndpoint).getOrigin()?.let { diff --git a/sdk/src/main/java/com/oursky/authgear/dpop/DPoPProvider.kt b/sdk/src/main/java/com/oursky/authgear/dpop/DPoPProvider.kt new file mode 100644 index 00000000..3e3b631b --- /dev/null +++ b/sdk/src/main/java/com/oursky/authgear/dpop/DPoPProvider.kt @@ -0,0 +1,6 @@ +package com.oursky.authgear.dpop + +internal interface DPoPProvider { + fun generateDPoPProof(htm: String, htu: String): String? + fun computeJKT(): String? +} \ No newline at end of file diff --git a/sdk/src/main/java/com/oursky/authgear/dpop/DefaultDPoPProvider.kt b/sdk/src/main/java/com/oursky/authgear/dpop/DefaultDPoPProvider.kt new file mode 100644 index 00000000..553d00f7 --- /dev/null +++ b/sdk/src/main/java/com/oursky/authgear/dpop/DefaultDPoPProvider.kt @@ -0,0 +1,70 @@ +package com.oursky.authgear.dpop + +import android.os.Build +import androidx.annotation.RequiresApi +import com.oursky.authgear.InterAppSharedStorage +import com.oursky.authgear.JWTHeader +import com.oursky.authgear.JWTHeaderType +import com.oursky.authgear.JWTPayload +import com.oursky.authgear.data.key.KeyRepo +import com.oursky.authgear.publicKeyToJWK +import com.oursky.authgear.signJWT +import com.oursky.authgear.toSHA256Thumbprint +import java.security.KeyPair +import java.security.Signature +import java.time.Instant +import java.util.UUID + +internal class DefaultDPoPProvider( + private val namespace: String, + private val sharedStorage: InterAppSharedStorage, + private val keyRepo: KeyRepo +) : DPoPProvider { + + override fun generateDPoPProof(htm: String, htu: String): String? { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + return null + } + val (kid, keypair) = getOrCreateDPoPPrivateKey() + val jwk = publicKeyToJWK(kid, keypair.public) + val header = JWTHeader( + typ = JWTHeaderType.DPOPJWT, + kid = kid, + alg = jwk.alg, + jwk = jwk + ) + val payload = JWTPayload( + now = Instant.now(), + jti = UUID.randomUUID().toString(), + htm = htm, + htu = htu + ) + val signature = Signature.getInstance("SHA256withRSA") + signature.initSign(keypair.private) + return signJWT(signature, header, payload) + } + + override fun computeJKT(): String? { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + return null + } + val (kid, keypair) = getOrCreateDPoPPrivateKey() + val jwk = publicKeyToJWK(kid, keypair.public) + return jwk.toSHA256Thumbprint() + } + + @RequiresApi(api = Build.VERSION_CODES.M) + private fun getOrCreateDPoPPrivateKey(): Pair { + val existingKeyId = sharedStorage.getDPoPKeyId(namespace) + existingKeyId?.let {kid -> + val existingKey = keyRepo.getDPoPKey(kid) + if (existingKey != null) { + return Pair(kid, existingKey) + } + } + val newKeyId = UUID.randomUUID().toString() + val newKey = keyRepo.generateDPoPKey(newKeyId) + sharedStorage.setDPoPKeyId(namespace, newKeyId) + return Pair(newKeyId, newKey) + } +} \ No newline at end of file diff --git a/sdk/src/main/java/com/oursky/authgear/oauth/OidcAuthenticationRequest.kt b/sdk/src/main/java/com/oursky/authgear/oauth/OidcAuthenticationRequest.kt index 83ad69f0..1c9469ee 100644 --- a/sdk/src/main/java/com/oursky/authgear/oauth/OidcAuthenticationRequest.kt +++ b/sdk/src/main/java/com/oursky/authgear/oauth/OidcAuthenticationRequest.kt @@ -23,7 +23,8 @@ internal data class OidcAuthenticationRequest constructor( var settingsAction: String? = null, var authenticationFlowGroup: String? = null, var responseMode: String? = null, - var xPreAuthenticatedURLToken: String? = null + var xPreAuthenticatedURLToken: String? = null, + var dpopJKT: String? = null ) internal fun OidcAuthenticationRequest.toQuery(clientID: String, codeVerifier: AuthgearCore.Verifier?): Map { @@ -95,6 +96,10 @@ internal fun OidcAuthenticationRequest.toQuery(clientID: String, codeVerifier: A query["x_pre_authenticated_url_token"] = it } + this.dpopJKT?.let { + query["dpop_jkt"] = it + } + val isSsoEnabled = this.isSsoEnabled ?: false if (!isSsoEnabled) {