diff --git a/EXAMPLES.md b/EXAMPLES.md index 6de7e987..2b569026 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -2878,232 +2878,6 @@ params.put("screen_hint", "signup"); ``` -## Management API - -The client provides a few methods to interact with the [Users Management API](https://auth0.com/docs/api/management/v2/#!/Users). - -Create a new instance passing the account and an access token with the Management API audience and the right scope: - -```kotlin -val users = UsersAPIClient(account, "api access token") -``` - -
- Using Java - -```java -Auth0 account = Auth0.getInstance("client id", "domain"); -UsersAPIClient users = new UsersAPIClient(account, "api token"); -``` -
- -### Link users - -```kotlin -users - .link("primary user id", "secondary user token") - .start(object: Callback, ManagementException> { - - override fun onFailure(exception: ManagementException) { } - - override fun onSuccess(identities: List) { } - }) -``` - -
- Using coroutines - -```kotlin -try { - val identities = users - .link("primary user id", "secondary user token") - .await() - println(identities) -} catch (e: ManagementException) { - e.printStacktrace() -} -``` -
- -
- Using Java - -```java -users - .link("primary user id", "secondary user token") - .start(new Callback, ManagementException>() { - @Override - public void onSuccess(List payload) { - //Got the updated identities! Accounts linked. - } - - @Override - public void onFailure(@NonNull ManagementException error) { - //Error! - } - }); -``` -
- -### Unlink users - -```kotlin -users - .unlink("primary user id", "secondary user id", "secondary provider") - .start(object: Callback, ManagementException> { - - override fun onFailure(exception: ManagementException) { } - - override fun onSuccess(identities: List) { } - }) -``` - -
- Using coroutines - -```kotlin -try { - val identities = users - .unlink("primary user id", "secondary user id", "secondary provider") - .await() - println(identities) -} catch (e: ManagementException) { - e.printStacktrace() -} -``` -
- -
- Using Java - -```java -users - .unlink("primary user id", "secondary user id", "secondary provider") - .start(new Callback, ManagementException>() { - @Override - public void onSuccess(List payload) { - //Got the updated identities! Accounts linked. - } - - @Override - public void onFailure(@NonNull ManagementException error) { - //Error! - } - }); -``` -
- -### Get User Profile - -```kotlin -users - .getProfile("user id") - .start(object: Callback { - - override fun onFailure(exception: ManagementException) { } - - override fun onSuccess(identities: UserProfile) { } - }) -``` - -
- Using coroutines - -```kotlin -try { - val user = users - .getProfile("user id") - .await() - println(user) -} catch (e: ManagementException) { - e.printStacktrace() -} -``` -
- -
- Using Java - -```java -users - .getProfile("user id") - .start(new Callback() { - @Override - public void onSuccess(@Nullable UserProfile payload) { - //Profile received - } - - @Override - public void onFailure(@NonNull ManagementException error) { - //Error! - } - }); -``` -
- -### Update User Metadata - -```kotlin -val metadata = mapOf( - "name" to listOf("My", "Name", "Is"), - "phoneNumber" to "1234567890" -) - -users - .updateMetadata("user id", metadata) - .start(object: Callback { - - override fun onFailure(exception: ManagementException) { } - - override fun onSuccess(identities: UserProfile) { } - }) -``` - -
- Using coroutines - -```kotlin -val metadata = mapOf( - "name" to listOf("My", "Name", "Is"), - "phoneNumber" to "1234567890" -) - -try { - val user = users - .updateMetadata("user id", metadata) - .await() - println(user) -} catch (e: ManagementException) { - e.printStacktrace() -} -``` -
- -
- Using Java - -```java -Map metadata = new HashMap<>(); -metadata.put("name", Arrays.asList("My", "Name", "Is")); -metadata.put("phoneNumber", "1234567890"); - -users - .updateMetadata("user id", metadata) - .start(new Callback() { - @Override - public void onSuccess(@Nullable UserProfile payload) { - //User Metadata updated - } - - @Override - public void onFailure(@NonNull ManagementException error) { - //Error! - } - }); -``` -
- -> In all the cases, the `user ID` parameter is the unique identifier of the auth0 account instance. i.e. in `google-oauth2|123456789` it would be the part after the '|' pipe: `123456789`. ## Token Validation The ID token received as part of the authentication flow is should be verified following the [OpenID Connect specification](https://openid.net/specs/openid-connect-core-1_0.html). diff --git a/V4_MIGRATION_GUIDE.md b/V4_MIGRATION_GUIDE.md index f96d03cb..ea4fe01e 100644 --- a/V4_MIGRATION_GUIDE.md +++ b/V4_MIGRATION_GUIDE.md @@ -96,6 +96,20 @@ buildscript { - [signupWithPasskey()](auth0/src/main/java/com/auth0/android/authentication/AuthenticationAPIClient.kt#L319-L344) - Sign up a user and returns a challenge for key generation +- The Management API support has been removed. This includes the `UsersAPIClient` class, `ManagementException`, and `ManagementCallback`. + + > **Note:** This only impacts you if your app used the Management API client (`UsersAPIClient`). + + **Impact:** Any code that references `UsersAPIClient`, `ManagementException`, or `ManagementCallback` will no longer compile. + + **Migration:** Instead of calling the Management API directly from your mobile app, expose dedicated endpoints in your own backend that perform the required operations, and call those from the app using the access token you already have. + + For example, if you were reading or updating user metadata: + + 1. Create a backend endpoint (e.g. `PATCH /me/metadata`) that accepts the operation your app needs. + 2. Call that endpoint from your app, passing the user's access token as a `Bearer` token in the `Authorization` header. + 3. On your backend, obtain a machine-to-machine token via the Client Credentials flow and use it to call the Management API with the precise scopes required. + ### DPoP Configuration Moved to Builder The `useDPoP(context: Context)` method has been moved from the `WebAuthProvider` object to the login diff --git a/auth0/src/main/java/com/auth0/android/callback/ManagementCallback.kt b/auth0/src/main/java/com/auth0/android/callback/ManagementCallback.kt deleted file mode 100644 index 11969adb..00000000 --- a/auth0/src/main/java/com/auth0/android/callback/ManagementCallback.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.auth0.android.callback - -import com.auth0.android.management.ManagementException - -public interface ManagementCallback : Callback \ No newline at end of file diff --git a/auth0/src/main/java/com/auth0/android/management/ManagementException.kt b/auth0/src/main/java/com/auth0/android/management/ManagementException.kt deleted file mode 100644 index 96cc52d9..00000000 --- a/auth0/src/main/java/com/auth0/android/management/ManagementException.kt +++ /dev/null @@ -1,90 +0,0 @@ -package com.auth0.android.management - -import com.auth0.android.Auth0Exception -import com.auth0.android.NetworkErrorException - - -@Deprecated( - """ManagementException is deprecated and will be removed in the next major version of the SDK. """, - level = DeprecationLevel.WARNING -) -public class ManagementException @JvmOverloads constructor( - message: String, - exception: Auth0Exception? = null -) : Auth0Exception(message, exception) { - private var code: String? = null - private var description: String? = null - - /** - * Http Response status code. Can have value of 0 if not set. - * - * @return the status code. - */ - public var statusCode: Int = 0 - private set - private var values: Map? = null - - public constructor(payload: String?, statusCode: Int) : this(DEFAULT_MESSAGE) { - code = if (payload != null) NON_JSON_ERROR else EMPTY_BODY_ERROR - description = payload ?: EMPTY_RESPONSE_BODY_DESCRIPTION - this.statusCode = statusCode - } - - public constructor(values: Map) : this(DEFAULT_MESSAGE) { - this.values = values - val codeValue = - (if (values.containsKey(ERROR_KEY)) values[ERROR_KEY] else values[CODE_KEY]) as String? - code = codeValue ?: UNKNOWN_ERROR - description = - (if (values.containsKey(DESCRIPTION_KEY)) values[DESCRIPTION_KEY] else values[ERROR_DESCRIPTION_KEY]) as String? - } - - /** - * Auth0 error code if the server returned one or an internal library code (e.g.: when the server could not be reached) - * - * @return the error code. - */ - @Suppress("MemberVisibilityCanBePrivate") - public fun getCode(): String { - return if (code != null) code!! else UNKNOWN_ERROR - } - - /** - * Description of the error. - * important: You should avoid displaying description to the user, it's meant for debugging only. - * - * @return the error description. - */ - @Suppress("unused") - public fun getDescription(): String { - if (description != null) { - return description!! - } - return if (UNKNOWN_ERROR == getCode()) { - String.format("Received error with code %s", getCode()) - } else "Failed with unknown error" - } - - /** - * Returns a value from the error map, if any. - * - * @param key key of the value to return - * @return the value if found or null - */ - public fun getValue(key: String): Any? { - return values?.get(key) - } - - // When the request failed due to network issues - public val isNetworkError: Boolean - get() = cause is NetworkErrorException - - private companion object { - private const val ERROR_KEY = "error" - private const val CODE_KEY = "code" - private const val DESCRIPTION_KEY = "description" - private const val ERROR_DESCRIPTION_KEY = "error_description" - private const val DEFAULT_MESSAGE = - "An error occurred when trying to authenticate with the server." - } -} \ No newline at end of file diff --git a/auth0/src/main/java/com/auth0/android/management/UsersAPIClient.kt b/auth0/src/main/java/com/auth0/android/management/UsersAPIClient.kt deleted file mode 100755 index 4967e364..00000000 --- a/auth0/src/main/java/com/auth0/android/management/UsersAPIClient.kt +++ /dev/null @@ -1,256 +0,0 @@ -package com.auth0.android.management - -import androidx.annotation.VisibleForTesting -import com.auth0.android.Auth0 -import com.auth0.android.Auth0Exception -import com.auth0.android.NetworkErrorException -import com.auth0.android.authentication.ParameterBuilder -import com.auth0.android.request.ErrorAdapter -import com.auth0.android.request.JsonAdapter -import com.auth0.android.request.NetworkingClient -import com.auth0.android.request.Request -import com.auth0.android.request.internal.BaseRequest -import com.auth0.android.request.internal.GsonAdapter -import com.auth0.android.request.internal.GsonAdapter.Companion.forListOf -import com.auth0.android.request.internal.GsonAdapter.Companion.forMap -import com.auth0.android.request.internal.GsonProvider -import com.auth0.android.request.internal.RequestFactory -import com.auth0.android.request.internal.ResponseUtils.isNetworkError -import com.auth0.android.result.UserIdentity -import com.auth0.android.result.UserProfile -import com.google.gson.Gson -import okhttp3.HttpUrl.Companion.toHttpUrl -import java.io.IOException -import java.io.Reader - -/** - * API client for Auth0 Management API. - * ``` - * val auth0 = Auth0.getInstance("your_client_id", "your_domain") - * val client = UsersAPIClient(auth0) - * ``` - * - * @see [Auth API docs](https://auth0.com/docs/api/management/v2) - */ - -@Deprecated( - """UsersAPIClient is deprecated and will be removed in the next major version of the SDK.""", - level = DeprecationLevel.WARNING -) -public class UsersAPIClient @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) internal constructor( - private val auth0: Auth0, - private val factory: RequestFactory, - private val gson: Gson -) { - /** - * Creates a new API client instance providing the Auth0 account info and the access token. - * - * @param auth0 account information - * @param token of the primary identity - */ - public constructor( - auth0: Auth0, - token: String - ) : this( - auth0, - factoryForToken(token, auth0.networkingClient), - GsonProvider.gson - ) - - public val clientId: String - get() = auth0.clientId - public val baseURL: String - get() = auth0.getDomainUrl() - - /** - * Link a user identity calling ['/api/v2/users/:primaryUserId/identities'](https://auth0.com/docs/link-accounts#the-management-api) endpoint - * Example usage: - * ``` - * client.link("{auth0 primary user id}", "{user secondary token}") - * .start(object: Callback, ManagementException> { - * override fun onSuccess(result: List) { } - * override fun onFailure(error: ManagementException) { } - * }) - * ``` - * - * @param primaryUserId of the identity to link - * @param secondaryToken of the secondary identity obtained after login - * @return a request to start - */ - public fun link( - primaryUserId: String, - secondaryToken: String - ): Request, ManagementException> { - val url = auth0.getDomainUrl().toHttpUrl().newBuilder() - .addPathSegment(API_PATH) - .addPathSegment(V2_PATH) - .addPathSegment(USERS_PATH) - .addPathSegment(primaryUserId) - .addPathSegment(IDENTITIES_PATH) - .build() - val parameters = ParameterBuilder.newBuilder() - .set(LINK_WITH_KEY, secondaryToken) - .asDictionary() - val userIdentitiesAdapter: JsonAdapter> = forListOf( - UserIdentity::class.java, gson - ) - return factory.post(url.toString(), userIdentitiesAdapter) - .addParameters(parameters) - } - - /** - * Unlink a user identity calling ['/api/v2/users/:primaryToken/identities/secondaryProvider/secondaryUserId'](https://auth0.com/docs/link-accounts#unlinking-accounts) endpoint - * Example usage: - * ``` - * client.unlink("{auth0 primary user id}", {auth0 secondary user id}, "{secondary provider}") - * .start(object: Callback, ManagementException> { - * override fun onSuccess(result: List) { } - * override fun onFailure(error: ManagementException) {} - * }) - * ``` - * - * @param primaryUserId of the primary identity to unlink - * @param secondaryUserId of the secondary identity you wish to unlink from the main one. - * @param secondaryProvider of the secondary identity you wish to unlink from the main one. - * @return a request to start - */ - public fun unlink( - primaryUserId: String, - secondaryUserId: String, - secondaryProvider: String - ): Request, ManagementException> { - val url = auth0.getDomainUrl().toHttpUrl().newBuilder() - .addPathSegment(API_PATH) - .addPathSegment(V2_PATH) - .addPathSegment(USERS_PATH) - .addPathSegment(primaryUserId) - .addPathSegment(IDENTITIES_PATH) - .addPathSegment(secondaryProvider) - .addPathSegment(secondaryUserId) - .build() - val userIdentitiesAdapter: JsonAdapter> = forListOf( - UserIdentity::class.java, gson - ) - return factory.delete(url.toString(), userIdentitiesAdapter) - } - - /** - * Update the user_metadata calling ['/api/v2/users/:userId'](https://auth0.com/docs/api/management/v2#!/Users/patch_users_by_id) endpoint - * Example usage: - * ``` - * client.updateMetadata("{user id}", "{user metadata}") - * .start(object: Callback { - * override fun onSuccess(result: UserProfile) { } - * override fun onFailure(error: ManagementException) { } - * }) - * ``` - * - * @param userId of the primary identity to unlink - * @param userMetadata to merge with the existing one - * @return a request to start - */ - public fun updateMetadata( - userId: String, - userMetadata: Map - ): Request { - val url = auth0.getDomainUrl().toHttpUrl().newBuilder() - .addPathSegment(API_PATH) - .addPathSegment(V2_PATH) - .addPathSegment(USERS_PATH) - .addPathSegment(userId) - .build() - val userProfileAdapter: JsonAdapter = GsonAdapter( - UserProfile::class.java, gson - ) - val patch = factory.patch( - url.toString(), - userProfileAdapter - ) as BaseRequest - patch.addParameter(USER_METADATA_KEY, userMetadata) - return patch - } - - /** - * Get the User Profile calling ['/api/v2/users/:userId'](https://auth0.com/docs/api/management/v2#!/Users/get_users_by_id) endpoint - * Example usage: - * ``` - * client.getProfile("{user id}") - * .start(object: Callback { - * override fun onSuccess(result: UserProfile) { } - * override fun onFailure(error: ManagementException) { } - * }) - * ``` - * - * @param userId identity of the user - * @return a request to start - */ - public fun getProfile(userId: String): Request { - val url = auth0.getDomainUrl().toHttpUrl().newBuilder() - .addPathSegment(API_PATH) - .addPathSegment(V2_PATH) - .addPathSegment(USERS_PATH) - .addPathSegment(userId) - .build() - val userProfileAdapter: JsonAdapter = GsonAdapter( - UserProfile::class.java, gson - ) - return factory.get(url.toString(), userProfileAdapter) - } - - private companion object { - private const val LINK_WITH_KEY = "link_with" - private const val API_PATH = "api" - private const val V2_PATH = "v2" - private const val USERS_PATH = "users" - private const val IDENTITIES_PATH = "identities" - private const val USER_METADATA_KEY = "user_metadata" - - private fun createErrorAdapter(): ErrorAdapter { - val mapAdapter = forMap(GsonProvider.gson) - return object : ErrorAdapter { - override fun fromRawResponse( - statusCode: Int, - bodyText: String, - headers: Map> - ): ManagementException { - return ManagementException(bodyText, statusCode) - } - - @Throws(IOException::class) - override fun fromJsonResponse( - statusCode: Int, - reader: Reader - ): ManagementException { - val values = mapAdapter.fromJson(reader) - return ManagementException(values) - } - - override fun fromException(cause: Throwable): ManagementException { - if (isNetworkError(cause)) { - return ManagementException( - "Failed to execute the network request", - NetworkErrorException(cause) - ) - } - return ManagementException( - "Something went wrong", - Auth0Exception("Something went wrong", cause) - ) - } - } - } - - private fun factoryForToken( - token: String, - client: NetworkingClient - ): RequestFactory { - val factory = RequestFactory(client, createErrorAdapter()) - factory.setHeader("Authorization", "Bearer $token") - return factory - } - } - - init { - factory.setAuth0ClientInfo(auth0.auth0UserAgent.value) - } -} \ No newline at end of file diff --git a/auth0/src/main/java/com/auth0/android/result/UserProfile.kt b/auth0/src/main/java/com/auth0/android/result/UserProfile.kt index 2f490313..911165e0 100755 --- a/auth0/src/main/java/com/auth0/android/result/UserProfile.kt +++ b/auth0/src/main/java/com/auth0/android/result/UserProfile.kt @@ -5,7 +5,7 @@ import java.util.* /** * Class that holds the information of a user's profile in Auth0. - * Used both in [com.auth0.android.management.UsersAPIClient] and [com.auth0.android.authentication.AuthenticationAPIClient]. + * Used in [com.auth0.android.authentication.AuthenticationAPIClient]. */ public class UserProfile( private val id: String?, diff --git a/auth0/src/test/java/com/auth0/android/management/ManagementExceptionTest.kt b/auth0/src/test/java/com/auth0/android/management/ManagementExceptionTest.kt deleted file mode 100644 index 9a97d132..00000000 --- a/auth0/src/test/java/com/auth0/android/management/ManagementExceptionTest.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.auth0.android.management - -import com.auth0.android.NetworkErrorException -import org.hamcrest.CoreMatchers -import org.hamcrest.MatcherAssert -import org.junit.Test -import java.io.IOException - -public class ManagementExceptionTest { - @Test - public fun shouldNotHaveNetworkError() { - val ex = ManagementException("Something else happened") - MatcherAssert.assertThat(ex.isNetworkError, CoreMatchers.`is`(false)) - } - - @Test - public fun shouldHaveNetworkError() { - val ex = ManagementException( - "Request has definitely failed", NetworkErrorException( - IOException() - ) - ) - MatcherAssert.assertThat(ex.isNetworkError, CoreMatchers.`is`(true)) - } -} \ No newline at end of file diff --git a/auth0/src/test/java/com/auth0/android/management/UsersAPIClientTest.kt b/auth0/src/test/java/com/auth0/android/management/UsersAPIClientTest.kt deleted file mode 100755 index 7c1d370e..00000000 --- a/auth0/src/test/java/com/auth0/android/management/UsersAPIClientTest.kt +++ /dev/null @@ -1,466 +0,0 @@ -package com.auth0.android.management - -import android.content.Context -import android.content.res.Resources -import com.auth0.android.Auth0 -import com.auth0.android.authentication.AuthenticationAPIClient -import com.auth0.android.authentication.AuthenticationAPIClientTest -import com.auth0.android.request.HttpMethod.GET -import com.auth0.android.request.NetworkingClient -import com.auth0.android.request.RequestOptions -import com.auth0.android.request.ServerResponse -import com.auth0.android.request.internal.RequestFactory -import com.auth0.android.request.internal.ThreadSwitcherShadow -import com.auth0.android.result.UserIdentity -import com.auth0.android.result.UserProfile -import com.auth0.android.util.* -import com.auth0.android.util.SSLTestUtils.testClient -import com.google.gson.Gson -import com.google.gson.GsonBuilder -import com.google.gson.reflect.TypeToken -import org.mockito.kotlin.* -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.runTest -import okhttp3.mockwebserver.RecordedRequest -import org.hamcrest.MatcherAssert.assertThat -import org.hamcrest.Matchers -import org.hamcrest.Matchers.instanceOf -import org.hamcrest.Matchers.notNullValue -import org.hamcrest.collection.IsMapContaining -import org.hamcrest.collection.IsMapWithSize -import org.junit.After -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config -import org.robolectric.shadows.ShadowLooper -import java.io.ByteArrayInputStream -import java.io.InputStream -import java.util.* - - -@RunWith(RobolectricTestRunner::class) -@Config(shadows = [ThreadSwitcherShadow::class]) -public class UsersAPIClientTest { - private lateinit var client: UsersAPIClient - private lateinit var gson: Gson - private lateinit var mockAPI: UsersAPIMockServer - - @Before - public fun setUp() { - mockAPI = UsersAPIMockServer() - val domain = mockAPI.domain - val auth0 = Auth0.getInstance(CLIENT_ID, domain, domain) - auth0.networkingClient = testClient - client = UsersAPIClient(auth0, TOKEN_PRIMARY) - gson = GsonBuilder().serializeNulls().create() - } - - @After - public fun tearDown() { - mockAPI.shutdown() - } - - @Test - public fun shouldUseCustomNetworkingClient() { - val account = Auth0.getInstance("client-id", "https://tenant.auth0.com/") - val jsonResponse = """{"id": "undercover"}""" - val inputStream: InputStream = ByteArrayInputStream(jsonResponse.toByteArray()) - val response = ServerResponse(200, inputStream, emptyMap()) - val networkingClient: NetworkingClient = mock() - - whenever(networkingClient.load(any(), any())).thenReturn(response) - account.networkingClient = networkingClient - val client = UsersAPIClient(account, "token.token") - val request = client.getProfile("undercover") - - request.execute() - ShadowLooper.idleMainLooper() - argumentCaptor().apply { - verify(networkingClient).load(eq("https://tenant.auth0.com/api/v2/users/undercover"), capture()) - assertThat(firstValue, Matchers.`is`(notNullValue())) - assertThat(firstValue.method, Matchers.`is`(instanceOf(GET::class.java))) - assertThat(firstValue.parameters, IsMapWithSize.anEmptyMap()) - assertThat(firstValue.headers, IsMapContaining.hasKey("Auth0-Client")) - } - } - - @Test - public fun shouldSetAuth0UserAgentIfPresent() { - val auth0UserAgent: Auth0UserAgent = mock() - val factory: RequestFactory = mock() - val account = Auth0.getInstance(CLIENT_ID, DOMAIN) - - whenever(auth0UserAgent.value).thenReturn("the-user-agent-data") - account.auth0UserAgent = auth0UserAgent - UsersAPIClient(account, factory, gson) - - verify(factory).setAuth0ClientInfo("the-user-agent-data") - } - - @Test - public fun shouldCreateClientWithAccountInfo() { - val client = UsersAPIClient(Auth0.getInstance(CLIENT_ID, DOMAIN), TOKEN_PRIMARY) - assertThat(client, Matchers.`is`(notNullValue())) - assertThat(client.clientId, Matchers.equalTo(CLIENT_ID)) - assertThat(client.baseURL, Matchers.equalTo("https://$DOMAIN/")) - } - - @Test - public fun shouldCreateClientWithContextInfo() { - val context: Context = mock() - val resources: Resources = mock() - - whenever(context.packageName).thenReturn("com.myapp") - whenever(context.resources).thenReturn(resources) - whenever(resources.getIdentifier( - eq("com_auth0_client_id"), - eq("string"), - eq("com.myapp") - )).thenReturn(222) - whenever(resources.getIdentifier( - eq("com_auth0_domain"), - eq("string"), - eq("com.myapp") - )).thenReturn(333) - whenever(context.getString(eq(222))).thenReturn(CLIENT_ID) - whenever(context.getString(eq(333))).thenReturn(DOMAIN) - - val client = UsersAPIClient(Auth0.getInstance(context), TOKEN_PRIMARY) - assertThat(client, Matchers.`is`(notNullValue())) - assertThat(client.clientId, Matchers.`is`(CLIENT_ID)) - assertThat(client.baseURL, Matchers.equalTo("https://$DOMAIN/")) - } - - @Test - public fun shouldLinkAccount() { - mockAPI.willReturnSuccessfulLink() - val callback = MockManagementCallback>() - client.link(USER_ID_PRIMARY, TOKEN_SECONDARY) - .start(callback) - ShadowLooper.idleMainLooper() - - val request = mockAPI.takeRequest() - assertThat( - request.path, - Matchers.equalTo("/api/v2/users/$USER_ID_PRIMARY/identities") - ) - assertThat( - request.getHeader(HEADER_AUTHORIZATION), - Matchers.equalTo(BEARER + TOKEN_PRIMARY) - ) - assertThat(request.method, Matchers.equalTo(METHOD_POST)) - - val body = bodyFromRequest(request) - assertThat(body, Matchers.hasEntry(KEY_LINK_WITH, TOKEN_SECONDARY)) - - val typeToken: TypeToken> = object : TypeToken>() {} - assertThat(callback, ManagementCallbackMatcher.hasPayloadOfType(typeToken)) - assertThat(callback.payload.size, Matchers.`is`(2)) - } - - @Test - public fun shouldLinkAccountSync() { - mockAPI.willReturnSuccessfulLink() - val result = client.link(USER_ID_PRIMARY, TOKEN_SECONDARY) - .execute() - val request = mockAPI.takeRequest() - - assertThat( - request.path, - Matchers.equalTo("/api/v2/users/$USER_ID_PRIMARY/identities") - ) - assertThat( - request.getHeader(HEADER_AUTHORIZATION), - Matchers.equalTo(BEARER + TOKEN_PRIMARY) - ) - assertThat(request.method, Matchers.equalTo(METHOD_POST)) - - val body = bodyFromRequest(request) - assertThat(body, Matchers.hasEntry(KEY_LINK_WITH, TOKEN_SECONDARY)) - - val typeToken: TypeToken> = object : TypeToken>() {} - assertThat(result, TypeTokenMatcher.isA(typeToken)) - assertThat(result.size, Matchers.`is`(2)) - } - - @Test - @ExperimentalCoroutinesApi - public fun shouldAwaitLinkAccount(): Unit = runTest { - mockAPI.willReturnSuccessfulLink() - val result = client.link(USER_ID_PRIMARY, TOKEN_SECONDARY) - .await() - val request = mockAPI.takeRequest() - - assertThat( - request.path, - Matchers.equalTo("/api/v2/users/$USER_ID_PRIMARY/identities") - ) - assertThat( - request.getHeader(HEADER_AUTHORIZATION), - Matchers.equalTo(BEARER + TOKEN_PRIMARY) - ) - assertThat(request.method, Matchers.equalTo(METHOD_POST)) - - val body = bodyFromRequest(request) - assertThat(body, Matchers.hasEntry(KEY_LINK_WITH, TOKEN_SECONDARY)) - - val typeToken: TypeToken> = object : TypeToken>() {} - assertThat(result, TypeTokenMatcher.isA(typeToken)) - assertThat(result.size, Matchers.`is`(2)) - } - - - @Test - public fun shouldUnlinkAccount() { - mockAPI.willReturnSuccessfulUnlink() - val callback = MockManagementCallback>() - client.unlink(USER_ID_PRIMARY, USER_ID_SECONDARY, PROVIDER) - .start(callback) - ShadowLooper.idleMainLooper() - - val request = mockAPI.takeRequest() - assertThat( - request.path, - Matchers.equalTo("/api/v2/users/$USER_ID_PRIMARY/identities/$PROVIDER/$USER_ID_SECONDARY") - ) - assertThat( - request.getHeader(HEADER_AUTHORIZATION), - Matchers.equalTo(BEARER + TOKEN_PRIMARY) - ) - assertThat(request.method, Matchers.equalTo(METHOD_DELETE)) - - val body = bodyFromRequest(request) - assertThat(body, IsMapWithSize.anEmptyMap()) - - val typeToken: TypeToken> = object : TypeToken>() {} - assertThat(callback, ManagementCallbackMatcher.hasPayloadOfType(typeToken)) - assertThat(callback.payload.size, Matchers.`is`(1)) - } - - @Test - public fun shouldUnlinkAccountSync() { - mockAPI.willReturnSuccessfulUnlink() - val result = client.unlink(USER_ID_PRIMARY, USER_ID_SECONDARY, PROVIDER) - .execute() - - val request = mockAPI.takeRequest() - assertThat( - request.path, - Matchers.equalTo("/api/v2/users/$USER_ID_PRIMARY/identities/$PROVIDER/$USER_ID_SECONDARY") - ) - assertThat( - request.getHeader(HEADER_AUTHORIZATION), - Matchers.equalTo(BEARER + TOKEN_PRIMARY) - ) - assertThat(request.method, Matchers.equalTo(METHOD_DELETE)) - - val body = bodyFromRequest(request) - assertThat(body, IsMapWithSize.anEmptyMap()) - - val typeToken: TypeToken> = object : TypeToken>() {} - assertThat(result, TypeTokenMatcher.isA(typeToken)) - assertThat(result.size, Matchers.`is`(1)) - } - - @Test - @ExperimentalCoroutinesApi - public fun shouldAwaitUnlinkAccount(): Unit = runTest { - mockAPI.willReturnSuccessfulUnlink() - val result = client.unlink(USER_ID_PRIMARY, USER_ID_SECONDARY, PROVIDER) - .await() - - val request = mockAPI.takeRequest() - assertThat( - request.path, - Matchers.equalTo("/api/v2/users/$USER_ID_PRIMARY/identities/$PROVIDER/$USER_ID_SECONDARY") - ) - assertThat( - request.getHeader(HEADER_AUTHORIZATION), - Matchers.equalTo(BEARER + TOKEN_PRIMARY) - ) - assertThat(request.method, Matchers.equalTo(METHOD_DELETE)) - - val body = bodyFromRequest(request) - assertThat(body, IsMapWithSize.anEmptyMap()) - - val typeToken: TypeToken> = object : TypeToken>() {} - assertThat(result, TypeTokenMatcher.isA(typeToken)) - assertThat(result.size, Matchers.`is`(1)) - } - - @Test - public fun shouldUpdateUserMetadata() { - mockAPI.willReturnUserProfile() - val metadata: MutableMap = HashMap() - metadata["boolValue"] = true - metadata["name"] = "my_name" - metadata["list"] = listOf("my", "name", "is") - val callback = MockManagementCallback() - client.updateMetadata(USER_ID_PRIMARY, metadata) - .start(callback) - ShadowLooper.idleMainLooper() - - val request = mockAPI.takeRequest() - assertThat(request.path, Matchers.equalTo("/api/v2/users/$USER_ID_PRIMARY")) - assertThat( - request.getHeader(HEADER_AUTHORIZATION), - Matchers.equalTo(BEARER + TOKEN_PRIMARY) - ) - assertThat(request.method, Matchers.equalTo(METHOD_PATCH)) - - val body = bodyFromRequest(request) - assertThat(body, Matchers.hasKey(KEY_USER_METADATA)) - assertThat(body[KEY_USER_METADATA], Matchers.`is`(Matchers.equalTo(metadata))) - assertThat( - callback, - ManagementCallbackMatcher.hasPayloadOfType(UserProfile::class.java) - ) - } - - @Test - public fun shouldUpdateUserMetadataSync() { - mockAPI.willReturnUserProfile() - val metadata: MutableMap = HashMap() - metadata["boolValue"] = true - metadata["name"] = "my_name" - metadata["list"] = listOf("my", "name", "is") - - val result = client.updateMetadata(USER_ID_PRIMARY, metadata) - .execute() - val request = mockAPI.takeRequest() - - assertThat( - request.path, - Matchers.equalTo("/api/v2/users/$USER_ID_PRIMARY") - ) - assertThat( - request.getHeader(HEADER_AUTHORIZATION), - Matchers.equalTo(BEARER + TOKEN_PRIMARY) - ) - assertThat(request.method, Matchers.equalTo(METHOD_PATCH)) - - val body = bodyFromRequest(request) - assertThat(body, Matchers.hasKey(KEY_USER_METADATA)) - assertThat(body[KEY_USER_METADATA], Matchers.`is`(Matchers.equalTo(metadata))) - assertThat(result, Matchers.isA(UserProfile::class.java)) - } - - @Test - @ExperimentalCoroutinesApi - public fun shouldAwaitUpdateUserMetadata(): Unit = runTest { - mockAPI.willReturnUserProfile() - val metadata: MutableMap = HashMap() - metadata["boolValue"] = true - metadata["name"] = "my_name" - metadata["list"] = listOf("my", "name", "is") - - val result = client.updateMetadata(USER_ID_PRIMARY, metadata) - .await() - val request = mockAPI.takeRequest() - - assertThat( - request.path, - Matchers.equalTo("/api/v2/users/$USER_ID_PRIMARY") - ) - assertThat( - request.getHeader(HEADER_AUTHORIZATION), - Matchers.equalTo(BEARER + TOKEN_PRIMARY) - ) - assertThat(request.method, Matchers.equalTo(METHOD_PATCH)) - - val body = bodyFromRequest(request) - assertThat(body, Matchers.hasKey(KEY_USER_METADATA)) - assertThat(body[KEY_USER_METADATA], Matchers.`is`(Matchers.equalTo(metadata))) - assertThat(result, Matchers.isA(UserProfile::class.java)) - } - - @Test - public fun shouldGetUserProfileSync() { - mockAPI.willReturnUserProfile() - val result = client.getProfile(USER_ID_PRIMARY) - .execute() - val request = mockAPI.takeRequest() - - assertThat( - request.path, - Matchers.equalTo("/api/v2/users/$USER_ID_PRIMARY") - ) - assertThat( - request.getHeader(HEADER_AUTHORIZATION), - Matchers.equalTo(BEARER + TOKEN_PRIMARY) - ) - assertThat( - request.method, - Matchers.equalTo(METHOD_GET) - ) - assertThat(result, Matchers.isA(UserProfile::class.java)) - } - - @Test - @ExperimentalCoroutinesApi - public fun shouldAwaitGetUserProfile(): Unit = runTest { - mockAPI.willReturnUserProfile() - val result = client.getProfile(USER_ID_PRIMARY) - .await() - val request = mockAPI.takeRequest() - - assertThat( - request.path, - Matchers.equalTo("/api/v2/users/$USER_ID_PRIMARY") - ) - assertThat( - request.getHeader(HEADER_AUTHORIZATION), - Matchers.equalTo(BEARER + TOKEN_PRIMARY) - ) - assertThat( - request.method, - Matchers.equalTo(METHOD_GET) - ) - assertThat(result, Matchers.isA(UserProfile::class.java)) - } - - @Test - public fun shouldGetUserProfile() { - mockAPI.willReturnUserProfile() - val callback = MockManagementCallback() - client.getProfile(USER_ID_PRIMARY) - .start(callback) - ShadowLooper.idleMainLooper() - - val request = mockAPI.takeRequest() - assertThat(request.path, Matchers.equalTo("/api/v2/users/$USER_ID_PRIMARY")) - assertThat(request.getHeader(HEADER_AUTHORIZATION), - Matchers.equalTo(BEARER + TOKEN_PRIMARY)) - assertThat(request.method, Matchers.equalTo(METHOD_GET)) - assertThat( - callback, ManagementCallbackMatcher.hasPayloadOfType( - UserProfile::class.java - ) - ) - } - - private inline fun bodyFromRequest(request: RecordedRequest): Map { - val mapType = object : TypeToken>() {}.type - return gson.fromJson(request.body.readUtf8(), mapType) - } - - private companion object { - private const val CLIENT_ID = "CLIENTID" - private const val DOMAIN = "samples.auth0.com" - private const val USER_ID_PRIMARY = "primaryUserId" - private const val USER_ID_SECONDARY = "secondaryUserId" - private const val TOKEN_PRIMARY = "primaryToken" - private const val TOKEN_SECONDARY = "secondaryToken" - private const val PROVIDER = "provider" - private const val HEADER_AUTHORIZATION = "Authorization" - private const val BEARER = "Bearer " - private const val METHOD_POST = "POST" - private const val METHOD_DELETE = "DELETE" - private const val METHOD_PATCH = "PATCH" - private const val METHOD_GET = "GET" - private const val KEY_LINK_WITH = "link_with" - private const val KEY_USER_METADATA = "user_metadata" - } -} \ No newline at end of file diff --git a/auth0/src/test/java/com/auth0/android/util/ManagementCallbackMatcher.java b/auth0/src/test/java/com/auth0/android/util/ManagementCallbackMatcher.java deleted file mode 100755 index 2f5bf4c9..00000000 --- a/auth0/src/test/java/com/auth0/android/util/ManagementCallbackMatcher.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.auth0.android.util; - -import com.auth0.android.callback.ManagementCallback; -import com.auth0.android.management.ManagementException; -import com.google.gson.reflect.TypeToken; -import com.jayway.awaitility.core.ConditionTimeoutException; - -import org.hamcrest.BaseMatcher; -import org.hamcrest.Description; -import org.hamcrest.Matcher; - -import static com.jayway.awaitility.Awaitility.await; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.isA; -import static org.hamcrest.Matchers.notNullValue; -import static org.hamcrest.Matchers.nullValue; - -public class ManagementCallbackMatcher extends BaseMatcher> { - private final Matcher payloadMatcher; - private final Matcher errorMatcher; - - public ManagementCallbackMatcher(Matcher payloadMatcher, Matcher errorMatcher) { - this.payloadMatcher = payloadMatcher; - this.errorMatcher = errorMatcher; - } - - @Override - @SuppressWarnings("unchecked") - public boolean matches(Object item) { - MockManagementCallback callback = (MockManagementCallback) item; - try { - await().until(callback.payload(), payloadMatcher); - await().until(callback.error(), errorMatcher); - return true; - } catch (ConditionTimeoutException e) { - return false; - } - } - - @Override - public void describeTo(Description description) { - description - .appendText("successful method be called"); - } - - public static Matcher> hasPayloadOfType(Class tClazz) { - return new ManagementCallbackMatcher<>(isA(tClazz), is(nullValue(ManagementException.class))); - } - - public static Matcher> hasPayloadOfType(TypeToken typeToken) { - return new ManagementCallbackMatcher<>(TypeTokenMatcher.isA(typeToken), is(nullValue(ManagementException.class))); - } - - public static Matcher> hasPayload(T payload) { - return new ManagementCallbackMatcher<>(equalTo(payload), is(nullValue(ManagementException.class))); - } - - public static Matcher> hasNoPayloadOfType(Class tClazz) { - return new ManagementCallbackMatcher<>(is(nullValue(tClazz)), is(notNullValue(ManagementException.class))); - } - - public static Matcher> hasNoPayloadOfType(TypeToken typeToken) { - return new ManagementCallbackMatcher<>(TypeTokenMatcher.isA(typeToken), is(nullValue(ManagementException.class))); - } - - public static Matcher> hasNoError() { - return new ManagementCallbackMatcher<>(is(notNullValue(Void.class)), is(nullValue(ManagementException.class))); - } - - public static Matcher> hasError() { - return new ManagementCallbackMatcher<>(is(nullValue(Void.class)), is(notNullValue(ManagementException.class))); - } -} diff --git a/auth0/src/test/java/com/auth0/android/util/MockManagementCallback.java b/auth0/src/test/java/com/auth0/android/util/MockManagementCallback.java deleted file mode 100755 index 74002f3d..00000000 --- a/auth0/src/test/java/com/auth0/android/util/MockManagementCallback.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.auth0.android.util; - -import androidx.annotation.NonNull; - -import com.auth0.android.callback.ManagementCallback; -import com.auth0.android.management.ManagementException; - -import java.util.concurrent.Callable; - -public class MockManagementCallback implements ManagementCallback { - - private ManagementException error; - private T payload; - - @Override - public void onFailure(@NonNull ManagementException error) { - this.error = error; - } - - @Override - public void onSuccess(@NonNull T result) { - this.payload = result; - } - - public Callable error() { - return () -> error; - } - - public Callable payload() { - return () -> payload; - } - - public ManagementException getError() { - return error; - } - - public T getPayload() { - return payload; - } -} diff --git a/sample/src/main/java/com/auth0/sample/DatabaseLoginFragment.kt b/sample/src/main/java/com/auth0/sample/DatabaseLoginFragment.kt index c4e54212..31084315 100644 --- a/sample/src/main/java/com/auth0/sample/DatabaseLoginFragment.kt +++ b/sample/src/main/java/com/auth0/sample/DatabaseLoginFragment.kt @@ -27,8 +27,6 @@ import com.auth0.android.authentication.storage.LocalAuthenticationOptions import com.auth0.android.authentication.storage.SecureCredentialsManager import com.auth0.android.authentication.storage.SharedPreferencesStorage import com.auth0.android.callback.Callback -import com.auth0.android.management.ManagementException -import com.auth0.android.management.UsersAPIClient import com.auth0.android.provider.WebAuthProvider import com.auth0.android.request.DefaultClient import com.auth0.android.request.PublicKeyCredentials @@ -36,7 +34,6 @@ import com.auth0.android.request.UserData import com.auth0.android.result.Credentials import com.auth0.android.result.PasskeyChallenge import com.auth0.android.result.PasskeyRegistrationChallenge -import com.auth0.android.result.UserProfile import com.auth0.sample.databinding.FragmentDatabaseLoginBinding import com.google.android.material.snackbar.Snackbar import com.google.gson.Gson @@ -50,7 +47,7 @@ import java.util.concurrent.Executors */ class DatabaseLoginFragment : Fragment() { - private val scope = "openid profile email read:current_user update:current_user_metadata" + private val scope = "openid profile email offline_access" private val account: Auth0 by lazy { // -- REPLACE this credentials with your own Auth0 app credentials! @@ -104,7 +101,7 @@ class DatabaseLoginFragment : Fragment() { .setDeviceCredentialFallback(true) .build() - private val callback = object: Callback { + private val callback = object : Callback { override fun onSuccess(result: Credentials) { credentialsManager.saveCredentials(result) Snackbar.make( @@ -187,22 +184,6 @@ class DatabaseLoginFragment : Fragment() { getCredsAsync() } } - binding.btGetProfile.setOnClickListener { - getProfile() - } - binding.btGetProfileAsync.setOnClickListener { - launchAsync { - getProfileAsync() - } - } - binding.btUpdateMeta.setOnClickListener { - updateMeta() - } - binding.btUpdateMetaAsync.setOnClickListener { - launchAsync { - updateMetaAsync() - } - } return binding.root } @@ -346,7 +327,8 @@ class DatabaseLoginFragment : Fragment() { } private fun getCreds() { - credentialsManager.getCredentials(null, + credentialsManager.getCredentials( + null, 300, emptyMap(), emptyMap(), @@ -409,104 +391,6 @@ class DatabaseLoginFragment : Fragment() { } } - private fun getProfile() { - credentialsManager.getCredentials(object : - Callback { - override fun onSuccess(result: Credentials) { - val users = UsersAPIClient(account, result.accessToken) - users.getProfile(result.user.getId()!!) - .start(object : Callback { - override fun onFailure(error: ManagementException) { - Snackbar.make( - requireView(), error.getDescription(), Snackbar.LENGTH_LONG - ).show() - } - - override fun onSuccess(result: UserProfile) { - Snackbar.make( - requireView(), - "Got profile for ${result.name}", - Snackbar.LENGTH_LONG - ).show() - } - }) - } - - override fun onFailure(error: CredentialsManagerException) { - Snackbar.make(requireView(), "${error.message}", Snackbar.LENGTH_LONG).show() - } - }) - } - - private suspend fun getProfileAsync() { - try { - val credentials = credentialsManager.awaitCredentials() - val users = UsersAPIClient(account, credentials.accessToken) - val user = users.getProfile(credentials.user.getId()!!).await() - Snackbar.make( - requireView(), "Got profile for ${user.name}", Snackbar.LENGTH_LONG - ).show() - } catch (error: CredentialsManagerException) { - Snackbar.make(requireView(), "${error.message}", Snackbar.LENGTH_LONG).show() - } catch (error: ManagementException) { - Snackbar.make(requireView(), error.getDescription(), Snackbar.LENGTH_LONG).show() - } - } - - private fun updateMeta() { - val metadata = mapOf( - "random" to (0..100).random(), - ) - - credentialsManager.getCredentials(object : - Callback { - override fun onSuccess(result: Credentials) { - val users = UsersAPIClient(account, result.accessToken) - users.updateMetadata(result.user.getId()!!, metadata) - .start(object : Callback { - override fun onFailure(error: ManagementException) { - Snackbar.make( - requireView(), error.getDescription(), Snackbar.LENGTH_LONG - ).show() - } - - override fun onSuccess(result: UserProfile) { - Snackbar.make( - requireView(), - "Updated metadata for ${result.name} to ${result.getUserMetadata()}", - Snackbar.LENGTH_LONG - ).show() - } - }) - } - - override fun onFailure(error: CredentialsManagerException) { - Snackbar.make(requireView(), "${error.message}", Snackbar.LENGTH_LONG).show() - } - }) - } - - private suspend fun updateMetaAsync() { - val metadata = mapOf( - "random" to (0..100).random(), - ) - - try { - val credentials = credentialsManager.awaitCredentials() - val users = UsersAPIClient(account, credentials.accessToken) - val user = users.updateMetadata(credentials.user.getId()!!, metadata).await() - Snackbar.make( - requireView(), - "Updated metadata for ${user.name} to ${user.getUserMetadata()}", - Snackbar.LENGTH_LONG - ).show() - } catch (error: CredentialsManagerException) { - Snackbar.make(requireView(), "${error.message}", Snackbar.LENGTH_LONG).show() - } catch (error: ManagementException) { - Snackbar.make(requireView(), error.getDescription(), Snackbar.LENGTH_LONG).show() - } - } - private fun launchAsync(runnable: suspend () -> Unit) { //Use a better scope like lifecycleScope or viewModelScope GlobalScope.launch(Dispatchers.Main) { @@ -529,7 +413,8 @@ class DatabaseLoginFragment : Fragment() { ) var response: CreatePublicKeyCredentialResponse? - credentialManager.createCredentialAsync(requireContext(), + credentialManager.createCredentialAsync( + requireContext(), request, CancellationSignal(), Executors.newSingleThreadExecutor(), @@ -596,7 +481,8 @@ class DatabaseLoginFragment : Fragment() { listOf(request) ) - credentialManager.getCredentialAsync(requireContext(), + credentialManager.getCredentialAsync( + requireContext(), getCredRequest, CancellationSignal(), Executors.newSingleThreadExecutor(), diff --git a/sample/src/main/res/layout/fragment_database_login.xml b/sample/src/main/res/layout/fragment_database_login.xml index 5d3731b2..4712c677 100644 --- a/sample/src/main/res/layout/fragment_database_login.xml +++ b/sample/src/main/res/layout/fragment_database_login.xml @@ -146,17 +146,6 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/btWebLogoutAsync" /> - -