Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

POST /self-service/settings/browser returns a 403 error when MFA is not available yet, with required_aal is highest_available. #409

Open
4 of 5 tasks
wewelll opened this issue Jan 3, 2025 · 9 comments
Labels
bug Something is not working.

Comments

@wewelll
Copy link

wewelll commented Jan 3, 2025

Preflight checklist

Ory Network Project

https://infallible-goldwasser-0tlns4dkt7.projects.oryapis.com

Describe the bug

This issue follows this discussion on Slack.

I have setup optional MFA on my Ory project. Users sign-up with email / password and then they can add MFA with SMS or TOTP.

When I create a new account, after signing-up with email and password, I try to load the settings flow on the /self-service/settings/browser endpoint but I get a 403 error.

At this time, my session is aal1 and I have no MFA available because the phoneNumber trait is empty. Also, my configuration for the settings flow is required_aal: highest_available, so an aal1 session should match the requirements.

Reproducing the bug

  1. Create a Ory project with the configuration found lower in the issue
  2. Sign-up with email and password
  3. Go the the settings page - there should be an error 403

Relevant log output

the 403 response: 


{
    "error": {
        "id": "session_aal2_required",
        "code": 403,
        "status": "Forbidden",
        "reason": "An active session was found but it does not fulfill the requested Authenticator Assurance Level. Please verify yourself with a second factor to resolve this issue.",
        "details": {
            "redirect_browser_to": "http://localhost:4200/self-service/login/browser?aal=aal2"
        },
        "message": "Session does not fulfill the requested Authenticator Assurance Level"
    },
    "redirect_browser_to": "http://localhost:4200/self-service/login/browser?aal=aal2"
}

Relevant configuration

identity config:

selfservice:
  flows:
    settings:
      required_aal: highest_available
# ...
session:
  whoami:
    required_aal: aal1

identity schema:

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://schemas.ory.sh/presets/kratos/identity.email.schema.json",
  "title": "Person",
  "type": "object",
  "properties": {
    "traits": {
      "type": "object",
      "properties": {
        "first_name": {
          "title": "First Name",
          "type": "string"
        },
        "last_name": {
          "title": "Last Name",
          "type": "string"
        },
        "email": {
          "type": "string",
          "format": "email",
          "title": "E-Mail",
          "ory.sh/kratos": {
            "credentials": {
              "password": {
                "identifier": true
              },
              "webauthn": {
                "identifier": true
              },
              "totp": {
                "account_name": true
              }
            },
            "recovery": {
              "via": "email"
            },
            "verification": {
              "via": "email"
            }
          },
          "maxLength": 320
        },
        "phoneNumber": {
          "type": "string",
          "format": "tel",
          "title": "Phone Number",
          "ory.sh/kratos": {
            "credentials": {
              "code": {
                "identifier": true,
                "via": "sms"
              }
            },
            "verification": {
              "via": "sms"
            }
          }
        },
        "preferences": {
          "type": "object",
          "properties": {
            "locale": {
              "type": "string",
              "title": "Locale",
              "enum": ["en", "fr"]
            },
            "theme": {
              "type": "string",
              "title": "Theme",
              "enum": ["light", "dark", "system"]
            }
          },
          "required": ["locale"],
          "additionalProperties": false
        },
        "referralCode": {
          "title": "Referral code",
          "type": "string"
        }
      },
      "required": ["first_name", "last_name", "email", "preferences"],
      "additionalProperties": false
    }
  }
}

Version

"@ory/client": "^1.15.7"

On which operating system are you observing this issue?

Ory Network

In which environment are you deploying?

Ory Network

Additional Context

No response

@wewelll wewelll added the bug Something is not working. label Jan 3, 2025
@aeneasr
Copy link
Member

aeneasr commented Jan 22, 2025

What does your code configuration look like? Did you disable the fallback setting?

@wewelll
Copy link
Author

wewelll commented Jan 22, 2025

@aeneasr here is the current identity config:

# yaml-language-server: $schema=https://raw.githubusercontent.com/ory/kratos/master/embedx/config.schema.json
cookies:
  same_site: Lax
