Skip to content
Open
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
155 changes: 155 additions & 0 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
- [Specify a Custom Logout URL](#specify-a-custom-logout-url)
- [Trusted Web Activity](#trusted-web-activity)
- [DPoP [EA]](#dpop-ea)
- [PAR (Pushed Authorization Request)](#par-pushed-authorization-request)
- [PAR with PKCE](#par-with-pkce)
- [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 Down Expand Up @@ -290,6 +292,159 @@ WebAuthProvider.logout(account)
> [!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.

## PAR (Pushed Authorization Request)

PAR (Pushed Authorization Request) enables a Backend-for-Frontend (BFF) pattern where your backend server handles the `/par` and `/token` endpoints while the SDK manages the browser-based authorization flow.

This is useful when:
- You need to use a confidential client with a `client_secret`
- You want to keep sensitive parameters server-side
- You're implementing a BFF architecture

### Usage

The PAR flow requires coordination between your backend (BFF) and the mobile app:

1. **Backend calls `/par` endpoint** - Your backend initiates the PAR request with the `client_secret` and receives a `request_uri`
2. **SDK opens `/authorize`** - The mobile app uses the `request_uri` to open the browser for user authentication
3. **SDK returns authorization code** - After authentication, the SDK returns the authorization code to the app
4. **Backend exchanges code for tokens** - Your backend exchanges the code for tokens using the `client_secret`

```kotlin
// Step 1: Your BFF calls /par and returns request_uri to the app
val requestUri = yourBffClient.initiatePAR(scope, audience)

// Step 2 & 3: SDK opens browser and returns authorization code
WebAuthProvider.authorizeWithRequestUri(account)
.start(context, requestUri, object : Callback<AuthorizationCode, AuthenticationException> {
override fun onSuccess(result: AuthorizationCode) {
// Step 4: Send code to BFF to exchange for tokens
yourBffClient.exchangeCode(result.code)
}

override fun onFailure(error: AuthenticationException) {
if (error.isCanceled) {
// User closed the browser
} else {
// Handle error
}
}
})
```

<details>
<summary>Using coroutines</summary>

```kotlin
try {
// Step 1: Your BFF calls /par and returns request_uri
val requestUri = yourBffClient.initiatePAR(scope, audience)

// Step 2 & 3: SDK opens browser and returns authorization code
val authCode = WebAuthProvider.authorizeWithRequestUri(account)
.await(context, requestUri)

// Step 4: Send code to BFF to exchange for tokens
val credentials = yourBffClient.exchangeCode(authCode.code)
} catch (e: AuthenticationException) {
if (e.isCanceled) {
// User closed the browser
} else {
// Handle error
}
}
```
</details>

> [!NOTE]
> The SDK only handles opening the browser with the `request_uri` and returning the authorization code. Token exchange must be performed by your backend server which holds the `client_secret`.
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The note says "The SDK only handles opening the browser with the request_uri" but this is slightly incomplete. The SDK also handles receiving the authorization code from the callback. Consider clarifying: "The SDK only handles opening the browser with the request_uri and returning the authorization code from the callback. Token exchange must be performed by your backend server which holds the client_secret."

Suggested change
> The SDK only handles opening the browser with the `request_uri` and returning the authorization code. Token exchange must be performed by your backend server which holds the `client_secret`.
> The SDK only handles opening the browser with the `request_uri` and returning the authorization code from the callback. Token exchange must be performed by your backend server which holds the `client_secret`.

Copilot uses AI. Check for mistakes.

### PAR with PKCE
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PKCE flow is something that the backend handles in this scenario. The SDK doesn't play any role other than providing the authorization code. Mentioning the PKCE nuances can cause confusion to the users. Lets just keep it simple with the normal flow description


When using PAR with PKCE (Proof Key for Code Exchange), your backend generates a `code_verifier` and `code_challenge` during the `/par` request, and includes the `code_verifier` when exchanging the authorization code for tokens.

The PKCE flow adds an extra layer of security by ensuring that only the party that initiated the authorization request can exchange the code for tokens.

```kotlin
// Step 1: Your BFF calls /par with code_challenge and returns request_uri + code_verifier
val parResponse = yourBffClient.initiatePARWithPKCE(scope, audience)
// parResponse contains: requestUri and codeVerifier

// Step 2 & 3: SDK opens browser and returns authorization code
WebAuthProvider.authorizeWithRequestUri(account)
.start(context, parResponse.requestUri, object : Callback<AuthorizationCode, AuthenticationException> {
override fun onSuccess(result: AuthorizationCode) {
// Step 4: Send code AND code_verifier to BFF to exchange for tokens
yourBffClient.exchangeCodeWithPKCE(result.code, parResponse.codeVerifier)
}

override fun onFailure(error: AuthenticationException) {
if (error.isCanceled) {
// User closed the browser
} else {
// Handle error
}
}
})
```

<details>
<summary>Using coroutines</summary>

```kotlin
try {
// Step 1: Your BFF calls /par with code_challenge and returns request_uri + code_verifier
val parResponse = yourBffClient.initiatePARWithPKCE(scope, audience)

// Step 2 & 3: SDK opens browser and returns authorization code
val authCode = WebAuthProvider.authorizeWithRequestUri(account)
.await(context, parResponse.requestUri)

// Step 4: Send code AND code_verifier to BFF to exchange for tokens
val credentials = yourBffClient.exchangeCodeWithPKCE(authCode.code, parResponse.codeVerifier)
} catch (e: AuthenticationException) {
if (e.isCanceled) {
// User closed the browser
} else {
// Handle error
}
}
```
</details>

#### Backend PKCE Implementation
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets just remove this section.


Your backend should generate the `code_verifier` and `code_challenge` during the `/par` request:

```kotlin
// Backend: Generate PKCE values
fun generateCodeVerifier(): String {
val randomBytes = ByteArray(32)
SecureRandom().nextBytes(randomBytes)
return Base64.encodeToString(randomBytes, Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING)
}

fun generateCodeChallenge(codeVerifier: String): String {
val bytes = codeVerifier.toByteArray(Charsets.US_ASCII)
val digest = MessageDigest.getInstance("SHA-256")
val hash = digest.digest(bytes)
return Base64.encodeToString(hash, Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING)
}

// Include in /par request
val codeVerifier = generateCodeVerifier()
val codeChallenge = generateCodeChallenge(codeVerifier)

// POST to /oauth/par with:
// - code_challenge: codeChallenge
// - code_challenge_method: "S256"

// Store codeVerifier and return it with request_uri to the app

// Later, in /oauth/token request, include:
// - code_verifier: codeVerifier
```
Comment on lines +419 to +446
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code example for backend PKCE implementation is written in Kotlin, but this is meant to demonstrate backend code. Backend services often use different languages or frameworks. Consider clarifying that this is pseudocode or example logic that should be adapted to the backend's language/framework, or provide a language-agnostic description instead.

Copilot uses AI. Check for mistakes.

## Authentication API

The client provides methods to authenticate the user against the Auth0 server.
Expand Down
110 changes: 110 additions & 0 deletions auth0/src/main/java/com/auth0/android/provider/PARCodeManager.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package com.auth0.android.provider

import android.content.Context
import android.net.Uri
import android.util.Log
import com.auth0.android.Auth0
import com.auth0.android.authentication.AuthenticationException
import com.auth0.android.callback.Callback
import com.auth0.android.result.AuthorizationCode

/**
* Manager for handling PAR (Pushed Authorization Request) code-only flows.
* This manager handles opening the authorize URL with a request_uri and
* returns the authorization code to the caller for BFF token exchange.
*/
internal class PARCodeManager(
private val account: Auth0,
private val callback: Callback<AuthorizationCode, AuthenticationException>,
private val requestUri: String,
private val ctOptions: CustomTabsOptions = CustomTabsOptions.newBuilder().build()
) : ResumableManager() {

private var requestCode = 0

private companion object {
private val TAG = PARCodeManager::class.java.simpleName
private const val KEY_CLIENT_ID = "client_id"
private const val KEY_REQUEST_URI = "request_uri"
private const val KEY_CODE = "code"
private const val KEY_STATE = "state"
private const val KEY_ERROR = "error"
private const val KEY_ERROR_DESCRIPTION = "error_description"
private const val ERROR_VALUE_ACCESS_DENIED = "access_denied"
}

fun startAuthentication(context: Context, requestCode: Int) {
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The startAuthentication function should have an explicit visibility modifier. Following the pattern used in other Manager classes like LogoutManager (which uses 'fun startLogout'), consider marking this as 'internal fun' for consistency, since it's only called internally by the PARBuilder.

Suggested change
fun startAuthentication(context: Context, requestCode: Int) {
internal fun startAuthentication(context: Context, requestCode: Int) {

Copilot uses AI. Check for mistakes.
this.requestCode = requestCode
val uri = buildAuthorizeUri()
AuthenticationActivity.authenticateUsingBrowser(context, uri, false, ctOptions)
}

override fun resume(result: AuthorizeResult): Boolean {
if (!result.isValid(requestCode)) {
Log.w(TAG, "The Authorize Result is invalid.")
return false
}

if (result.isCanceled) {
val exception = AuthenticationException(
AuthenticationException.ERROR_VALUE_AUTHENTICATION_CANCELED,
"The user closed the browser app and the authentication was canceled."
)
callback.onFailure(exception)
return true
}

val values = CallbackHelper.getValuesFromUri(result.intentData)
if (values.isEmpty()) {
Log.w(TAG, "The response didn't contain any values: code")
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The log message "The response didn't contain any values: code" is misleading. Unlike OAuthManager which logs "The response didn't contain any of these values: code, state", this PAR flow only requires the 'code' parameter. However, the check 'values.isEmpty()' is too broad - it will reject callbacks that have query parameters but are missing the code. Consider changing the log message to be more accurate (e.g., "The callback response is empty") or adjust the check to specifically validate for missing 'code' after checking for errors, similar to how it's done later in the function.

Suggested change
Log.w(TAG, "The response didn't contain any values: code")
Log.w(TAG, "The callback response is empty")

Copilot uses AI. Check for mistakes.
return false
}

Log.d(TAG, "The parsed CallbackURI contains the following parameters: ${values.keys}")

// Check for error response
val error = values[KEY_ERROR]
if (error != null) {
val description = values[KEY_ERROR_DESCRIPTION] ?: error
val authError = AuthenticationException(error, description)
callback.onFailure(authError)
return true
}

// Extract code
val code = values[KEY_CODE]
if (code == null) {
val exception = AuthenticationException(
ERROR_VALUE_ACCESS_DENIED,
"No authorization code was received in the callback."
)
callback.onFailure(exception)
return true
}

// Extract optional state
val state = values[KEY_STATE]

// Success - return authorization code with optional state
val authorizationCode = AuthorizationCode(code = code, state = state)
callback.onSuccess(authorizationCode)
return true
}

override fun failure(exception: AuthenticationException) {
callback.onFailure(exception)
}

private fun buildAuthorizeUri(): Uri {
val authorizeUri = Uri.parse(account.authorizeUrl)
val builder = authorizeUri.buildUpon()

// Only add client_id and request_uri for PAR flow
builder.appendQueryParameter(KEY_CLIENT_ID, account.clientId)
builder.appendQueryParameter(KEY_REQUEST_URI, requestUri)

val uri = builder.build()
Log.d(TAG, "Using the following PAR Authorize URI: $uri")
return uri
}
}
Loading