Skip to content

Commit 9b1d161

Browse files
committed
Merge conflicts with master resolved
2 parents 6fd8364 + 3c9aa2c commit 9b1d161

File tree

14 files changed

+473
-345
lines changed

14 files changed

+473
-345
lines changed

EXAMPLES.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
- [Sign Up with a database connection](#sign-up-with-a-database-connection)
1919
- [Get user information](#get-user-information)
2020
- [Custom Token Exchange](#custom-token-exchange)
21+
- [Native to Web SSO login](#native-to-web-sso-login)
2122
- [Credentials Manager](#credentials-manager)
2223
- [Secure Credentials Manager](#secure-credentials-manager)
2324
- [Usage](#usage)
@@ -540,6 +541,61 @@ authentication
540541
</details>
541542

542543

544+
## Native to Web SSO login
545+
546+
This feature allows you to authenticate a user in a web session using the refresh token obtained from the native session without requiring the user to log in again.
547+
548+
Call the API to fetch a webSessionTransferToken in exchange for a refresh token. Use the obtained token to authenticate the user by calling the `/authorize` endpoint, passing the token as a query parameter or a cookie value.
549+
550+
```kotlin
551+
authentication
552+
.ssoExchange("refresh_token")
553+
.start(object : Callback<SSOCredentials, AuthenticationException> {
554+
override fun onSuccess(result: SSOCredentials) {
555+
// Use the sessionTransferToken token to authenticate the user in a web session in your app
556+
}
557+
558+
override fun onFailure(exception: AuthenticationException) {
559+
// Handle error
560+
}
561+
562+
})
563+
```
564+
565+
<details>
566+
<summary>Using coroutines</summary>
567+
568+
``` kotlin
569+
try {
570+
val ssoCredentials = authentication
571+
.ssoExchange("refresh_token")
572+
.await()
573+
} catch (e: AuthenticationException) {
574+
e.printStacktrace()
575+
}
576+
```
577+
</details>
578+
579+
<details>
580+
<summary>Using Java</summary>
581+
582+
```java
583+
authentication
584+
.ssoExchange("refresh_token")
585+
.start(new Callback<SSOCredentials, AuthenticationException>() {
586+
@Override
587+
public void onSuccess(@Nullable SSOCredentials result) {
588+
// Handle success
589+
}
590+
@Override
591+
public void onFailure(@NonNull AuthenticationException error) {
592+
// Handle error
593+
}
594+
});
595+
```
596+
</details>
597+
598+
543599
## Credentials Manager
544600

545601
### Secure Credentials Manager

README.md

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -290,11 +290,7 @@ The callback will get invoked when the user returns to your application. There a
290290

291291
If the `returnTo` URL is not found in the **Allowed Logout URLs** of your Auth0 Application, the server will not make the redirection and the browser will remain open.
292292

293-
### Trusted Web Activity (Experimental Release)
294-
295-
> **⚠️ Warning:** Trusted Web Activity support in Auth0.Android is still experimental and can change in the future.
296-
>
297-
> Please test it thoroughly in all the targeted browsers and OS variants and let us know your feedback.
293+
### Trusted Web Activity
298294

299295
Trusted Web Activity is a feature provided by some browsers to provide a native look and feel.
300296

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

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -938,18 +938,23 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
938938
}
939939

940940
/**
941-
* Creates a new request to fetch a session token in exchange for a refresh token.
941+
* Creates a new request to exchange a refresh token for a session transfer token that can be used to perform web single sign-on.
942+
*
943+
* When opening your website on any browser or web view, add the session transfer token to the URL as a query
944+
* parameter. Then your website can redirect the user to Auth0's `/authorize` endpoint, passing along the query
945+
* parameter with the session transfer token. For example,
946+
* `https://example.com/login?session_transfer_token=THE_TOKEN`.
947+
*
942948
*
943949
* @param refreshToken A valid refresh token obtained as part of Auth0 authentication
944-
* @return a request to fetch a session token
950+
* @return a request to fetch a session transfer token
951+
*
945952
*/
946-
internal fun fetchSessionToken(refreshToken: String): Request<SSOCredentials, AuthenticationException> {
953+
public fun ssoExchange(refreshToken: String): Request<SSOCredentials, AuthenticationException> {
947954
val params = ParameterBuilder.newBuilder()
948-
.setClientId(clientId)
949-
.setGrantType(ParameterBuilder.GRANT_TYPE_TOKEN_EXCHANGE)
950-
.set(SUBJECT_TOKEN_KEY, refreshToken)
951-
.set(SUBJECT_TOKEN_TYPE_KEY, ParameterBuilder.TOKEN_TYPE_REFRESH_TOKEN)
952-
.set(REQUESTED_TOKEN_TYPE_KEY, ParameterBuilder.TOKEN_TYPE_SESSION_TOKEN)
955+
.setGrantType(ParameterBuilder.REFRESH_TOKEN_KEY)
956+
.setAudience("urn:${auth0.domain}:session_transfer")
957+
.set(ParameterBuilder.REFRESH_TOKEN_KEY, refreshToken)
953958
.asDictionary()
954959
return loginWithTokenGeneric<SSOCredentials>(params)
955960
}

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,9 +160,6 @@ public class ParameterBuilder private constructor(parameters: Map<String, String
160160
public const val GRANT_TYPE_TOKEN_EXCHANGE: String =
161161
"urn:ietf:params:oauth:grant-type:token-exchange"
162162
public const val GRANT_TYPE_PASSKEY :String = "urn:okta:params:oauth:grant-type:webauthn"
163-
public const val TOKEN_TYPE_REFRESH_TOKEN :String = "urn:ietf:params:oauth:token-type:refresh_token"
164-
public const val TOKEN_TYPE_SESSION_TOKEN :String = "urn:auth0:params:oauth:token-type:session_token"
165-
public const val SCOPE_OPENID: String = "openid"
166163
public const val SCOPE_OFFLINE_ACCESS: String = "openid offline_access"
167164
public const val SCOPE_KEY: String = "scope"
168165
public const val REFRESH_TOKEN_KEY: String = "refresh_token"

auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,16 @@ public abstract class BaseCredentialsManager internal constructor(
3232
@Throws(CredentialsManagerException::class)
3333
public abstract fun saveCredentials(credentials: Credentials)
3434
public abstract fun saveApiCredentials(apiCredentials: APICredentials, audience: String)
35-
public abstract fun saveSsoCredentials(ssoCredentials: SSOCredentials)
3635
public abstract fun getCredentials(callback: Callback<Credentials, CredentialsManagerException>)
37-
internal abstract fun getSsoCredentials(callback: Callback<SSOCredentials, CredentialsManagerException>)
36+
public abstract fun getSsoCredentials(
37+
parameters: Map<String, String>,
38+
callback: Callback<SSOCredentials, CredentialsManagerException>
39+
)
40+
41+
public abstract fun getSsoCredentials(
42+
callback: Callback<SSOCredentials, CredentialsManagerException>
43+
)
44+
3845
public abstract fun getCredentials(
3946
scope: String?,
4047
minTtl: Int,
@@ -76,7 +83,13 @@ public abstract class BaseCredentialsManager internal constructor(
7683

7784
@JvmSynthetic
7885
@Throws(CredentialsManagerException::class)
79-
internal abstract suspend fun awaitSsoCredentials(): SSOCredentials
86+
public abstract suspend fun awaitSsoCredentials(parameters: Map<String, String>)
87+
: SSOCredentials
88+
89+
@JvmSynthetic
90+
@Throws(CredentialsManagerException::class)
91+
public abstract suspend fun awaitSsoCredentials()
92+
: SSOCredentials
8093

8194
@JvmSynthetic
8295
@Throws(CredentialsManagerException::class)

auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt

Lines changed: 87 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -73,51 +73,54 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting
7373
}
7474

7575
/**
76-
* Stores the given [SSOCredentials] refresh token in the storage.
77-
* This method must be called if the SSOCredentials are obtained by directly invoking [AuthenticationAPIClient.fetchSessionToken] api and
78-
* [rotating refresh token](https://auth0.com/docs/secure/tokens/refresh-tokens/refresh-token-rotation) are enabled for
79-
* the client. Method will silently return ,if the passed credentials has no refresh token.
76+
* Creates a new request to exchange a refresh token for a session transfer token that can be used to perform web single sign-on.
8077
*
81-
* @param ssoCredentials the credentials to save in the storage.
78+
* When opening your website on any browser or web view, add the session transfer token to the URL as a query
79+
* parameter. Then your website can redirect the user to Auth0's `/authorize` endpoint, passing along the query
80+
* parameter with the session transfer token. For example,
81+
* `https://example.com/login?session_transfer_token=THE_TOKEN`.
82+
*
83+
* It will fail with [CredentialsManagerException] if the existing refresh_token is null or no longer valid.
84+
* This method will handle saving the refresh_token, if a new one is issued.
8285
*/
83-
override fun saveSsoCredentials(ssoCredentials: SSOCredentials) {
84-
if (ssoCredentials.refreshToken.isNullOrEmpty())
85-
return // No refresh token to save
86-
serialExecutor.execute {
87-
val existingRefreshToken = storage.retrieveString(KEY_REFRESH_TOKEN)
88-
// Checking if the existing one needs to be replaced with the new one
89-
if (ssoCredentials.refreshToken == existingRefreshToken)
90-
return@execute
91-
storage.store(KEY_REFRESH_TOKEN, ssoCredentials.refreshToken)
92-
}
86+
override fun getSsoCredentials(callback: Callback<SSOCredentials, CredentialsManagerException>) {
87+
getSsoCredentials(emptyMap(), callback)
9388
}
9489

9590
/**
96-
* Fetches a new [SSOCredentials] . It will fail with [CredentialsManagerException]
97-
* if the existing refresh_token is null or no longer valid. This method will handle saving the refresh_token,
98-
* if a new one is issued
91+
* Creates a new request to exchange a refresh token for a session transfer token that can be used to perform web single sign-on.
92+
*
93+
* When opening your website on any browser or web view, add the session transfer token to the URL as a query
94+
* parameter. Then your website can redirect the user to Auth0's `/authorize` endpoint, passing along the query
95+
* parameter with the session transfer token. For example,
96+
* `https://example.com/login?session_transfer_token=THE_TOKEN`.
97+
*
98+
* It will fail with [CredentialsManagerException] if the existing refresh_token is null or no longer valid.
99+
* This method will handle saving the refresh_token, if a new one is issued.
99100
*/
100-
override fun getSsoCredentials(callback: Callback<SSOCredentials, CredentialsManagerException>) {
101+
override fun getSsoCredentials(
102+
parameters: Map<String, String>,
103+
callback: Callback<SSOCredentials, CredentialsManagerException>
104+
) {
101105
serialExecutor.execute {
102106
val refreshToken = storage.retrieveString(KEY_REFRESH_TOKEN)
103107
if (refreshToken.isNullOrEmpty()) {
104108
callback.onFailure(CredentialsManagerException.NO_REFRESH_TOKEN)
105109
return@execute
106110
}
107111

112+
val request = authenticationClient.ssoExchange(refreshToken)
108113
try {
109-
val sessionCredentials =
110-
authenticationClient.fetchSessionToken(refreshToken)
111-
.execute()
112-
saveSsoCredentials(sessionCredentials)
113-
callback.onSuccess(sessionCredentials)
114+
if (parameters.isNotEmpty()) {
115+
request.addParameters(parameters)
116+
}
117+
val sessionTransferCredentials = request.execute()
118+
saveSsoCredentials(sessionTransferCredentials)
119+
callback.onSuccess(sessionTransferCredentials)
114120
} catch (error: AuthenticationException) {
115121
val exception = when {
116-
error.isRefreshTokenDeleted ||
117-
error.isInvalidRefreshToken -> CredentialsManagerException.Code.RENEW_FAILED
118-
119122
error.isNetworkError -> CredentialsManagerException.Code.NO_NETWORK
120-
else -> CredentialsManagerException.Code.API_ERROR
123+
else -> CredentialsManagerException.Code.SSO_EXCHANGE_FAILED
121124
}
122125
callback.onFailure(
123126
CredentialsManagerException(
@@ -130,23 +133,48 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting
130133
}
131134

132135
/**
133-
* Fetches a new [SSOCredentials] . It will fail with [CredentialsManagerException]
134-
* if the existing refresh_token is null or no longer valid. This method will handle saving the refresh_token,
135-
* if a new one is issued
136+
* Creates a new request to exchange a refresh token for a session transfer token that can be used to perform web single sign-on.
137+
*
138+
* When opening your website on any browser or web view, add the session transfer token to the URL as a query
139+
* parameter. Then your website can redirect the user to Auth0's `/authorize` endpoint, passing along the query
140+
* parameter with the session transfer token. For example,
141+
* `https://example.com/login?session_transfer_token=THE_TOKEN`.
142+
*
143+
* It will fail with [CredentialsManagerException] if the existing refresh_token is null or no longer valid.
144+
* This method will handle saving the refresh_token, if a new one is issued.
136145
*/
137146
@JvmSynthetic
138147
@Throws(CredentialsManagerException::class)
139148
override suspend fun awaitSsoCredentials(): SSOCredentials {
149+
return awaitSsoCredentials(emptyMap())
150+
}
151+
152+
/**
153+
* Creates a new request to exchange a refresh token for a session transfer token that can be used to perform web single sign-on.
154+
*
155+
* When opening your website on any browser or web view, add the session transfer token to the URL as a query
156+
* parameter. Then your website can redirect the user to Auth0's `/authorize` endpoint, passing along the query
157+
* parameter with the session transfer token. For example,
158+
* `https://example.com/login?session_transfer_token=THE_TOKEN`.
159+
*
160+
* It will fail with [CredentialsManagerException] if the existing refresh_token is null or no longer valid.
161+
* This method will handle saving the refresh_token, if a new one is issued.
162+
*/
163+
@JvmSynthetic
164+
@Throws(CredentialsManagerException::class)
165+
override suspend fun awaitSsoCredentials(parameters: Map<String, String>): SSOCredentials {
140166
return suspendCancellableCoroutine { continuation ->
141-
getSsoCredentials(object : Callback<SSOCredentials, CredentialsManagerException> {
142-
override fun onSuccess(result: SSOCredentials) {
143-
continuation.resume(result)
144-
}
167+
getSsoCredentials(
168+
parameters,
169+
object : Callback<SSOCredentials, CredentialsManagerException> {
170+
override fun onSuccess(result: SSOCredentials) {
171+
continuation.resume(result)
172+
}
145173

146-
override fun onFailure(error: CredentialsManagerException) {
147-
continuation.resumeWithException(error)
148-
}
149-
})
174+
override fun onFailure(error: CredentialsManagerException) {
175+
continuation.resumeWithException(error)
176+
}
177+
})
150178
}
151179
}
152180

@@ -241,7 +269,8 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting
241269
forceRefresh: Boolean
242270
): Credentials {
243271
return suspendCancellableCoroutine { continuation ->
244-
getCredentials(scope,
272+
getCredentials(
273+
scope,
245274
minTtl,
246275
parameters,
247276
headers,
@@ -614,6 +643,24 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting
614643
storage.remove(audience)
615644
}
616645

646+
/**
647+
* Helper method to store the given [SSOCredentials] refresh token in the storage.
648+
* Method will silently return if the passed credentials have no refresh token.
649+
*
650+
* @param ssoCredentials the credentials to save in the storage.
651+
*/
652+
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
653+
internal fun saveSsoCredentials(ssoCredentials: SSOCredentials) {
654+
storage.store(KEY_ID_TOKEN, ssoCredentials.idToken)
655+
val existingRefreshToken = storage.retrieveString(KEY_REFRESH_TOKEN)
656+
// Checking if the existing one needs to be replaced with the new one
657+
if (ssoCredentials.refreshToken.isNullOrEmpty())
658+
return // No refresh token to save
659+
if (ssoCredentials.refreshToken == existingRefreshToken)
660+
return // Same refresh token, no need to save
661+
storage.store(KEY_REFRESH_TOKEN, ssoCredentials.refreshToken)
662+
}
663+
617664
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
618665
internal fun recreateCredentials(
619666
idToken: String,

auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManagerException.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ public class CredentialsManagerException :
4444
BIOMETRICS_INVALID_USER,
4545
BIOMETRIC_AUTHENTICATION_FAILED,
4646
NO_NETWORK,
47-
API_ERROR
47+
API_ERROR,
48+
SSO_EXCHANGE_FAILED,
4849
}
4950

5051
private var code: Code?
@@ -142,6 +143,8 @@ public class CredentialsManagerException :
142143
CredentialsManagerException(Code.NO_NETWORK)
143144
public val API_ERROR: CredentialsManagerException =
144145
CredentialsManagerException(Code.API_ERROR)
146+
public val SSO_EXCHANGE_FAILED: CredentialsManagerException =
147+
CredentialsManagerException(Code.SSO_EXCHANGE_FAILED)
145148

146149

147150
private fun getMessage(code: Code): String {
@@ -187,6 +190,7 @@ public class CredentialsManagerException :
187190
Code.BIOMETRIC_AUTHENTICATION_FAILED -> "Biometric authentication failed."
188191
Code.NO_NETWORK -> "Failed to execute the network request."
189192
Code.API_ERROR -> "An error occurred while processing the request."
193+
Code.SSO_EXCHANGE_FAILED ->"The exchange of the refresh token for SSO credentials failed."
190194
}
191195
}
192196
}

0 commit comments

Comments
 (0)