1+ package com.auth0.android.myaccount
2+
3+ import androidx.annotation.VisibleForTesting
4+ import com.auth0.android.Auth0
5+ import com.auth0.android.Auth0Exception
6+ import com.auth0.android.NetworkErrorException
7+ import com.auth0.android.authentication.ParameterBuilder
8+ import com.auth0.android.request.ErrorAdapter
9+ import com.auth0.android.request.JsonAdapter
10+ import com.auth0.android.request.PublicKeyCredentials
11+ import com.auth0.android.request.Request
12+ import com.auth0.android.request.internal.GsonAdapter
13+ import com.auth0.android.request.internal.GsonAdapter.Companion.forMap
14+ import com.auth0.android.request.internal.GsonProvider
15+ import com.auth0.android.request.internal.RequestFactory
16+ import com.auth0.android.request.internal.ResponseUtils.isNetworkError
17+ import com.auth0.android.result.PasskeyAuthenticationMethod
18+ import com.auth0.android.result.PasskeyEnrollmentChallenge
19+ import com.auth0.android.result.PasskeyRegistrationChallenge
20+ import com.google.gson.Gson
21+ import okhttp3.HttpUrl
22+ import okhttp3.HttpUrl.Companion.toHttpUrl
23+ import java.io.IOException
24+ import java.io.Reader
25+ import java.net.URLDecoder
26+
27+
28+ /* *
29+ * Auth0 My Account API client for managing the current user's account.
30+ *
31+ * You can use the refresh token to get an access token for the My Account API. Refer to [com.auth0.android.authentication.storage.CredentialsManager.getApiCredentials]
32+ * , or alternatively [com.auth0.android.authentication.AuthenticationAPIClient.renewAuth] if you are not using CredentialsManager.
33+ *
34+ * ## Usage
35+ * ```kotlin
36+ * val auth0 = Auth0.getInstance("YOUR_CLIENT_ID", "YOUR_DOMAIN")
37+ * val client = MyAccountAPIClient(auth0,accessToken)
38+ * ```
39+ *
40+ *
41+ */
42+ public class MyAccountAPIClient @VisibleForTesting(otherwise = VisibleForTesting .PRIVATE ) internal constructor(
43+ private val auth0 : Auth0 ,
44+ private val accessToken : String ,
45+ private val factory : RequestFactory <MyAccountException >,
46+ private val gson : Gson
47+ ) {
48+
49+ /* *
50+ * Creates a new MyAccountAPI client instance.
51+ *
52+ * Example usage:
53+ *
54+ * ```
55+ * val auth0 = Auth0.getInstance("YOUR_CLIENT_ID", "YOUR_DOMAIN")
56+ * val client = MyAccountAPIClient(auth0, accessToken)
57+ * ```
58+ * @param auth0 account information
59+ */
60+ public constructor (
61+ auth0: Auth0 ,
62+ accessToken: String
63+ ) : this (
64+ auth0,
65+ accessToken,
66+ RequestFactory <MyAccountException >(auth0.networkingClient, createErrorAdapter()),
67+ Gson ()
68+ )
69+
70+
71+ /* *
72+ * Requests a challenge for enrolling a new passkey. This is the first part of the enrollment flow.
73+ *
74+ * You can specify an optional user identity identifier and an optional database connection name.
75+ * If a connection name is not specified, your tenant's default directory will be used.
76+ *
77+ * ## Availability
78+ *
79+ * This feature is currently available in
80+ * [Early Access](https://auth0.com/docs/troubleshoot/product-lifecycle/product-release-stages#early-access).
81+ * Please reach out to Auth0 support to get it enabled for your tenant.
82+ *
83+ * ## Scopes Required
84+ *
85+ * `create:me:authentication_methods`
86+ *
87+ * ## Usage
88+ *
89+ * ```kotlin
90+ * val auth0 = Auth0.getInstance("YOUR_CLIENT_ID", "YOUR_DOMAIN")
91+ * val apiClient = MyAccountAPIClient(auth0, accessToken)
92+ *
93+ * apiClient.passkeyEnrollmentChallenge()
94+ * .start(object : Callback<PasskeyEnrollmentChallenge, MyAccountException> {
95+ * override fun onSuccess(result: PasskeyEnrollmentChallenge) {
96+ * // Use the challenge with Credential Manager API to generate a new passkey credential
97+ * Log.d("MyApp", "Obtained enrollment challenge: $result")
98+ * }
99+ *
100+ * override fun onFailure(error: MyAccountException) {
101+ * Log.e("MyApp", "Failed with: ${error.message}")
102+ * }
103+ * })
104+ * ```
105+ * Use the challenge with [Google Credential Manager API](https://developer.android.com/identity/sign-in/credential-manager) to generate a new passkey credential.
106+ *
107+ * ``` kotlin
108+ * CreatePublicKeyCredentialRequest( Gson().
109+ * toJson( passkeyEnrollmentChallenge.authParamsPublicKey ))
110+ * var response: CreatePublicKeyCredentialResponse?
111+ * credentialManager.createCredentialAsync(
112+ * requireContext(),
113+ * request,
114+ * CancellationSignal(),
115+ * Executors.newSingleThreadExecutor(),
116+ * object :
117+ * CredentialManagerCallback<CreateCredentialResponse, CreateCredentialException> {
118+ * override fun onError(e: CreateCredentialException) {
119+ * }
120+ *
121+ * override fun onResult(result: CreateCredentialResponse) {
122+ * response = result as CreatePublicKeyCredentialResponse
123+ * val credentials = Gson().fromJson(
124+ * response?.registrationResponseJson, PublicKeyCredentials::class.java
125+ * )
126+ * }
127+ * ```
128+ *
129+ * Then, call ``enroll()`` with the created passkey credential and the challenge to complete
130+ * the enrollment.
131+ *
132+ * @param userIdentity Unique identifier of the current user's identity. Needed if the user logged in with a [linked account](https://auth0.com/docs/manage-users/user-accounts/user-account-linking)
133+ * @param connection Name of the database connection where the user is stored
134+ * @return A request to obtain a passkey enrollment challenge
135+ *
136+ * */
137+ public fun passkeyEnrollmentChallenge (
138+ userIdentity : String? = null, connection : String? = null
139+ ): Request <PasskeyEnrollmentChallenge , MyAccountException > {
140+
141+ val url = getDomainUrlBuilder()
142+ .addPathSegment(AUTHENTICATION_METHODS )
143+ .build()
144+
145+ val params = ParameterBuilder .newBuilder().apply {
146+ set(TYPE_KEY , " passkey" )
147+ userIdentity?.let {
148+ set(USER_IDENTITY_ID_KEY , userIdentity)
149+ }
150+ connection?.let {
151+ set(CONNECTION_KEY , connection)
152+ }
153+ }.asDictionary()
154+
155+ val passkeyEnrollmentAdapter: JsonAdapter <PasskeyEnrollmentChallenge > =
156+ object : JsonAdapter <PasskeyEnrollmentChallenge > {
157+ override fun fromJson (
158+ reader : Reader , metadata : Map <String , Any >
159+ ): PasskeyEnrollmentChallenge {
160+ val headers = metadata.mapValues { (_, value) ->
161+ when (value) {
162+ is List <* > -> value.filterIsInstance<String >()
163+ else -> emptyList()
164+ }
165+ }
166+ val locationHeader = headers[LOCATION_KEY ]?.get(0 )?.split(" /" )?.lastOrNull()
167+ locationHeader ? : throw MyAccountException (" Authentication method ID not found" )
168+ val authenticationId =
169+ URLDecoder .decode(
170+ locationHeader,
171+ " UTF-8"
172+ )
173+
174+ val passkeyRegistrationChallenge = gson.fromJson<PasskeyRegistrationChallenge >(
175+ reader, PasskeyRegistrationChallenge ::class .java
176+ )
177+ return PasskeyEnrollmentChallenge (
178+ authenticationId,
179+ passkeyRegistrationChallenge.authSession,
180+ passkeyRegistrationChallenge.authParamsPublicKey
181+ )
182+ }
183+ }
184+ val post = factory.post(url.toString(), passkeyEnrollmentAdapter)
185+ .addParameters(params)
186+ .addHeader(AUTHORIZATION_KEY , " Bearer $accessToken " )
187+
188+ return post
189+ }
190+
191+ /* *
192+ * Enrolls a new passkey credential. This is the last part of the enrollment flow.
193+ *
194+ * ## Availability
195+ *
196+ * This feature is currently available in
197+ * [Early Access](https://auth0.com/docs/troubleshoot/product-lifecycle/product-release-stages#early-access).
198+ * Please reach out to Auth0 support to get it enabled for your tenant.
199+ *
200+ * ## Scopes Required
201+ *
202+ * `create:me:authentication_methods`
203+ *
204+ * ## Usage
205+ *
206+ * ```kotlin
207+ * val auth0 = Auth0.getInstance("YOUR_CLIENT_ID", "YOUR_DOMAIN")
208+ * val apiClient = MyAccountAPIClient(auth0, accessToken)
209+ *
210+ * // After obtaining the passkey credential from the [Credential Manager API](https://developer.android.com/identity/sign-in/credential-manager)
211+ * apiClient.enroll(publicKeyCredentials, enrollmentChallenge)
212+ * .start(object : Callback<PasskeyAuthenticationMethod, MyAccountException> {
213+ * override fun onSuccess(result: AuthenticationMethodVerified) {
214+ * Log.d("MyApp", "Enrolled passkey: $result")
215+ * }
216+ *
217+ * override fun onFailure(error: MyAccountException) {
218+ * Log.e("MyApp", "Failed with: ${error.message}")
219+ * }
220+ * })
221+ * ```
222+ *
223+ * @param credentials The passkey credentials obtained from the [Credential Manager API](https://developer.android.com/identity/sign-in/credential-manager).
224+ * @param challenge The enrollment challenge obtained from the `passkeyEnrollmentChallenge()` method.
225+ * @return A request to enroll the passkey credential.
226+ */
227+ public fun enroll (
228+ credentials : PublicKeyCredentials , challenge : PasskeyEnrollmentChallenge
229+ ): Request <PasskeyAuthenticationMethod , MyAccountException > {
230+ val authMethodId = challenge.authenticationMethodId
231+ val url =
232+ getDomainUrlBuilder()
233+ .addPathSegment(AUTHENTICATION_METHODS )
234+ .addPathSegment(authMethodId)
235+ .addPathSegment(VERIFY )
236+ .build()
237+
238+ val authenticatorResponse = mapOf (
239+ " authenticatorAttachment" to " platform" ,
240+ " clientExtensionResults" to credentials.clientExtensionResults,
241+ " id" to credentials.id,
242+ " rawId" to credentials.rawId,
243+ " type" to " public-key" ,
244+ " response" to mapOf (
245+ " clientDataJSON" to credentials.response.clientDataJSON,
246+ " attestationObject" to credentials.response.attestationObject
247+ )
248+ )
249+
250+ val params = ParameterBuilder .newBuilder().apply {
251+ set(AUTH_SESSION_KEY , challenge.authSession)
252+ }.asDictionary()
253+
254+ val passkeyAuthenticationAdapter = GsonAdapter (
255+ PasskeyAuthenticationMethod ::class .java
256+ )
257+
258+ val request = factory.post(
259+ url.toString(), passkeyAuthenticationAdapter
260+ ).addParameters(params)
261+ .addParameter(AUTHN_RESPONSE_KEY , authenticatorResponse)
262+ .addHeader(AUTHORIZATION_KEY , " Bearer $accessToken " )
263+ return request
264+ }
265+
266+ private fun getDomainUrlBuilder (): HttpUrl .Builder {
267+ return auth0.getDomainUrl().toHttpUrl().newBuilder()
268+ .addPathSegment(ME_PATH )
269+ .addPathSegment(API_VERSION )
270+ }
271+
272+
273+ private companion object {
274+ private const val AUTHENTICATION_METHODS = " authentication-methods"
275+ private const val VERIFY = " verify"
276+ private const val API_VERSION = " v1"
277+ private const val ME_PATH = " me"
278+ private const val TYPE_KEY = " type"
279+ private const val USER_IDENTITY_ID_KEY = " identity_user_id"
280+ private const val CONNECTION_KEY = " connection"
281+ private const val AUTHORIZATION_KEY = " Authorization"
282+ private const val LOCATION_KEY = " location"
283+ private const val AUTH_SESSION_KEY = " auth_session"
284+ private const val AUTHN_RESPONSE_KEY = " authn_response"
285+ private fun createErrorAdapter (): ErrorAdapter <MyAccountException > {
286+ val mapAdapter = forMap(GsonProvider .gson)
287+ return object : ErrorAdapter <MyAccountException > {
288+ override fun fromRawResponse (
289+ statusCode : Int , bodyText : String , headers : Map <String , List <String >>
290+ ): MyAccountException {
291+ return MyAccountException (bodyText, statusCode)
292+ }
293+
294+ @Throws(IOException ::class )
295+ override fun fromJsonResponse (
296+ statusCode : Int , reader : Reader
297+ ): MyAccountException {
298+ val values = mapAdapter.fromJson(reader)
299+ return MyAccountException (values, statusCode)
300+ }
301+
302+ override fun fromException (cause : Throwable ): MyAccountException {
303+ if (isNetworkError(cause)) {
304+ return MyAccountException (
305+ " Failed to execute the network request" , NetworkErrorException (cause)
306+ )
307+ }
308+ return MyAccountException (
309+ cause.message ? : " Something went wrong" ,
310+ Auth0Exception (cause.message ? : " Something went wrong" , cause)
311+ )
312+ }
313+ }
314+ }
315+ }
316+ }
0 commit comments