Skip to content
Merged
56 changes: 56 additions & 0 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
- [Sign Up with a database connection](#sign-up-with-a-database-connection)
- [Get user information](#get-user-information)
- [Custom Token Exchange](#custom-token-exchange)
- [Native to Web SSO login](#native-to-web-sso-login)
- [Credentials Manager](#credentials-manager)
- [Secure Credentials Manager](#secure-credentials-manager)
- [Usage](#usage)
Expand Down Expand Up @@ -540,6 +541,61 @@ authentication
</details>


## Native to Web SSO login

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.

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.

```kotlin
authentication
.ssoExchange("refresh_token")
.start(object : Callback<SSOCredentials, AuthenticationException> {
override fun onSuccess(result: SSOCredentials) {
// Use the sessionTransferToken token to authenticate the user in a web session in your app
}

override fun onFailure(exception: AuthenticationException) {
// Handle error
}

})
```

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

``` kotlin
try {
val ssoCredentials = authentication
.ssoExchange("refresh_token")
.await()
} catch (e: AuthenticationException) {
e.printStacktrace()
}
```
</details>

<details>
<summary>Using Java</summary>

```java
authentication
.ssoExchange("refresh_token")
.start(new Callback<SSOCredentials, AuthenticationException>() {
@Override
public void onSuccess(@Nullable SSOCredentials result) {
// Handle success
}
@Override
public void onFailure(@NonNull AuthenticationException error) {
// Handle error
}
});
```
</details>


## Credentials Manager

### Secure Credentials Manager
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -923,26 +923,31 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
}

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

/**
* Helper function to make a request to the /oauth/token endpoint with a custom response type.
*/
private inline fun <reified T> loginWithTokenGeneric(parameters: Map<String, String>): Request<T,AuthenticationException> {
private inline fun <reified T> loginWithTokenGeneric(parameters: Map<String, String>): Request<T, AuthenticationException> {
val url = auth0.getDomainUrl().toHttpUrl().newBuilder()
.addPathSegment(OAUTH_PATH)
.addPathSegment(TOKEN_PATH)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,9 +160,6 @@ public class ParameterBuilder private constructor(parameters: Map<String, String
public const val GRANT_TYPE_TOKEN_EXCHANGE: String =
"urn:ietf:params:oauth:grant-type:token-exchange"
public const val GRANT_TYPE_PASSKEY :String = "urn:okta:params:oauth:grant-type:webauthn"
public const val TOKEN_TYPE_REFRESH_TOKEN :String = "urn:ietf:params:oauth:token-type:refresh_token"
public const val TOKEN_TYPE_SESSION_TOKEN :String = "urn:auth0:params:oauth:token-type:session_token"
public const val SCOPE_OPENID: String = "openid"
public const val SCOPE_OFFLINE_ACCESS: String = "openid offline_access"
public const val SCOPE_KEY: String = "scope"
public const val REFRESH_TOKEN_KEY: String = "refresh_token"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,16 @@ public abstract class BaseCredentialsManager internal constructor(

@Throws(CredentialsManagerException::class)
public abstract fun saveCredentials(credentials: Credentials)
public abstract fun saveSsoCredentials(ssoCredentials: SSOCredentials)
public abstract fun getCredentials(callback: Callback<Credentials, CredentialsManagerException>)
public abstract fun getSsoCredentials(callback: Callback<SSOCredentials, CredentialsManagerException>)
public abstract fun getSsoCredentials(
parameters: Map<String, String>,
callback: Callback<SSOCredentials, CredentialsManagerException>
)

public abstract fun getSsoCredentials(
callback: Callback<SSOCredentials, CredentialsManagerException>
)

public abstract fun getCredentials(
scope: String?,
minTtl: Int,
Expand Down Expand Up @@ -65,7 +72,13 @@ public abstract class BaseCredentialsManager internal constructor(

@JvmSynthetic
@Throws(CredentialsManagerException::class)
public abstract suspend fun awaitSsoCredentials(): SSOCredentials
public abstract suspend fun awaitSsoCredentials(parameters: Map<String, String>)
: SSOCredentials

@JvmSynthetic
@Throws(CredentialsManagerException::class)
public abstract suspend fun awaitSsoCredentials()
: SSOCredentials

@JvmSynthetic
@Throws(CredentialsManagerException::class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,53 +54,55 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting
storage.store(LEGACY_KEY_CACHE_EXPIRES_AT, credentials.expiresAt.time)
}


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

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

val request = authenticationClient.ssoExchange(refreshToken)
try {
val sessionCredentials =
authenticationClient.fetchSessionToken(refreshToken)
.execute()
saveSsoCredentials(sessionCredentials)
callback.onSuccess(sessionCredentials)
if (parameters.isNotEmpty()) {
request.addParameters(parameters)
}
val sessionTransferCredentials = request.execute()
saveSsoCredentials(sessionTransferCredentials)
callback.onSuccess(sessionTransferCredentials)
} catch (error: AuthenticationException) {
val exception = when {
error.isRefreshTokenDeleted ||
error.isInvalidRefreshToken -> CredentialsManagerException.Code.RENEW_FAILED

error.isNetworkError -> CredentialsManagerException.Code.NO_NETWORK
else -> CredentialsManagerException.Code.API_ERROR
else -> CredentialsManagerException.Code.SSO_EXCHANGE_FAILED
}
callback.onFailure(
CredentialsManagerException(
Expand All @@ -113,23 +115,48 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting
}

/**
* Fetches a new [SSOCredentials] . It will fail with [CredentialsManagerException]
* if the existing refresh_token is null or no longer valid. This method will handle saving the refresh_token,
* if a new one is issued
* Creates a new request to exchange a refresh token for a session transfer token that can be used to perform web single sign-on.
*
* When opening your website on any browser or web view, add the session transfer token to the URL as a query
* parameter. Then your website can redirect the user to Auth0's `/authorize` endpoint, passing along the query
* parameter with the session transfer token. For example,
* `https://example.com/login?session_transfer_token=THE_TOKEN`.
*
* It will fail with [CredentialsManagerException] if the existing refresh_token is null or no longer valid.
* This method will handle saving the refresh_token, if a new one is issued.
*/
@JvmSynthetic
@Throws(CredentialsManagerException::class)
override suspend fun awaitSsoCredentials(): SSOCredentials {
return awaitSsoCredentials(emptyMap())
}

/**
* Creates a new request to exchange a refresh token for a session transfer token that can be used to perform web single sign-on.
*
* When opening your website on any browser or web view, add the session transfer token to the URL as a query
* parameter. Then your website can redirect the user to Auth0's `/authorize` endpoint, passing along the query
* parameter with the session transfer token. For example,
* `https://example.com/login?session_transfer_token=THE_TOKEN`.
*
* It will fail with [CredentialsManagerException] if the existing refresh_token is null or no longer valid.
* This method will handle saving the refresh_token, if a new one is issued.
*/
@JvmSynthetic
@Throws(CredentialsManagerException::class)
override suspend fun awaitSsoCredentials(parameters: Map<String, String>): SSOCredentials {
return suspendCancellableCoroutine { continuation ->
getSsoCredentials(object : Callback<SSOCredentials, CredentialsManagerException> {
override fun onSuccess(result: SSOCredentials) {
continuation.resume(result)
}
getSsoCredentials(
parameters,
object : Callback<SSOCredentials, CredentialsManagerException> {
override fun onSuccess(result: SSOCredentials) {
continuation.resume(result)
}

override fun onFailure(error: CredentialsManagerException) {
continuation.resumeWithException(error)
}
})
override fun onFailure(error: CredentialsManagerException) {
continuation.resumeWithException(error)
}
})
}
}

Expand Down Expand Up @@ -224,7 +251,8 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting
forceRefresh: Boolean
): Credentials {
return suspendCancellableCoroutine { continuation ->
getCredentials(scope,
getCredentials(
scope,
minTtl,
parameters,
headers,
Expand Down Expand Up @@ -458,6 +486,24 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting
storage.remove(LEGACY_KEY_CACHE_EXPIRES_AT)
}

/**
* Helper method to store the given [SSOCredentials] refresh token in the storage.
* Method will silently return if the passed credentials have no refresh token.
*
* @param ssoCredentials the credentials to save in the storage.
*/
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun saveSsoCredentials(ssoCredentials: SSOCredentials) {
storage.store(KEY_ID_TOKEN, ssoCredentials.idToken)
val existingRefreshToken = storage.retrieveString(KEY_REFRESH_TOKEN)
// Checking if the existing one needs to be replaced with the new one
if (ssoCredentials.refreshToken.isNullOrEmpty())
return // No refresh token to save
if (ssoCredentials.refreshToken == existingRefreshToken)
return // Same refresh token, no need to save
storage.store(KEY_REFRESH_TOKEN, ssoCredentials.refreshToken)
}

@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun recreateCredentials(
idToken: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ public class CredentialsManagerException :
BIOMETRICS_INVALID_USER,
BIOMETRIC_AUTHENTICATION_FAILED,
NO_NETWORK,
API_ERROR
API_ERROR,
SSO_EXCHANGE_FAILED,
}

private var code: Code?
Expand Down Expand Up @@ -142,6 +143,8 @@ public class CredentialsManagerException :
CredentialsManagerException(Code.NO_NETWORK)
public val API_ERROR: CredentialsManagerException =
CredentialsManagerException(Code.API_ERROR)
public val SSO_EXCHANGE_FAILED: CredentialsManagerException =
CredentialsManagerException(Code.SSO_EXCHANGE_FAILED)


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