Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for Passkey Authentication #770

Merged
merged 6 commits into from
Oct 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 75 additions & 7 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
- [Specify Parameter](#specify-parameter)
- [Customize the Custom Tabs UI](#customize-the-custom-tabs-ui)
- [Changing the Return To URL scheme](#changing-the-return-to-url-scheme)
- [Trusted Web Activity](#trusted-web-activity-experimental)
- [Trusted Web Activity](#trusted-web-activity)
- [Authentication API](#authentication-api)
- [Login with database connection](#login-with-database-connection)
- [Login using MFA with One Time Password code](#login-using-mfa-with-one-time-password-code)
Expand All @@ -22,6 +22,7 @@
- [Usage](#usage)
- [Requiring Authentication](#requiring-authentication)
- [Handling Credentials Manager exceptions](#handling-credentials-manager-exceptions)
- [Passkeys](#passkeys)
- [Bot Protection](#bot-protection)
- [Management API](#management-api)
- [Link users](#link-users)
Expand Down Expand Up @@ -131,11 +132,7 @@ WebAuthProvider.logout(account)
.start(this, logoutCallback)
```

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

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

Expand Down Expand Up @@ -607,6 +604,76 @@ when(credentialsManagerException) {
}
```

## Passkeys
User should have a custom domain configured and passkey grant-type enabled in the Auth0 dashboard to use passkeys.

To sign up a user with passkey

```kotlin
PasskeyAuthProvider.signUp(account)
.setEmail("user email")
.setUserName("user name")
.setPhoneNumber("phone number")
.setRealm("optional connection name")
.start(object: Callback<Credentials, AuthenticationException> {
override fun onFailure(exception: AuthenticationException) { }

override fun onSuccess(credentials: Credentials) { }
})
```
<details>
<summary>Using Java</summary>

```java
PasskeyAuthProvider authProvider = new PasskeyAuthProvider();
authProvider.signUp(account)
.setEmail("user email")
.setUserName("user name")
.setPhoneNumber("phone number")
.setRealm("optional connection name")
.start(new Callback<Credentials, AuthenticationException>() {
@Override
public void onFailure(@NonNull AuthenticationException exception) { }

@Override
public void onSuccess(@Nullable Credentials credentials) { }
});
```
</details>

To sign in a user with passkey
```kotlin
PasskeyAuthProvider.signin(account)
.setRealm("Optional connection name")
.start(object: Callback<Credentials, AuthenticationException> {
override fun onFailure(exception: AuthenticationException) { }

override fun onSuccess(credentials: Credentials) { }
})
```
<details>
<summary>Using Java</summary>

```java
PasskeyAuthProvider authProvider = new PasskeyAuthProvider();
authProvider.signin(account)
.setRealm("optional connection name")
.start(new Callback<Credentials, AuthenticationException>() {
@Override
public void onFailure(@NonNull AuthenticationException exception) { }

@Override
public void onSuccess(@Nullable Credentials credentials) { }
});
```
</details>

**Points to be Noted**:

Passkeys are supported only on devices that run Android 9 (API level 28) or higher.
To use passkeys ,user needs to add support for Digital Asset Links.


## Bot Protection
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.

Expand Down Expand Up @@ -1242,4 +1309,5 @@ You might encounter errors similar to `PKIX path building failed: sun.security.p
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).
By default you should at least use the following files:
* `proguard-okio.pro`
* `proguard-gson.pro`
* `proguard-gson.pro`
* `proguard-jetpack.pro`
12 changes: 8 additions & 4 deletions auth0/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,18 @@ version = getVersionFromFile()
logger.lifecycle("Using version ${version} for ${name}")

android {
compileSdkVersion 31
compileSdkVersion 34

defaultConfig {
minSdkVersion 21
targetSdkVersion 31
targetSdkVersion 34
versionCode 1
versionName project.version

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

consumerProguardFiles '../proguard/proguard-gson.pro', '../proguard/proguard-okio.pro'
consumerProguardFiles '../proguard/proguard-gson.pro', '../proguard/proguard-okio.pro', '../proguard/proguard-jetpack.pro'
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
lintOptions {
Expand Down Expand Up @@ -77,13 +77,14 @@ ext {
powermockVersion = '2.0.9'
coroutinesVersion = '1.6.2'
biometricLibraryVersion = '1.1.0'
credentialManagerVersion = "1.3.0"
}


dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation 'androidx.core:core-ktx:1.6.0'
implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'androidx.appcompat:appcompat:1.6.0'
implementation 'androidx.browser:browser:1.4.0'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion"
implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
Expand All @@ -110,6 +111,9 @@ dependencies {
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"

testImplementation "androidx.biometric:biometric:$biometricLibraryVersion"

implementation "androidx.credentials:credentials-play-services-auth:$credentialManagerVersion"
implementation "androidx.credentials:credentials:$credentialManagerVersion"
}

apply from: rootProject.file('gradle/jacoco.gradle')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import com.auth0.android.request.internal.ResponseUtils.isNetworkError
import com.auth0.android.result.Challenge
import com.auth0.android.result.Credentials
import com.auth0.android.result.DatabaseUser
import com.auth0.android.result.PasskeyChallengeResponse
import com.auth0.android.result.PasskeyRegistrationResponse
import com.auth0.android.result.UserProfile
import com.google.gson.Gson
import okhttp3.HttpUrl.Companion.toHttpUrl
Expand Down Expand Up @@ -151,6 +153,102 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
return loginWithToken(parameters)
}


/**
* Log in a user using passkeys.
* This should be called after the client has received the Passkey challenge and Auth-session from the server .
* Requires the client to have the **Passkey** Grant Type enabled. See [Client Grant Types](https://auth0.com/docs/clients/client-grant-types)
* to learn how to enable it.
*
* @param authSession the auth session received from the server as part of the public challenge request.
* @param authResponse the public key credential response to be sent to the server
* @param parameters additional parameters to be sent as part of the request
* @return a request to configure and start that will yield [Credentials]
*/
internal fun signinWithPasskey(
authSession: String,
authResponse: PublicKeyCredentialResponse,
parameters: Map<String, String>
): AuthenticationRequest {
val params = ParameterBuilder.newBuilder().apply {
setGrantType(ParameterBuilder.GRANT_TYPE_PASSKEY)
set(AUTH_SESSION_KEY, authSession)
addAll(parameters)
}.asDictionary()

return loginWithToken(params)
.addParameter(
AUTH_RESPONSE_KEY,
Gson().toJsonTree(authResponse)
) as AuthenticationRequest
}


/**
* Register a user and returns a challenge.
* Requires the client to have the **Passkey** Grant Type enabled. See [Client Grant Types](https://auth0.com/docs/clients/client-grant-types)
* to learn how to enable it.
*
* @param userMetadata user information of the client
* @param parameters additional parameter to be sent as part of the request
* @return a request to configure and start that will yield [PasskeyRegistrationResponse]
*/
internal fun signupWithPasskey(
userMetadata: UserMetadataRequest,
parameters: Map<String, String>,
): Request<PasskeyRegistrationResponse, AuthenticationException> {
val user = Gson().toJsonTree(userMetadata)
val url = auth0.getDomainUrl().toHttpUrl().newBuilder()
.addPathSegment(PASSKEY_PATH)
.addPathSegment(REGISTER_PATH)
.build()

val params = ParameterBuilder.newBuilder().apply {
setClientId(clientId)
parameters[ParameterBuilder.REALM_KEY]?.let {
setRealm(it)
}
}.asDictionary()

val passkeyRegistrationAdapter: JsonAdapter<PasskeyRegistrationResponse> = GsonAdapter(
PasskeyRegistrationResponse::class.java, gson
)
val post = factory.post(url.toString(), passkeyRegistrationAdapter)
.addParameters(params) as BaseRequest<PasskeyRegistrationResponse, AuthenticationException>
post.addParameter(USER_PROFILE_KEY, user)
return post
}


/**
* Request for a challenge to initiate a passkey login flow
* Requires the client to have the **Passkey** Grant Type enabled. See [Client Grant Types](https://auth0.com/docs/clients/client-grant-types)
* to learn how to enable it.
*
* @param realm An optional connection name
* @return a request to configure and start that will yield [PasskeyChallengeResponse]
*/
internal fun passkeyChallenge(
realm: String?
): Request<PasskeyChallengeResponse, AuthenticationException> {
val url = auth0.getDomainUrl().toHttpUrl().newBuilder()
.addPathSegment(PASSKEY_PATH)
.addPathSegment(CHALLENGE_PATH)
.build()

val parameters = ParameterBuilder.newBuilder().apply {
setClientId(clientId)
realm?.let { setRealm(it) }
}.asDictionary()

val passkeyChallengeAdapter: JsonAdapter<PasskeyChallengeResponse> = GsonAdapter(
PasskeyChallengeResponse::class.java, gson
)

return factory.post(url.toString(), passkeyChallengeAdapter)
.addParameters(parameters)
}

/**
* Log in a user using an Out Of Band authentication code after they have received the 'mfa_required' error.
* The MFA token tells the server the username or email, password, and realm values sent on the first request.
Expand Down Expand Up @@ -695,8 +793,7 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
val parameters = ParameterBuilder.newBuilder()
.setClientId(clientId)
.setGrantType(ParameterBuilder.GRANT_TYPE_AUTHORIZATION_CODE)
.set(OAUTH_CODE_KEY, authorizationCode)
.set(REDIRECT_URI_KEY, redirectUri)
.set(OAUTH_CODE_KEY, authorizationCode).set(REDIRECT_URI_KEY, redirectUri)
.set("code_verifier", codeVerifier)
.asDictionary()
val url = auth0.getDomainUrl().toHttpUrl().newBuilder()
Expand Down Expand Up @@ -736,26 +833,26 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
.addPathSegment(OAUTH_PATH)
.addPathSegment(TOKEN_PATH)
.build()
val requestParameters = ParameterBuilder.newBuilder()
.setClientId(clientId)
.addAll(parameters)
.asDictionary()
val requestParameters =
ParameterBuilder.newBuilder()
.setClientId(clientId)
.addAll(parameters)
.asDictionary()
val credentialsAdapter: JsonAdapter<Credentials> = GsonAdapter(
Credentials::class.java, gson
)
val request = BaseAuthenticationRequest(
factory.post(url.toString(), credentialsAdapter),
clientId,
baseURL
factory.post(url.toString(), credentialsAdapter), clientId, baseURL
)
request.addParameters(requestParameters)
return request
}

private fun profileRequest(): Request<UserProfile, AuthenticationException> {
val url = auth0.getDomainUrl().toHttpUrl().newBuilder()
.addPathSegment(USER_INFO_PATH)
.build()
val url =
auth0.getDomainUrl().toHttpUrl().newBuilder()
.addPathSegment(USER_INFO_PATH)
.build()
val userProfileAdapter: JsonAdapter<UserProfile> = GsonAdapter(
UserProfile::class.java, gson
)
Expand All @@ -782,6 +879,9 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
private const val SUBJECT_TOKEN_KEY = "subject_token"
private const val SUBJECT_TOKEN_TYPE_KEY = "subject_token_type"
private const val USER_METADATA_KEY = "user_metadata"
private const val AUTH_SESSION_KEY = "auth_session"
private const val AUTH_RESPONSE_KEY = "authn_response"
private const val USER_PROFILE_KEY = "user_profile"
private const val SIGN_UP_PATH = "signup"
private const val DB_CONNECTIONS_PATH = "dbconnections"
private const val CHANGE_PASSWORD_PATH = "change_password"
Expand All @@ -793,24 +893,23 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
private const val REVOKE_PATH = "revoke"
private const val MFA_PATH = "mfa"
private const val CHALLENGE_PATH = "challenge"
private const val PASSKEY_PATH = "passkey"
private const val REGISTER_PATH = "register"
private const val HEADER_AUTHORIZATION = "Authorization"
private const val WELL_KNOWN_PATH = ".well-known"
private const val JWKS_FILE_PATH = "jwks.json"
private fun createErrorAdapter(): ErrorAdapter<AuthenticationException> {
val mapAdapter = forMap(GsonProvider.gson)
return object : ErrorAdapter<AuthenticationException> {
override fun fromRawResponse(
statusCode: Int,
bodyText: String,
headers: Map<String, List<String>>
statusCode: Int, bodyText: String, headers: Map<String, List<String>>
): AuthenticationException {
return AuthenticationException(bodyText, statusCode)
}

@Throws(IOException::class)
override fun fromJsonResponse(
statusCode: Int,
reader: Reader
statusCode: Int, reader: Reader
): AuthenticationException {
val values = mapAdapter.fromJson(reader)
return AuthenticationException(values, statusCode)
Expand All @@ -819,13 +918,11 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
override fun fromException(cause: Throwable): AuthenticationException {
if (isNetworkError(cause)) {
return AuthenticationException(
"Failed to execute the network request",
NetworkErrorException(cause)
"Failed to execute the network request", NetworkErrorException(cause)
)
}
return AuthenticationException(
"Something went wrong",
Auth0Exception("Something went wrong", cause)
"Something went wrong", Auth0Exception("Something went wrong", cause)
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ public class ParameterBuilder private constructor(parameters: Map<String, String
"http://auth0.com/oauth/grant-type/passwordless/otp"
public const val GRANT_TYPE_TOKEN_EXCHANGE: String =
"urn:ietf:params:oauth:grant-type:token-exchange"
public const val GRANT_TYPE_PASSKEY :String = "urn:okta:params:oauth:grant-type:webauthn"
public const val SCOPE_OPENID: String = "openid"
public const val SCOPE_OFFLINE_ACCESS: String = "openid offline_access"
public const val SCOPE_KEY: String = "scope"
Expand Down
Loading
Loading