Skip to content

Commit d0b610a

Browse files
neelanshsahaicy245
authored andcommitted
Add Signal API implementation (RP Side) (#175)
* Cherry Pick Signal API implementation from branch * Fix conflicts and suggestions * Extract common code from CredentialManagerUtils
1 parent f1ae2e7 commit d0b610a

30 files changed

+592
-60
lines changed

Shrine/app/src/main/java/com/authentication/shrine/CredentialManagerUtils.kt

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package com.authentication.shrine
1717

1818
import android.annotation.SuppressLint
1919
import android.content.Context
20+
import android.util.Log
2021
import androidx.credentials.ClearCredentialStateRequest
2122
import androidx.credentials.ClearCredentialStateRequest.Companion.TYPE_CLEAR_RESTORE_CREDENTIAL
2223
import androidx.credentials.CreateCredentialRequest
@@ -33,11 +34,20 @@ import androidx.credentials.GetCredentialResponse
3334
import androidx.credentials.GetPasswordOption
3435
import androidx.credentials.GetPublicKeyCredentialOption
3536
import androidx.credentials.GetRestoreCredentialOption
37+
import androidx.credentials.SignalAllAcceptedCredentialIdsRequest
38+
import androidx.credentials.SignalCurrentUserDetailsRequest
39+
import androidx.credentials.SignalUnknownCredentialRequest
3640
import androidx.credentials.exceptions.CreateCredentialException
3741
import androidx.credentials.exceptions.GetCredentialCancellationException
3842
import androidx.credentials.exceptions.publickeycredential.GetPublicKeyCredentialDomException
43+
import androidx.datastore.core.DataStore
44+
import androidx.datastore.preferences.core.Preferences
45+
import com.authentication.shrine.repository.AuthRepository.Companion.RP_ID_KEY
46+
import com.authentication.shrine.repository.AuthRepository.Companion.USER_ID_KEY
47+
import com.authentication.shrine.repository.AuthRepository.Companion.read
3948
import com.authentication.shrine.repository.SERVER_CLIENT_ID
4049
import com.google.android.libraries.identity.googleid.GetGoogleIdOption
50+
import org.json.JSONArray
4151
import org.json.JSONObject
4252
import javax.inject.Inject
4353

@@ -48,8 +58,20 @@ import javax.inject.Inject
4858
*/
4959
class CredentialManagerUtils @Inject constructor(
5060
private val credentialManager: CredentialManager,
61+
private val dataStore: DataStore<Preferences>,
5162
) {
5263

64+
private val TAG = "CredentialManagerUtils"
65+
66+
private object JSON_KEYS {
67+
const val RP_ID = "rpId"
68+
const val CREDENTIAL_ID = "credentialId"
69+
const val USER_ID = "userId"
70+
const val ALL_ACCEPTED_CREDENTIAL_IDS = "allAcceptedCredentialIds"
71+
const val NAME = "name"
72+
const val DISPLAY_NAME = "displayName"
73+
}
74+
5375
/**
5476
* Retrieves a passkey or password credential from the credential manager.
5577
*
@@ -108,7 +130,7 @@ class CredentialManagerUtils @Inject constructor(
108130
.setServerClientId(SERVER_CLIENT_ID)
109131
.setFilterByAuthorizedAccounts(false)
110132
.build(),
111-
)
133+
),
112134
)
113135
result = credentialManager.getCredential(context, credentialRequest)
114136
} catch (e: GetCredentialCancellationException) {
@@ -249,6 +271,73 @@ class CredentialManagerUtils @Inject constructor(
249271
val clearRequest = ClearCredentialStateRequest(requestType = TYPE_CLEAR_RESTORE_CREDENTIAL)
250272
credentialManager.clearCredentialState(clearRequest)
251273
}
274+
275+
@SuppressLint("RestrictedApi")
276+
suspend fun signalUnknown(
277+
credentialId: String,
278+
) {
279+
dataStore.read(RP_ID_KEY)?.let { rpId ->
280+
credentialManager.signalCredentialState(
281+
request = SignalUnknownCredentialRequest(
282+
requestJson = JSONObject().apply {
283+
put(JSON_KEYS.RP_ID, rpId)
284+
put(JSON_KEYS.CREDENTIAL_ID, credentialId)
285+
}.toString()
286+
),
287+
)
288+
} ?: Log.e(TAG, "RP ID not present")
289+
}
290+
291+
@SuppressLint("RestrictedApi")
292+
suspend fun signalAcceptedIds(
293+
credentialIds: List<String>,
294+
) {
295+
fetchDataAndPerformAction { rpId, userId ->
296+
credentialManager.signalCredentialState(
297+
request = SignalAllAcceptedCredentialIdsRequest(
298+
requestJson = JSONObject().apply {
299+
put(JSON_KEYS.RP_ID, rpId)
300+
put(JSON_KEYS.USER_ID, userId)
301+
put(JSON_KEYS.ALL_ACCEPTED_CREDENTIAL_IDS, JSONArray(credentialIds))
302+
}.toString()
303+
),
304+
)
305+
}
306+
}
307+
308+
@SuppressLint("RestrictedApi")
309+
suspend fun signalUserDetails(
310+
newName: String,
311+
newDisplayName: String,
312+
) {
313+
fetchDataAndPerformAction { rpId, userId ->
314+
credentialManager.signalCredentialState(
315+
request = SignalCurrentUserDetailsRequest(
316+
requestJson = JSONObject().apply {
317+
put(JSON_KEYS.RP_ID, rpId)
318+
put(JSON_KEYS.USER_ID, userId)
319+
put(JSON_KEYS.NAME, newName)
320+
put(JSON_KEYS.DISPLAY_NAME, newDisplayName)
321+
}.toString()
322+
),
323+
)
324+
}
325+
}
326+
327+
suspend fun fetchDataAndPerformAction(
328+
credentialManagerAction: suspend (rpId: String, userId: String) -> Unit
329+
) {
330+
val rpId = dataStore.read(RP_ID_KEY)
331+
val userId = dataStore.read(USER_ID_KEY)
332+
333+
if (rpId.isNullOrBlank()) {
334+
Log.e(TAG, "RP ID not present")
335+
} else if (userId.isNullOrBlank()) {
336+
Log.e(TAG, "User ID not present")
337+
} else {
338+
credentialManagerAction(rpId, userId)
339+
}
340+
}
252341
}
253342

254343
sealed class GenericCredentialManagerResponse {

Shrine/app/src/main/java/com/authentication/shrine/ShrineApplication.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,12 @@ object AppModule {
109109
@Provides
110110
fun providesCredentialManagerUtils(
111111
credentialManager: CredentialManager,
112+
dataStore: DataStore<Preferences>,
112113
): CredentialManagerUtils {
113-
return CredentialManagerUtils(credentialManager)
114+
return CredentialManagerUtils(
115+
credentialManager = credentialManager,
116+
dataStore = dataStore,
117+
)
114118
}
115119

116120
@Singleton

Shrine/app/src/main/java/com/authentication/shrine/api/AuthApiService.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ interface AuthApiService {
166166
*/
167167
@POST("federation/options")
168168
suspend fun getFederationOptions(
169-
@Body urls: FederationOptionsRequest
169+
@Body urls: FederationOptionsRequest,
170170
): Response<GenericAuthResponse>
171171

172172
/**
Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,24 @@
1+
/*
2+
* Copyright 2025 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
116
package com.authentication.shrine.model
217

318
/**
419
* Represents the request body for getting federation options from the server.
520
* @param urls a list of urls to send for federated requests.
621
*/
722
data class FederationOptionsRequest(
8-
val urls: List<String> = listOf("https://accounts.google.com")
9-
)
23+
val urls: List<String> = listOf("https://accounts.google.com"),
24+
)

Shrine/app/src/main/java/com/authentication/shrine/model/PasskeysList.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,5 @@ data class PasskeyCredential(
4747
val aaguid: String,
4848
val registeredAt: Long,
4949
val providerIcon: String,
50+
val isSelected: Boolean = false,
5051
)
Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,18 @@
1+
/*
2+
* Copyright 2025 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
116
package com.authentication.shrine.model
217

318
/**
@@ -7,5 +22,5 @@ package com.authentication.shrine.model
722
*/
823
data class SignInWithGoogleRequest(
924
val token: String,
10-
val url: String = "https://accounts.google.com"
11-
)
25+
val url: String = "https://accounts.google.com",
26+
)

