Homepage

[DEPRECATED] Resetting Password of an Authenticated User

Last edit: Sep 16, 2024

Warning

This article series promotes UserProfiles and Forms, which are deprecated. We decided to reduce the learning curve by promoting explicit implementation via Liquid, Pages and GraphQL, instead of built-in features, which add magic into the mix increasing the learning curve and making debugging harder. Please refer to our Get Started to read up-to date articles, including User Authentication

This guide will help you learn how to add password reset functionality to your site. As this functionality is very complex, it uses many platformOS features.

Requirements

To follow the steps in this tutorial, you should be familiar with the required directory structure for your codebase, have a test email set up, and understand the concept of pages, users, Tables, Properties, and Authorization Policies. You should also know how to create a form.

Steps

Resetting a password is a six-step process:

Step 1: Create Table to store password reset request

app/schema/reset_password.yml

name: reset_password
properties:
- name: email
  type: string

Step 2: Create form to get user email

Note

If you’re using modules, make sure to adjust paths accordingly, for example, change resource: reset_password to resource: modules/users/reset_password. For more information, visit Modules.

app/forms/recover_password.liquid

---
name: recover_password
resource: reset_password
resource_owner: anyone
redirect_to: /recover-password
flash_notice: If you provided the right email, we will send you reset password instructions.
fields:
  properties:
    email:
      validation:
        presence: true
        email: true
callback_actions: |-
  {% graphql g = 'generate_user_temporary_token', email: form.properties.email %}
  {% if g.users.results[0] %}
    {% graphql res = 'update_password_token', id: g.users.results[0].id, token: g.users.results[0].temporary_token %}
  {% endif %}
---

{% form %}
  <label for="email">Email</label>
  <input name="{{ form.fields.properties.email.name }}" id="email" type="email">

  <button>Recover password</button>
{% endform %}

Step 3: Check user email

Create a GraphQL query to check if the provided email address has a user associated with it in the DB, and if yes, generate a temporary token, used to authenticate the request.

app/graphql/generate_user_temporary_token.graphql
query generate_user_temporary_token($email: String) {
  users(
    per_page: 1
    filter: {
      email: {
        value: $email
      }
    }
  ) {
    results{
      id
      temporary_token(valid_for: 24)
    }
  }
}

Step 4: Store token

If the user exists, you will store the value of the generated token in a Property associated with the default profile.

app/user_profile_types/default.yml
name: default
properties:
- name: password_token
  type: string

This way, you can make sure that only the most recent token can be used. To store the actual value, create a GraphQL mutation.

app/graphql/update_password_token.graphql
mutation update_password_token(
  $id: ID!
  $token: String!
) {
  user_update(
    id: $id
    user: {
      profiles: [
        {
          name: "default",
          values: {
            properties: [
              { name: "password_token", value: $token }
            ]
          }
        }
      ]
    }
    form_name: "update_password_token"
  ) {
    id
  }
}

Each GraphQL mutation has associated form configuration with it, in this case it's update_password_token.

app/forms/update_password_token.liquid
---
name: update_password_token
resource: User
resource_owner: anyone
fields:
  email:
  profiles:
    default:
      properties:
        password_token:
          validation: { presence: true }
email_notifications:
  - send_recover_password
---

Step 5: Send email with password reset instructions

After storing the token, send an email to the user with password reset instructions.

app/emails/send_recover_password.liquid
---
name: send_recover_password
to: '{{ form.email }}'
from: 'Example Site <info@example.com>'
reply_to: no-reply@example.com
subject: Reset password instructions
layout: mailer
---

{%- graphql g = 'get_user_with_password_token', email: form.email -%}
<h1>Hi {{ g.users.results[0].first_name }}!</h1>

<p>To reset your password, follow the link: <a href="{{ platform_context.host }}/reset-password?token={{ g.users.results[0].default[0].password_token | url_encode }}&amp;email={{ g.users.results[0].email | url_encode }}">reset password!</a></p>

The email relies on a GraphQL query called get_user_with_password_token, to fetch user's first name and the value of the token.

app/graphql/get_user_with_password_token.graphql
query get_user_with_password_token($email: String, $id: ID) {
  users(
    per_page:1
    filter: {
      email: {
        value: $email
      }
      id: {
        value: $id
      }
    }
  ) {
    results{
      id
      email
      first_name
      default: profiles(profile_type: "default") {
        password_token: property(name: "password_token")
      }
    }
  }
}

Step 6: Create entry point to the workflow

Create a page as an entry point to the workflow.

app/views/pages/recover_password.liquid

---
slug: recover-password
---
<h2>Forgotten Password</h2>
{% if flash.notice != blank %}
  <p>{{ flash.notice }}</p>
{% endif %}
{% include_form 'recover_password' %}

The email contains a link to the /reset-password page, so create this page:

app/views/pages/reset-password.liquid

---
slug: reset-password
---
{% liquid
  graphql g = 'get_user_with_password_token', email: params.email
  assign token_valid = params.token | is_token_valid: g.users.results[0].id
%}
{% if g.users.results[0].id == blank or token_valid == false or g.users.results[0].default[0].password_token != params.token %}
  Unfortunately, provided token is not valid anymore. Please request password instructions again.
{% else %}
  <h2>Reset Password</h2>
  {% include_form 'reset_password', id: g.users.results[0].id %}
{% endif %}

On this page you check, if the provided token is valid as well as fetch the user's id based on the email provided in the parameter. You re-use the GraphQL query created earlier.

On this page, you embed the form to reset the password.

app/forms/reset_password.liquid
---
name: reset_password
resource: User
resource_owner: anyone
redirect_to: /sign-in
flash_notice: Your password has been updated. You can now log in.
fields:
  email:
    property_options:
      readonly: true
  password:
    validation:
      confirmation: true
  password_confirmation:
    property_options:
      virtual: true
authorization_policies:
  - token_is_valid
---

{% form %}
  <input name="token" value="{{ context.params.token }}" type="hidden">
  <input name="email" value="{{ form.email }}" type="hidden">

  <label for="password">Password</label>
  <input name="{{ form.fields.password.name }}" id="password" type="password">

  <label for="password_confirmation">Password confirmation</label>
  <input name="{{ form.fields.password_confirmation.name }}" id="password_confirmation" type="password">
  {% if form.errors.password_confirmation %}
    <p>{{ form.errors.password_confirmation }}</p>
  {% endif %}

  <button>Reset password</button>
{% endform %}

To be able to re-render the previous page if the password validation fails, you forward parameters which came from the email: email and token. The token will also be used in an Authorization Policy, where you authenticate the user with this token to make sure the user can't change any other user's password.
You can add more rules to the password field by validation. By default, we require that password should be minimum 6 characters long.

Create this Authorization Policy

app/authorization_policies/token_is_valid.liquid

---
name: token_is_valid
---
{% liquid
  graphql g = 'get_user_with_password_token', id: params.id
  assign token_valid = params.token | is_token_valid: g.users.results[0].id
%}
{% if g.users.results[0].id != blank and token_valid == true and g.users.results[0].default[0].password_token == params.token %}true{% endif %}

This concludes the password reset process. If the user provides a valid password and confirms it, the password will be changed and the user will be redirected to the /sign_in page. If the password is not valid, the system will re-render the form and display a message explaining what is wrong. Finally, if the user tries to hijack someone else's account by manually changing the id or providing an invalid token, 403 status will be returned and the request will not be processed.

Important

Temporary token will be invalidated on user password change.

Next steps

Congratulations! You know how to build a password reset process. Now you can learn about accessing user data:

Questions?

We are always happy to help with any questions you may have.

contact us