Skip to content

Commit 3dba49a

Browse files
feat: Update examples and methods for request_uri based authorization flows
1 parent f45cb47 commit 3dba49a

File tree

3 files changed

+99
-12
lines changed

3 files changed

+99
-12
lines changed

EXAMPLES.md

Lines changed: 89 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
- [Trusted Web Activity](#trusted-web-activity)
1414
- [DPoP [EA]](#dpop-ea)
1515
- [PAR (Pushed Authorization Request)](#par-pushed-authorization-request)
16+
- [PAR with PKCE](#par-with-pkce)
1617
- [Authentication API](#authentication-api)
1718
- [Login with database connection](#login-with-database-connection)
1819
- [Login using MFA with One Time Password code](#login-using-mfa-with-one-time-password-code)
@@ -314,7 +315,7 @@ The PAR flow requires coordination between your backend (BFF) and the mobile app
314315
val requestUri = yourBffClient.initiatePAR(scope, audience)
315316

316317
// Step 2 & 3: SDK opens browser and returns authorization code
317-
WebAuthProvider.authorizeWithPAR(account)
318+
WebAuthProvider.authorizeWithRequestUri(account)
318319
.start(context, requestUri, object : Callback<AuthorizationCode, AuthenticationException> {
319320
override fun onSuccess(result: AuthorizationCode) {
320321
// Step 4: Send code to BFF to exchange for tokens
@@ -340,7 +341,7 @@ try {
340341
val requestUri = yourBffClient.initiatePAR(scope, audience)
341342

342343
// Step 2 & 3: SDK opens browser and returns authorization code
343-
val authCode = WebAuthProvider.authorizeWithPAR(account)
344+
val authCode = WebAuthProvider.authorizeWithRequestUri(account)
344345
.await(context, requestUri)
345346

346347
// Step 4: Send code to BFF to exchange for tokens
@@ -358,6 +359,92 @@ try {
358359
> [!NOTE]
359360
> 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`.
360361
362+
### PAR with PKCE
363+
364+
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.
365+
366+
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.
367+
368+
```kotlin
369+
// Step 1: Your BFF calls /par with code_challenge and returns request_uri + code_verifier
370+
val parResponse = yourBffClient.initiatePARWithPKCE(scope, audience)
371+
// parResponse contains: requestUri and codeVerifier
372+
373+
// Step 2 & 3: SDK opens browser and returns authorization code
374+
WebAuthProvider.authorizeWithRequestUri(account)
375+
.start(context, parResponse.requestUri, object : Callback<AuthorizationCode, AuthenticationException> {
376+
override fun onSuccess(result: AuthorizationCode) {
377+
// Step 4: Send code AND code_verifier to BFF to exchange for tokens
378+
yourBffClient.exchangeCodeWithPKCE(result.code, parResponse.codeVerifier)
379+
}
380+
381+
override fun onFailure(error: AuthenticationException) {
382+
if (error.isCanceled) {
383+
// User closed the browser
384+
} else {
385+
// Handle error
386+
}
387+
}
388+
})
389+
```
390+
391+
<details>
392+
<summary>Using coroutines</summary>
393+
394+
```kotlin
395+
try {
396+
// Step 1: Your BFF calls /par with code_challenge and returns request_uri + code_verifier
397+
val parResponse = yourBffClient.initiatePARWithPKCE(scope, audience)
398+
399+
// Step 2 & 3: SDK opens browser and returns authorization code
400+
val authCode = WebAuthProvider.authorizeWithRequestUri(account)
401+
.await(context, parResponse.requestUri)
402+
403+
// Step 4: Send code AND code_verifier to BFF to exchange for tokens
404+
val credentials = yourBffClient.exchangeCodeWithPKCE(authCode.code, parResponse.codeVerifier)
405+
} catch (e: AuthenticationException) {
406+
if (e.isCanceled) {
407+
// User closed the browser
408+
} else {
409+
// Handle error
410+
}
411+
}
412+
```
413+
</details>
414+
415+
#### Backend PKCE Implementation
416+
417+
Your backend should generate the `code_verifier` and `code_challenge` during the `/par` request:
418+
419+
```kotlin
420+
// Backend: Generate PKCE values
421+
fun generateCodeVerifier(): String {
422+
val randomBytes = ByteArray(32)
423+
SecureRandom().nextBytes(randomBytes)
424+
return Base64.encodeToString(randomBytes, Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING)
425+
}
426+
427+
fun generateCodeChallenge(codeVerifier: String): String {
428+
val bytes = codeVerifier.toByteArray(Charsets.US_ASCII)
429+
val digest = MessageDigest.getInstance("SHA-256")
430+
val hash = digest.digest(bytes)
431+
return Base64.encodeToString(hash, Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING)
432+
}
433+
434+
// Include in /par request
435+
val codeVerifier = generateCodeVerifier()
436+
val codeChallenge = generateCodeChallenge(codeVerifier)
437+
438+
// POST to /oauth/par with:
439+
// - code_challenge: codeChallenge
440+
// - code_challenge_method: "S256"
441+
442+
// Store codeVerifier and return it with request_uri to the app
443+
444+
// Later, in /oauth/token request, include:
445+
// - code_verifier: codeVerifier
446+
```
447+
361448
## Authentication API
362449

363450
The client provides methods to authenticate the user against the Auth0 server.

auth0/src/main/java/com/auth0/android/provider/WebAuthProvider.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,15 +81,15 @@ public object WebAuthProvider : SenderConstraining<WebAuthProvider> {
8181
}
8282

8383
/**
84-
* Initialize the WebAuthProvider instance for PAR (Pushed Authorization Request) flows.
84+
* Initialize the WebAuthProvider instance for request_uri based authorization flows.
8585
* Use this when your BFF has already called the /oauth/par endpoint and you need to
8686
* complete the authorization by opening /authorize with the request_uri.
8787
*
8888
* @param account to use for authentication
8989
* @return a new PARBuilder instance to customize.
9090
*/
9191
@JvmStatic
92-
public fun authorizeWithPAR(account: Auth0): PARBuilder {
92+
public fun authorizeWithRequestUri(account: Auth0): PARBuilder {
9393
return PARBuilder(account)
9494
}
9595

@@ -671,7 +671,7 @@ public object WebAuthProvider : SenderConstraining<WebAuthProvider> {
671671
*
672672
* Example usage:
673673
* ```kotlin
674-
* WebAuthProvider.authorizeWithPAR(account)
674+
* WebAuthProvider.authorizeWithRequestUri(account)
675675
* .start(context, requestURI, callback)
676676
* ```
677677
*/

auth0/src/test/java/com/auth0/android/provider/PARCodeManagerTest.kt

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import android.net.Uri
66
import com.auth0.android.Auth0
77
import com.auth0.android.authentication.AuthenticationException
88
import com.auth0.android.callback.Callback
9-
import com.auth0.android.provider.WebAuthProvider.authorizeWithPAR
9+
import com.auth0.android.provider.WebAuthProvider.authorizeWithRequestUri
1010
import com.auth0.android.provider.WebAuthProvider.resume
1111
import com.auth0.android.request.internal.ThreadSwitcherShadow
1212
import com.auth0.android.result.AuthorizationCode
@@ -69,7 +69,7 @@ public class PARCodeManagerTest {
6969

7070
@Test
7171
public fun shouldStartPARFlowWithCorrectAuthorizeUri() {
72-
authorizeWithPAR(account)
72+
authorizeWithRequestUri(account)
7373
.start(activity, REQUEST_URI, callback)
7474

7575
Assert.assertNotNull(WebAuthProvider.managerInstance)
@@ -87,7 +87,7 @@ public class PARCodeManagerTest {
8787

8888
@Test
8989
public fun shouldResumeWithValidCode() {
90-
authorizeWithPAR(account)
90+
authorizeWithRequestUri(account)
9191
.start(activity, REQUEST_URI, callback)
9292

9393
verify(activity).startActivity(intentCaptor.capture())
@@ -105,7 +105,7 @@ public class PARCodeManagerTest {
105105

106106
@Test
107107
public fun shouldFailWithMissingCode() {
108-
authorizeWithPAR(account)
108+
authorizeWithRequestUri(account)
109109
.start(activity, REQUEST_URI, callback)
110110

111111
verify(activity).startActivity(intentCaptor.capture())
@@ -123,7 +123,7 @@ public class PARCodeManagerTest {
123123

124124
@Test
125125
public fun shouldFailWithErrorResponse() {
126-
authorizeWithPAR(account)
126+
authorizeWithRequestUri(account)
127127
.start(activity, REQUEST_URI, callback)
128128

129129
verify(activity).startActivity(intentCaptor.capture())
@@ -141,7 +141,7 @@ public class PARCodeManagerTest {
141141

142142
@Test
143143
public fun shouldHandleCanceledAuthentication() {
144-
authorizeWithPAR(account)
144+
authorizeWithRequestUri(account)
145145
.start(activity, REQUEST_URI, callback)
146146

147147
verify(activity).startActivity(intentCaptor.capture())
@@ -167,7 +167,7 @@ public class PARCodeManagerTest {
167167
null
168168
)
169169

170-
authorizeWithPAR(account)
170+
authorizeWithRequestUri(account)
171171
.start(activity, REQUEST_URI, callback)
172172

173173
verify(callback).onFailure(authExceptionCaptor.capture())

0 commit comments

Comments
 (0)