diff --git a/EXAMPLES.md b/EXAMPLES.md index 157ee6270..ebfa05f37 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -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) @@ -540,6 +541,61 @@ authentication +## 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 { + 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 + } + + }) +``` + +
+ Using coroutines + +``` kotlin +try { + val ssoCredentials = authentication + .ssoExchange("refresh_token") + .await() +} catch (e: AuthenticationException) { + e.printStacktrace() +} +``` +
+ +
+ Using Java + +```java +authentication + .ssoExchange("refresh_token") + .start(new Callback() { + @Override + public void onSuccess(@Nullable SSOCredentials result) { + // Handle success + } + @Override + public void onFailure(@NonNull AuthenticationException error) { + // Handle error + } + }); +``` +
+ + ## Credentials Manager ### Secure Credentials Manager diff --git a/auth0/src/main/java/com/auth0/android/authentication/AuthenticationAPIClient.kt b/auth0/src/main/java/com/auth0/android/authentication/AuthenticationAPIClient.kt index a57bfd3d5..e8dcb63ae 100755 --- a/auth0/src/main/java/com/auth0/android/authentication/AuthenticationAPIClient.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/AuthenticationAPIClient.kt @@ -923,18 +923,23 @@ 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 { + public fun ssoExchange(refreshToken: String): Request { 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(params) } @@ -942,7 +947,7 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe /** * Helper function to make a request to the /oauth/token endpoint with a custom response type. */ - private inline fun loginWithTokenGeneric(parameters: Map): Request { + private inline fun loginWithTokenGeneric(parameters: Map): Request { val url = auth0.getDomainUrl().toHttpUrl().newBuilder() .addPathSegment(OAUTH_PATH) .addPathSegment(TOKEN_PATH) diff --git a/auth0/src/main/java/com/auth0/android/authentication/ParameterBuilder.kt b/auth0/src/main/java/com/auth0/android/authentication/ParameterBuilder.kt index f92546895..a7d5d3532 100755 --- a/auth0/src/main/java/com/auth0/android/authentication/ParameterBuilder.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/ParameterBuilder.kt @@ -160,9 +160,6 @@ public class ParameterBuilder private constructor(parameters: Map) - public abstract fun getSsoCredentials(callback: Callback) + public abstract fun getSsoCredentials( + parameters: Map, + callback: Callback + ) + + public abstract fun getSsoCredentials( + callback: Callback + ) + public abstract fun getCredentials( scope: String?, minTtl: Int, @@ -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) + : SSOCredentials + + @JvmSynthetic + @Throws(CredentialsManagerException::class) + public abstract suspend fun awaitSsoCredentials() + : SSOCredentials @JvmSynthetic @Throws(CredentialsManagerException::class) diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt index 8f6b3e510..3afaae7b1 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt @@ -54,33 +54,36 @@ 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) { + 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) { + override fun getSsoCredentials( + parameters: Map, + callback: Callback + ) { serialExecutor.execute { val refreshToken = storage.retrieveString(KEY_REFRESH_TOKEN) if (refreshToken.isNullOrEmpty()) { @@ -88,19 +91,18 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting 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( @@ -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): SSOCredentials { return suspendCancellableCoroutine { continuation -> - getSsoCredentials(object : Callback { - override fun onSuccess(result: SSOCredentials) { - continuation.resume(result) - } + getSsoCredentials( + parameters, + object : Callback { + override fun onSuccess(result: SSOCredentials) { + continuation.resume(result) + } - override fun onFailure(error: CredentialsManagerException) { - continuation.resumeWithException(error) - } - }) + override fun onFailure(error: CredentialsManagerException) { + continuation.resumeWithException(error) + } + }) } } @@ -224,7 +251,8 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting forceRefresh: Boolean ): Credentials { return suspendCancellableCoroutine { continuation -> - getCredentials(scope, + getCredentials( + scope, minTtl, parameters, headers, @@ -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, diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManagerException.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManagerException.kt index 4367b9c02..10210e286 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManagerException.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManagerException.kt @@ -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? @@ -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 { @@ -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." } } } diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt index 955827f9a..feaf17296 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt @@ -111,61 +111,55 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT storage.store(KEY_CAN_REFRESH, canRefresh) } catch (e: IncompatibleDeviceException) { throw CredentialsManagerException( - CredentialsManagerException.Code.INCOMPATIBLE_DEVICE, - e + CredentialsManagerException.Code.INCOMPATIBLE_DEVICE, e ) - } catch (e: CryptoException) { - /* + } catch (e: CryptoException) {/* * If the keys were invalidated in the call above a good new pair is going to be available * to use on the next call. We clear any existing credentials so #hasValidCredentials returns * a true value. Retrying this operation will succeed. */ clearCredentials() throw CredentialsManagerException( - CredentialsManagerException.Code.CRYPTO_EXCEPTION, - e + CredentialsManagerException.Code.CRYPTO_EXCEPTION, e ) } } /** - * 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 { - lateinit var existingCredentials: Credentials - try { - existingCredentials = getExistingCredentials() - } catch (exception: CredentialsManagerException) { - Log.e(TAG,"Error while fetching existing credentials", exception) - return@execute - } - // Checking if the existing one needs to be replaced with the new one - if (existingCredentials.refreshToken == ssoCredentials.refreshToken) - return@execute - val newCredentials = - existingCredentials.copy(refreshToken = ssoCredentials.refreshToken) - saveCredentials(newCredentials) - } + override fun getSsoCredentials(callback: Callback) { + 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) { + override fun getSsoCredentials( + parameters: Map, + callback: Callback + ) { serialExecutor.execute { - lateinit var existingCredentials:Credentials - try{ + lateinit var existingCredentials: Credentials + try { existingCredentials = getExistingCredentials() - }catch (exception:CredentialsManagerException){ + } catch (exception: CredentialsManagerException) { callback.onFailure(exception) return@execute } @@ -173,24 +167,24 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT callback.onFailure(CredentialsManagerException.NO_REFRESH_TOKEN) return@execute } + + val request = + authenticationClient.ssoExchange(existingCredentials.refreshToken!!) try { - val sessionCredentials = - authenticationClient.fetchSessionToken(existingCredentials.refreshToken!!) - .execute() + if (parameters.isNotEmpty()) { + request.addParameters(parameters) + } + val sessionCredentials = request.execute() saveSsoCredentials(sessionCredentials) callback.onSuccess(sessionCredentials) } 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( - exception, - error + exception, error ) ) } catch (error: CredentialsManagerException) { @@ -203,23 +197,48 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT } /** - * 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): SSOCredentials { return suspendCancellableCoroutine { continuation -> - getSsoCredentials(object : Callback { - override fun onSuccess(result: SSOCredentials) { - continuation.resume(result) - } + getSsoCredentials( + parameters, + object : Callback { + override fun onSuccess(result: SSOCredentials) { + continuation.resume(result) + } - override fun onFailure(error: CredentialsManagerException) { - continuation.resumeWithException(error) - } - }) + override fun onFailure(error: CredentialsManagerException) { + continuation.resumeWithException(error) + } + }) } } @@ -254,8 +273,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT @JvmSynthetic @Throws(CredentialsManagerException::class) override suspend fun awaitCredentials( - scope: String?, - minTtl: Int + scope: String?, minTtl: Int ): Credentials { return awaitCredentials(scope, minTtl, emptyMap()) } @@ -276,15 +294,10 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT @JvmSynthetic @Throws(CredentialsManagerException::class) override suspend fun awaitCredentials( - scope: String?, - minTtl: Int, - parameters: Map + scope: String?, minTtl: Int, parameters: Map ): Credentials { return awaitCredentials( - scope, - minTtl, - parameters, - false + scope, minTtl, parameters, false ) } @@ -311,11 +324,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT forceRefresh: Boolean, ): Credentials { return awaitCredentials( - scope, - minTtl, - parameters, - mapOf(), - forceRefresh + scope, minTtl, parameters, mapOf(), forceRefresh ) } @@ -391,9 +400,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT * @param callback the callback to receive the result in. */ override fun getCredentials( - scope: String?, - minTtl: Int, - callback: Callback + scope: String?, minTtl: Int, callback: Callback ) { getCredentials(scope, minTtl, emptyMap(), callback) } @@ -418,11 +425,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT callback: Callback ) { getCredentials( - scope, - minTtl, - parameters, - false, - callback + scope, minTtl, parameters, false, callback ) } @@ -449,12 +452,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT callback: Callback ) { getCredentials( - scope, - minTtl, - parameters, - mapOf(), - forceRefresh, - callback + scope, minTtl, parameters, mapOf(), forceRefresh, callback ) } @@ -492,12 +490,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT activity = fragmentActivity, authenticationOptions = localAuthenticationOptions, resultCallback = localAuthenticationResultCallback( - scope, - minTtl, - parameters, - headers, - forceRefresh, - callback + scope, minTtl, parameters, headers, forceRefresh, callback ) ) localAuthenticationManager.authenticate() @@ -515,8 +508,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT object : Callback { override fun onSuccess(result: Boolean) { continueGetCredentials( - scope, minTtl, parameters, headers, forceRefresh, - callback + scope, minTtl, parameters, headers, forceRefresh, callback ) } @@ -562,10 +554,8 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT val canRefresh = storage.retrieveBoolean(KEY_CAN_REFRESH) val emptyCredentials = TextUtils.isEmpty(encryptedEncoded) return !(emptyCredentials || willExpire( - expiresAt, - minTtl - ) && - (canRefresh == null || !canRefresh)) + expiresAt, minTtl + ) && (canRefresh == null || !canRefresh)) } @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) @@ -590,8 +580,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT } catch (e: IncompatibleDeviceException) { callback.onFailure( CredentialsManagerException( - CredentialsManagerException.Code.INCOMPATIBLE_DEVICE, - e + CredentialsManagerException.Code.INCOMPATIBLE_DEVICE, e ) ) return@execute @@ -600,14 +589,12 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT clearCredentials() callback.onFailure( CredentialsManagerException( - CredentialsManagerException.Code.CRYPTO_EXCEPTION, - e + CredentialsManagerException.Code.CRYPTO_EXCEPTION, e ) ) return@execute } - val bridgeCredentials = gson.fromJson(json, OptionalCredentials::class.java) - /* OPTIONAL CREDENTIALS + val bridgeCredentials = gson.fromJson(json, OptionalCredentials::class.java)/* OPTIONAL CREDENTIALS * This bridge is required to prevent users from being logged out when * migrating from Credentials with optional Access Token and ID token */ @@ -658,8 +645,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT if (willAccessTokenExpire) { val tokenLifetime = (expiresAt - currentTimeInMillis - minTtl * 1000) / -1000 val wrongTtlException = CredentialsManagerException( - CredentialsManagerException.Code.LARGE_MIN_TTL, - String.format( + CredentialsManagerException.Code.LARGE_MIN_TTL, String.format( Locale.getDefault(), "The lifetime of the renewed Access Token (%d) is less than the minTTL requested (%d). Increase the 'Token Expiration' setting of your Auth0 API in the dashboard, or request a lower minTTL.", tokenLifetime, @@ -683,16 +669,14 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT ) } catch (error: AuthenticationException) { val exception = when { - error.isRefreshTokenDeleted || - error.isInvalidRefreshToken -> CredentialsManagerException.Code.RENEW_FAILED + error.isRefreshTokenDeleted || error.isInvalidRefreshToken -> CredentialsManagerException.Code.RENEW_FAILED error.isNetworkError -> CredentialsManagerException.Code.NO_NETWORK else -> CredentialsManagerException.Code.API_ERROR } callback.onFailure( CredentialsManagerException( - exception, - error + exception, error ) ) return@execute @@ -725,18 +709,15 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT throw CredentialsManagerException.NO_CREDENTIALS } val encrypted = Base64.decode(encryptedEncoded, Base64.DEFAULT) - val json: String - try { - json = String(crypto.decrypt(encrypted)) + val json: String = try { + String(crypto.decrypt(encrypted)) } catch (e: IncompatibleDeviceException) { throw CredentialsManagerException( - CredentialsManagerException.Code.INCOMPATIBLE_DEVICE, - e + CredentialsManagerException.Code.INCOMPATIBLE_DEVICE, e ) } catch (e: CryptoException) { throw CredentialsManagerException( - CredentialsManagerException.Code.CRYPTO_EXCEPTION, - e + CredentialsManagerException.Code.CRYPTO_EXCEPTION, e ) } val bridgeCredentials = gson.fromJson(json, OptionalCredentials::class.java) @@ -756,6 +737,27 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT fragmentActivity!!.clear() } + /** + * Helper method to stores 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) { + val existingCredentials: Credentials = try { + getExistingCredentials() + } catch (exception: CredentialsManagerException) { + Log.e(TAG, "Error while fetching existing credentials", exception) + return + } + val newCredentials = existingCredentials.copy( + refreshToken = ssoCredentials.refreshToken + ?: existingCredentials.refreshToken, idToken = ssoCredentials.idToken + ) + saveCredentials(newCredentials) + } + internal companion object { private val TAG = SecureCredentialsManager::class.java.simpleName diff --git a/auth0/src/main/java/com/auth0/android/result/SSOCredentials.kt b/auth0/src/main/java/com/auth0/android/result/SSOCredentials.kt index bad5432fd..f56812087 100644 --- a/auth0/src/main/java/com/auth0/android/result/SSOCredentials.kt +++ b/auth0/src/main/java/com/auth0/android/result/SSOCredentials.kt @@ -3,25 +3,34 @@ package com.auth0.android.result import com.google.gson.annotations.SerializedName /** - * Holds the session token credentials required for web SSO . - * - * * *sessionToken*: Session Token for web SSO - * * *refreshToken*: Refresh Token that can be used to request new tokens without signing in again - * * *tokenType*: Contains information about how the token should be used. - * * *expiresIn*: The token expiration duration. - * * *issuedTokenType*: Type of the token issued. - * + * Holds the token credentials required for web SSO. */ public data class SSOCredentials( /** - * The Session Token used for web SSO . + * The token used for web SSO. * - * @return the Session Token. + * @return the session transfer token. */ - @field:SerializedName("access_token") public val sessionToken: String, + @field:SerializedName("access_token") public val sessionTransferToken: String, /** - * Type of the token issued.In this case, an Auth0 session token + * Identity Token with user information. + * + * - Important: You must [validate](https://auth0.com/docs/secure/tokens/id-tokens/validate-id-tokens) any ID + * tokens received from the Authentication API client before using the information they contain. + * + * ## See Also + * + * - [ID Tokens](https://auth0.com/docs/secure/tokens/id-tokens) + * - [JSON Web Tokens](https://auth0.com/docs/secure/tokens/json-web-tokens) + * - [jwt.io](https://jwt.io) + * + * @return the Identity Token. + */ + @field:SerializedName("id_token") public val idToken: String, + + /** + * Type of the token issued. In this case, an Auth0 session transfer token. * * @return the issued token type. */ @@ -37,18 +46,27 @@ public data class SSOCredentials( @field:SerializedName("token_type") public val tokenType: String, /** - * Expiration duration of the session token in seconds. Session tokens are short-lived and expire after a few minutes. - * Once expired, the Session Token can no longer be used for SSO. + * Expiration duration of the session transfer token in seconds. Session transfer tokens are short-lived and expire after a few minutes. + * Once expired, the session transfer tokens can no longer be used for web SSO. * - * @return the expiration duration of this Session Token + * @return the expiration duration of this session transfer token */ @field:SerializedName("expires_in") public val expiresIn: Int, - /** - * Refresh Token that can be used to request new tokens without signing in again. + * Rotated refresh token. Only available when Refresh Token Rotation is enabled. + * - Important: If you're using the Authentication API client directly to perform the SSO exchange, make sure to store this + * new refresh token replacing the previous one. + * + * ## See Also + * - [Refresh Token Rotation](https://auth0.com/docs/secure/tokens/refresh-tokens/refresh-token-rotation) * * @return the Refresh Token. */ @field:SerializedName("refresh_token") public val refreshToken: String? = null -) \ No newline at end of file +) { + + override fun toString(): String { + return "SSOCredentials(sessionTransferToken = ****, idToken = ****,issuedTokenType = $issuedTokenType, tokenType = $tokenType, expiresIn = $expiresIn, refreshToken = ****)" + } +} \ No newline at end of file diff --git a/auth0/src/test/java/com/auth0/android/authentication/AuthenticationAPIClientTest.kt b/auth0/src/test/java/com/auth0/android/authentication/AuthenticationAPIClientTest.kt index 3ea781de0..ffc78cf66 100755 --- a/auth0/src/test/java/com/auth0/android/authentication/AuthenticationAPIClientTest.kt +++ b/auth0/src/test/java/com/auth0/android/authentication/AuthenticationAPIClientTest.kt @@ -2277,7 +2277,7 @@ public class AuthenticationAPIClientTest { public fun shouldCustomTokenExchange() { mockAPI.willReturnSuccessfulLogin() val callback = MockAuthenticationCallback() - client.customTokenExchange( "subject-token-type","subject-token") + client.customTokenExchange("subject-token-type", "subject-token") .start(callback) ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() @@ -2355,10 +2355,10 @@ public class AuthenticationAPIClientTest { } @Test - public fun shouldFetchSessionToken(){ + public fun shouldSsoExchange() { mockAPI.willReturnSuccessfulLogin() val callback = MockAuthenticationCallback() - client.fetchSessionToken( "refresh-token") + client.ssoExchange("refresh-token") .start(callback) ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() @@ -2372,11 +2372,13 @@ public class AuthenticationAPIClientTest { assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat( body, - Matchers.hasEntry("grant_type", ParameterBuilder.GRANT_TYPE_TOKEN_EXCHANGE) + Matchers.hasEntry("grant_type", ParameterBuilder.REFRESH_TOKEN_KEY) + ) + assertThat(body, Matchers.hasEntry("audience", "urn:${auth0.domain}:session_transfer")) + assertThat( + body, + Matchers.hasEntry("refresh_token", "refresh-token") ) - assertThat(body, Matchers.hasEntry("subject_token", "refresh-token")) - assertThat(body, Matchers.hasEntry("subject_token_type", ParameterBuilder.TOKEN_TYPE_REFRESH_TOKEN)) - assertThat(body, Matchers.hasEntry("requested_token_type", ParameterBuilder.TOKEN_TYPE_SESSION_TOKEN)) assertThat( callback, AuthenticationCallbackMatcher.hasPayloadOfType( SSOCredentials::class.java @@ -2385,9 +2387,9 @@ public class AuthenticationAPIClientTest { } @Test - public fun shouldFetchSessionTokenSync(){ + public fun shouldSsoExchangeSync() { mockAPI.willReturnSuccessfulLogin() - val ssoCredentials= client.fetchSessionToken( "refresh-token") + val sessionTransferCredentials = client.ssoExchange("refresh-token") .execute() val request = mockAPI.takeRequest() assertThat( @@ -2400,20 +2402,19 @@ public class AuthenticationAPIClientTest { assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat( body, - Matchers.hasEntry("grant_type", ParameterBuilder.GRANT_TYPE_TOKEN_EXCHANGE) + Matchers.hasEntry("grant_type", ParameterBuilder.REFRESH_TOKEN_KEY) ) - assertThat(body, Matchers.hasEntry("subject_token", "refresh-token")) - assertThat(body, Matchers.hasEntry("subject_token_type", ParameterBuilder.TOKEN_TYPE_REFRESH_TOKEN)) - assertThat(body, Matchers.hasEntry("requested_token_type", ParameterBuilder.TOKEN_TYPE_SESSION_TOKEN)) - assertThat(ssoCredentials, Matchers.`is`(Matchers.notNullValue())) + assertThat(body, Matchers.hasEntry("audience", "urn:${auth0.domain}:session_transfer")) + assertThat(body, Matchers.hasEntry("refresh_token", "refresh-token")) + assertThat(sessionTransferCredentials, Matchers.`is`(Matchers.notNullValue())) } @Test @ExperimentalCoroutinesApi - public fun shouldAwaitFetchSessionToken(): Unit = runTest { + public fun shouldAwaitSsoExchange(): Unit = runTest { mockAPI.willReturnSuccessfulLogin() val ssoCredentials = client - .fetchSessionToken("refresh-token") + .ssoExchange("refresh-token") .await() val request = mockAPI.takeRequest() assertThat( @@ -2426,11 +2427,13 @@ public class AuthenticationAPIClientTest { assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat( body, - Matchers.hasEntry("grant_type", ParameterBuilder.GRANT_TYPE_TOKEN_EXCHANGE) + Matchers.hasEntry("grant_type", ParameterBuilder.REFRESH_TOKEN_KEY) + ) + assertThat(body, Matchers.hasEntry("refresh_token", "refresh-token")) + assertThat( + body, + Matchers.hasEntry("audience", "urn:${auth0.domain}:session_transfer") ) - assertThat(body, Matchers.hasEntry("subject_token", "refresh-token")) - assertThat(body, Matchers.hasEntry("subject_token_type", ParameterBuilder.TOKEN_TYPE_REFRESH_TOKEN)) - assertThat(body, Matchers.hasEntry("requested_token_type", ParameterBuilder.TOKEN_TYPE_SESSION_TOKEN)) assertThat(ssoCredentials, Matchers.`is`(Matchers.notNullValue())) } diff --git a/auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.kt b/auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.kt index 44b1353ce..b12b23601 100644 --- a/auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.kt +++ b/auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.kt @@ -9,7 +9,7 @@ import com.auth0.android.request.internal.Jwt import com.auth0.android.result.Credentials import com.auth0.android.result.CredentialsMock import com.auth0.android.result.SSOCredentials -import com.auth0.android.result.SsoCredentialsMock +import com.auth0.android.result.SSOCredentialsMock import com.auth0.android.util.Clock import com.nhaarman.mockitokotlin2.KArgumentCaptor import com.nhaarman.mockitokotlin2.any @@ -57,7 +57,7 @@ public class CredentialsManagerTest { private lateinit var request: Request @Mock - private lateinit var ssoCredentialsRequest: Request + private lateinit var SSOCredentialsRequest: Request @Mock private lateinit var ssoCallback: Callback @@ -71,7 +71,7 @@ public class CredentialsManagerTest { private val exceptionCaptor: KArgumentCaptor = argumentCaptor() - private val ssoCredentialsCaptor: KArgumentCaptor = argumentCaptor() + private val SSOCredentialsCaptor: KArgumentCaptor = argumentCaptor() @get:Rule public var exception: ExpectedException = ExpectedException.none() @@ -222,21 +222,21 @@ public class CredentialsManagerTest { } @Test - public fun shouldNotSaveIfTheSsoCredentialsHasNoRefreshToken() { - val ssoCredentials = SsoCredentialsMock.create( - "accessToken", - "issuedTokenType", "tokenType", null,60 + public fun shouldNotSaveIfTheSSOCredentialsHasNoRefreshToken() { + verifyZeroInteractions(storage) + val ssoCredentials = SSOCredentialsMock.create( + "accessToken", "identityToken", + "issuedTokenType", "tokenType", null, 60 ) manager.saveSsoCredentials(ssoCredentials) - verifyZeroInteractions(storage) } @Test - public fun shouldNotSaveIfTheNewSsoCredentialRefreshTokenIsSameAsTheExistingOne() { + public fun shouldNotSaveIfTheNewSSOCredentialRefreshTokenIsSameAsTheExistingOne() { verifyNoMoreInteractions(storage) - val ssoCredentials = SsoCredentialsMock.create( - "accessToken", - "issuedTokenType", "tokenType", "refresh_token",60 + val ssoCredentials = SSOCredentialsMock.create( + "accessToken", "identityToken", + "issuedTokenType", "tokenType", "refresh_token", 60 ) Mockito.`when`(storage.retrieveString("com.auth0.refresh_token")) .thenReturn("refresh_token") @@ -245,11 +245,11 @@ public class CredentialsManagerTest { } @Test - public fun shouldSaveTheRefreshTokenIfTheNewSsoRefreshTokenIsNotSameAsTheOldOne() { + public fun shouldSaveTheRefreshTokenIfTheNewSSOCredentialsRefreshTokenIsNotSameAsTheOldOne() { verifyNoMoreInteractions(storage) - val ssoCredentials = SsoCredentialsMock.create( - "accessToken", - "issuedTokenType", "tokenType", "refresh_token",60 + val ssoCredentials = SSOCredentialsMock.create( + "accessToken", "identityToken", + "issuedTokenType", "tokenType", "refresh_token", 60 ) Mockito.`when`(storage.retrieveString("com.auth0.refresh_token")) .thenReturn("refresh-token") @@ -277,20 +277,25 @@ public class CredentialsManagerTest { public fun shouldSaveTheNewRefreshTokenWhenGettingTheSSOCredentials() { Mockito.`when`(storage.retrieveString("com.auth0.refresh_token")) .thenReturn("refresh_token_old") - Mockito.`when`(client.fetchSessionToken("refresh_token_old")) - .thenReturn(ssoCredentialsRequest) - Mockito.`when`(ssoCredentialsRequest.execute()).thenReturn( - SsoCredentialsMock.create( - "session-token", "issued-token-type", "token-type", "refresh-token", 60 + Mockito.`when`(client.ssoExchange("refresh_token_old")) + .thenReturn(SSOCredentialsRequest) + Mockito.`when`(SSOCredentialsRequest.execute()).thenReturn( + SSOCredentialsMock.create( + "web-sso-token", + "identity-token", + "issued-token-type", + "token-type", + "refresh-token", + 60 ) ) manager.getSsoCredentials(ssoCallback) verify(ssoCallback).onSuccess( - ssoCredentialsCaptor.capture() + SSOCredentialsCaptor.capture() ) - val credentials = ssoCredentialsCaptor.firstValue - MatcherAssert.assertThat(credentials.sessionToken, Is.`is`(Matchers.notNullValue())) - MatcherAssert.assertThat(credentials.sessionToken, Is.`is`("session-token")) + val credentials = SSOCredentialsCaptor.firstValue + MatcherAssert.assertThat(credentials.sessionTransferToken, Is.`is`(Matchers.notNullValue())) + MatcherAssert.assertThat(credentials.sessionTransferToken, Is.`is`("web-sso-token")) MatcherAssert.assertThat(credentials.tokenType, Is.`is`("token-type")) MatcherAssert.assertThat(credentials.issuedTokenType, Is.`is`("issued-token-type")) MatcherAssert.assertThat(credentials.refreshToken, Is.`is`("refresh-token")) @@ -302,14 +307,14 @@ public class CredentialsManagerTest { public fun shouldFailOnGetNewSSOCredentialsWhenRefreshTokenExpired() { Mockito.`when`(storage.retrieveString("com.auth0.refresh_token")).thenReturn("refreshToken") Mockito.`when`( - client.fetchSessionToken("refreshToken") - ).thenReturn(ssoCredentialsRequest) + client.ssoExchange("refreshToken") + ).thenReturn(SSOCredentialsRequest) //Trigger failure val authenticationException = AuthenticationException( "invalid_grant", "Unknown or invalid refresh token." ) - Mockito.`when`(ssoCredentialsRequest.execute()).thenThrow(authenticationException) + Mockito.`when`(SSOCredentialsRequest.execute()).thenThrow(authenticationException) manager.getSsoCredentials(ssoCallback) verify(ssoCallback).onFailure( exceptionCaptor.capture() @@ -319,13 +324,13 @@ public class CredentialsManagerTest { MatcherAssert.assertThat(exception.cause, Is.`is`(authenticationException)) MatcherAssert.assertThat( exception.message, - Is.`is`("An error occurred while trying to use the Refresh Token to renew the Credentials.") + Is.`is`("The exchange of the refresh token for SSO credentials failed.") ) } @Test @ExperimentalCoroutinesApi - public fun shouldFailOnAwaitSsoCredentialsWhenNoRefreshTokenWasSaved(): Unit = runTest { + public fun shouldFailOnAwaitSSOCredentialsWhenNoRefreshTokenWasSaved(): Unit = runTest { verifyNoMoreInteractions(client) Mockito.`when`(storage.retrieveString("com.auth0.refresh_token")).thenReturn(null) val exception = assertThrows(CredentialsManagerException::class.java) { @@ -340,19 +345,24 @@ public class CredentialsManagerTest { @Test @ExperimentalCoroutinesApi - public fun shouldSaveNewRefreshingTokenOnAwaitSsoCredentials(): Unit = runTest { + public fun shouldSaveNewRefreshingTokenOnAwaitSSOCredentials(): Unit = runTest { Mockito.`when`(storage.retrieveString("com.auth0.refresh_token")) .thenReturn("refresh_token_old") - Mockito.`when`(client.fetchSessionToken("refresh_token_old")) - .thenReturn(ssoCredentialsRequest) - Mockito.`when`(ssoCredentialsRequest.execute()).thenReturn( - SsoCredentialsMock.create( - "session-token", "issued-token-type", "token-type", "refresh-token", 60 + Mockito.`when`(client.ssoExchange("refresh_token_old")) + .thenReturn(SSOCredentialsRequest) + Mockito.`when`(SSOCredentialsRequest.execute()).thenReturn( + SSOCredentialsMock.create( + "web-sso-token", + "identity-token", + "issued-token-type", + "token-type", + "refresh-token", + 60 ) ) val credentials = manager.awaitSsoCredentials() - MatcherAssert.assertThat(credentials.sessionToken, Is.`is`(Matchers.notNullValue())) - MatcherAssert.assertThat(credentials.sessionToken, Is.`is`("session-token")) + MatcherAssert.assertThat(credentials.sessionTransferToken, Is.`is`(Matchers.notNullValue())) + MatcherAssert.assertThat(credentials.sessionTransferToken, Is.`is`("web-sso-token")) MatcherAssert.assertThat(credentials.tokenType, Is.`is`("token-type")) MatcherAssert.assertThat(credentials.issuedTokenType, Is.`is`("issued-token-type")) MatcherAssert.assertThat(credentials.refreshToken, Is.`is`("refresh-token")) diff --git a/auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerTest.kt b/auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerTest.kt index 2de21a2e8..42866e796 100644 --- a/auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerTest.kt +++ b/auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerTest.kt @@ -16,7 +16,7 @@ import com.auth0.android.request.internal.Jwt import com.auth0.android.result.Credentials import com.auth0.android.result.CredentialsMock import com.auth0.android.result.SSOCredentials -import com.auth0.android.result.SsoCredentialsMock +import com.auth0.android.result.SSOCredentialsMock import com.auth0.android.util.Clock import com.google.gson.Gson import com.nhaarman.mockitokotlin2.KArgumentCaptor @@ -28,7 +28,6 @@ import com.nhaarman.mockitokotlin2.never import com.nhaarman.mockitokotlin2.times import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions -import com.nhaarman.mockitokotlin2.verifyZeroInteractions import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest @@ -80,7 +79,7 @@ public class SecureCredentialsManagerTest { private lateinit var request: Request @Mock - private lateinit var ssoCredentialsRequest: Request + private lateinit var SSOCredentialsRequest: Request @Mock private lateinit var crypto: CryptoUtil @@ -103,7 +102,7 @@ public class SecureCredentialsManagerTest { private val credentialsCaptor: KArgumentCaptor = argumentCaptor() private val exceptionCaptor: KArgumentCaptor = argumentCaptor() - private val ssoCredentialsCaptor: KArgumentCaptor = argumentCaptor() + private val SSOCredentialsCaptor: KArgumentCaptor = argumentCaptor() private val stringCaptor: KArgumentCaptor = argumentCaptor() @@ -172,21 +171,12 @@ public class SecureCredentialsManagerTest { /* * SAVE SSO credentials test */ - @Test - public fun shouldNotSaveIfTheSsoCredentialsHasNoRefreshToken() { - val ssoCredentials = SsoCredentialsMock.create( - "accessToken", - "issuedTokenType", "tokenType", null, 60 - ) - manager.saveSsoCredentials(ssoCredentials) - verifyZeroInteractions(storage) - } @Test public fun shouldNotSaveIfThereIsErrorInGettingTheExistingCredentials() { verifyNoMoreInteractions(storage) - val ssoCredentials = SsoCredentialsMock.create( - "accessToken", + val ssoCredentials = SSOCredentialsMock.create( + "accessToken", "identityToken", "issuedTokenType", "tokenType", "refresh_token", 60 ) val expiresAt = Date(CredentialsMock.ONE_HOUR_AHEAD_MS) @@ -205,30 +195,10 @@ public class SecureCredentialsManagerTest { } @Test - public fun shouldNotSaveIfTheNewSsoCredentialRefreshTokenIsSameAsTheExistingOne() { + public fun shouldSaveIfTheNewSSOCredentialRefreshAndIdTokenIsNotSameAsTheExistingOne() { verifyNoMoreInteractions(storage) - val ssoCredentials = SsoCredentialsMock.create( - "accessToken", - "issuedTokenType", "tokenType", "refreshToken", 60 - ) - val expiresAt = Date(CredentialsMock.ONE_HOUR_AHEAD_MS) - val storedJson = insertTestCredentials( - hasIdToken = true, - hasAccessToken = true, - hasRefreshToken = true, - willExpireAt = expiresAt, - scope = "scope" - ) - manager.saveSsoCredentials(ssoCredentials) - verify(storage, times(0)).store("com.auth0.credentials", storedJson) - verify(storage, times(0)).store("com.auth0.credentials_can_refresh", true) - } - - @Test - public fun shouldSaveIfTheNewSsoCredentialRefreshTokenIsNotSameAsTheExistingOne() { - verifyNoMoreInteractions(storage) - val ssoCredentials = SsoCredentialsMock.create( - "accessToken", + val sessionTransferCredentials = SSOCredentialsMock.create( + "accessToken", "identityToken", "issuedTokenType", "tokenType", "refresh_token", 60 ) val expiresAt = Date(CredentialsMock.ONE_HOUR_AHEAD_MS) @@ -240,7 +210,7 @@ public class SecureCredentialsManagerTest { scope = "scope" ) val newCredentials = CredentialsMock.create( - "idToken", + "identityToken", "accessToken", "type", "refresh_token", @@ -249,22 +219,22 @@ public class SecureCredentialsManagerTest { ) val json = gson.toJson(newCredentials) Mockito.`when`(crypto.encrypt(any())).thenReturn(json.toByteArray()) - manager.saveSsoCredentials(ssoCredentials) + manager.saveSsoCredentials(sessionTransferCredentials) verify(storage).store(eq("com.auth0.credentials"), stringCaptor.capture()) val encodedJson = stringCaptor.firstValue MatcherAssert.assertThat(encodedJson, Is.`is`(Matchers.notNullValue())) val decoded = Base64.decode(encodedJson, Base64.DEFAULT) val storedCredentials = gson.fromJson(String(decoded), Credentials::class.java) MatcherAssert.assertThat(storedCredentials.accessToken, Is.`is`("accessToken")) - MatcherAssert.assertThat(storedCredentials.idToken, Is.`is`("idToken")) MatcherAssert.assertThat(storedCredentials.refreshToken, Is.`is`("refresh_token")) + MatcherAssert.assertThat(storedCredentials.idToken, Is.`is`("identityToken")) } /* * GET SSO credentials test */ @Test - public fun shouldThrowExceptionIfNoCredentialsExistOnGetSsoCredentials() { + public fun shouldThrowExceptionIfNoCredentialsExistOnGetSSOCredentials() { Mockito.`when`(storage.retrieveString("com.auth0.credentials")) .thenReturn(null) manager.getSsoCredentials(ssoCallback) @@ -280,7 +250,7 @@ public class SecureCredentialsManagerTest { } @Test - public fun shouldThrowExceptionIfFetchingExistingCredentialsFailsOnGetSsoCredentials() { + public fun shouldThrowExceptionIfFetchingExistingCredentialsFailsOnGetSSOCredentials() { val expiresAt = Date(CredentialsMock.ONE_HOUR_AHEAD_MS) val storedJson = insertTestCredentials( hasIdToken = true, @@ -304,7 +274,7 @@ public class SecureCredentialsManagerTest { } @Test - public fun shouldThrowExceptionIfExistingCredentialsHasNoRefreshTokenOnGetSsoCredentials() { + public fun shouldThrowExceptionIfExistingCredentialsHasNoRefreshTokenOnGetSSOCredentials() { val expiresAt = Date(CredentialsMock.ONE_HOUR_AHEAD_MS) insertTestCredentials( hasIdToken = true, @@ -326,13 +296,18 @@ public class SecureCredentialsManagerTest { } @Test - public fun shouldFetchTheNewRefreshTokenOnGetSSoCredentials() { + public fun shouldFetchTheNewRefreshTokenOnGetSSOCredentials() { val expiresAt = Date(CredentialsMock.ONE_HOUR_AHEAD_MS) - Mockito.`when`(client.fetchSessionToken("refreshToken")) - .thenReturn(ssoCredentialsRequest) - Mockito.`when`(ssoCredentialsRequest.execute()).thenReturn( - SsoCredentialsMock.create( - "session-token", "issued-token-type", "token-type", "refresh-token", 60 + Mockito.`when`(client.ssoExchange("refreshToken")) + .thenReturn(SSOCredentialsRequest) + Mockito.`when`(SSOCredentialsRequest.execute()).thenReturn( + SSOCredentialsMock.create( + "web-sso-token", + "identity-token", + "issued-token-type", + "token-type", + "refresh-token", + 60 ) ) insertTestCredentials( @@ -354,11 +329,11 @@ public class SecureCredentialsManagerTest { Mockito.`when`(crypto.encrypt(any())).thenReturn(json.toByteArray()) manager.getSsoCredentials(ssoCallback) verify(ssoCallback).onSuccess( - ssoCredentialsCaptor.capture() + SSOCredentialsCaptor.capture() ) - val credentials = ssoCredentialsCaptor.firstValue - MatcherAssert.assertThat(credentials.sessionToken, Is.`is`(Matchers.notNullValue())) - MatcherAssert.assertThat(credentials.sessionToken, Is.`is`("session-token")) + val credentials = SSOCredentialsCaptor.firstValue + MatcherAssert.assertThat(credentials.sessionTransferToken, Is.`is`(Matchers.notNullValue())) + MatcherAssert.assertThat(credentials.sessionTransferToken, Is.`is`("web-sso-token")) MatcherAssert.assertThat(credentials.tokenType, Is.`is`("token-type")) MatcherAssert.assertThat(credentials.issuedTokenType, Is.`is`("issued-token-type")) MatcherAssert.assertThat(credentials.refreshToken, Is.`is`("refresh-token")) @@ -373,10 +348,10 @@ public class SecureCredentialsManagerTest { } @Test - public fun shouldFailWhenRefreshTokenExpiredOnGetSsoCredentials() { + public fun shouldFailWhenRefreshTokenExpiredOnGetSSOCredentials() { val expiresAt = Date(CredentialsMock.ONE_HOUR_AHEAD_MS) - Mockito.`when`(client.fetchSessionToken("refreshToken")) - .thenReturn(ssoCredentialsRequest) + Mockito.`when`(client.ssoExchange("refreshToken")) + .thenReturn(SSOCredentialsRequest) insertTestCredentials( hasIdToken = true, hasAccessToken = true, @@ -389,7 +364,7 @@ public class SecureCredentialsManagerTest { "invalid_grant", "Unknown or invalid refresh token." ) - Mockito.`when`(ssoCredentialsRequest.execute()).thenThrow(authenticationException) + Mockito.`when`(SSOCredentialsRequest.execute()).thenThrow(authenticationException) manager.getSsoCredentials(ssoCallback) verify(ssoCallback).onFailure( exceptionCaptor.capture() @@ -399,7 +374,7 @@ public class SecureCredentialsManagerTest { MatcherAssert.assertThat(exception.cause, Is.`is`(authenticationException)) MatcherAssert.assertThat( exception.message, - Is.`is`("An error occurred while trying to use the Refresh Token to renew the Credentials.") + Is.`is`("The exchange of the refresh token for SSO credentials failed.") ) } @@ -408,7 +383,7 @@ public class SecureCredentialsManagerTest { */ @Test @ExperimentalCoroutinesApi - public fun shouldFailWhenNoExistingCredentialsWasSavedOnAwaitSsoCredentials(): Unit = runTest { + public fun shouldFailWhenNoExistingCredentialsWasSavedOnAwaitSSOCredentials(): Unit = runTest { verifyNoMoreInteractions(client) Mockito.`when`(storage.retrieveString("com.auth0.credentials")) .thenReturn(null) @@ -424,7 +399,7 @@ public class SecureCredentialsManagerTest { @Test @ExperimentalCoroutinesApi - public fun shouldThrowExceptionIfFetchingExistingCredentialsFailsOnAwaitSsoCredentials(): Unit = + public fun shouldThrowExceptionIfFetchingExistingCredentialsFailsOnAwaitSSOCredentials(): Unit = runTest { val expiresAt = Date(CredentialsMock.ONE_HOUR_AHEAD_MS) val storedJson = insertTestCredentials( @@ -448,7 +423,7 @@ public class SecureCredentialsManagerTest { @Test @ExperimentalCoroutinesApi - public fun shouldThrowExceptionIfExistingCredentialsHasNoRefreshTokenOnAwaitSsoCredentials(): Unit = + public fun shouldThrowExceptionIfExistingCredentialsHasNoRefreshTokenOnAwaitSSOCredentials(): Unit = runTest { val expiresAt = Date(CredentialsMock.ONE_HOUR_AHEAD_MS) insertTestCredentials( @@ -471,13 +446,18 @@ public class SecureCredentialsManagerTest { @Test @ExperimentalCoroutinesApi - public fun shouldFetchTheNewRefreshTokenOnAwaitSSoCredentials(): Unit = runTest { + public fun shouldFetchTheNewRefreshTokenOnAwaitSSOCredentials(): Unit = runTest { val expiresAt = Date(CredentialsMock.ONE_HOUR_AHEAD_MS) - Mockito.`when`(client.fetchSessionToken("refreshToken")) - .thenReturn(ssoCredentialsRequest) - Mockito.`when`(ssoCredentialsRequest.execute()).thenReturn( - SsoCredentialsMock.create( - "session-token", "issued-token-type", "token-type", "refresh-token", 60 + Mockito.`when`(client.ssoExchange("refreshToken")) + .thenReturn(SSOCredentialsRequest) + Mockito.`when`(SSOCredentialsRequest.execute()).thenReturn( + SSOCredentialsMock.create( + "web-sso-token", + "identity-token", + "issued-token-type", + "token-type", + "refresh-token", + 60 ) ) insertTestCredentials( @@ -500,8 +480,8 @@ public class SecureCredentialsManagerTest { val credentials = runBlocking { manager.awaitSsoCredentials() } - MatcherAssert.assertThat(credentials.sessionToken, Is.`is`(Matchers.notNullValue())) - MatcherAssert.assertThat(credentials.sessionToken, Is.`is`("session-token")) + MatcherAssert.assertThat(credentials.sessionTransferToken, Is.`is`(Matchers.notNullValue())) + MatcherAssert.assertThat(credentials.sessionTransferToken, Is.`is`("web-sso-token")) MatcherAssert.assertThat(credentials.tokenType, Is.`is`("token-type")) MatcherAssert.assertThat(credentials.issuedTokenType, Is.`is`("issued-token-type")) MatcherAssert.assertThat(credentials.refreshToken, Is.`is`("refresh-token")) @@ -517,10 +497,10 @@ public class SecureCredentialsManagerTest { @Test @ExperimentalCoroutinesApi - public fun shouldFailWhenRefreshTokenExpiredOnAwaitSsoCredentials(): Unit = runTest { + public fun shouldFailWhenRefreshTokenExpiredOnAwaitSSOCredentials(): Unit = runTest { val expiresAt = Date(CredentialsMock.ONE_HOUR_AHEAD_MS) - Mockito.`when`(client.fetchSessionToken("refreshToken")) - .thenReturn(ssoCredentialsRequest) + Mockito.`when`(client.ssoExchange("refreshToken")) + .thenReturn(SSOCredentialsRequest) insertTestCredentials( hasIdToken = true, hasAccessToken = true, @@ -533,7 +513,7 @@ public class SecureCredentialsManagerTest { "invalid_grant", "Unknown or invalid refresh token." ) - Mockito.`when`(ssoCredentialsRequest.execute()).thenThrow(authenticationException) + Mockito.`when`(SSOCredentialsRequest.execute()).thenThrow(authenticationException) val exception = assertThrows(CredentialsManagerException::class.java) { runBlocking { manager.awaitSsoCredentials() } } @@ -541,7 +521,7 @@ public class SecureCredentialsManagerTest { MatcherAssert.assertThat(exception.cause, Is.`is`(authenticationException)) MatcherAssert.assertThat( exception.message, - Is.`is`("An error occurred while trying to use the Refresh Token to renew the Credentials.") + Is.`is`("The exchange of the refresh token for SSO credentials failed.") ) } @@ -1859,32 +1839,32 @@ public class SecureCredentialsManagerTest { ) secureCredsManager.getCredentials(object : Callback { - override fun onFailure(exception: CredentialsManagerException) { - throw exception + override fun onFailure(error: CredentialsManagerException) { + throw error } - override fun onSuccess(credentials: Credentials) { + override fun onSuccess(result: Credentials) { // Verify all instances retrieved the same credentials MatcherAssert.assertThat( renewedCredentials.accessToken, - Is.`is`(credentials.accessToken) + Is.`is`(result.accessToken) ) MatcherAssert.assertThat( renewedCredentials.idToken, - Is.`is`(credentials.idToken) + Is.`is`(result.idToken) ) MatcherAssert.assertThat( renewedCredentials.refreshToken, - Is.`is`(credentials.refreshToken) + Is.`is`(result.refreshToken) ) - MatcherAssert.assertThat(renewedCredentials.type, Is.`is`(credentials.type)) + MatcherAssert.assertThat(renewedCredentials.type, Is.`is`(result.type)) MatcherAssert.assertThat( renewedCredentials.expiresAt, - Is.`is`(credentials.expiresAt) + Is.`is`(result.expiresAt) ) MatcherAssert.assertThat( renewedCredentials.scope, - Is.`is`(credentials.scope) + Is.`is`(result.scope) ) latch.countDown() } diff --git a/auth0/src/test/java/com/auth0/android/result/SsoCredentialsMock.kt b/auth0/src/test/java/com/auth0/android/result/SSOCredentialsMock.kt similarity index 70% rename from auth0/src/test/java/com/auth0/android/result/SsoCredentialsMock.kt rename to auth0/src/test/java/com/auth0/android/result/SSOCredentialsMock.kt index 47290941b..203fc6424 100644 --- a/auth0/src/test/java/com/auth0/android/result/SsoCredentialsMock.kt +++ b/auth0/src/test/java/com/auth0/android/result/SSOCredentialsMock.kt @@ -1,18 +1,19 @@ package com.auth0.android.result -public class SsoCredentialsMock { +public class SSOCredentialsMock { public companion object { public fun create( accessToken: String, + idToken:String , issuedTokenType: String, type: String, refreshToken: String?, expiresIn: Int ): SSOCredentials { return SSOCredentials( - accessToken, issuedTokenType, type, expiresIn, refreshToken + accessToken,idToken, issuedTokenType, type, expiresIn, refreshToken ) } }