Skip to content

Commit 48a0127

Browse files
neelanshsahaiNeelansh Sahai
and
Neelansh Sahai
authored
Add snippets for migration from Fido2 to Credman (#495)
Co-authored-by: Neelansh Sahai <[email protected]>
1 parent 4493417 commit 48a0127

File tree

3 files changed

+249
-1
lines changed

3 files changed

+249
-1
lines changed

Diff for: gradle/libs.versions.toml

+4
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ horologist = "0.6.22"
4343
junit = "4.13.2"
4444
kotlin = "2.1.10"
4545
kotlinxCoroutinesGuava = "1.9.0"
46+
kotlinCoroutinesOkhttp = "1.0"
4647
kotlinxSerializationJson = "1.8.0"
4748
ksp = "2.1.10-1.0.30"
4849
maps-compose = "6.4.4"
@@ -65,6 +66,7 @@ wearComposeFoundation = "1.4.1"
6566
wearComposeMaterial = "1.4.1"
6667
wearToolingPreview = "1.0.0"
6768
activityKtx = "1.10.0"
69+
okHttp = "4.12.0"
6870

6971
[libraries]
7072
accompanist-adaptive = { module = "com.google.accompanist:accompanist-adaptive", version.ref = "accompanist" }
@@ -158,13 +160,15 @@ hilt-compiler = { module = "com.google.dagger:hilt-android-compiler", version.re
158160
horologist-compose-layout = { module = "com.google.android.horologist:horologist-compose-layout", version.ref = "horologist" }
159161
horologist-compose-material = { module = "com.google.android.horologist:horologist-compose-material", version.ref = "horologist" }
160162
junit = { module = "junit:junit", version.ref = "junit" }
163+
kotlin-coroutines-okhttp = { module = "ru.gildor.coroutines:kotlin-coroutines-okhttp", version.ref = "kotlinCoroutinesOkhttp" }
161164
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" }
162165
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
163166
kotlinx-coroutines-guava = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-guava", version.ref = "kotlinxCoroutinesGuava" }
164167
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }
165168
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
166169
play-services-wearable = { module = "com.google.android.gms:play-services-wearable", version.ref = "playServicesWearable" }
167170
androidx-activity-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref = "activityKtx" }
171+
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okHttp" }
168172

169173
[plugins]
170174
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }

Diff for: identity/credentialmanager/build.gradle.kts

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
plugins {
22
alias(libs.plugins.android.application)
3+
// [START android_identity_fido2_migration_dependency]
34
alias(libs.plugins.kotlin.android)
5+
// [END android_identity_fido2_migration_dependency]
46
alias(libs.plugins.compose.compiler)
57
}
68