Shrine/app/src/main/java/com/authentication/shrine/repository/AuthRepository.kt

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ class AuthRepository @Inject constructor(
8181
val DISPLAYNAME = stringPreferencesKey("displayname")
8282
val IS_SIGNED_IN_THROUGH_PASSKEYS = booleanPreferencesKey("is_signed_passkeys")
8383
val SESSION_ID = stringPreferencesKey("session_id")
84+
val RP_ID_KEY = stringPreferencesKey("rp_id_key")
85+
val USER_ID_KEY = stringPreferencesKey("user_id_key")
86+
val CRED_ID = stringPreferencesKey("cred_id")
8487
val RESTORE_KEY_CREDENTIAL_ID = stringPreferencesKey("restore_key_credential_id")
8588

8689
// Value for restore credential AuthApiService parameter
@@ -163,7 +166,6 @@ class AuthRepository @Inject constructor(
163166
}
164167
}
165168

166-
167169
/**
168170
* Signs in with a password.
169171
*
@@ -231,6 +233,7 @@ class AuthRepository @Inject constructor(
231233
)
232234
if (response.isSuccessful) {
233235
dataStore.edit { prefs ->
236+
prefs[RP_ID_KEY] = response.body()?.rp?.id ?: ""
234237
response.getSessionId()?.also {
235238
prefs[SESSION_ID] = it
236239
}
@@ -304,6 +307,7 @@ class AuthRepository @Inject constructor(
304307
)
305308
if (apiResult.isSuccessful) {
306309
dataStore.edit { prefs ->
310+
prefs[CRED_ID] = rawId
307311
if (credentialResponse is CreateRestoreCredentialResponse) {
308312
prefs[RESTORE_KEY_CREDENTIAL_ID] = rawId
309313
}
@@ -338,6 +342,7 @@ class AuthRepository @Inject constructor(
338342
val response = authApiService.signInRequest()
339343
if (response.isSuccessful) {
340344
dataStore.edit { prefs ->
345+
prefs[RP_ID_KEY] = response.body()?.rpId ?: ""
341346
response.getSessionId()?.also {
342347
prefs[SESSION_ID] = it
343348
}
@@ -401,6 +406,7 @@ class AuthRepository @Inject constructor(
401406
)
402407
return if (apiResult.isSuccessful) {
403408
dataStore.edit { prefs ->
409+
prefs[CRED_ID] = credentialId
404410
apiResult.getSessionId()?.also {
405411
prefs[SESSION_ID] = it
406412
}
@@ -453,7 +459,7 @@ class AuthRepository @Inject constructor(
453459
val isSuccess = verifyIdToken(
454460
sessionId,
455461
GoogleIdTokenCredential
456-
.createFrom(credential.data).idToken
462+
.createFrom(credential.data).idToken,
457463
)
458464
if (isSuccess) {
459465
AuthResult.Success(Unit)
@@ -581,6 +587,9 @@ class AuthRepository @Inject constructor(
581587
cookie = sessionId.createCookieHeader(),
582588
)
583589
if (apiResult.isSuccessful) {
590+
dataStore.edit { prefs ->
591+
prefs[USER_ID_KEY] = apiResult.body()?.userId ?: ""
592+
}
584593
return apiResult.body()
585594
} else if (apiResult.code() == 401) {
586595
signOut()
@@ -673,7 +682,7 @@ class AuthRepository @Inject constructor(
673682
suspend fun verifyIdToken(sessionId: String, token: String): Boolean {
674683
val apiResult = authApiService.verifyIdToken(
675684
cookie = sessionId.createCookieHeader(),
676-
requestParams = SignInWithGoogleRequest(token = token)
685+
requestParams = SignInWithGoogleRequest(token = token),
677686
)
678687

679688
if (apiResult.isSuccessful) {

Shrine/app/src/main/java/com/authentication/shrine/ui/AuthenticationScreen.kt

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ fun AuthenticationScreen(
6969
credentialManagerUtils: CredentialManagerUtils,
7070
) {
7171
val uiState = viewModel.uiState.collectAsState().value
72+
var isFirstCheckForRestoreKey by remember { mutableStateOf(true) }
7273

7374
// Passing in the lambda / context to the VM
7475
val context = LocalContext.current
@@ -113,6 +114,18 @@ fun AuthenticationScreen(
113114
)
114115
}
115116

117+
if (isFirstCheckForRestoreKey) {
118+
isFirstCheckForRestoreKey = false
119+
viewModel.checkForStoredRestoreKey(
120+
getRestoreKey = { requestResult ->
121+
credentialManagerUtils.getRestoreKey(requestResult, context)
122+
},
123+
onSuccess = {
124+
navigateToHome(true)
125+
},
126+
)
127+
}
128+
116129
AuthenticationScreen(
117130
onSignInWithPasskeyOrPasswordRequest = onSignInWithPasskeyOrPasswordRequest,
118131
onSignInWithSignInWithGoogleRequest = onSignInWithSignInWithGoogleRequest,
@@ -185,7 +198,8 @@ fun AuthenticationScreen(
185198
.height(dimensionResource(R.dimen.siwg_button_height))
186199
.clickable(
187200
enabled = !uiState.isLoading,
188-
onClick = onSignInWithSignInWithGoogleRequest)
201+
onClick = onSignInWithSignInWithGoogleRequest
202+
)
189203
)
190204
}
191205
}
@@ -201,6 +215,7 @@ fun AuthenticationScreen(
201215
!uiState.isSignInWithPasskeysSuccess && stringResource(uiState.passkeyResponseMessageResourceId).isNotBlank() -> {
202216
stringResource(uiState.passkeyResponseMessageResourceId)
203217
}
218+
204219
else -> null
205220
}
206221
LaunchedEffect(uiState) {

0 commit comments

Comments
 (0)