Skip to content

Commit 3c9aa2c

Browse files
authored
Added Native to Web support (#803)
1 parent 83ddf2d commit 3c9aa2c

File tree

12 files changed

+468
-333
lines changed

12 files changed

+468
-333
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

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

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -923,26 +923,31 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
923923
}
924924

925925
/**
926-
* Creates a new request to fetch a session token in exchange for a refresh token.
926+
* Creates a new request to exchange a refresh token for a session transfer token that can be used to perform web single sign-on.
927+
*
928+
* When opening your website on any browser or web view, add the session transfer token to the URL as a query
929+
* parameter. Then your website can redirect the user to Auth0's `/authorize` endpoint, passing along the query
930+
* parameter with the session transfer token. For example,
931+
* `https://example.com/login?session_transfer_token=THE_TOKEN`.
932+
*
927933
*
928934
* @param refreshToken A valid refresh token obtained as part of Auth0 authentication
929-
* @return a request to fetch a session token
935+
* @return a request to fetch a session transfer token
936+
*
930937
*/
931-
public fun fetchSessionToken(refreshToken: String): Request<SSOCredentials, AuthenticationException> {
938+
public fun ssoExchange(refreshToken: String): Request<SSOCredentials, AuthenticationException> {
932939
val params = ParameterBuilder.newBuilder()
933-
.setClientId(clientId)
934-
.setGrantType(ParameterBuilder.GRANT_TYPE_TOKEN_EXCHANGE)
935-
.set(SUBJECT_TOKEN_KEY, refreshToken)
936-
.set(SUBJECT_TOKEN_TYPE_KEY, ParameterBuilder.TOKEN_TYPE_REFRESH_TOKEN)
937-
.set(REQUESTED_TOKEN_TYPE_KEY, ParameterBuilder.TOKEN_TYPE_SESSION_TOKEN)
940+
.setGrantType(ParameterBuilder.REFRESH_TOKEN_KEY)
941+
.setAudience("urn:${auth0.domain}:session_transfer")
942+
.set(ParameterBuilder.REFRESH_TOKEN_KEY, refreshToken)
938943
.asDictionary()
939944
return loginWithTokenGeneric<SSOCredentials>(params)
940945
}
941946

942947
/**
943948
* Helper function to make a request to the /oauth/token endpoint with a custom response type.
944949
*/
945-
private inline fun <reified T> loginWithTokenGeneric(parameters: Map<String, String>): Request<T,AuthenticationException> {
950+
private inline fun <reified T> loginWithTokenGeneric(parameters: Map<String, String>): Request<T, AuthenticationException> {
946951
val url = auth0.getDomainUrl().toHttpUrl().newBuilder()
947952
.addPathSegment(OAUTH_PATH)
948953
.addPathSegment(TOKEN_PATH)

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
@@ -30,9 +30,16 @@ public abstract class BaseCredentialsManager internal constructor(
3030

3131
@Throws(CredentialsManagerException::class)
3232
public abstract fun saveCredentials(credentials: Credentials)
33-
public abstract fun saveSsoCredentials(ssoCredentials: SSOCredentials)
3433
public abstract fun getCredentials(callback: Callback<Credentials, CredentialsManagerException>)
35-
public abstract fun getSsoCredentials(callback: Callback<SSOCredentials, CredentialsManagerException>)
34+
public abstract fun getSsoCredentials(
35+
parameters: Map<String, String>,
36+
callback: Callback<SSOCredentials, CredentialsManagerException>
37+
)
38+
39+
public abstract fun getSsoCredentials(
40+
callback: Callback<SSOCredentials, CredentialsManagerException>
41+
)
42+
3643
public abstract fun getCredentials(
3744
scope: String?,
3845
minTtl: Int,
@@ -65,7 +72,13 @@ public abstract class BaseCredentialsManager internal constructor(
6572

6673
@JvmSynthetic
6774
@Throws(CredentialsManagerException::class)
68-
public abstract suspend fun awaitSsoCredentials(): SSOCredentials
75+
public abstract suspend fun awaitSsoCredentials(parameters: Map<String, String>)
76+
: SSOCredentials
77+
78+
@JvmSynthetic
79+
@Throws(CredentialsManagerException::class)
80+
public abstract suspend fun awaitSsoCredentials()
81+
: SSOCredentials
6982

7083
@JvmSynthetic
7184
@Throws(CredentialsManagerException::class)

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

Lines changed: 87 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -54,53 +54,55 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting
5454
storage.store(LEGACY_KEY_CACHE_EXPIRES_AT, credentials.expiresAt.time)
5555
}
5656

57-
5857
/**
59-
* Stores the given [SSOCredentials] refresh token in the storage.
60-
* This method must be called if the SSOCredentials are obtained by directly invoking [AuthenticationAPIClient.fetchSessionToken] api and
61-
* [rotating refresh token](https://auth0.com/docs/secure/tokens/refresh-tokens/refresh-token-rotation) are enabled for
62-
* the client. Method will silently return ,if the passed credentials has no refresh token.
58+
* Creates a new request to exchange a refresh token for a session transfer token that can be used to perform web single sign-on.
6359
*
64-
* @param ssoCredentials the credentials to save in the storage.
60+
* When opening your website on any browser or web view, add the session transfer token to the URL as a query
61+
* parameter. Then your website can redirect the user to Auth0's `/authorize` endpoint, passing along the query
62+
* parameter with the session transfer token. For example,
63+
* `https://example.com/login?session_transfer_token=THE_TOKEN`.
64+
*
65+
* It will fail with [CredentialsManagerException] if the existing refresh_token is null or no longer valid.
66+
* This method will handle saving the refresh_token, if a new one is issued.
6567
*/
66-
override fun saveSsoCredentials(ssoCredentials: SSOCredentials) {
67-
if (ssoCredentials.refreshToken.isNullOrEmpty())
68-
return // No refresh token to save
69-
serialExecutor.execute {
70-
val existingRefreshToken = storage.retrieveString(KEY_REFRESH_TOKEN)
71-
// Checking if the existing one needs to be replaced with the new one
72-
if (ssoCredentials.refreshToken == existingRefreshToken)
73-
return@execute
74-
storage.store(KEY_REFRESH_TOKEN, ssoCredentials.refreshToken)
75-
}
68+
override fun getSsoCredentials(callback: Callback<SSOCredentials, CredentialsManagerException>) {
69+
getSsoCredentials(emptyMap(), callback)
7670
}
7771

7872
/**
79-
* Fetches a new [SSOCredentials] . It will fail with [CredentialsManagerException]
80-
* if the existing refresh_token is null or no longer valid. This method will handle saving the refresh_token,
81-
* if a new one is issued
73+
* Creates a new request to exchange a refresh token for a session transfer token that can be used to perform web single sign-on.
74+
*
75+
* When opening your website on any browser or web view, add the session transfer token to the URL as a query
76+
* parameter. Then your website can redirect the user to Auth0's `/authorize` endpoint, passing along the query
77+
* parameter with the session transfer token. For example,
78+
* `https://example.com/login?session_transfer_token=THE_TOKEN`.
79+
*
80+
* It will fail with [CredentialsManagerException] if the existing refresh_token is null or no longer valid.
81+
* This method will handle saving the refresh_token, if a new one is issued.
8282
*/
83-
override fun getSsoCredentials(callback: Callback<SSOCredentials, CredentialsManagerException>) {
83+
override fun getSsoCredentials(
84+
parameters: Map<String, String>,
85+
callback: Callback<SSOCredentials, CredentialsManagerException>
86+
) {
8487
serialExecutor.execute {
8588
val refreshToken = storage.retrieveString(KEY_REFRESH_TOKEN)
8689
if (refreshToken.isNullOrEmpty()) {
8790
callback.onFailure(CredentialsManagerException.NO_REFRESH_TOKEN)
8891
return@execute
8992
}
9093

94+
val request = authenticationClient.ssoExchange(refreshToken)
9195
try {
92-
val sessionCredentials =
93-
authenticationClient.fetchSessionToken(refreshToken)
94-
.execute()
95-
saveSsoCredentials(sessionCredentials)
96-
callback.onSuccess(sessionCredentials)
96+
if (parameters.isNotEmpty()) {
97+
request.addParameters(parameters)
98+
}
99+
val sessionTransferCredentials = request.execute()
100+
saveSsoCredentials(sessionTransferCredentials)
101+
callback.onSuccess(sessionTransferCredentials)
97102
} catch (error: AuthenticationException) {
98103
val exception = when {
99-
error.isRefreshTokenDeleted ||
100-
error.isInvalidRefreshToken -> CredentialsManagerException.Code.RENEW_FAILED
101-
102104
error.isNetworkError -> CredentialsManagerException.Code.NO_NETWORK
103-
else -> CredentialsManagerException.Code.API_ERROR
105+
else -> CredentialsManagerException.Code.SSO_EXCHANGE_FAILED
104106
}
105107
callback.onFailure(
106108
CredentialsManagerException(
@@ -113,23 +115,48 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting
113115
}
114116

115117
/**
116-
* Fetches a new [SSOCredentials] . It will fail with [CredentialsManagerException]
117-
* if the existing refresh_token is null or no longer valid. This method will handle saving the refresh_token,
118-
* if a new one is issued
118+
* Creates a new request to exchange a refresh token for a session transfer token that can be used to perform web single sign-on.
119+
*
120+
* When opening your website on any browser or web view, add the session transfer token to the URL as a query
121+
* parameter. Then your website can redirect the user to Auth0's `/authorize` endpoint, passing along the query
122+
* parameter with the session transfer token. For example,
123+
* `https://example.com/login?session_transfer_token=THE_TOKEN`.
124+
*
125+
* It will fail with [CredentialsManagerException] if the existing refresh_token is null or no longer valid.
126+
* This method will handle saving the refresh_token, if a new one is issued.
119127
*/
120128
@JvmSynthetic
121129
@Throws(CredentialsManagerException::class)
122130
override suspend fun awaitSsoCredentials(): SSOCredentials {
131+
return awaitSsoCredentials(emptyMap())
132+
}
133+
134+
/**
135+
* Creates a new request to exchange a refresh token for a session transfer token that can be used to perform web single sign-on.
136+
*
137+
* When opening your website on any browser or web view, add the session transfer token to the URL as a query
138+
* parameter. Then your website can redirect the user to Auth0's `/authorize` endpoint, passing along the query
139+
* parameter with the session transfer token. For example,
140+
* `https://example.com/login?session_transfer_token=THE_TOKEN`.
141+
*
142+
* It will fail with [CredentialsManagerException] if the existing refresh_token is null or no longer valid.
143+
* This method will handle saving the refresh_token, if a new one is issued.
144+
*/
145+
@JvmSynthetic
146+
@Throws(CredentialsManagerException::class)
147+
override suspend fun awaitSsoCredentials(parameters: Map<String, String>): SSOCredentials {
123148
return suspendCancellableCoroutine { continuation ->
124-
getSsoCredentials(object : Callback<SSOCredentials, CredentialsManagerException> {
125-
override fun onSuccess(result: SSOCredentials) {
126-
continuation.resume(result)
127-
}
149+
getSsoCredentials(
150+
parameters,
151+
object : Callback<SSOCredentials, CredentialsManagerException> {
152+
override fun onSuccess(result: SSOCredentials) {
153+
continuation.resume(result)
154+
}
128155

129-
override fun onFailure(error: CredentialsManagerException) {
130-
continuation.resumeWithException(error)
131-
}
132-
})
156+
override fun onFailure(error: CredentialsManagerException) {
157+
continuation.resumeWithException(error)
158+
}
159+
})
133160
}
134161
}
135162

@@ -224,7 +251,8 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting
224251
forceRefresh: Boolean
225252
): Credentials {
226253
return suspendCancellableCoroutine { continuation ->
227-
getCredentials(scope,
254+
getCredentials(
255+
scope,
228256
minTtl,
229257
parameters,
230258
headers,
@@ -458,6 +486,24 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting
458486
storage.remove(LEGACY_KEY_CACHE_EXPIRES_AT)
459487
}
460488

489+
/**
490+
* Helper method to store the given [SSOCredentials] refresh token in the storage.
491+
* Method will silently return if the passed credentials have no refresh token.
492+
*
493+
* @param ssoCredentials the credentials to save in the storage.
494+
*/
495+
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
496+
internal fun saveSsoCredentials(ssoCredentials: SSOCredentials) {
497+
storage.store(KEY_ID_TOKEN, ssoCredentials.idToken)
498+
val existingRefreshToken = storage.retrieveString(KEY_REFRESH_TOKEN)
499+
// Checking if the existing one needs to be replaced with the new one
500+
if (ssoCredentials.refreshToken.isNullOrEmpty())
501+
return // No refresh token to save
502+
if (ssoCredentials.refreshToken == existingRefreshToken)
503+
return // Same refresh token, no need to save
504+
storage.store(KEY_REFRESH_TOKEN, ssoCredentials.refreshToken)
505+
}
506+
461507
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
462508
internal fun recreateCredentials(
463509
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)