authgear / authgear-server

Open source alternative to Auth0 / Firebase Auth
https://www.authgear.com
Apache License 2.0
73 stars 30 forks source link

Execute ForgotPasswordFlow and ResetPasswordFlow #3370

Closed louischan-oursky closed 8 months ago

louischan-oursky commented 10 months ago
tung2744 commented 9 months ago

send_account_recovery_code_flows:
  # The current forgot password flow
  # 1. Take a email or phone, it won't tell the user if it is correct or not
  # 2. Send the code if the email or phone is valid
  - name: may_recover
    steps:
    - type: identify
      on_failure: ignore
      one_of:
      - identification: email
        steps:
        - type: send_account_recovery_code
          on_failure: ignore
          channel: email
      - identification: phone
        steps:
        - type: send_account_recovery_code
          on_failure: ignore
          channel: sms
  # Forgot password for reauth flow
  # 1. Take a id token and identify the user
  # 2. Take a channel (sms/email)
  # 3. Must send the code
  - name: must_recover
     steps:
     - type: identify
       on_failure: error
       one_of:
       - identification: id_token
         steps:
         - type: select_account_recovery_code_channel
         - type: send_account_recovery_code

account_recovery_flows:
  # Reset password flow
  - name: default
    steps:
      - type: input_recovery_code
      - type: reset_password

@louischan-oursky Please see the proposed design above. You can add comments here or have a huddle if you want to have a discussion. :adore:

louischan-oursky commented 9 months ago
request_account_recovery_flows:
- name: default
  steps:
  - type: identify
    # on_failure is moved to the branch.
    one_of:
    # Available identification are "email", "phone", and "id_token".
    - identification: email
      # on_failure is either "ignore" or "error"
      # If it is "ignore", do not report error and proceed.
      # If it is "error", report not found (i.e. allow account enumeration).
      on_failure: ignore
    - identification: phone
      on_failure: ignore
    - identification: id_token
      on_failure: ignore
  # This step outputs an array of options, where the options look like
  # [
  #   {"masked_display_name": "+852*****325", "channel": "sms", "otp_form": "code"},
  #   {"masked_display_name": "+852*****325", "channel": "whatsapp", "otp_form": "code"},
  #   {"masked_display_name": "lou******@oursky.com", "channel": "email", "otp_form": "link"}
  # ]
  - type: select_destination
  # No more steps because we have gathered enough information to deliver
  # the account recovery code.

account_recovery_flows:
- name: default
  steps:
  # This step verify if the account recovery code is valid.
  - type: verify_account_recovery_code
  # This step is the same as type: change_password in login flow.
  # That is, no old password is required.
  - type: reset_password
  # In addition to step: reset_password
  # we may support more type to recover an account.
  # For example, when 2FA becomes mandatory, and lock out an end-user.
  # The customer support can issue an account recovery code.
  # And the flow guides the end-user to set up TOTP.
  # type: create_authenticator is the same as type: create_authenticator in signup flow.
  - type: create_authenticator
    one_of:
    - authentication: secondary_totp
  # Finishing an account recovery flow DOES NOT authenticate the end-user,
  # no session is created.
tung2744 commented 9 months ago

@louischan-oursky

  1. This step outputs an array of options, ... This actually tells the user whether his input correctly identified a user, therefore on_failure: ignore becomes useless.
  2. There are actually two cases in forgot password flow: a. Do not identity a user, but send directly to a login_id b. Identity a user, and let user choose a channel from the user's identities So I am thinking about maybe we actually need to separate these cases into two types. input_destination and identity. If they are separated types then we don't need on_failure.
louischan-oursky commented 9 months ago
  1. The options depend on the input of the previous step, it does not output the options by user ID. In case of ID token, we only show masked PII.
tung2744 commented 9 months ago
request_account_recovery_flows:
  # The current forgot password flow
  # 1. Take a email or phone, it won't tell the user if it is correct or not
  # 2. Send the code if the email or phone is valid
  - name: default
    steps:
    # This step take an input from user,
    # and send the code to the input no matter it is correct or not
    - type: input_destination
  # Forgot password for reauth flow
  # 1. Take a id token and identify the user
  # 2. Take a destination (sms/email)
  # 3. Always send the code
  - name: reauth_forgot_password
      steps:
      - type: identify
        one_of:
        - identification: id_token
      # This step outputs an array of options with masked login ids
      - type: select_destination

See if this updated version looks good?

tung2744 commented 9 months ago