courier:
  smtp:
    connection_uri: smtp://${POSTMARK_SEVER_API_TOKEN}:${POSTMARK_SEVER_API_TOKEN}@smtp.postmarkapp.com:587/
    from_address: ${COURIER_SMTP_FROM_ADDRESS}
    from_name: ${COURIER_SMTP_FROM_NAME}
  channels:
    - id: sms
      type: http
      request_config:
        url: https://api.twilio.com/2010-04-01/Accounts/${TWILIO_ACCOUNT_SID}/Messages.json
        method: POST
        body: base64://${TWILIO_MESSAGE_JSONNET_BASE64}
        headers:
          Content-Type: application/x-www-form-urlencoded
        auth:
          type: basic_auth
          config:
            user: ${TWILIO_ACCOUNT_SID}
            password: ${TWILIO_AUTH_TOKEN}
  templates:
    login_code:
      valid:
        email:
          subject: base64://${COURIER_TEMPLATES_LOGIN_CODE_VALID_EMAIL_SUBJECT}
          body:
            html: base64://${COURIER_TEMPLATES_LOGIN_CODE_VALID_EMAIL_BODY_HTML}
            plaintext: base64://${COURIER_TEMPLATES_LOGIN_CODE_VALID_EMAIL_BODY_PLAINTEXT}
        sms:
          body:
            plaintext: base64://${COURIER_TEMPLATES_LOGIN_CODE_VALID_SMS_BODY_PLAINTEXT}
    recovery:
      invalid:
        email:
          body: {}
      valid:
        email:
          body: {}
    recovery_code:
      invalid:
        email:
          body: {}
      valid:
        email:
          subject: base64://${COURIER_TEMPLATES_RECOVERY_CODE_VALID_EMAIL_SUBJECT}
          body:
            html: base64://${COURIER_TEMPLATES_RECOVERY_CODE_VALID_EMAIL_BODY_HTML}
            plaintext: base64://${COURIER_TEMPLATES_RECOVERY_CODE_VALID_EMAIL_BODY_PLAINTEXT}
    registration_code:
      valid:
        email:
          subject: base64://${COURIER_TEMPLATES_REGISTRATION_CODE_VALID_EMAIL_SUBJECT}
          body:
            html: base64://${COURIER_TEMPLATES_REGISTRATION_CODE_VALID_EMAIL_BODY_HTML}
            plaintext: base64://${COURIER_TEMPLATES_REGISTRATION_CODE_VALID_EMAIL_BODY_PLAINTEXT}
    verification:
      invalid:
        email:
          body: {}
      valid:
        email:
          body: {}
    verification_code:
      invalid:
        email:
          body: {}
      valid:
        email:
          subject: base64://${COURIER_TEMPLATES_VERIFICATION_CODE_VALID_EMAIL_SUBJECT}
          body:
            html: base64://${COURIER_TEMPLATES_VERIFICATION_CODE_VALID_EMAIL_BODY_HTML}
            plaintext: base64://${COURIER_TEMPLATES_VERIFICATION_CODE_VALID_EMAIL_BODY_PLAINTEXT}
        sms:
          body:
            plaintext: base64://${COURIER_TEMPLATES_VERIFICATION_CODE_VALID_SMS_BODY_PLAINTEXT}
feature_flags:
  cacheable_sessions: false
identity:
  default_schema_id: ${SCHEMA_ID}
  schemas:
    - id: ${SCHEMA_ID}
      url: base64://${SPIKO_IDENTITY_SCHEMA_V0_BASE64}
oauth2_provider:
  override_return_to: true
