Skip to content

Commit

Permalink
Breaks logins in flavor of passkeys
Browse files Browse the repository at this point in the history
  • Loading branch information
ForceTower committed Mar 5, 2024
1 parent 1c13447 commit b0acf75
Show file tree
Hide file tree
Showing 21 changed files with 518 additions and 48 deletions.
4 changes: 4 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,10 @@ dependencies {
implementation "com.google.firebase:firebase-crashlytics-ktx"
implementation "com.firebaseui:firebase-ui-storage:$firebase_ui_storage"

// passkeys
implementation("androidx.credentials:credentials:1.3.0-alpha01")
implementation("androidx.credentials:credentials-play-services-auth:1.3.0-alpha01")

debugImplementation "com.github.chuckerteam.chucker:library:4.0.0"
releaseImplementation "com.github.chuckerteam.chucker:library-no-op:4.0.0"

Expand Down
5 changes: 5 additions & 0 deletions app/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@
public static *** i(...);
}

-if class androidx.credentials.CredentialManager
-keep class androidx.credentials.playservices.** {
*;
}

# Work around android fragment artifact bug
-keep class androidx.navigation.fragment.NavHostFragment { *; }
-keep class androidx.navigation.dynamicfeatures.fragment.DynamicNavHostFragment { *; }
Expand Down
3 changes: 3 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,9 @@
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="@string/google_maps_key" />
<meta-data
android:name="asset_statements"
android:resource="@string/asset_statements" />

<provider
android:name="androidx.core.content.FileProvider"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import com.forcetower.uefs.core.constants.Constants
import com.forcetower.uefs.core.storage.cookies.CachedCookiePersistor
import com.forcetower.uefs.core.storage.database.UDatabase
import com.forcetower.uefs.core.storage.network.APIService
import com.forcetower.uefs.core.storage.network.EdgeService
import com.forcetower.uefs.core.storage.network.UService
import com.forcetower.uefs.core.storage.network.github.GithubService
import com.forcetower.uefs.core.util.ObjectUtils
Expand Down Expand Up @@ -185,4 +186,15 @@ object NetworkModule {
.build()
.create(APIService::class.java)
}