@@ -60,6 +62,8 @@ dependencies {
6062
implementation(libs.androidx.credentials.play.services.auth)
6163
implementation(libs.android.identity.googleid)
6264
// [END android_identity_siwg_gradle_dependencies]
65+
implementation(libs.okhttp)
66+
implementation(libs.kotlin.coroutines.okhttp)
6367
debugImplementation(libs.androidx.compose.ui.tooling)
6468
debugImplementation(libs.androidx.compose.ui.test.manifest)
65-
}
69+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
package com.example.identity.credentialmanager
2+
3+
import android.app.Activity
4+
import android.content.Context
5+
import android.util.JsonWriter
6+
import android.util.Log
7+
import android.widget.Toast
8+
import androidx.credentials.CreateCredentialRequest
9+
import androidx.credentials.CreatePublicKeyCredentialRequest
10+
import androidx.credentials.CreatePublicKeyCredentialResponse
11+
import androidx.credentials.CredentialManager
12+
import androidx.credentials.GetCredentialRequest
13+
import androidx.credentials.GetCredentialResponse
14+
import androidx.credentials.GetPasswordOption
15+
import androidx.credentials.GetPublicKeyCredentialOption
16+
import androidx.credentials.PublicKeyCredential
17+
import androidx.credentials.exceptions.CreateCredentialException
18+
import com.example.identity.credentialmanager.ApiResult.Success
19+
import okhttp3.MediaType.Companion.toMediaTypeOrNull
20+
import okhttp3.OkHttpClient
21+
import okhttp3.Request.Builder
22+
import okhttp3.RequestBody
23+
import okhttp3.RequestBody.Companion.toRequestBody
24+
import okhttp3.Response
25+
import okhttp3.ResponseBody
26+
import org.json.JSONObject
27+
import java.io.StringWriter
28+
import ru.gildor.coroutines.okhttp.await
29+
30+
class Fido2ToCredmanMigration(
31+
private val context: Context,
32+
private val client: OkHttpClient,
33+
) {
34+
private val BASE_URL = ""
35+
private val JSON = "".toMediaTypeOrNull()
36+
private val PUBLIC_KEY = ""
37+
38+
// [START android_identity_fido2_credman_init]
39+
val credMan = CredentialManager.create(context)
40+
// [END android_identity_fido2_credman_init]
41+
42+
// [START android_identity_fido2_migration_post_request_body]
43+
suspend fun registerRequest() {
44+
// ...
45+
val call = client.newCall(
46+
Builder()
47+
.method("POST", jsonRequestBody {
48+
name("attestation").value("none")
49+
name("authenticatorSelection").objectValue {
50+
name("residentKey").value("required")
51+
}
52+
}).build()
53+
)
54+
// ...
55+
}
56+
// [END android_identity_fido2_migration_post_request_body]
57+
58+
// [START android_identity_fido2_migration_register_request]
59+
suspend fun registerRequest(sessionId: String): ApiResult<JSONObject> {
60+
val call = client.newCall(
61+
Builder()
62+
.url("$BASE_URL/<your api url>")
63+
.addHeader("Cookie", formatCookie(sessionId))
64+
.method("POST", jsonRequestBody {
65+
name("attestation").value("none")
66+
name("authenticatorSelection").objectValue {
67+
name("authenticatorAttachment").value("platform")
68+
name("userVerification").value("required")
69+
name("residentKey").value("required")
70+
}
71+
}).build()
72+
)
73+
val response = call.await()
74+
return response.result("Error calling the api") {
75+
parsePublicKeyCredentialCreationOptions(
76+
body ?: throw ApiException("Empty response from the api call")
77+
)
78+
}
79+
}
80+
// [END android_identity_fido2_migration_register_request]
81+
82+
// [START android_identity_fido2_migration_create_passkey]
83+
suspend fun createPasskey(
84+
activity: Activity,
85+
requestResult: JSONObject
86+
): CreatePublicKeyCredentialResponse? {
87+
val request = CreatePublicKeyCredentialRequest(requestResult.toString())
88+
var response: CreatePublicKeyCredentialResponse? = null
89+
try {
90+
response = credMan.createCredential(
91+
request = request as CreateCredentialRequest,
92+
context = activity
93+
) as CreatePublicKeyCredentialResponse
94+
} catch (e: CreateCredentialException) {
95+
96+
showErrorAlert(activity, e)
97+
98+
return null
99+
}
100+
return response
101+
}
102+
// [END android_identity_fido2_migration_create_passkey]
103+
104+
// [START android_identity_fido2_migration_auth_with_passkeys]
105+
/**
106+
* @param sessionId The session ID to be used for the sign-in.
107+
* @param credentialId The credential ID of this device.
108+
* @return a JSON object.
109+
*/
110+
suspend fun signinRequest(): ApiResult<JSONObject> {
111+
val call = client.newCall(Builder().url(buildString {
112+
append("$BASE_URL/signinRequest")
113+
}).method("POST", jsonRequestBody {})
114+
.build()
115+
)
116+
val response = call.await()
117+
return response.result("Error calling /signinRequest") {
118+
parsePublicKeyCredentialRequestOptions(
119+
body ?: throw ApiException("Empty response from /signinRequest")
120+
)
121+
}
122+
}
123+
124+
/**
125+
* @param sessionId The session ID to be used for the sign-in.
126+
* @param response The JSONObject for signInResponse.
127+
* @param credentialId id/rawId.
128+
* @return A list of all the credentials registered on the server,
129+
* including the newly-registered one.
130+
*/
131+
suspend fun signinResponse(
132+
sessionId: String, response: JSONObject, credentialId: String
133+
): ApiResult<Unit> {
134+
135+
val call = client.newCall(
136+
Builder().url("$BASE_URL/signinResponse")
137+
.addHeader("Cookie",formatCookie(sessionId))
138+
.method("POST", jsonRequestBody {
139+
name("id").value(credentialId)
140+
name("type").value(PUBLIC_KEY.toString())
141+
name("rawId").value(credentialId)
142+
name("response").objectValue {
143+
name("clientDataJSON").value(
144+
response.getString("clientDataJSON")
145+
)
146+
name("authenticatorData").value(
147+
response.getString("authenticatorData")
148+
)
149+
name("signature").value(
150+
response.getString("signature")
151+
)
152+
name("userHandle").value(
153+
response.getString("userHandle")
154+
)
155+
}
156+
}).build()
157+
)
158+
val apiResponse = call.await()
159+
return apiResponse.result("Error calling /signingResponse") {
160+
}
161+
}
162+
// [END android_identity_fido2_migration_auth_with_passkeys]
163+
164+
// [START android_identity_fido2_migration_get_passkeys]
165+
suspend fun getPasskey(
166+
activity: Activity,
167+
creationResult: JSONObject
168+
): GetCredentialResponse? {
169+
Toast.makeText(
170+
activity,
171+
"Fetching previously stored credentials",
172+
Toast.LENGTH_SHORT)
173+
.show()
174+
var result: GetCredentialResponse? = null
175+
try {
176+
val request= GetCredentialRequest(
177+
listOf(
178+
GetPublicKeyCredentialOption(
179+
creationResult.toString(),
180+
null
181+
),
182+
GetPasswordOption()
183+
)
184+
)
185+
result = credMan.getCredential(activity, request)
186+
if (result.credential is PublicKeyCredential) {
187+
val publicKeycredential = result.credential as PublicKeyCredential
188+
Log.i("TAG", "Passkey ${publicKeycredential.authenticationResponseJson}")
189+
return result
190+
}
191+
} catch (e: Exception) {
192+
showErrorAlert(activity, e)
193+
}
194+
return result
195+
}
196+
// [END android_identity_fido2_migration_get_passkeys]
197+
198+
private fun showErrorAlert(
199+
activity: Activity,
200+
e: Exception
201+
) {}
202+
203+
private fun jsonRequestBody(body: JsonWriter.() -> Unit): RequestBody {
204+
val output = StringWriter()
205+
JsonWriter(output).use { writer ->
206+
writer.beginObject()
207+
writer.body()
208+
writer.endObject()
209+
}
210+
return output.toString().toRequestBody(JSON)
211+
}
212+
213+
private fun JsonWriter.objectValue(body: JsonWriter.() -> Unit) {
214+
beginObject()
215+
body()
216+
endObject()
217+
}
218+
219+
private fun formatCookie(sessionId: String): String {
220+
return ""
221+
}
222+
223+
private fun parsePublicKeyCredentialCreationOptions(body: ResponseBody): JSONObject {
224+
return JSONObject()
225+
}
226+
227+
private fun parsePublicKeyCredentialRequestOptions(body: ResponseBody): JSONObject {
228+
return JSONObject()
229+
}
230+
231+
private fun <T> Response.result(errorMessage: String, data: Response.() -> T): ApiResult<T> {
232+
return Success()
233+
}
234+
}
235+
236+
sealed class ApiResult<out R> {
237+
class Success<T>: ApiResult<T>()
238+
}
239+
240+
class ApiException(message: String) : RuntimeException(message)

0 commit comments

Comments
 (0)