organizations: []
selfservice:
  allowed_return_urls:
    - https://${ORY_PROJECT_SLUG}.projects.oryapis.com
    - https://${AUTH_DOMAIN}
  default_browser_return_url: /profile
  flows:
    error:
      ui_url: /ui/error
    login:
      after:
        code:
          hooks: []
        hooks:
          - hook: revoke_active_sessions
          - hook: web_hook
            config:
              url: https://api.segment.io/v1/identify
              method: POST
              body: base64://${SEGMENT_IDENTIFY_JSONNET_BASE64}
              auth:
                type: basic_auth
                config:
                  user: ${SEGMENT_WRITE_KEY}
                  password: ''
              can_interrupt: false
              response:
                ignore: false
                parse: false
        lookup_secret:
          hooks: []
        oidc:
          hooks: []
        password:
          hooks: []
        totp:
          hooks: []
        webauthn:
          hooks: []
      before:
        hooks: []
      lifespan: 30m0s
      ui_url: ${APP_URL}/signin
    logout:
      after: {}
    recovery:
      after:
        hooks: []
      before:
        hooks: []
      enabled: true
      lifespan: 30m0s
      notify_unknown_recipients: false
      ui_url: ${APP_URL}/recover
      use: code
    registration:
      after:
        hooks:
          - hook: web_hook
            config:
              url: https://api.segment.io/v1/identify
              method: POST
              body: base64://${SEGMENT_IDENTIFY_JSONNET_BASE64}
              auth:
                type: basic_auth
                config:
                  user: ${SEGMENT_WRITE_KEY}
                  password: ''
              can_interrupt: false
              response:
                ignore: false
                parse: false
        code:
          hooks:
            - hook: session
        oidc:
          hooks:
            - hook: session
        password:
          hooks:
            - hook: show_verification_ui
            - hook: session
        webauthn:
          hooks:
            - hook: session
      before:
        hooks: []
      enabled: true
      lifespan: 30m0s
      login_hints: true
      ui_url: ${APP_URL}/signup
    settings:
      after:
        hooks:
          - hook: web_hook
            config:
              url: https://api.segment.io/v1/identify
              method: POST
              body: base64://${SEGMENT_IDENTIFY_JSONNET_BASE64}
              auth:
                type: basic_auth
                config:
                  user: ${SEGMENT_WRITE_KEY}
                  password: ''
              can_interrupt: false
              response:
                ignore: false
                parse: false
        lookup_secret:
          hooks: []
        oidc:
          hooks: []
        password:
          hooks: []
        profile:
          hooks: []
        totp:
          hooks: []
        webauthn:
          hooks: []
      before:
        hooks: []
      lifespan: 30m0s
      privileged_session_max_age: 15m0s
      required_aal: aal1
      ui_url: ${APP_URL}/settings
    verification:
      after:
        hooks: []
      before:
        hooks: []
      enabled: true
      lifespan: 30m0s
      notify_unknown_recipients: false
      ui_url: ${APP_URL}/verify
      use: code
  methods:
    code:
      config:
        lifespan: 15m0s
      enabled: true
      passwordless_enabled: false
      mfa_enabled: true
      missing_credential_fallback_enabled: true
    link:
      config:
        base_url: ''
        lifespan: 15m0s
      enabled: true
    lookup_secret:
      enabled: true
    oidc:
      config:
        providers: []
      enabled: false
    password:
      config:
        haveibeenpwned_enabled: true
        identifier_similarity_check_enabled: true
        ignore_network_errors: true
        max_breaches: 1
        min_password_length: 8
      enabled: true
    profile:
      enabled: true
    totp:
      config:
        issuer: ${TOTP_ISSUER}
      enabled: true
    webauthn:
      config:
        passwordless: false
        rp:
          display_name: My App
          id: ${ORY_PROJECT_SLUG}.projects.oryapis.com
          origins:
            - https://${ORY_PROJECT_SLUG}.projects.oryapis.com
      enabled: false
session:
  cookie:
    persistent: false
    same_site: Lax
  lifespan: ${SESSION_LIFESPAN}
  whoami:
    required_aal: aal1
    tokenizer:
      templates: {}

@wewelll
Copy link
Author

wewelll commented Jan 27, 2025

@aeneasr any idea where it could come from ?

@aeneasr
Copy link
Member

aeneasr commented Jan 31, 2025

Set missing_credential_fallback_enabled to false

@wewelll
Copy link
Author

wewelll commented Feb 11, 2025

@aeneasr it does not fixes the problem.

I have modified my current config by:

  • setting the missing_credential_fallback_enabled to false
  • setting the selfservice.flows.settings.required_aal to highest_available

I still have a 403 when I request a settings flow with a aal1 session.

@aeneasr
Copy link
Member

aeneasr commented Feb 11, 2025 via email

@wewelll
Copy link
Author

wewelll commented Feb 11, 2025

I have the issue for new accounts.

The identity schema is unchanged

@aeneasr
Copy link
Member

aeneasr commented Feb 11, 2025

In that case it might be a bug in the implementation.

Since I‘m currently PTO I am unable to reproduce and someone from the team needs to take some time to look into this.

I think the problem is that the available AAL calculation does not take into account that the phone number is empty.

To work around this I suggest to require the phone number during sign up.

@aeneasr
Copy link
Member

aeneasr commented Feb 11, 2025

I took a brief look and it appears that CountSecondFactorCredentials for the code method is not respecting the identity credentials but instead always returning true if enabled. The problem is that normally the identity identifier and the code credential are the same field and hence the value is always set. Here however that is not the case as the code credential is an optional secondary field and not an identifier for the user. This is an edge case we have not encountered before. To work around this limitation here are several options:

  1. Allow the user to use the email login identifier for MFA - hence it is always set
  2. Force the user to use their phone number as the login identifier and use it for mfa - here too it is always set
  3. Force the user to set their phone number during sign up
  4. Use another MFA method like TOTP (way more secure anyways)
  5. Use aal1 as required_aal and check the true session aal value in your middleware

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something is not working.
Projects
None yet
Development

No branches or pull requests

2 participants