From b48cf4860025b1e5d11c90b2a184a082d8c9816b Mon Sep 17 00:00:00 2001 From: Lefteris Date: Mon, 10 Jun 2024 16:23:48 +0300 Subject: [PATCH 01/14] Create DeviceAuthGrant with extra function to share OAuth2Grand if need it. --- .../client/services/AuthorizationService.kt | 85 ++++++++++++------- .../models/extensions/OAuth2Grants.kt | 7 ++ 2 files changed, 62 insertions(+), 30 deletions(-) diff --git a/Identity/src/main/java/gr/indice/identity/client/services/AuthorizationService.kt b/Identity/src/main/java/gr/indice/identity/client/services/AuthorizationService.kt index af2fb15..131821a 100644 --- a/Identity/src/main/java/gr/indice/identity/client/services/AuthorizationService.kt +++ b/Identity/src/main/java/gr/indice/identity/client/services/AuthorizationService.kt @@ -11,6 +11,7 @@ import gr.indice.identity.models.extensions.AuthCodeGrant import gr.indice.identity.models.extensions.AuthRequest import gr.indice.identity.models.extensions.ClientCredentialsGrand import gr.indice.identity.models.extensions.DeviceAuthGrant +import gr.indice.identity.models.extensions.DeviceAuthGrant.Info.* import gr.indice.identity.models.extensions.PasswordGrant import gr.indice.identity.models.extensions.RefreshTokenGrant import gr.indice.identity.models.extensions.biometricAuth @@ -28,6 +29,11 @@ import java.security.Signature import java.util.concurrent.CancellationException interface AuthorizationService { + /** + * Create oAuth2Grand for biometric or pin. Also return the grand if need it. + */ + suspend fun generateGrand(type: DeviceAuthGrant.Info): OAuth2Grant + /** Try login with any grant */ @Throws(ServiceErrorException::class) suspend fun login(grand: OAuth2Grant) @@ -78,6 +84,53 @@ internal class AuthorizationServiceImpl( private val client: Client, private val configuration: IdentityConfig ): BaseService(), AuthorizationService { + override suspend fun generateGrand(type: DeviceAuthGrant.Info): OAuth2Grant { + return when(type) { + is Biometric -> { + val codeVerifier = CryptoUtils.createCodeVerifier() + val verifierHash = CryptoUtils.sha256(codeVerifier) + + val authRequest = DeviceAuthentications.AuthorizationRequest.biometricAuth( + codeChallenge = verifierHash, deviceIds = thisDeviceRepository.ids, client = client + ) + + val challenge = load { devicesRepository.authorize(authRequest = authRequest) }.challenge!! + + try { + val signature = CryptoUtils.getSignature() + val key = CryptoUtils.getPrivateKey(CryptoUtils.KeyType.BIOMETRIC) + signature.initSign(key) + + val signed = type.signatureUnlock(signature).run { + update(challenge.toByteArray()) + sign().let { Base64.encodeToString(it, Base64.NO_WRAP) } + } + + val public = CryptoUtils.getPemFromKey(CryptoUtils.KeyType.BIOMETRIC) + + DeviceAuthGrant.biometric( + challenge = challenge, + codeSignature = signed, + verifier = codeVerifier, + deviceIds = thisDeviceRepository.ids, + publicKey = public, + client = client) + + } catch (e: Exception) { + if (e is CancellationException) { // Canceled prompt by user + throw e + } + deviceService.removeRegistrationFingerprint() + throw e + } + } + is Pin -> { + val pinHash = CryptoUtils.createPinHash(type.value, thisDeviceRepository.ids.device) + DeviceAuthGrant.pin(pin = pinHash, deviceIds = thisDeviceRepository.ids, client = client) + } + } + } + override suspend fun login(grand: OAuth2Grant) { val tokenResponse = load { authRepositoryRepository.authorize(grand) } tokenStorage.parse(tokenResponse) @@ -89,8 +142,7 @@ internal class AuthorizationServiceImpl( override suspend fun login(pin: String) { try { - val pinHash = CryptoUtils.createPinHash(pin, thisDeviceRepository.ids.device) - login(DeviceAuthGrant.pin(pin = pinHash, deviceIds = thisDeviceRepository.ids, client = client)) + login(generateGrand(type = Pin(pin))) } catch (e: Exception) { throw e } @@ -98,35 +150,8 @@ internal class AuthorizationServiceImpl( override suspend fun loginBiometric(signatureUnlock: suspend (Signature) -> Signature) { - val codeVerifier = CryptoUtils.createCodeVerifier() - val verifierHash = CryptoUtils.sha256(codeVerifier) - - val authRequest = DeviceAuthentications.AuthorizationRequest.biometricAuth( - codeChallenge = verifierHash, deviceIds = thisDeviceRepository.ids, client = client - ) - - val challenge = load { devicesRepository.authorize(authRequest = authRequest) }.challenge!! - try { - val signature = CryptoUtils.getSignature() - val key = CryptoUtils.getPrivateKey(CryptoUtils.KeyType.BIOMETRIC) - signature.initSign(key) - - val signed = signatureUnlock(signature).run { - update(challenge.toByteArray()) - sign().let { Base64.encodeToString(it, Base64.NO_WRAP) } - } - - val public = CryptoUtils.getPemFromKey(CryptoUtils.KeyType.BIOMETRIC) - - login(grand = DeviceAuthGrant.biometric( - challenge = challenge, - codeSignature = signed, - verifier = codeVerifier, - deviceIds = thisDeviceRepository.ids, - publicKey = public, - client = client)) - + login(grand = generateGrand(Biometric(signatureUnlock))) } catch (e: Exception) { if (e is CancellationException) { // Canceled prompt by user throw e diff --git a/Identity/src/main/java/gr/indice/identity/models/extensions/OAuth2Grants.kt b/Identity/src/main/java/gr/indice/identity/models/extensions/OAuth2Grants.kt index b893de7..195ca64 100644 --- a/Identity/src/main/java/gr/indice/identity/models/extensions/OAuth2Grants.kt +++ b/Identity/src/main/java/gr/indice/identity/models/extensions/OAuth2Grants.kt @@ -4,6 +4,7 @@ import gr.indice.identity.apis.OpenIdApi import gr.indice.identity.apis.ThisDeviceIds import gr.indice.identity.protocols.Client import gr.indice.identity.protocols.OAuth2Grant +import java.security.Signature private fun Map.filterNulls() = @@ -89,6 +90,12 @@ data class DeviceAuthGrant( val client_id: String?, val scope: String?, ): OAuth2Grant { + + sealed interface Info { + data class Biometric(val signatureUnlock: suspend (Signature) -> Signature): Info + data class Pin(val value: String): Info + } + override val grantType = "device_authentication" override val params: Map get() = mapOf( From c113d9df350ceea999f949cafda3e8b648b89f38 Mon Sep 17 00:00:00 2001 From: Lefteris Date: Mon, 10 Jun 2024 17:17:45 +0300 Subject: [PATCH 02/14] Generic params for OAuth2Grant --- .../src/main/java/gr/indice/identity/apis/OpenIdApi.kt | 2 +- .../indice/identity/models/extensions/OAuth2Grants.kt | 10 +++++----- .../main/java/gr/indice/identity/protocols/Grand.kt | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Identity/src/main/java/gr/indice/identity/apis/OpenIdApi.kt b/Identity/src/main/java/gr/indice/identity/apis/OpenIdApi.kt index a20aa50..5403f51 100644 --- a/Identity/src/main/java/gr/indice/identity/apis/OpenIdApi.kt +++ b/Identity/src/main/java/gr/indice/identity/apis/OpenIdApi.kt @@ -12,7 +12,7 @@ interface OpenIdApi { @POST suspend fun authorize( @Url url: String, - @FieldMap params: Map + @FieldMap params: Map ): Response @FormUrlEncoded diff --git a/Identity/src/main/java/gr/indice/identity/models/extensions/OAuth2Grants.kt b/Identity/src/main/java/gr/indice/identity/models/extensions/OAuth2Grants.kt index 195ca64..4b3a352 100644 --- a/Identity/src/main/java/gr/indice/identity/models/extensions/OAuth2Grants.kt +++ b/Identity/src/main/java/gr/indice/identity/models/extensions/OAuth2Grants.kt @@ -13,7 +13,7 @@ private fun Map.filterNulls() = data class ClientCredentialsGrand(val client: Client): OAuth2Grant { override val grantType: String = "client_credentials" - override val params: Map get() = mapOf( + override val params: Map get() = mapOf( "grant_type" to grantType, "client_id" to client.id, "client_secret" to client.secret, @@ -30,7 +30,7 @@ data class PasswordGrant( val client: Client ) : OAuth2Grant { override val grantType = "password" - override val params: Map get() = mapOf( + override val params: Map get() = mapOf( "grant_type" to grantType, "client_id" to client.id, "client_secret" to client.secret, @@ -51,7 +51,7 @@ data class AuthCodeGrant( val client: Client ): OAuth2Grant { override val grantType: String = "authorization_code" - override val params: Map = mapOf( + override val params: Map = mapOf( "grant_type" to grantType, "client_id" to client.id, "client_secret" to client.secret, @@ -67,7 +67,7 @@ data class RefreshTokenGrant( val client: Client ) : OAuth2Grant { override val grantType = "refresh_token" - override val params: Map get() = mapOf( + override val params: Map get() = mapOf( "grant_type" to grantType, "refresh_token" to refreshToken, "client_id" to client.id, @@ -98,7 +98,7 @@ data class DeviceAuthGrant( override val grantType = "device_authentication" - override val params: Map get() = mapOf( + override val params: Map get() = mapOf( "grant_type" to grantType, "mode" to mode, "pin" to pin, diff --git a/Identity/src/main/java/gr/indice/identity/protocols/Grand.kt b/Identity/src/main/java/gr/indice/identity/protocols/Grand.kt index 2d32d02..0b73948 100644 --- a/Identity/src/main/java/gr/indice/identity/protocols/Grand.kt +++ b/Identity/src/main/java/gr/indice/identity/protocols/Grand.kt @@ -1,7 +1,7 @@ package gr.indice.identity.protocols interface OAuth2Grant { - val params: Map + val params: Map val grantType: String } @@ -11,8 +11,8 @@ fun OAuth2Grant.with(authorizationDetails: Any): OAuth2Grant { return OAuthParamsWrapper(parent = this, extras = extras) } -private class OAuthParamsWrapper(private val parent: OAuth2Grant, private val extras: Pair): OAuth2Grant { - override val params: Map +private class OAuthParamsWrapper(private val parent: OAuth2Grant, private val extras: Pair): OAuth2Grant { + override val params: Map get() = parent.params + mapOf(extras) override val grantType get() = parent.grantType From 7f79e6d434bca7a711530c63cde94fd39ecb3cce Mon Sep 17 00:00:00 2001 From: Lefteris Date: Tue, 11 Jun 2024 13:25:19 +0300 Subject: [PATCH 03/14] Return OpenIdAuth params to Map --- .../src/main/java/gr/indice/identity/apis/OpenIdApi.kt | 2 +- .../java/gr/indice/identity/models/ProblemDetails.kt | 3 ++- .../indice/identity/models/extensions/OAuth2Grants.kt | 10 +++++----- .../main/java/gr/indice/identity/protocols/Grand.kt | 6 +++--- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/Identity/src/main/java/gr/indice/identity/apis/OpenIdApi.kt b/Identity/src/main/java/gr/indice/identity/apis/OpenIdApi.kt index 5403f51..a20aa50 100644 --- a/Identity/src/main/java/gr/indice/identity/apis/OpenIdApi.kt +++ b/Identity/src/main/java/gr/indice/identity/apis/OpenIdApi.kt @@ -12,7 +12,7 @@ interface OpenIdApi { @POST suspend fun authorize( @Url url: String, - @FieldMap params: Map + @FieldMap params: Map ): Response @FormUrlEncoded diff --git a/Identity/src/main/java/gr/indice/identity/models/ProblemDetails.kt b/Identity/src/main/java/gr/indice/identity/models/ProblemDetails.kt index b202964..dae209f 100644 --- a/Identity/src/main/java/gr/indice/identity/models/ProblemDetails.kt +++ b/Identity/src/main/java/gr/indice/identity/models/ProblemDetails.kt @@ -2,6 +2,7 @@ package gr.indice.identity.models import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import org.json.JSONObject @JsonClass(generateAdapter = true) data class ProblemDetails( @@ -14,7 +15,7 @@ data class ProblemDetails( @Json(name = "error_description") val errorDescription: String? = null, @Json(name = "authorization_details") - val authorizationDetails: Any? = null + val authorizationDetails: JSONObject? = null ) { val description : String get() { diff --git a/Identity/src/main/java/gr/indice/identity/models/extensions/OAuth2Grants.kt b/Identity/src/main/java/gr/indice/identity/models/extensions/OAuth2Grants.kt index 4b3a352..195ca64 100644 --- a/Identity/src/main/java/gr/indice/identity/models/extensions/OAuth2Grants.kt +++ b/Identity/src/main/java/gr/indice/identity/models/extensions/OAuth2Grants.kt @@ -13,7 +13,7 @@ private fun Map.filterNulls() = data class ClientCredentialsGrand(val client: Client): OAuth2Grant { override val grantType: String = "client_credentials" - override val params: Map get() = mapOf( + override val params: Map get() = mapOf( "grant_type" to grantType, "client_id" to client.id, "client_secret" to client.secret, @@ -30,7 +30,7 @@ data class PasswordGrant( val client: Client ) : OAuth2Grant { override val grantType = "password" - override val params: Map get() = mapOf( + override val params: Map get() = mapOf( "grant_type" to grantType, "client_id" to client.id, "client_secret" to client.secret, @@ -51,7 +51,7 @@ data class AuthCodeGrant( val client: Client ): OAuth2Grant { override val grantType: String = "authorization_code" - override val params: Map = mapOf( + override val params: Map = mapOf( "grant_type" to grantType, "client_id" to client.id, "client_secret" to client.secret, @@ -67,7 +67,7 @@ data class RefreshTokenGrant( val client: Client ) : OAuth2Grant { override val grantType = "refresh_token" - override val params: Map get() = mapOf( + override val params: Map get() = mapOf( "grant_type" to grantType, "refresh_token" to refreshToken, "client_id" to client.id, @@ -98,7 +98,7 @@ data class DeviceAuthGrant( override val grantType = "device_authentication" - override val params: Map get() = mapOf( + override val params: Map get() = mapOf( "grant_type" to grantType, "mode" to mode, "pin" to pin, diff --git a/Identity/src/main/java/gr/indice/identity/protocols/Grand.kt b/Identity/src/main/java/gr/indice/identity/protocols/Grand.kt index 0b73948..2d32d02 100644 --- a/Identity/src/main/java/gr/indice/identity/protocols/Grand.kt +++ b/Identity/src/main/java/gr/indice/identity/protocols/Grand.kt @@ -1,7 +1,7 @@ package gr.indice.identity.protocols interface OAuth2Grant { - val params: Map + val params: Map val grantType: String } @@ -11,8 +11,8 @@ fun OAuth2Grant.with(authorizationDetails: Any): OAuth2Grant { return OAuthParamsWrapper(parent = this, extras = extras) } -private class OAuthParamsWrapper(private val parent: OAuth2Grant, private val extras: Pair): OAuth2Grant { - override val params: Map +private class OAuthParamsWrapper(private val parent: OAuth2Grant, private val extras: Pair): OAuth2Grant { + override val params: Map get() = parent.params + mapOf(extras) override val grantType get() = parent.grantType From 58bcc2a08544a438127f282c523c26fac2f6df93 Mon Sep 17 00:00:00 2001 From: Lefteris Date: Tue, 11 Jun 2024 13:38:19 +0300 Subject: [PATCH 04/14] Clear ProblemDetails --- .../src/main/java/gr/indice/identity/models/ProblemDetails.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Identity/src/main/java/gr/indice/identity/models/ProblemDetails.kt b/Identity/src/main/java/gr/indice/identity/models/ProblemDetails.kt index dae209f..e1bcef6 100644 --- a/Identity/src/main/java/gr/indice/identity/models/ProblemDetails.kt +++ b/Identity/src/main/java/gr/indice/identity/models/ProblemDetails.kt @@ -15,7 +15,7 @@ data class ProblemDetails( @Json(name = "error_description") val errorDescription: String? = null, @Json(name = "authorization_details") - val authorizationDetails: JSONObject? = null + val authorizationDetails: Any? = null ) { val description : String get() { From 06c51e7bd14223d6905f3537728634c9645968a7 Mon Sep 17 00:00:00 2001 From: Lefteris Date: Tue, 11 Jun 2024 13:51:20 +0300 Subject: [PATCH 05/14] Clear ProblemDetails --- .../src/main/java/gr/indice/identity/models/ProblemDetails.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/Identity/src/main/java/gr/indice/identity/models/ProblemDetails.kt b/Identity/src/main/java/gr/indice/identity/models/ProblemDetails.kt index e1bcef6..b202964 100644 --- a/Identity/src/main/java/gr/indice/identity/models/ProblemDetails.kt +++ b/Identity/src/main/java/gr/indice/identity/models/ProblemDetails.kt @@ -2,7 +2,6 @@ package gr.indice.identity.models import com.squareup.moshi.Json import com.squareup.moshi.JsonClass -import org.json.JSONObject @JsonClass(generateAdapter = true) data class ProblemDetails( From d9ee195ac6ae203afbe01817319fcae03623fa98 Mon Sep 17 00:00:00 2001 From: Lefteris Date: Tue, 11 Jun 2024 15:31:02 +0300 Subject: [PATCH 06/14] Add Gson to create again Json for authorizationDetails. --- Identity/build.gradle.kts | 1 + Identity/src/main/java/gr/indice/identity/protocols/Grand.kt | 4 +++- gradle/libs.versions.toml | 2 ++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Identity/build.gradle.kts b/Identity/build.gradle.kts index 126f627..d437284 100644 --- a/Identity/build.gradle.kts +++ b/Identity/build.gradle.kts @@ -77,4 +77,5 @@ dependencies { api(libs.moshi.kotlin) implementation(libs.kotlinx.coroutines.core) + implementation(libs.gson) } \ No newline at end of file diff --git a/Identity/src/main/java/gr/indice/identity/protocols/Grand.kt b/Identity/src/main/java/gr/indice/identity/protocols/Grand.kt index 2d32d02..0f34d5e 100644 --- a/Identity/src/main/java/gr/indice/identity/protocols/Grand.kt +++ b/Identity/src/main/java/gr/indice/identity/protocols/Grand.kt @@ -1,12 +1,14 @@ package gr.indice.identity.protocols +import com.google.gson.Gson + interface OAuth2Grant { val params: Map val grantType: String } fun OAuth2Grant.with(authorizationDetails: Any): OAuth2Grant { - val extras = "authorization_details" to authorizationDetails.toString() + val extras = "authorization_details" to Gson().toJson(authorizationDetails) return OAuthParamsWrapper(parent = this, extras = extras) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b7e5c93..da3b89a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,7 @@ [versions] agp = "8.3.2" kotlin = "1.9.23" +gson = "2.10.1" kotlinxCoroutinesCore = "1.8.0" loggingInterceptor = "5.0.0-alpha.14" @@ -13,6 +14,7 @@ kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-c logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "loggingInterceptor" } moshi-kotlin = { module = "com.squareup.moshi:moshi-kotlin", version.ref = "moshiKotlin" } retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } +gson = { module = "com.google.code.gson:gson", version.ref = "gson" } [plugins] androidLibrary = { id = "com.android.library", version.ref = "agp" } From bf8ddd88d5040fbeb6fd3d64be5abfc72924498d Mon Sep 17 00:00:00 2001 From: Lefteris Date: Wed, 12 Jun 2024 11:07:03 +0300 Subject: [PATCH 07/14] Convert Exception to IOException.. --- Identity/src/main/java/gr/indice/identity/utils/Exceptions.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Identity/src/main/java/gr/indice/identity/utils/Exceptions.kt b/Identity/src/main/java/gr/indice/identity/utils/Exceptions.kt index 4ebc5d8..319f2c8 100644 --- a/Identity/src/main/java/gr/indice/identity/utils/Exceptions.kt +++ b/Identity/src/main/java/gr/indice/identity/utils/Exceptions.kt @@ -1,10 +1,11 @@ package gr.indice.identity.utils import okhttp3.ResponseBody +import java.io.IOException /** * Throw when something goes wrong with Identity. * @param code [Int] * @param error [ResponseBody] */ -class ServiceErrorException(val code: Int?, val error: ResponseBody): Exception() \ No newline at end of file +class ServiceErrorException(val code: Int?, val error: ResponseBody): IOException() \ No newline at end of file From 42ada3cf0582140bfb22ec25ae2c9cb423e59ddd Mon Sep 17 00:00:00 2001 From: Lefteris Date: Wed, 25 Sep 2024 12:39:38 +0300 Subject: [PATCH 08/14] Add check for biometric login failure message and purpose. --- Identity/build.gradle.kts | 2 +- .../gr/indice/identity/adapters/Converter.kt | 9 ++++++ .../client/services/AuthorizationService.kt | 28 +++++++++++-------- gradle/libs.versions.toml | 2 +- 4 files changed, 28 insertions(+), 13 deletions(-) create mode 100644 Identity/src/main/java/gr/indice/identity/adapters/Converter.kt diff --git a/Identity/build.gradle.kts b/Identity/build.gradle.kts index d437284..2c73afc 100644 --- a/Identity/build.gradle.kts +++ b/Identity/build.gradle.kts @@ -12,7 +12,7 @@ publishing { register("release") { groupId = "gr.indice" artifactId = "identity" - version = "0.0.1" + version = "0.0.2" afterEvaluate { from(components["release"]) diff --git a/Identity/src/main/java/gr/indice/identity/adapters/Converter.kt b/Identity/src/main/java/gr/indice/identity/adapters/Converter.kt new file mode 100644 index 0000000..8faddbb --- /dev/null +++ b/Identity/src/main/java/gr/indice/identity/adapters/Converter.kt @@ -0,0 +1,9 @@ +package gr.indice.identity.adapters + +import gr.indice.identity.utils.Serializer +import okhttp3.ResponseBody + + +fun ResponseBody.toType(target: Class) = + Serializer.moshi.adapter(target) + .run { fromJson(source().peek()) } diff --git a/Identity/src/main/java/gr/indice/identity/client/services/AuthorizationService.kt b/Identity/src/main/java/gr/indice/identity/client/services/AuthorizationService.kt index 131821a..f188880 100644 --- a/Identity/src/main/java/gr/indice/identity/client/services/AuthorizationService.kt +++ b/Identity/src/main/java/gr/indice/identity/client/services/AuthorizationService.kt @@ -2,10 +2,12 @@ package gr.indice.identity.client.services import android.net.Uri import android.util.Base64 +import gr.indice.identity.adapters.toType import gr.indice.identity.apis.AuthRepositoryRepository import gr.indice.identity.apis.DevicesRepository import gr.indice.identity.apis.ThisDeviceRepository import gr.indice.identity.models.DeviceAuthentications +import gr.indice.identity.models.ProblemDetails import gr.indice.identity.models.TokenResponse import gr.indice.identity.models.extensions.AuthCodeGrant import gr.indice.identity.models.extensions.AuthRequest @@ -87,16 +89,15 @@ internal class AuthorizationServiceImpl( override suspend fun generateGrand(type: DeviceAuthGrant.Info): OAuth2Grant { return when(type) { is Biometric -> { - val codeVerifier = CryptoUtils.createCodeVerifier() - val verifierHash = CryptoUtils.sha256(codeVerifier) - - val authRequest = DeviceAuthentications.AuthorizationRequest.biometricAuth( - codeChallenge = verifierHash, deviceIds = thisDeviceRepository.ids, client = client - ) + try { + val codeVerifier = CryptoUtils.createCodeVerifier() + val verifierHash = CryptoUtils.sha256(codeVerifier) - val challenge = load { devicesRepository.authorize(authRequest = authRequest) }.challenge!! + val authRequest = DeviceAuthentications.AuthorizationRequest.biometricAuth( + codeChallenge = verifierHash, deviceIds = thisDeviceRepository.ids, client = client + ) - try { + val challenge = load { devicesRepository.authorize(authRequest = authRequest) }.challenge!! val signature = CryptoUtils.getSignature() val key = CryptoUtils.getPrivateKey(CryptoUtils.KeyType.BIOMETRIC) signature.initSign(key) @@ -117,10 +118,15 @@ internal class AuthorizationServiceImpl( client = client) } catch (e: Exception) { - if (e is CancellationException) { // Canceled prompt by user - throw e + if (e is ServiceErrorException) { + e.error.toType(ProblemDetails::class.java)?.let { error -> + if (error.detail == "Device is unknown" || error.title == "invalid_request") { + deviceService.removeRegistrationFingerprint() + //If device doesn't exist or is invalid request clear also pin registration + deviceService.removeRegistrationDevicePin() + } + } } - deviceService.removeRegistrationFingerprint() throw e } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index da3b89a..9f1241e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ agp = "8.3.2" kotlin = "1.9.23" gson = "2.10.1" -kotlinxCoroutinesCore = "1.8.0" +kotlinxCoroutinesCore = "1.8.1" loggingInterceptor = "5.0.0-alpha.14" moshiKotlin = "1.15.1" retrofit = "2.11.0" From 22c68b48dbe5537f2fa4c2c4b4b5ce4f66ccf783 Mon Sep 17 00:00:00 2001 From: Lefteris Date: Thu, 3 Oct 2024 17:32:42 +0300 Subject: [PATCH 09/14] Clear userInfo. --- .../java/gr/indice/identity/client/services/UserService.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Identity/src/main/java/gr/indice/identity/client/services/UserService.kt b/Identity/src/main/java/gr/indice/identity/client/services/UserService.kt index 50969a5..ee1ab28 100644 --- a/Identity/src/main/java/gr/indice/identity/client/services/UserService.kt +++ b/Identity/src/main/java/gr/indice/identity/client/services/UserService.kt @@ -12,6 +12,7 @@ interface UserService { val userInfo: StateFlow @Throws(ServiceErrorException::class) suspend fun refreshUserInfo() + fun clearUserInfo() } internal class UserServiceImpl(private val userInfoRepository: UserInfoRepository): BaseService(), UserService { @@ -22,4 +23,8 @@ internal class UserServiceImpl(private val userInfoRepository: UserInfoRepositor _userInfo.value = load { userInfoRepository.getUserInfo() } } + override fun clearUserInfo() { + _userInfo.value = null + } + } From 59065f320872094301e7795ca175f0117ce3731f Mon Sep 17 00:00:00 2001 From: Lefteris Date: Wed, 9 Oct 2024 11:43:57 +0300 Subject: [PATCH 10/14] Remove suspend fun for remove device fingerprint and pin. --- .../gr/indice/identity/client/services/DevicesService.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Identity/src/main/java/gr/indice/identity/client/services/DevicesService.kt b/Identity/src/main/java/gr/indice/identity/client/services/DevicesService.kt index 2131b14..80fb11a 100644 --- a/Identity/src/main/java/gr/indice/identity/client/services/DevicesService.kt +++ b/Identity/src/main/java/gr/indice/identity/client/services/DevicesService.kt @@ -98,9 +98,9 @@ interface DevicesService { @Throws(ServiceErrorException::class) suspend fun registerDeviceFingerprint(signatureUnlock: suspend (Signature) -> Signature) : suspend (CallbackType.OtpResult) -> Unit /** Remove a device pin registration */ - suspend fun removeRegistrationDevicePin() + fun removeRegistrationDevicePin() /** Remove a fingerprint registration */ - suspend fun removeRegistrationFingerprint() + fun removeRegistrationFingerprint() /** Trigger enable current device's trust status */ @Throws(ServiceErrorException::class) suspend fun enableDeviceTrust(deviceSelection: DeviceSelection) @@ -289,13 +289,13 @@ internal class DevicesServiceImpl( } } - override suspend fun removeRegistrationDevicePin() { + override fun removeRegistrationDevicePin() { CryptoUtils.deleteKeyPair(CryptoUtils.KeyType.PIN) encryptedStorage.storeBoolean(StorageKey.devicePinKey, false) _hasDevicePin.value = false } - override suspend fun removeRegistrationFingerprint() { + override fun removeRegistrationFingerprint() { CryptoUtils.deleteKeyPair(CryptoUtils.KeyType.BIOMETRIC) encryptedStorage.storeBoolean(StorageKey.hasFingerPrint, false) _hasFingerPrint.value = false From 923551f938ff0775a4dbd45cb3f69348b675d5e2 Mon Sep 17 00:00:00 2001 From: Lefteris Date: Tue, 5 Nov 2024 14:35:16 +0200 Subject: [PATCH 11/14] Calculate only native device for trust device count. --- .../java/gr/indice/identity/client/services/DevicesService.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Identity/src/main/java/gr/indice/identity/client/services/DevicesService.kt b/Identity/src/main/java/gr/indice/identity/client/services/DevicesService.kt index 80fb11a..46a2c9c 100644 --- a/Identity/src/main/java/gr/indice/identity/client/services/DevicesService.kt +++ b/Identity/src/main/java/gr/indice/identity/client/services/DevicesService.kt @@ -7,6 +7,7 @@ import gr.indice.identity.apis.ThisDeviceRepository import gr.indice.identity.client.IdentityClientOptions import gr.indice.identity.models.CreateDeviceRequest import gr.indice.identity.models.DeviceAuthentications +import gr.indice.identity.models.DeviceClientType import gr.indice.identity.models.DeviceInfo import gr.indice.identity.models.UpdateDeviceRequest import gr.indice.identity.models.extensions.biometric @@ -310,7 +311,7 @@ internal class DevicesServiceImpl( val devices = (devicesInfo.userDevices.value ?: emptyList()).filter { it.deviceId != ids.device } - val currentTrustedCount = devices.count { it.isTrusted == true } + val currentTrustedCount = devices.count { it.isTrusted == true && it.clientType != DeviceClientType.BROWSER } val swapDeviceId = if (currentTrustedCount >= identityOptions.maxTrustedDevicesCount) { when(val selection = deviceSelection(devices)) { From 134463ccc4d0097d915a3824b270765806f8af14 Mon Sep 17 00:00:00 2001 From: Lefteris Date: Wed, 6 Nov 2024 11:27:37 +0200 Subject: [PATCH 12/14] Add requiresOtp property to ProblemDetails --- .../src/main/java/gr/indice/identity/models/ProblemDetails.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Identity/src/main/java/gr/indice/identity/models/ProblemDetails.kt b/Identity/src/main/java/gr/indice/identity/models/ProblemDetails.kt index b202964..d5ef627 100644 --- a/Identity/src/main/java/gr/indice/identity/models/ProblemDetails.kt +++ b/Identity/src/main/java/gr/indice/identity/models/ProblemDetails.kt @@ -14,7 +14,8 @@ data class ProblemDetails( @Json(name = "error_description") val errorDescription: String? = null, @Json(name = "authorization_details") - val authorizationDetails: Any? = null + val authorizationDetails: Any? = null, + val requiresOtp: Boolean? = null ) { val description : String get() { From 4a6fab50f0c4d3013ab00b1ba1b07e14d9439050 Mon Sep 17 00:00:00 2001 From: Lefteris Date: Mon, 15 Sep 2025 14:36:53 +0300 Subject: [PATCH 13/14] Add Verify Email Also encode auth2Grands with UTF-8 --- Identity/build.gradle.kts | 2 +- .../gr/indice/identity/client/services/AccountService.kt | 6 ++++++ .../gr/indice/identity/models/extensions/OAuth2Grants.kt | 4 ++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Identity/build.gradle.kts b/Identity/build.gradle.kts index 2c73afc..b1d8c57 100644 --- a/Identity/build.gradle.kts +++ b/Identity/build.gradle.kts @@ -37,7 +37,7 @@ publishing { android { namespace = "gr.indice.identity" - compileSdk = 34 + compileSdk = 36 defaultConfig { minSdk = 26 diff --git a/Identity/src/main/java/gr/indice/identity/client/services/AccountService.kt b/Identity/src/main/java/gr/indice/identity/client/services/AccountService.kt index 935b1b8..827ed7d 100644 --- a/Identity/src/main/java/gr/indice/identity/client/services/AccountService.kt +++ b/Identity/src/main/java/gr/indice/identity/client/services/AccountService.kt @@ -19,6 +19,9 @@ interface AccountService { /** Update the user's current email */ @Throws(ServiceErrorException::class) suspend fun updateEmail(email: String) + /** Confirm the user's current email */ + @Throws(ServiceErrorException::class) + suspend fun confirmEmail(token: String) /** Update the user's current password */ @Throws(ServiceErrorException::class) suspend fun updatePassword(password: UpdatePasswordRequest) @@ -51,6 +54,9 @@ internal class AccountServiceImpl( override suspend fun updateEmail(email: String) = load { accountRepository.update(UpdateEmailRequest(email = email, returnUrl = null)) } + override suspend fun confirmEmail(token: String) { + load { accountRepository.verifyEmail(OtpTokenRequest(token)) } + } override suspend fun updatePassword(password: UpdatePasswordRequest) = load { accountRepository.update(password) } diff --git a/Identity/src/main/java/gr/indice/identity/models/extensions/OAuth2Grants.kt b/Identity/src/main/java/gr/indice/identity/models/extensions/OAuth2Grants.kt index 195ca64..7fddbb6 100644 --- a/Identity/src/main/java/gr/indice/identity/models/extensions/OAuth2Grants.kt +++ b/Identity/src/main/java/gr/indice/identity/models/extensions/OAuth2Grants.kt @@ -4,6 +4,7 @@ import gr.indice.identity.apis.OpenIdApi import gr.indice.identity.apis.ThisDeviceIds import gr.indice.identity.protocols.Client import gr.indice.identity.protocols.OAuth2Grant +import java.net.URLEncoder import java.security.Signature @@ -39,6 +40,9 @@ data class PasswordGrant( "password" to password, "device_id" to deviceId ).filterNulls() + .mapValues { entry -> + URLEncoder.encode(entry.value,"UTF-8") + } } //endregion Password grant From 6a2e2c43a324a9414e4ff4511c753551626a63c0 Mon Sep 17 00:00:00 2001 From: Lefteris Date: Tue, 16 Sep 2025 14:59:18 +0300 Subject: [PATCH 14/14] Undo encode at OAuth2Grant body --- .../gr/indice/identity/models/extensions/OAuth2Grants.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Identity/src/main/java/gr/indice/identity/models/extensions/OAuth2Grants.kt b/Identity/src/main/java/gr/indice/identity/models/extensions/OAuth2Grants.kt index 7fddbb6..b00956f 100644 --- a/Identity/src/main/java/gr/indice/identity/models/extensions/OAuth2Grants.kt +++ b/Identity/src/main/java/gr/indice/identity/models/extensions/OAuth2Grants.kt @@ -40,9 +40,10 @@ data class PasswordGrant( "password" to password, "device_id" to deviceId ).filterNulls() - .mapValues { entry -> - URLEncoder.encode(entry.value,"UTF-8") - } + //Comment this because converts the scopes to urlEncode -> invalid_scope replaces the + with %2B + //.mapValues { entry -> + // URLEncoder.encode(entry.value,"UTF-8") + //} } //endregion Password grant