Conclusion of offline discussion:

  1. We focus on supporting forgot password flow which is same as the current auth ui first. Which is:
    • Input email / phone
    • Send link to the inputted email / phone if it exist
  2. Do not expose the authflow config for now, the config is always generated.
tung2744 commented 9 months ago

@louischan-oursky Please check the following example of supported steps in the flows.

request_account_recovery_flows:
- name: default
  steps:
    - type: identify
      one_of:
      - identification: email
        # If "ignore", it still pass the step even the email is wrong.
        on_failure: ignore
        # If true, all login id of the user will be enumerated in next step.
        enumerate_destinations: false
      - identification: phone
        on_failure: ignore
        enumerate_destinations: false
    - type: select_destination

account_recovery_flows:
- name: default
  steps:
    - type: verify_account_recovery_code
    - type: reset_password
louischan-oursky commented 9 months ago
request_account_recovery_flows:
- name: default
  steps:
  - type: identify
    one_of:
    - identification: email
      on_failure: ignore
      steps:
      - type: select_destination
        enumerate_destinations: false
    - identification: phone
      on_failure: ignore
      steps:
      - type: select_destination
        enumerate_destinations: true

account_recovery_flows:
- name: default
  steps:
    - type: verify_account_recovery_code
    - type: reset_password

I suggest we move enumerate_destinations to type: select_destination (because it is a property that controls the behavior of the step). And we allow nested steps to do branching.

tung2744 commented 8 months ago

Account recovery examples:

  1. Initiate the flow
// Request
{
    "type": "request_account_recovery",
    "name": "default",
    "batch_input": []
}

// Response
{
    "result": {
        "state_token": "authflowstate_HAQPWJ80JFQSBPQZYFQY2HXJPVSDBYNC",
        "id": "authflow_3JGC7RXSK41381NJ189SAAYZH7KMSSCQ",
        "type": "account_recovery",
        "name": "default",
        "action": {
            "type": "identify",
            "data": {
                "options": [
                    {
                        "identification": "email"
                    },
                    {
                        "identification": "phone"
                    }
                ]
            }
        }
    }
}
  1. Select an identification option
// Request
{
    "state_token": "authflowstate_56KJBZWYFDPFBGB62T1QQGWZNNT8JCQA",
    "batch_input": [
        {
            "identification": "email",
            "login_id": "test@example.com"
        }
    ]
}

// Response
{
    "result": {
        "state_token": "authflowstate_D2Y301GPK0E6NH81MCDFAXJ72XXSG4MW",
        "id": "authflow_JGR9YYY0KGVA610VK2KVYCQEHMY3TKXV",
        "type": "account_recovery",
        "name": "default",
        "action": {
            "type": "select_destination",
            "data": {
                "options": [
                    {
                        "id": "0",
                        "masked_display_name": "te**@example.com",
                        "channel": "email"
                    }
                ]
            }
        }
    }
}
  1. Select the destination
// Request
{
    "state_token": "authflowstate_D2Y301GPK0E6NH81MCDFAXJ72XXSG4MW",
    "batch_input": [
        {
            "option_id": "0"
        }
    ]
}

// Response
{
    "result": {
        "state_token": "authflowstate_QH09EWN95VTVEW2QT893SF9FQPXB6RWH",
        "id": "authflow_3JGC7RXSK41381NJ189SAAYZH7KMSSCQ",
        "type": "account_recovery",
        "name": "default",
        "action": {
            "type": "finished",
            "data": {}
        }
    }
}

@louischan-oursky I've added an example request & response of a complete flow. Please see if any opinion? Maybe @chpapa can also leave comments if any.

tung2744 commented 8 months ago
account_recovery_flows:
- name: default
  steps:
  - type: identify
    one_of:
    - identification: email
      on_failure: ignore
      steps:
      - type: select_destination
        enumerate_destinations: false
    - identification: phone
      on_failure: ignore
      steps:
      - type: select_destination
        enumerate_destinations: false
  - type: verify_account_recovery_code
  - type: reset_password
louischan-oursky commented 8 months ago

The two steps verify_account_recovery_code and reset_password were mistakenly repeated.

tung2744 commented 8 months ago

Config

account_recovery_flows:
  - name: default
    steps:
      - type: identify # Identity the user by one of the following method
        one_of:
          - identification: email
            on_failure: ignore # ignore / error. If error, this step will return error if the login id doesn't exist.
            steps:
              - type: select_destination
                enumerate_destinations: false # If true, this step list all possible destinations (email / phone) of the identified user.
          - identification: phone
            on_failure: ignore
            steps:
              - type: select_destination
                enumerate_destinations: false
      - type: verify_account_recovery_code # Input & verify a account recovery code
      - type: reset_password # Input a new password and reset password

