diff --git a/EXAMPLES.md b/EXAMPLES.md index 2caa7c738..b018c4426 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -31,6 +31,7 @@ - [Step 1: Issue an OTP challenge](#step-1-issue-an-otp-challenge) - [Step 2: Verify the code and log in](#step-2-verify-the-code-and-log-in) - [Sign Up with a database connection](#sign-up-with-a-database-connection) + - [Reset a password](#reset-a-password) - [Get user information](#get-user-information) - [Custom Token Exchange](#custom-token-exchange) - [Custom Token Exchange with Actor Token (Delegation/Impersonation)](#custom-token-exchange-with-actor-token-delegationimpersonation) @@ -1786,6 +1787,69 @@ authentication > The default scope used is `openid profile email`. Regardless of the scopes set to the request, the `openid` scope is always enforced. +### Reset a password + +Send a password reset email to a database user. + +```kotlin +authentication + .resetPassword("info@auth0.com", "my-database-connection") + .start(object: Callback { + override fun onFailure(exception: AuthenticationException) { } + + override fun onSuccess(result: Void?) { } + }) +``` + +If the user belongs to an [organization](#organizations), pass its identifier to associate the reset request with that organization. Auth0 then includes the `organization_id` and `organization_name` values in the password reset redirect URL, and makes them available as variables in customized email templates. + +```kotlin +authentication + .resetPassword("info@auth0.com", "my-database-connection", "org_abc123") + .start(object: Callback { + override fun onFailure(exception: AuthenticationException) { } + + override fun onSuccess(result: Void?) { } + }) +``` + +
+ Using coroutines + +```kotlin +try { + authentication + .resetPassword("info@auth0.com", "my-database-connection", "org_abc123") + .await() + println("Password reset email sent") +} catch (e: AuthenticationException) { + e.printStacktrace() +} +``` +
+ +
+ Using Java + +```java +authentication + .resetPassword("info@auth0.com", "my-database-connection", "org_abc123") + .start(new Callback() { + @Override + public void onSuccess(@Nullable Void payload) { + //Password reset email sent! + } + + @Override + public void onFailure(@NonNull AuthenticationException error) { + //Error! + } + }); +``` +
+ +> The `organization` parameter must be the organization ID (for example, `org_abc123`), not the organization name. It is optional — omit it to send a password reset that is not associated with an organization. + ### Get user information ```kotlin @@ -3656,6 +3720,8 @@ client [Organizations](https://auth0.com/docs/organizations) is a set of features that provide better support for developers who build and maintain SaaS and Business-to-Business (B2B) applications. Note that Organizations is currently only available to customers on our Enterprise and Startup subscription plans. +Besides Web Auth login, the Authentication API client can also associate a password reset request with an organization. See [Reset a password](#reset-a-password). + ### Log in to an organization ```kotlin 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 d280a89a3..8a616efb5 100755 --- a/auth0/src/main/java/com/auth0/android/authentication/AuthenticationAPIClient.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/AuthenticationAPIClient.kt @@ -606,7 +606,7 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe * Example usage: * * ``` - * client.resetPassword("{email}", "{database connection name}") + * client.resetPassword("{email}", "{database connection name}", "{organization}") * .start(object: Callback { * override fun onSuccess(result: Void?) { } * override fun onFailure(error: AuthenticationException) { } @@ -615,21 +615,27 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe * * @param email of the user to request the password reset. An email will be sent with the reset instructions. * @param connection of the database to request the reset password on + * @param organization id of the organization the user belongs to. When set, Auth0 associates the password + * reset request with this organization, making the organization details available in the reset redirect URL + * and in customized email templates. Must be the organization id, not the organization name. * @return a request to configure and start */ + @JvmOverloads public fun resetPassword( email: String, - connection: String + connection: String, + organization: String? = null ): Request { val url = auth0.getDomainUrl().toHttpUrl().newBuilder() .addPathSegment(DB_CONNECTIONS_PATH) .addPathSegment(CHANGE_PASSWORD_PATH) .build() - val parameters = ParameterBuilder.newBuilder() - .set(EMAIL_KEY, email) - .setClientId(clientId) - .setConnection(connection) - .asDictionary() + val parameters = ParameterBuilder.newBuilder().apply { + set(EMAIL_KEY, email) + setClientId(clientId) + setConnection(connection) + organization?.let { set(ORGANIZATION_KEY, it) } + }.asDictionary() return factory.post(url.toString()) .addParameters(parameters) } 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 ba435a449..e9a07b654 100755 --- a/auth0/src/test/java/com/auth0/android/authentication/AuthenticationAPIClientTest.kt +++ b/auth0/src/test/java/com/auth0/android/authentication/AuthenticationAPIClientTest.kt @@ -1512,6 +1512,89 @@ public class AuthenticationAPIClientTest { assertThat(body, Matchers.hasEntry("connection", MY_CONNECTION)) } + @Test + public fun shouldChangePasswordWithOrganization() { + mockAPI.willReturnSuccessfulChangePassword() + val callback = MockAuthenticationCallback() + client.resetPassword(SUPPORT_AUTH0_COM, MY_CONNECTION, "org_12345") + .start(callback) + ShadowLooper.idleMainLooper() + val request = mockAPI.takeRequest() + assertThat(request.path, Matchers.equalTo("/dbconnections/change_password")) + val body = bodyFromRequest(request) + assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) + assertThat(body, Matchers.hasEntry("connection", MY_CONNECTION)) + assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) + assertThat(body, Matchers.hasEntry("organization", "org_12345")) + assertThat(callback, AuthenticationCallbackMatcher.hasNoError()) + } + + @Test + public fun shouldChangePasswordWithOrganizationSync() { + mockAPI.willReturnSuccessfulChangePassword() + client.resetPassword(SUPPORT_AUTH0_COM, MY_CONNECTION, "org_12345") + .execute() + val request = mockAPI.takeRequest() + assertThat(request.path, Matchers.equalTo("/dbconnections/change_password")) + val body = bodyFromRequest(request) + assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) + assertThat(body, Matchers.hasEntry("connection", MY_CONNECTION)) + assertThat(body, Matchers.hasEntry("organization", "org_12345")) + } + + @Test + @ExperimentalCoroutinesApi + public fun shouldAwaitChangePasswordWithOrganization(): Unit = runTest { + mockAPI.willReturnSuccessfulChangePassword() + client.resetPassword(SUPPORT_AUTH0_COM, MY_CONNECTION, "org_12345") + .await() + val request = mockAPI.takeRequest() + assertThat(request.path, Matchers.equalTo("/dbconnections/change_password")) + val body = bodyFromRequest(request) + assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) + assertThat(body, Matchers.hasEntry("connection", MY_CONNECTION)) + assertThat(body, Matchers.hasEntry("organization", "org_12345")) + } + + @Test + public fun shouldNotSendOrganizationOnChangePasswordWhenNull() { + mockAPI.willReturnSuccessfulChangePassword() + client.resetPassword(SUPPORT_AUTH0_COM, MY_CONNECTION, null) + .execute() + val request = mockAPI.takeRequest() + assertThat(request.path, Matchers.equalTo("/dbconnections/change_password")) + val body = bodyFromRequest(request) + assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) + assertThat(body, Matchers.hasEntry("connection", MY_CONNECTION)) + assertThat(body, Matchers.not(Matchers.hasKey("organization"))) + } + + @Test + public fun shouldNotSendOrganizationOnChangePasswordWhenNotProvided() { + mockAPI.willReturnSuccessfulChangePassword() + client.resetPassword(SUPPORT_AUTH0_COM, MY_CONNECTION) + .execute() + val request = mockAPI.takeRequest() + assertThat(request.path, Matchers.equalTo("/dbconnections/change_password")) + val body = bodyFromRequest(request) + assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) + assertThat(body, Matchers.hasEntry("connection", MY_CONNECTION)) + assertThat(body, Matchers.not(Matchers.hasKey("organization"))) + } + + @Test + public fun shouldSendEmptyOrganizationOnChangePasswordWithoutValidating() { + mockAPI.willReturnSuccessfulChangePassword() + client.resetPassword(SUPPORT_AUTH0_COM, MY_CONNECTION, "") + .execute() + val request = mockAPI.takeRequest() + assertThat(request.path, Matchers.equalTo("/dbconnections/change_password")) + val body = bodyFromRequest(request) + assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) + assertThat(body, Matchers.hasEntry("connection", MY_CONNECTION)) + assertThat(body, Matchers.hasEntry("organization", "")) + } + @Test public fun shouldSendEmailCodeWithCustomConnection() { mockAPI.willReturnSuccessfulPasswordlessStart()