@Provides
@Singleton
fun provideEdgeService(client: OkHttpClient): EdgeService {
return Retrofit.Builder()
.baseUrl("https://edge-unes.forcetower.dev/api/")
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(EdgeService::class.java)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.forcetower.uefs.core.model.edge

data class AssertionData(
val flowId: String,
val challenge: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.forcetower.uefs.core.model.edge

data class CompleteAssertionData(
val flowId: String,
val credential: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.forcetower.uefs.core.model.edge

data class SimplifiedPublicKey(
val challenge: String,
val timeout: Int,
val rpId: String,
val userVerification: String,
val extensions: Map<String, Any?>?
)

data class PasskeyAssert(
val publicKey: SimplifiedPublicKey
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.forcetower.uefs.core.model.edge

data class Rp(val name: String, val id: String)
data class User(val name: String, val displayName: String, val id: String)
data class PublicKeyCredParam(val alg: Int, val type: String)
data class ExcludedCredential(val type: String, val id: String)
data class AuthenticatorSelection(
val authenticatorAttachment: String?,
val requireResidentKey: Boolean?,
val residentKey: String?,
val userVerification: String?
)
data class Extensions(val credProps: Boolean)

data class PublicKey(
val rp: Rp,
val user: User,
val challenge: String,
val pubKeyCredParams: List<PublicKeyCredParam>,
val timeout: Int,
val excludeCredentials: List<ExcludedCredential>,
val authenticatorSelection: AuthenticatorSelection,
val attestation: String,
val extensions: Extensions
)

data class PasskeyRegister(
val publicKey: PublicKey
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.forcetower.uefs.core.model.edge

data class RegisterPasskeyCredential(
val flowId: String,
val credential: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.forcetower.uefs.core.model.edge

data class RegisterPasskeyStart(
val flowId: String,
val create: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.forcetower.uefs.core.storage.network

import com.forcetower.uefs.core.model.edge.AssertionData
import com.forcetower.uefs.core.model.edge.CompleteAssertionData
import com.forcetower.uefs.core.model.edge.RegisterPasskeyCredential
import com.forcetower.uefs.core.model.edge.RegisterPasskeyStart
import com.forcetower.uefs.core.model.unes.AccessToken
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Headers
import retrofit2.http.POST

interface EdgeService {
@GET("auth/login/passkey/assertion/start")
suspend fun startAssertion(): AssertionData

@POST("auth/login/passkey/assertion/finish")
suspend fun completeAssertion(@Body data: CompleteAssertionData): AccessToken

@GET("passkeys/register/start")
suspend fun registerPasskeyStart(): RegisterPasskeyStart

@POST("passkeys/register/finish")
suspend fun registerPasskeyFinish(@Body data: RegisterPasskeyCredential)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.forcetower.uefs.core.storage.repository.cloud

import com.forcetower.uefs.core.model.edge.AssertionData
import com.forcetower.uefs.core.model.edge.CompleteAssertionData
import com.forcetower.uefs.core.model.edge.RegisterPasskeyCredential
import com.forcetower.uefs.core.model.edge.RegisterPasskeyStart
import com.forcetower.uefs.core.storage.network.EdgeService
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class EdgeAuthRepository @Inject constructor(
private val service: EdgeService
) {
suspend fun startAssertion(): AssertionData {
return service.startAssertion()
}

suspend fun completeAssertion(flowId: String, response: String) {
val token = service.completeAssertion(CompleteAssertionData(flowId, response))
Timber.d("Token $token")
}

suspend fun registerStart(): RegisterPasskeyStart {
return service.registerPasskeyStart()
}

suspend fun registerFinish(flowId: String, credential: String) {
return service.registerPasskeyFinish(RegisterPasskeyCredential(flowId, credential))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.forcetower.uefs.domain.usecase

import com.forcetower.uefs.core.storage.repository.cloud.EdgeAuthRepository
import dagger.Reusable
import timber.log.Timber
import javax.inject.Inject

@Reusable
class CompleteAssertionUseCase @Inject constructor(
private val auth: EdgeAuthRepository
) {
suspend operator fun invoke(flowId: String, response: String) {
Timber.d("Credential: $response")
auth.completeAssertion(flowId, response)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.forcetower.uefs.domain.usecase

import com.forcetower.uefs.core.model.edge.PasskeyRegister
import com.forcetower.uefs.core.model.edge.RegisterPasskeyStart
import com.forcetower.uefs.core.storage.repository.cloud.EdgeAuthRepository
import com.google.gson.Gson
import dagger.Reusable
import timber.log.Timber
import javax.inject.Inject

@Reusable
class RegisterPasskeyUseCase @Inject constructor(
private val edge: EdgeAuthRepository,
private val gson: Gson
) {
suspend fun start(): RegisterPasskeyStart {
val data = edge.registerStart()
Timber.d("Original data: ${data.create}")
val register = gson.fromJson(data.create, PasskeyRegister::class.java)
return data.copy(create = gson.toJson(register.publicKey))
}

suspend fun finish(flowId: String, credential: String) {
return edge.registerFinish(flowId, credential)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.forcetower.uefs.domain.usecase

import com.forcetower.uefs.core.model.edge.AssertionData
import com.forcetower.uefs.core.model.edge.PasskeyAssert
import com.forcetower.uefs.core.storage.repository.cloud.EdgeAuthRepository
import com.google.gson.Gson
import dagger.Reusable
import javax.inject.Inject

@Reusable
class StartAssertionUseCase @Inject constructor(
private val auth: EdgeAuthRepository,
private val gson: Gson
) {
suspend operator fun invoke(): AssertionData {
val data = auth.startAssertion()
val parsed = gson.fromJson(data.challenge, PasskeyAssert::class.java)
return data.copy(challenge = gson.toJson(parsed.publicKey))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.forcetower.uefs.feature.login

import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.forcetower.uefs.core.model.edge.RegisterPasskeyStart
import com.forcetower.uefs.domain.usecase.CompleteAssertionUseCase
import com.forcetower.uefs.domain.usecase.RegisterPasskeyUseCase
import com.forcetower.uefs.domain.usecase.StartAssertionUseCase
import com.forcetower.uefs.feature.shared.SingleLiveEvent
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject

@HiltViewModel
class LoginFormViewModel @Inject constructor(
private val getLoginChallenge: StartAssertionUseCase,
private val completeAssertion: CompleteAssertionUseCase,
private val registerPasskey: RegisterPasskeyUseCase
) : ViewModel() {
private val _data = SingleLiveEvent<String>()
val challenge: LiveData<String> = _data

private val _register = SingleLiveEvent<RegisterPasskeyStart>()
val register: LiveData<RegisterPasskeyStart> = _register

private var flowId = ""

fun startRegister() {
viewModelScope.launch {
runCatching {
val data = registerPasskey.start()
_register.value = data
}.onFailure {
Timber.e(it, "Failed to request challenge")
}
}
}

fun finishRegister(flowId: String, credential: String) {
viewModelScope.launch {
runCatching {
registerPasskey.finish(flowId, credential)
}.onFailure {
Timber.e(it, "Failed to register")
}
}
}

fun startAssertion() {
viewModelScope.launch {
runCatching {
val data = getLoginChallenge()
flowId = data.flowId
val challenge = data.challenge
_data.value = challenge
}.onFailure {
Timber.e(it, "Failed to request assertion")
}
}
}

fun completeAssertion(responseJson: String) {
if (flowId.isBlank()) return
viewModelScope.launch {
runCatching {
completeAssertion(flowId, responseJson)
}.onFailure {
Timber.e(it, "Failed to authenticate user")
}
}
}
}
Loading

0 comments on commit b0acf75

Please sign in to comment.