HTTP API

  1. Initiate the flow
// Request POST /api/v1/authentication_flows
{
  "type": "account_recovery",
  "name": "default",
  "batch_input": []
}

// Response
{
  "result": {
    "state_token": "authflowstate_00000000000000000000001",
    "type": "account_recovery",
    "name": "default",
    "action": {
      "type": "identify",
      "data": {
        "options": [
          {
            "identification": "email"
          },
          {
            "identification": "phone"
          }
        ]
      }
    }
  }
}
  1. Input the login id
// Request POST /api/v1/authentication_flows/states/input
{
  "state_token": "authflowstate_00000000000000000000001",
  "batch_input": [
    {
      "identification": "email",
      "login_id": "test@example.com"
    }
  ]
}

// Response
{
  "result": {
    "state_token": "authflowstate_00000000000000000000002",
    "type": "account_recovery",
    "name": "default",
    "action": {
      "type": "select_destination",
      "data": {
        "options": [
          {
            "masked_display_name": "te**@example.com",
            "channel": "email"
          }
        ]
      }
    }
  }
}
  1. Select the destination for receiving the account recovery code / link
// Request POST /api/v1/authentication_flows/states/input
{
  "state_token": "authflowstate_00000000000000000000002",
  "batch_input": [
    {
      "index": 0
    }
  ]
}

// Response
{
  "result": {
    "state_token": "authflowstate_00000000000000000000003",
    "type": "account_recovery",
    "name": "default",
    "action": {
      "type": "verify_account_recovery_code",
      "data": {
        "destination": {
            "id": "0",
            "masked_display_name": "te**@example.com",
            "channel": "email"
        }
      }
    }
  }
}
  1. Open the link in email / sms and get the code
// Request POST /api/v1/authentication_flows/states/input
{
  // Note that `state_token` is not needed in this request.
  // This is an exception case only for account_recovery_code input
  // We expect user to continue the flow in another device which might not know the current state_token, therefore we allow an input with the code only.
  // The server will continue the flow according to the code.
  "batch_input": [
    {
      "account_recovery_code": "123456ABCDEFG"
    }
  ]
}

// Response
{
  "result": {
    "state_token": "authflowstate_00000000000000000000004",
    "type": "account_recovery",
    "name": "default",
    "action": {
      "type": "reset_password",
      "data": {
        "password_policy": {
          "minimum_length": 8,
          "uppercase_required": true,
          "lowercase_required": true,
          "alphabet_required": true,
          "digit_required": true,
          "symbol_required": true,
          "minimum_zxcvbn_score": 3
        }
      }
    }
  }
}
  1. Reset the password
// Request POST /api/v1/authentication_flows/states/input
{
  "state_token": "authflowstate_00000000000000000000004",
  "batch_input": [
    {
      "new_password": "12345678"
    }
  ]
}

// Response
{
  "result": {
    "state_token": "authflowstate_00000000000000000000005",
    "type": "account_recovery",
    "name": "default",
    "action": {
      "type": "finish",
      "data": {}
    }
  }
}

API Design / Behavior Changes

  1. state_token of input api is no longer a required field. It can be omitted if the first input can be used to determine the flow state. Currently, only account_recovery_code can be used to determine the flow state. Meanwhile, we define the follow behavior:

    • If state_token is provided, and at the same time account_recovery_code is used: state_token take precedence.
    • If batch_input is used, and multiple account_recovery_code inputs are given: only the first account_recovery_code will be used to determine the flow state.
  2. Originally, the lifetime of a flow state is 20 minutes. However, the lifetime of forgot password code is configurable in each project. Therefore, we decided to allow account_recovery flow to be continued at step verify_account_recovery_code even the lifetime of flow state is passed, respecting the lifetime of forgot password code.

@louischan-oursky @chpapa

I've updated the config and api design according to our previous discussion, please take a look and see if any comments.

louischan-oursky commented 8 months ago
  1. If we want to add option_id, then we should add them in other flows as well.
  2. reset_password should output the password policy, see how change_password does this.
tung2744 commented 8 months ago

@louischan-oursky

1

Lets use index.

2

Updated in the above example.

fungc-io commented 8 months ago

tested ok