Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions Identity/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ publishing {
register<MavenPublication>("release") {
groupId = "gr.indice"
artifactId = "identity"
version = "0.0.1"
version = "0.0.2"

afterEvaluate {
from(components["release"])
Expand All @@ -37,7 +37,7 @@ publishing {

android {
namespace = "gr.indice.identity"
compileSdk = 34
compileSdk = 36

defaultConfig {
minSdk = 26
Expand Down Expand Up @@ -77,4 +77,5 @@ dependencies {
api(libs.moshi.kotlin)

implementation(libs.kotlinx.coroutines.core)
implementation(libs.gson)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package gr.indice.identity.adapters

import gr.indice.identity.utils.Serializer
import okhttp3.ResponseBody


fun <T> ResponseBody.toType(target: Class<T>) =
Serializer.moshi.adapter(target)
.run { fromJson(source().peek()) }
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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) }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,18 @@ 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
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
Expand All @@ -28,6 +31,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)
Expand Down Expand Up @@ -78,6 +86,57 @@ 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 -> {
try {
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!!
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 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()
}
}
}
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)
Expand All @@ -89,44 +148,16 @@ 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
}
}


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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -98,9 +99,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)
Expand Down Expand Up @@ -289,13 +290,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
Expand All @@ -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)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ interface UserService {
val userInfo: StateFlow<UserInfo?>
@Throws(ServiceErrorException::class)
suspend fun refreshUserInfo()
fun clearUserInfo()
}

internal class UserServiceImpl(private val userInfoRepository: UserInfoRepository): BaseService(), UserService {
Expand All @@ -22,4 +23,8 @@ internal class UserServiceImpl(private val userInfoRepository: UserInfoRepositor
_userInfo.value = load { userInfoRepository.getUserInfo() }
}

override fun clearUserInfo() {
_userInfo.value = null
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ 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


private fun Map<String, String?>.filterNulls() =
Expand Down Expand Up @@ -38,6 +40,10 @@ data class PasswordGrant(
"password" to password,
"device_id" to deviceId
).filterNulls()
//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

Expand Down Expand Up @@ -89,6 +95,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<String, String> get() = mapOf(
Expand Down
4 changes: 3 additions & 1 deletion Identity/src/main/java/gr/indice/identity/protocols/Grand.kt
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package gr.indice.identity.protocols

import com.google.gson.Gson

interface OAuth2Grant {
val params: Map<String, String>
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)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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()
class ServiceErrorException(val code: Int?, val error: ResponseBody): IOException()
4 changes: 3 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
[versions]
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"
Expand All @@ -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" }
Expand Down