Skip to content

Commit 9fb47c8

Browse files
authored
Support for Passkey Authentication (#770)
2 parents 7729927 + 2bcffdc commit 9fb47c8

24 files changed

+1460
-50
lines changed

EXAMPLES.md

Lines changed: 75 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
- [Specify Parameter](#specify-parameter)
99
- [Customize the Custom Tabs UI](#customize-the-custom-tabs-ui)
1010
- [Changing the Return To URL scheme](#changing-the-return-to-url-scheme)
11-
- [Trusted Web Activity](#trusted-web-activity-experimental)
11+
- [Trusted Web Activity](#trusted-web-activity)
1212
- [Authentication API](#authentication-api)
1313
- [Login with database connection](#login-with-database-connection)
1414
- [Login using MFA with One Time Password code](#login-using-mfa-with-one-time-password-code)
@@ -22,6 +22,7 @@
2222
- [Usage](#usage)
2323
- [Requiring Authentication](#requiring-authentication)
2424
- [Handling Credentials Manager exceptions](#handling-credentials-manager-exceptions)
25+
- [Passkeys](#passkeys)
2526
- [Bot Protection](#bot-protection)
2627
- [Management API](#management-api)
2728
- [Link users](#link-users)
@@ -131,11 +132,7 @@ WebAuthProvider.logout(account)
131132
.start(this, logoutCallback)
132133
```
133134

134-
## Trusted Web Activity (Experimental)
135-
> **Warning**
136-
> Trusted Web Activity support in Auth0.Android is still experimental and can change in the future.
137-
>
138-
> Please test it thoroughly in all the targeted browsers and OS variants and let us know your feedback.
135+
## Trusted Web Activity
139136

140137
Trusted Web Activity is a feature provided by some browsers to provide a native look and feel to the custom tabs.
141138

@@ -607,6 +604,76 @@ when(credentialsManagerException) {
607604
}
608605
```
609606

607+
## Passkeys
608+
User should have a custom domain configured and passkey grant-type enabled in the Auth0 dashboard to use passkeys.
609+
610+
To sign up a user with passkey
611+
612+
```kotlin
613+
PasskeyAuthProvider.signUp(account)
614+
.setEmail("user email")
615+
.setUserName("user name")
616+
.setPhoneNumber("phone number")
617+
.setRealm("optional connection name")
618+
.start(object: Callback<Credentials, AuthenticationException> {
619+
override fun onFailure(exception: AuthenticationException) { }
620+
621+
override fun onSuccess(credentials: Credentials) { }
622+
})
623+
```
624+
<details>
625+
<summary>Using Java</summary>
626+
627+
```java
628+
PasskeyAuthProvider authProvider = new PasskeyAuthProvider();
629+
authProvider.signUp(account)
630+
.setEmail("user email")
631+
.setUserName("user name")
632+
.setPhoneNumber("phone number")
633+
.setRealm("optional connection name")
634+
.start(new Callback<Credentials, AuthenticationException>() {
635+
@Override
636+
public void onFailure(@NonNull AuthenticationException exception) { }
637+
638+
@Override
639+
public void onSuccess(@Nullable Credentials credentials) { }
640+
});
641+
```
642+
</details>
643+
644+
To sign in a user with passkey
645+
```kotlin
646+
PasskeyAuthProvider.signin(account)
647+
.setRealm("Optional connection name")
648+
.start(object: Callback<Credentials, AuthenticationException> {
649+
override fun onFailure(exception: AuthenticationException) { }
650+
651+
override fun onSuccess(credentials: Credentials) { }
652+
})
653+
```
654+
<details>
655+
<summary>Using Java</summary>
656+
657+
```java
658+
PasskeyAuthProvider authProvider = new PasskeyAuthProvider();
659+
authProvider.signin(account)
660+
.setRealm("optional connection name")
661+
.start(new Callback<Credentials, AuthenticationException>() {
662+
@Override
663+
public void onFailure(@NonNull AuthenticationException exception) { }
664+
665+
@Override
666+
public void onSuccess(@Nullable Credentials credentials) { }
667+
});
668+
```
669+
</details>
670+
671+
**Points to be Noted**:
672+
673+
Passkeys are supported only on devices that run Android 9 (API level 28) or higher.
674+
To use passkeys ,user needs to add support for Digital Asset Links.
675+
676+
610677
## Bot Protection
611678
If you are using the [Bot Protection](https://auth0.com/docs/anomaly-detection/bot-protection) feature and performing database login/signup via the Authentication API, you need to handle the `AuthenticationException#isVerificationRequired()` error. It indicates that the request was flagged as suspicious and an additional verification step is necessary to log the user in. That verification step is web-based, so you need to use Universal Login to complete it.
612679

@@ -1242,4 +1309,5 @@ You might encounter errors similar to `PKIX path building failed: sun.security.p
12421309
The rules should be applied automatically if your application is using `minifyEnabled = true`. If you want to include them manually check the [proguard directory](proguard).
12431310
By default you should at least use the following files:
12441311
* `proguard-okio.pro`
1245-
* `proguard-gson.pro`
1312+
* `proguard-gson.pro`
1313+
* `proguard-jetpack.pro`

auth0/build.gradle

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,18 +34,18 @@ version = getVersionFromFile()
3434
logger.lifecycle("Using version ${version} for ${name}")
3535

3636
android {
37-
compileSdkVersion 31
37+
compileSdkVersion 34
3838

3939
defaultConfig {
4040
minSdkVersion 21
41-
targetSdkVersion 31
41+
targetSdkVersion 34
4242
versionCode 1
4343
versionName project.version
4444

4545
buildConfigField "String", "LIBRARY_NAME", "\"$project.rootProject.name\""
4646
buildConfigField "String", "VERSION_NAME", "\"${project.version}\""
4747

48-
consumerProguardFiles '../proguard/proguard-gson.pro', '../proguard/proguard-okio.pro'
48+
consumerProguardFiles '../proguard/proguard-gson.pro', '../proguard/proguard-okio.pro', '../proguard/proguard-jetpack.pro'
4949
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
5050
}
5151
lintOptions {
@@ -77,13 +77,14 @@ ext {
7777
powermockVersion = '2.0.9'
7878
coroutinesVersion = '1.6.2'
7979
biometricLibraryVersion = '1.1.0'
80+
credentialManagerVersion = "1.3.0"
8081
}
8182

8283

8384
dependencies {
8485
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
8586
implementation 'androidx.core:core-ktx:1.6.0'
86-
implementation 'androidx.appcompat:appcompat:1.3.0'
87+
implementation 'androidx.appcompat:appcompat:1.6.0'
8788
implementation 'androidx.browser:browser:1.4.0'
8889
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion"
8990
implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
@@ -110,6 +111,9 @@ dependencies {
110111
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
111112

112113
testImplementation "androidx.biometric:biometric:$biometricLibraryVersion"
114+
115+
implementation "androidx.credentials:credentials-play-services-auth:$credentialManagerVersion"
116+
implementation "androidx.credentials:credentials:$credentialManagerVersion"
113117
}
114118

115119
apply from: rootProject.file('gradle/jacoco.gradle')

auth0/src/main/java/com/auth0/android/authentication/AuthenticationAPIClient.kt

Lines changed: 118 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import com.auth0.android.request.internal.ResponseUtils.isNetworkError
1212
import com.auth0.android.result.Challenge
1313
import com.auth0.android.result.Credentials
1414
import com.auth0.android.result.DatabaseUser
15+
import com.auth0.android.result.PasskeyChallengeResponse
16+
import com.auth0.android.result.PasskeyRegistrationResponse
1517
import com.auth0.android.result.UserProfile
1618
import com.google.gson.Gson
1719
import okhttp3.HttpUrl.Companion.toHttpUrl
@@ -151,6 +153,102 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
151153
return loginWithToken(parameters)
152154
}
153155

156+
157+
/**
158+
* Log in a user using passkeys.
159+
* This should be called after the client has received the Passkey challenge and Auth-session from the server .
160+
* Requires the client to have the **Passkey** Grant Type enabled. See [Client Grant Types](https://auth0.com/docs/clients/client-grant-types)
161+
* to learn how to enable it.
162+
*
163+
* @param authSession the auth session received from the server as part of the public challenge request.
164+
* @param authResponse the public key credential response to be sent to the server
165+
* @param parameters additional parameters to be sent as part of the request
166+
* @return a request to configure and start that will yield [Credentials]
167+
*/
168+
internal fun signinWithPasskey(
169+
authSession: String,
170+
authResponse: PublicKeyCredentialResponse,
171+
parameters: Map<String, String>
172+
): AuthenticationRequest {
173+
val params = ParameterBuilder.newBuilder().apply {
174+
setGrantType(ParameterBuilder.GRANT_TYPE_PASSKEY)
175+
set(AUTH_SESSION_KEY, authSession)
176+
addAll(parameters)
177+
}.asDictionary()
178+
179+
return loginWithToken(params)
180+
.addParameter(
181+
AUTH_RESPONSE_KEY,
182+
Gson().toJsonTree(authResponse)
183+
) as AuthenticationRequest
184+
}
185+
186+
187+
/**
188+
* Register a user and returns a challenge.
189+
* Requires the client to have the **Passkey** Grant Type enabled. See [Client Grant Types](https://auth0.com/docs/clients/client-grant-types)
190+
* to learn how to enable it.
191+
*
192+
* @param userMetadata user information of the client
193+
* @param parameters additional parameter to be sent as part of the request
194+
* @return a request to configure and start that will yield [PasskeyRegistrationResponse]
195+
*/
196+
internal fun signupWithPasskey(
197+
userMetadata: UserMetadataRequest,
198+
parameters: Map<String, String>,
199+
): Request<PasskeyRegistrationResponse, AuthenticationException> {
200+
val user = Gson().toJsonTree(userMetadata)
201+
val url = auth0.getDomainUrl().toHttpUrl().newBuilder()
202+
.addPathSegment(PASSKEY_PATH)
203+
.addPathSegment(REGISTER_PATH)
204+
.build()
205+
206+
val params = ParameterBuilder.newBuilder().apply {
207+
setClientId(clientId)
208+
parameters[ParameterBuilder.REALM_KEY]?.let {
209+
setRealm(it)
210+
}
211+
}.asDictionary()
212+
213+
val passkeyRegistrationAdapter: JsonAdapter<PasskeyRegistrationResponse> = GsonAdapter(
214+
PasskeyRegistrationResponse::class.java, gson
215+
)
216+
val post = factory.post(url.toString(), passkeyRegistrationAdapter)
217+
.addParameters(params) as BaseRequest<PasskeyRegistrationResponse, AuthenticationException>
218+
post.addParameter(USER_PROFILE_KEY, user)
219+
return post
220+
}
221+
222+
223+
/**
224+
* Request for a challenge to initiate a passkey login flow
225+
* Requires the client to have the **Passkey** Grant Type enabled. See [Client Grant Types](https://auth0.com/docs/clients/client-grant-types)
226+
* to learn how to enable it.
227+
*
228+
* @param realm An optional connection name
229+
* @return a request to configure and start that will yield [PasskeyChallengeResponse]
230+
*/
231+
internal fun passkeyChallenge(
232+
realm: String?
233+
): Request<PasskeyChallengeResponse, AuthenticationException> {
234+
val url = auth0.getDomainUrl().toHttpUrl().newBuilder()
235+
.addPathSegment(PASSKEY_PATH)
236+
.addPathSegment(CHALLENGE_PATH)
237+
.build()
238+
239+
val parameters = ParameterBuilder.newBuilder().apply {
240+
setClientId(clientId)
241+
realm?.let { setRealm(it) }
242+
}.asDictionary()
243+
244+
val passkeyChallengeAdapter: JsonAdapter<PasskeyChallengeResponse> = GsonAdapter(
245+
PasskeyChallengeResponse::class.java, gson
246+
)
247+
248+
return factory.post(url.toString(), passkeyChallengeAdapter)
249+
.addParameters(parameters)
250+
}
251+
154252
/**
155253
* Log in a user using an Out Of Band authentication code after they have received the 'mfa_required' error.
156254
* The MFA token tells the server the username or email, password, and realm values sent on the first request.
@@ -695,8 +793,7 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
695793
val parameters = ParameterBuilder.newBuilder()
696794
.setClientId(clientId)
697795
.setGrantType(ParameterBuilder.GRANT_TYPE_AUTHORIZATION_CODE)
698-
.set(OAUTH_CODE_KEY, authorizationCode)
699-
.set(REDIRECT_URI_KEY, redirectUri)
796+
.set(OAUTH_CODE_KEY, authorizationCode).set(REDIRECT_URI_KEY, redirectUri)
700797
.set("code_verifier", codeVerifier)
701798
.asDictionary()
702799
val url = auth0.getDomainUrl().toHttpUrl().newBuilder()
@@ -736,26 +833,26 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
736833
.addPathSegment(OAUTH_PATH)
737834
.addPathSegment(TOKEN_PATH)
738835
.build()
739-
val requestParameters = ParameterBuilder.newBuilder()
740-
.setClientId(clientId)
741-
.addAll(parameters)
742-
.asDictionary()
836+
val requestParameters =
837+
ParameterBuilder.newBuilder()
838+
.setClientId(clientId)
839+
.addAll(parameters)
840+
.asDictionary()
743841
val credentialsAdapter: JsonAdapter<Credentials> = GsonAdapter(
744842
Credentials::class.java, gson
745843
)
746844
val request = BaseAuthenticationRequest(
747-
factory.post(url.toString(), credentialsAdapter),
748-
clientId,
749-
baseURL
845+
factory.post(url.toString(), credentialsAdapter), clientId, baseURL
750846
)
751847
request.addParameters(requestParameters)
752848
return request
753849
}
754850

755851
private fun profileRequest(): Request<UserProfile, AuthenticationException> {
756-
val url = auth0.getDomainUrl().toHttpUrl().newBuilder()
757-
.addPathSegment(USER_INFO_PATH)
758-
.build()
852+
val url =
853+
auth0.getDomainUrl().toHttpUrl().newBuilder()
854+
.addPathSegment(USER_INFO_PATH)
855+
.build()
759856
val userProfileAdapter: JsonAdapter<UserProfile> = GsonAdapter(
760857
UserProfile::class.java, gson
761858
)
@@ -782,6 +879,9 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
782879
private const val SUBJECT_TOKEN_KEY = "subject_token"
783880
private const val SUBJECT_TOKEN_TYPE_KEY = "subject_token_type"
784881
private const val USER_METADATA_KEY = "user_metadata"
882+
private const val AUTH_SESSION_KEY = "auth_session"
883+
private const val AUTH_RESPONSE_KEY = "authn_response"
884+
private const val USER_PROFILE_KEY = "user_profile"
785885
private const val SIGN_UP_PATH = "signup"
786886
private const val DB_CONNECTIONS_PATH = "dbconnections"
787887
private const val CHANGE_PASSWORD_PATH = "change_password"
@@ -793,24 +893,23 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
793893
private const val REVOKE_PATH = "revoke"
794894
private const val MFA_PATH = "mfa"
795895
private const val CHALLENGE_PATH = "challenge"
896+
private const val PASSKEY_PATH = "passkey"
897+
private const val REGISTER_PATH = "register"
796898
private const val HEADER_AUTHORIZATION = "Authorization"
797899
private const val WELL_KNOWN_PATH = ".well-known"
798900
private const val JWKS_FILE_PATH = "jwks.json"
799901
private fun createErrorAdapter(): ErrorAdapter<AuthenticationException> {
800902
val mapAdapter = forMap(GsonProvider.gson)
801903
return object : ErrorAdapter<AuthenticationException> {
802904
override fun fromRawResponse(
803-
statusCode: Int,
804-
bodyText: String,
805-
headers: Map<String, List<String>>
905+
statusCode: Int, bodyText: String, headers: Map<String, List<String>>
806906
): AuthenticationException {
807907
return AuthenticationException(bodyText, statusCode)
808908
}
809909

810910
@Throws(IOException::class)
811911
override fun fromJsonResponse(
812-
statusCode: Int,
813-
reader: Reader
912+
statusCode: Int, reader: Reader
814913
): AuthenticationException {
815914
val values = mapAdapter.fromJson(reader)
816915
return AuthenticationException(values, statusCode)
@@ -819,13 +918,11 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
819918
override fun fromException(cause: Throwable): AuthenticationException {
820919
if (isNetworkError(cause)) {
821920
return AuthenticationException(
822-
"Failed to execute the network request",
823-
NetworkErrorException(cause)
921+
"Failed to execute the network request", NetworkErrorException(cause)
824922
)
825923
}
826924
return AuthenticationException(
827-
"Something went wrong",
828-
Auth0Exception("Something went wrong", cause)
925+
"Something went wrong", Auth0Exception("Something went wrong", cause)
829926
)
830927
}
831928
}

auth0/src/main/java/com/auth0/android/authentication/ParameterBuilder.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ public class ParameterBuilder private constructor(parameters: Map<String, String
159159
"http://auth0.com/oauth/grant-type/passwordless/otp"
160160
public const val GRANT_TYPE_TOKEN_EXCHANGE: String =
161161
"urn:ietf:params:oauth:grant-type:token-exchange"
162+
public const val GRANT_TYPE_PASSKEY :String = "urn:okta:params:oauth:grant-type:webauthn"
162163
public const val SCOPE_OPENID: String = "openid"
163164
public const val SCOPE_OFFLINE_ACCESS: String = "openid offline_access"
164165
public const val SCOPE_KEY: String = "scope"

0 commit comments

Comments
 (0)