Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
128 changes: 128 additions & 0 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- [Changing the Return To URL scheme](#changing-the-return-to-url-scheme)
- [Specify a Custom Logout URL](#specify-a-custom-logout-url)
- [Trusted Web Activity](#trusted-web-activity)
- [DPoP [EA]](#dpop-ea)
- [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 @@ -21,6 +22,7 @@
- [Get user information](#get-user-information)
- [Custom Token Exchange](#custom-token-exchange)
- [Native to Web SSO login [EA]](#native-to-web-sso-login-ea)
- [DPoP [EA]](#dpop-ea-1)
- [My Account API](#my-account-api)
- [Enroll a new passkey](#enroll-a-new-passkey)
- [Credentials Manager](#credentials-manager)
Expand Down Expand Up @@ -208,6 +210,76 @@ WebAuthProvider.login(account)
.await(this)
```

## DPoP [EA]

> [!NOTE]
> This feature is currently available in [Early Access](https://auth0.com/docs/troubleshoot/product-lifecycle/product-release-stages#early-access). Please reach out to Auth0 support to get it enabled for your tenant.

[DPoP](https://www.rfc-editor.org/rfc/rfc9449.html) (Demonstrating Proof of Possession) is an application-level mechanism for sender-constraining OAuth 2.0 access and refresh tokens by proving that the app is in possession of a certain private key. You can enable it by calling the `useDPoP()` method.

```kotlin
WebAuthProvider
.useDPoP()
.login(account)
.start(requireContext(), object : Callback<Credentials, AuthenticationException> {
override fun onSuccess(result: Credentials) {
println("Credentials $result")
}
override fun onFailure(error: AuthenticationException) {
print("Error $error")
}
})
```

> [!IMPORTANT]
> DPoP will only be used for new user sessions created after enabling it. DPoP **will not** be applied to any requests involving existing access and refresh tokens (such as exchanging the refresh token for new credentials).
>
> This means that, after you've enabled it in your app, DPoP will only take effect when users log in again. It's up to you to decide how to roll out this change to your users. For example, you might require users to log in again the next time they open your app. You'll need to implement the logic to handle this transition based on your app's requirements.

When making requests to your own APIs, use the `DPoP.getHeaderData()` method to get the `Authorization` and `DPoP` header values to be used. The `Authorization` header value is generated using the access token and token type, while the `DPoP` header value is the generated DPoP proof.

```kotlin
val url ="https://example.com/api/endpoint"
val httpMethod = "GET"
val headerData = DPoP.getHeaderData(
httpMethod, url,
accessToken, tokenType
)
httpRequest.apply{
addHeader("Authorization", headerData.authorizationHeader)
headerData.dpopProof?.let {
addHeader("DPoP", it)
}
}
```
If your API is issuing DPoP nonces to prevent replay attacks, you can pass the nonce value to the `getHeaderData()` method to include it in the DPoP proof. Use the `DPoP.isNonceRequiredError(response: Response)` method to check if a particular API response failed because a nonce is required.

```kotlin
if (DPoP.isNonceRequiredError(response)) {
val nonce = response.headers["DPoP-Nonce"]
val dpopProof = DPoPProvider.generateProof(
url, httpMethod, accessToken, nonce
)
// Retry the request with the new proof
}
```

On logout, you should call `DPoP.clearKeyPair()` to delete the user's key pair from the Keychain.

```kotlin
WebAuthProvider.logout(account)
.start(requireContext(), object : Callback<Void?, AuthenticationException> {
override fun onSuccess(result: Void?) {
DPoPProvider.clearKeyPair()
}
override fun onFailure(error: AuthenticationException) {
}

})
```
> [!NOTE]
> DPoP is supported only on Android version 6.0 (API level 23) and above. Trying to use DPoP in any older versions will result in an exception.

## Authentication API

The client provides methods to authenticate the user against the Auth0 server.
Expand Down Expand Up @@ -651,6 +723,62 @@ authentication
```
</details>

## DPoP [EA]

> [!NOTE]
> This feature is currently available in [Early Access](https://auth0.com/docs/troubleshoot/product-lifecycle/product-release-stages#early-access). Please reach out to Auth0 support to get it enabled for your tenant.

[DPoP](https://www.rfc-editor.org/rfc/rfc9449.html) (Demonstrating Proof of Posession) is an application-level mechanism for sender-constraining OAuth 2.0 access and refresh tokens by proving that the app is in possession of a certain private key. You can enable it by calling the `useDPoP()` method. This ensures that DPoP proofs are generated for requests made through the AuthenticationAPI client.

```kotlin
val client = AuthenticationAPIClient(account).useDPoP()
```

[!IMPORTANT]
> DPoP will only be used for new user sessions created after enabling it. DPoP **will not** be applied to any requests involving existing access and refresh tokens (such as exchanging the refresh token for new credentials).
>
> This means that, after you've enabled it in your app, DPoP will only take effect when users log in again. It's up to you to decide how to roll out this change to your users. For example, you might require users to log in again the next time they open your app. You'll need to implement the logic to handle this transition based on your app's requirements.

When making requests to your own APIs, use the `DPoP.getHeaderData()` method to get the `Authorization` and `DPoP` header values to be used. The `Authorization` header value is generated using the access token and token type, while the `DPoP` header value is the generated DPoP proof.

```kotlin
val url ="https://example.com/api/endpoint"
val httpMethod = "GET"
val headerData = DPoP.getHeaderData(
httpMethod, url,
accessToken, tokenType
)
httpRequest.apply{
addHeader("Authorization", headerData.authorizationHeader)
headerData.dpopProof?.let {
addHeader("DPoP", it)
}
}
```
If your API is issuing DPoP nonces to prevent replay attacks, you can pass the nonce value to the `getHeaderData()` method to include it in the DPoP proof. Use the `DPoP.isNonceRequiredError(response: Response)` method to check if a particular API response failed because a nonce is required.

```kotlin
if (DPoP.isNonceRequiredError(response)) {
val nonce = response.headers["DPoP-Nonce"]
val dpopProof = DPoPProvider.generateProof(
url, httpMethod, accessToken, nonce
)
// Retry the request with the new proof
}
```

On logout, you should call `DPoP.clearKeyPair()` to delete the user's key pair from the Keychain.

```kotlin

DPoP.clearKeyPair()

```

> [!NOTE]
> DPoP is supported only on Android version 6.0 (API level 23) and above. Trying to use DPoP in any older versions will result in an exception.



## My Account API

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package com.auth0.android.authentication

import android.content.Context
import androidx.annotation.VisibleForTesting
import com.auth0.android.Auth0
import com.auth0.android.Auth0Exception
import com.auth0.android.NetworkErrorException
import com.auth0.android.dpop.DPoP
import com.auth0.android.dpop.DPoPException
import com.auth0.android.dpop.SenderConstraining
import com.auth0.android.request.*
import com.auth0.android.request.internal.*
import com.auth0.android.request.internal.GsonAdapter.Companion.forMap
Expand Down Expand Up @@ -35,7 +39,9 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
private val auth0: Auth0,
private val factory: RequestFactory<AuthenticationException>,
private val gson: Gson
) {
) : SenderConstraining<AuthenticationAPIClient> {

private var dPoP: DPoP? = null

/**
* Creates a new API client instance providing Auth0 account info.
Expand All @@ -59,6 +65,14 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
public val baseURL: String
get() = auth0.getDomainUrl()

/**
* Enable DPoP for this client.
*/
public override fun useDPoP(context: Context): AuthenticationAPIClient {
dPoP = DPoP(context)
return this
}

/**
* Log in a user with email/username and password for a connection/realm.
* It will use the password-realm grant type for the `/oauth/token` endpoint
Expand Down Expand Up @@ -561,9 +575,11 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
* @param accessToken used to fetch it's information
* @return a request to start
*/
public fun userInfo(accessToken: String): Request<UserProfile, AuthenticationException> {
public fun userInfo(
accessToken: String, tokenType: String = "Bearer"
): Request<UserProfile, AuthenticationException> {
return profileRequest()
.addHeader(HEADER_AUTHORIZATION, "Bearer $accessToken")
.addHeader(HEADER_AUTHORIZATION, "$tokenType $accessToken")
}

/**
Expand Down Expand Up @@ -790,8 +806,9 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
val credentialsAdapter = GsonAdapter(
Credentials::class.java, gson
)
return factory.post(url.toString(), credentialsAdapter)
val request = factory.post(url.toString(), credentialsAdapter, dPoP)
.addParameters(parameters)
return request
}

/**
Expand Down Expand Up @@ -926,8 +943,8 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
val credentialsAdapter: JsonAdapter<Credentials> = GsonAdapter(
Credentials::class.java, gson
)
val request = factory.post(url.toString(), credentialsAdapter)
request.addParameters(parameters)
val request = factory.post(url.toString(), credentialsAdapter, dPoP)
.addParameters(parameters)
return request
}

Expand Down Expand Up @@ -992,8 +1009,8 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
val adapter: JsonAdapter<T> = GsonAdapter(
T::class.java, gson
)
val request = factory.post(url.toString(), adapter)
request.addParameters(requestParameters)
val request = factory.post(url.toString(), adapter, dPoP)
.addParameters(requestParameters)
return request
}

Expand All @@ -1014,7 +1031,7 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
Credentials::class.java, gson
)
val request = BaseAuthenticationRequest(
factory.post(url.toString(), credentialsAdapter), clientId, baseURL
factory.post(url.toString(), credentialsAdapter, dPoP), clientId, baseURL
)
request.addParameters(requestParameters)
return request
Expand Down Expand Up @@ -1043,7 +1060,7 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
val userProfileAdapter: JsonAdapter<UserProfile> = GsonAdapter(
UserProfile::class.java, gson
)
return factory.get(url.toString(), userProfileAdapter)
return factory.get(url.toString(), userProfileAdapter, dPoP)
}

private companion object {
Expand Down Expand Up @@ -1086,6 +1103,7 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
private const val HEADER_AUTHORIZATION = "Authorization"
private const val WELL_KNOWN_PATH = ".well-known"
private const val JWKS_FILE_PATH = "jwks.json"
private const val TAG = "AuthenticationAPIClient"
private fun createErrorAdapter(): ErrorAdapter<AuthenticationException> {
val mapAdapter = forMap(GsonProvider.gson)
return object : ErrorAdapter<AuthenticationException> {
Expand All @@ -1109,6 +1127,11 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
"Failed to execute the network request", NetworkErrorException(cause)
)
}
if (cause is DPoPException) {
return AuthenticationException(
cause.message ?: "Error while attaching DPoP proof", cause
)
}
return AuthenticationException(
"Something went wrong", Auth0Exception("Something went wrong", cause)
)
Expand Down
Loading