goauthentik / authentik

The authentication glue you need.
https://goauthentik.io
Other
7.83k stars 601 forks source link

Passwordless login using webauthn still prompts for password #9062

Open shotor opened 3 months ago

shotor commented 3 months ago

Describe the bug

I followed this guide https://docs.goauthentik.io/docs/flow/stages/password/

But after setting it up it still prompts for my password. After entering my password I can select my webauthn device.

To Reproduce Steps to reproduce the behavior:

Make sure you have webauthn device enabled

  1. Create new expression policy pending_user supports webauthn with contents:
from authentik.stages.authenticator_webauthn.models import WebAuthnDevice
return WebAuthnDevice.objects.filter(user=request.context['pending_user'], confirmed=True).exists()
  1. Go to default-authentication-flow
  2. Go to state bindings
  3. Expand default-authentication-password
  4. Click bind existing policy
  5. Select pending_user supports webauthn, order 0, timeout 30, don't pass failure result
  6. Logout
  7. Login
  8. Enter username

You will now see a password prompt

Expected behavior

It should allow me to choose my webauthn device instead of the password prompt

Screenshots

image

image

image

Logs

INF auth_via=unauthenticated domain_url=auth.my-domain.com event=/ host=auth.my-domain.com logger=authentik.asgi method=GET pid=28526 remote=192.168.1.55 request_id=efccc25853c545778355725cdf27fade runtime=7 schema_name=public scheme=https status=302 timestamp=2024-03-28T20:52:39.637855 user= user_agent=Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36
INF auth_via=unauthenticated domain_url=auth.my-domain.com event=/flows/-/default/authentication/?next=/ host=auth.my-domain.com logger=authentik.asgi method=GET pid=28526 remote=192.168.1.55 request_id=e74664c5d9104af186e5cd247d36b4a0 runtime=12 schema_name=public scheme=https status=302 timestamp=2024-03-28T20:52:39.676271 user= user_agent=Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36
INF auth_via=unauthenticated domain_url=auth.my-domain.com event=/if/flow/default-authentication-flow/?next=%2F host=auth.my-domain.com logger=authentik.asgi method=GET pid=28526 remote=192.168.1.55 request_id=6086da179b9a4a70a34a1ac84203fdf7 runtime=25 schema_name=public scheme=https status=200 timestamp=2024-03-28T20:52:39.727065 user= user_agent=Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36
INF domain_url=null event=/ws/client/ logger=authentik.asgi pid=31163 remote=192.168.1.55 schema_name=public scheme=ws timestamp=2024-03-28T20:52:39.861005 user_agent=Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36
INF auth_via=unauthenticated domain_url=auth.my-domain.com event=/api/v3/flows/executor/default-authentication-flow/?query=next%3D%252F host=auth.my-domain.com logger=authentik.asgi method=GET pid=28526 remote=192.168.1.55 request_id=2cc5197e20cd4da88d9e5d93d73b56ae runtime=27 schema_name=public scheme=https status=200 timestamp=2024-03-28T20:52:39.910195 user= user_agent=Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36
INF auth_via=unauthenticated domain_url=auth.my-domain.com event=/api/v3/flows/executor/default-authentication-flow/?query=next%3D%252F host=auth.my-domain.com logger=authentik.asgi method=POST pid=28526 remote=192.168.1.55 request_id=2e5fc40bf98844b9b10756764a69379f runtime=25 schema_name=public scheme=https status=302 timestamp=2024-03-28T20:52:40.994757 user= user_agent=Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36
INF auth_via=unauthenticated domain_url=auth.my-domain.com event=/api/v3/flows/executor/default-authentication-flow/?query=next%3D%252F host=auth.my-domain.com logger=authentik.asgi method=GET pid=28526 remote=192.168.1.55 request_id=65194450509e4de7b86481fb06e7910f runtime=48 schema_name=public scheme=https status=200 timestamp=2024-03-28T20:52:41.069561 user= user_agent=Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36

Version and Deployment (please complete the following information):

Additional context

Flow looks weird, feel like a step is missing

dugite-code commented 3 months ago

I'm just setting this up again myself, looks like you need to:

  1. Negate the pending_user supports webauthn policy via the edit binding button.
  2. set the default-authentication-password stage binding, policy engine mode to all image
shotor commented 3 months ago

That works! Thanks.

I think this probably not the the best practice though security wise. This method also allows TOTP as a passwordless login method, where I think you really want just webauthn as the passwordless method.

I'll try the "passwordless flow" setting on the authentication flow.

Issue may be closed if you wish!

dugite-code commented 3 months ago

Ah that is a very good point. Just adjust the policy to be explicit with True/False (uncheck the Negate the pending_user supports webauthn policy via the edit binding button.)

from authentik.stages.authenticator_webauthn.models import WebAuthnDevice
if WebAuthnDevice.objects.filter(user=request.context['pending_user'], confirmed=True).exists():
  return False # Bypass password auth
else:
  return True # Proceed to password auth

It looks to me that the Doc's need updating

shotor commented 3 months ago

Ah that is a very good point. Just adjust the policy to be explicit with True/False (uncheck the Negate the pending_user supports webauthn policy via the edit binding button.)

from authentik.stages.authenticator_webauthn.models import WebAuthnDevice
if WebAuthnDevice.objects.filter(user=request.context['pending_user'], confirmed=True).exists():
  return False # Bypass password auth
else:
  return True # Proceed to password auth

It looks to me that the Doc's need updating

Unfortunately that gives the same result using the default-mfa-validation stage. That stage contains both Webauthn and TOTP as valid MFA methods.

To do it all in the same flow; I think you'd need a separate webauthn-only-mfa-validation stage.

Not sure how to program it or if it's even possible, but would have to be something like this:

Welcome to Authentik!
 |
 Stage (Identification Stage)
 |
 ├─ Policy (has webauthn true)
 |    └─ Stage (Authenticator Validation) NEW webauthn only
 |          └─ Stage (User Login Stage) default-authentication-login
 |                 └─ End of the flow
 |
 └─ Policy (has webauthn false)
       └─ Stage (Password Stage) default-authentiation-password
             └─ Stage (Authenticator Validation) default-authentication-mfa-validation (totp only available if you got this far)
                    └─ Stage (User Login Stage) default-authentication-login
                           └─ End of the flow

Where:
- Stage (Authenticator Validation) NEW webauthn only
- Stage (Authenticator Validation) default-authentication-mfa-validation (totp only available if you got this far)

Point to the same Stage (User Login Stage) default-authentication-login

Then again there's this option, which seems to add a "passwordless" button to your login form:

image

dugite-code commented 3 months ago

That stage contains both Webauthn and TOTP as valid MFA methods.

Ah sorry I miss-read that, I thought you were able to enter TOTP instead of the password. If you really want to skip TOTP for Webauthn I also suggest as you said that you would need to create a second MFA stage with only Webauthn, add it to the flow then use a policy on both the original and new stage to either pass or use that stage depending on the Webauthn exists state.

But as you said, you can just add the password-less flow button. It's a touch less user friendly but gets the job done. At any rate this isn't a bug and should be closed.

SpiderD555 commented 3 months ago

Just wanted to say thank you to all participants in this thread. I wanted to make the flow that @shotor describes 2 comments above, but his work didn't work exactly as I wanted, so with a bit of tinkering I did this: image It works wonders (if the user you supply exists), with a bit of tinkering I should be able to make it not error with unknown users.

shotor commented 3 months ago

Just wanted to say thank you to all participants in this thread. I wanted to make the flow that @shotor describes 2 comments above, but his work didn't work exactly as I wanted, so with a bit of tinkering I did this: ... It works wonders (if the user you supply exists), with a bit of tinkering I should be able to make it not error with unknown users.

@SpiderD555

Maybe that error is good. Otherwise you'd have to validate the user actually exists which means someone can just try different users to see which one exists.

If you want to validate the user there's a flag on default-authentication-identification. That might solve the error with unknown user but I don't know if that's what you want.

Pretend user exists
When enabled, the stage will always accept the given user identifier and continue.

Would you mind sharing your stage bindings with me? So I can more easily reverse engineer it.

SpiderD555 commented 3 months ago

@shotor

Maybe that error is good. Otherwise you'd have to validate the user actually exists which means someone can just try different users to see which one exists.

I wonder about that. Once you make a typo in the username, then you have to go through a bother of clearing cookies in the browser to fix it. Moreover each such situation generates an error notification in admin interface. I have just managed to make it work without error, see details below.

Would you mind sharing your stage bindings with me? So I can more easily reverse engineer it.

Here is the generic configuration: image

As for policy configurations ... "pending user supports webauthn" has this statement:

if ak_user_by(username=request.context['pending_user']) != None:
  if ak_user_has_authenticator(request.context['pending_user'], device_type = "webauthn"):
    return True # Bypass password auth
  else:
    return False # Proceed to password auth
else:
  return False # Proceed to password auth

"pending user does not support webauthn" has this statement:

if ak_user_by(username=request.context['pending_user']) != None:
  if not ak_user_has_authenticator(request.context['pending_user'], device_type = "webauthn"):
    return True # Proceed to password auth
  else:
    return False # Bypass password auth
else:
  return True # Proceed to password auth

Notice how both policies are very similar, second one has a reverse logic, so it only sends you to password flow, when webauthn is not configured for the user (or when user does not exists).

This is my webauthn-validator stage (it's mostly a copy paste from default MFA validator): image

=======

Edit: Updated policies that work without errors.

shotor commented 3 months ago

Thank you @SpiderD555 That's amazing!!

I think we should update the docs. The configuration in the docs section doesn't work at all. @dugite-code what do you think of the solution proposed by @SpiderD555 ?

dugite-code commented 3 months ago

It looks like what I had in mind with what we were talking about above, makes sense there would be an issue when the user didn't exist. Nicely fixed @SpiderD555, when you have time maybe you could open a pull request against the docs and see if the authentik team would approve it? it's a simple static site generator so it isn't too difficult.

SpiderD555 commented 3 months ago

@shotor @dugite-code Can you check if the exported flow loads for you ? I am just a bit worried that I may be using non-default flow elements, and as a result it won't work if it is loaded into non-default Authentik instance. If that's the case it wouldn't make sense to post it as an example in docs.

Just copy the script into "flows-login-webauthn-password.yaml" file and try to import it

context: {}
entries:
- attrs:
    authentication: none
    denied_action: message_continue
    designation: authentication
    layout: stacked
    name: webauthn-password-flow
    policy_engine_mode: any
    title: webauthn-password-flow
  conditions: []
  id: null
  identifiers:
    pk: d732b59f-14ab-4ef1-a5e7-983d2b7e40fb
    slug: webauthn-password-flow
  model: authentik_flows.flow
  state: present
- attrs:
    expression: "if ak_user_by(username=request.context['pending_user']) != None:\n\
      \  if not ak_user_has_authenticator(request.context['pending_user'], device_type\
      \ = \"webauthn\"):\n    return True # Proceed to password auth\n  else:\n  \
      \  return False # Bypass password auth\nelse:\n  return True # Proceed to password\
      \ auth"
    name: pending user does not support webauthn
  conditions: []
  id: null
  identifiers:
    pk: 831a2eed-3d25-48ff-9bd7-596494ab83a9
  model: authentik_policies_expression.expressionpolicy
  state: present
- attrs:
    expression: "if ak_user_by(username=request.context['pending_user']) != None:\n\
      \  if ak_user_has_authenticator(request.context['pending_user'], device_type\
      \ = \"webauthn\"):\n    return True # Bypass password auth\n  else:\n    return\
      \ False # Proceed to password auth\nelse:\n  return False # Proceed to password\
      \ auth"
    name: pending user supports webauthn
  conditions: []
  id: null
  identifiers:
    pk: 0f358213-f839-4fd7-99e8-7a777341c66f
  model: authentik_policies_expression.expressionpolicy
  state: present
- attrs:
    geoip_binding: no_binding
    network_binding: no_binding
    remember_me_offset: seconds=0
    session_duration: seconds=0
  conditions: []
  id: null
  identifiers:
    name: default-authentication-login
    pk: c15babcd-bbf6-470d-80a0-ebc92ecaec6f
  model: authentik_stages_user_login.userloginstage
  state: present
- attrs:
    backends:
    - authentik.core.auth.InbuiltBackend
    - authentik.sources.ldap.auth.LDAPBackend
    - authentik.core.auth.TokenBackend
    configure_flow: 63204909-a37d-4dc1-ab3d-92fd141d6569
    failed_attempts_before_cancel: 5
  conditions: []
  id: null
  identifiers:
    name: default-authentication-password
    pk: 3ce948fd-8741-4205-b5d0-5e72a9600de7
  model: authentik_stages_password.passwordstage
  state: present
- attrs:
    case_insensitive_matching: true
    pretend_user_exists: true
    show_matched_user: true
    user_fields:
    - username
    - email
  conditions: []
  id: null
  identifiers:
    name: default-authentication-identification
    pk: 0f9390c6-c469-4a73-b419-dfc3e617e741
  model: authentik_stages_identification.identificationstage
  state: present
- attrs:
    configuration_stages:
    - 4561d2b1-1699-4977-8367-944437e84bd3
    device_classes:
    - webauthn
    last_auth_threshold: seconds=0
    not_configured_action: deny
    webauthn_user_verification: required
  conditions: []
  id: null
  identifiers:
    name: webauthn-validation
    pk: 96aff095-0b17-44c9-ad95-e035d4e5cd7f
  model: authentik_stages_authenticator_validate.authenticatorvalidatestage
  state: present
- attrs:
    invalid_response_action: retry
    policy_engine_mode: any
    re_evaluate_policies: true
  conditions: []
  id: null
  identifiers:
    order: 10
    pk: 26096990-bec2-4349-97ab-6d2267dd573c
    stage: 0f9390c6-c469-4a73-b419-dfc3e617e741
    target: d732b59f-14ab-4ef1-a5e7-983d2b7e40fb
  model: authentik_flows.flowstagebinding
  state: present
- attrs:
    invalid_response_action: retry
    policy_engine_mode: any
    re_evaluate_policies: true
  conditions: []
  id: null
  identifiers:
    order: 20
    pk: 07f1e191-21c8-4e83-8342-1769a10fb395
    stage: 96aff095-0b17-44c9-ad95-e035d4e5cd7f
    target: d732b59f-14ab-4ef1-a5e7-983d2b7e40fb
  model: authentik_flows.flowstagebinding
  state: present
- attrs:
    invalid_response_action: retry
    policy_engine_mode: any
    re_evaluate_policies: true
  conditions: []
  id: null
  identifiers:
    order: 30
    pk: a3ab64e4-865c-4d58-b35b-552284e26b38
    stage: 3ce948fd-8741-4205-b5d0-5e72a9600de7
    target: d732b59f-14ab-4ef1-a5e7-983d2b7e40fb
  model: authentik_flows.flowstagebinding
  state: present
- attrs:
    invalid_response_action: retry
    policy_engine_mode: any
    re_evaluate_policies: true
  conditions: []
  id: null
  identifiers:
    order: 40
    pk: fb4b2e1a-97ca-45c4-b2f3-1938f39f124a
    stage: c15babcd-bbf6-470d-80a0-ebc92ecaec6f
    target: d732b59f-14ab-4ef1-a5e7-983d2b7e40fb
  model: authentik_flows.flowstagebinding
  state: present
- attrs:
    enabled: true
    timeout: 30
  conditions: []
  id: null
  identifiers:
    order: 0
    pk: 61a6d543-2b83-4fa6-8f6a-9d7918d017e0
    policy: 0f358213-f839-4fd7-99e8-7a777341c66f
    target: 07f1e191-21c8-4e83-8342-1769a10fb395
  model: authentik_policies.policybinding
  state: present
- attrs:
    enabled: true
    timeout: 30
  conditions: []
  id: null
  identifiers:
    order: 0
    pk: 768d6097-41c9-427e-a24b-1e204a03e935
    policy: 831a2eed-3d25-48ff-9bd7-596494ab83a9
    target: a3ab64e4-865c-4d58-b35b-552284e26b38
  model: authentik_policies.policybinding
  state: present
metadata:
  labels:
    blueprints.goauthentik.io/generated: 'true'
  name: authentik Export - 2024-04-08 10:25:41.303872+00:00
version: 1
BeastleeUK commented 1 month ago

@shotor @dugite-code Can you check if the exported flow loads for you ? I am just a bit worried that I may be using non-default flow elements, and as a result it won't work if it is loaded into non-default Authentik instance. If that's the case it wouldn't make sense to post it as an example in docs.

Just copy the script into "flows-login-webauthn-password.yaml" file and try to import it

context: {}
entries:
- attrs:
    authentication: none
    denied_action: message_continue
    designation: authentication
    layout: stacked
    name: webauthn-password-flow
    policy_engine_mode: any
    title: webauthn-password-flow
  conditions: []
  id: null
  identifiers:
    pk: d732b59f-14ab-4ef1-a5e7-983d2b7e40fb
    slug: webauthn-password-flow
  model: authentik_flows.flow
  state: present
- attrs:
    expression: "if ak_user_by(username=request.context['pending_user']) != None:\n\
      \  if not ak_user_has_authenticator(request.context['pending_user'], device_type\
      \ = \"webauthn\"):\n    return True # Proceed to password auth\n  else:\n  \
      \  return False # Bypass password auth\nelse:\n  return True # Proceed to password\
      \ auth"
    name: pending user does not support webauthn
  conditions: []
  id: null
  identifiers:
    pk: 831a2eed-3d25-48ff-9bd7-596494ab83a9
  model: authentik_policies_expression.expressionpolicy
  state: present
- attrs:
    expression: "if ak_user_by(username=request.context['pending_user']) != None:\n\
      \  if ak_user_has_authenticator(request.context['pending_user'], device_type\
      \ = \"webauthn\"):\n    return True # Bypass password auth\n  else:\n    return\
      \ False # Proceed to password auth\nelse:\n  return False # Proceed to password\
      \ auth"
    name: pending user supports webauthn
  conditions: []
  id: null
  identifiers:
    pk: 0f358213-f839-4fd7-99e8-7a777341c66f
  model: authentik_policies_expression.expressionpolicy
  state: present
- attrs:
    geoip_binding: no_binding
    network_binding: no_binding
    remember_me_offset: seconds=0
    session_duration: seconds=0
  conditions: []
  id: null
  identifiers:
    name: default-authentication-login
    pk: c15babcd-bbf6-470d-80a0-ebc92ecaec6f
  model: authentik_stages_user_login.userloginstage
  state: present
- attrs:
    backends:
    - authentik.core.auth.InbuiltBackend
    - authentik.sources.ldap.auth.LDAPBackend
    - authentik.core.auth.TokenBackend
    configure_flow: 63204909-a37d-4dc1-ab3d-92fd141d6569
    failed_attempts_before_cancel: 5
  conditions: []
  id: null
  identifiers:
    name: default-authentication-password
    pk: 3ce948fd-8741-4205-b5d0-5e72a9600de7
  model: authentik_stages_password.passwordstage
  state: present
- attrs:
    case_insensitive_matching: true
    pretend_user_exists: true
    show_matched_user: true
    user_fields:
    - username
    - email
  conditions: []
  id: null
  identifiers:
    name: default-authentication-identification
    pk: 0f9390c6-c469-4a73-b419-dfc3e617e741
  model: authentik_stages_identification.identificationstage
  state: present
- attrs:
    configuration_stages:
    - 4561d2b1-1699-4977-8367-944437e84bd3
    device_classes:
    - webauthn
    last_auth_threshold: seconds=0
    not_configured_action: deny
    webauthn_user_verification: required
  conditions: []
  id: null
  identifiers:
    name: webauthn-validation
    pk: 96aff095-0b17-44c9-ad95-e035d4e5cd7f
  model: authentik_stages_authenticator_validate.authenticatorvalidatestage
  state: present
- attrs:
    invalid_response_action: retry
    policy_engine_mode: any
    re_evaluate_policies: true
  conditions: []
  id: null
  identifiers:
    order: 10
    pk: 26096990-bec2-4349-97ab-6d2267dd573c
    stage: 0f9390c6-c469-4a73-b419-dfc3e617e741
    target: d732b59f-14ab-4ef1-a5e7-983d2b7e40fb
  model: authentik_flows.flowstagebinding
  state: present
- attrs:
    invalid_response_action: retry
    policy_engine_mode: any
    re_evaluate_policies: true
  conditions: []
  id: null
  identifiers:
    order: 20
    pk: 07f1e191-21c8-4e83-8342-1769a10fb395
    stage: 96aff095-0b17-44c9-ad95-e035d4e5cd7f
    target: d732b59f-14ab-4ef1-a5e7-983d2b7e40fb
  model: authentik_flows.flowstagebinding
  state: present
- attrs:
    invalid_response_action: retry
    policy_engine_mode: any
    re_evaluate_policies: true
  conditions: []
  id: null
  identifiers:
    order: 30
    pk: a3ab64e4-865c-4d58-b35b-552284e26b38
    stage: 3ce948fd-8741-4205-b5d0-5e72a9600de7
    target: d732b59f-14ab-4ef1-a5e7-983d2b7e40fb
  model: authentik_flows.flowstagebinding
  state: present
- attrs:
    invalid_response_action: retry
    policy_engine_mode: any
    re_evaluate_policies: true
  conditions: []
  id: null
  identifiers:
    order: 40
    pk: fb4b2e1a-97ca-45c4-b2f3-1938f39f124a
    stage: c15babcd-bbf6-470d-80a0-ebc92ecaec6f
    target: d732b59f-14ab-4ef1-a5e7-983d2b7e40fb
  model: authentik_flows.flowstagebinding
  state: present
- attrs:
    enabled: true
    timeout: 30
  conditions: []
  id: null
  identifiers:
    order: 0
    pk: 61a6d543-2b83-4fa6-8f6a-9d7918d017e0
    policy: 0f358213-f839-4fd7-99e8-7a777341c66f
    target: 07f1e191-21c8-4e83-8342-1769a10fb395
  model: authentik_policies.policybinding
  state: present
- attrs:
    enabled: true
    timeout: 30
  conditions: []
  id: null
  identifiers:
    order: 0
    pk: 768d6097-41c9-427e-a24b-1e204a03e935
    policy: 831a2eed-3d25-48ff-9bd7-596494ab83a9
    target: a3ab64e4-865c-4d58-b35b-552284e26b38
  model: authentik_policies.policybinding
  state: present
metadata:
  labels:
    blueprints.goauthentik.io/generated: 'true'
  name: authentik Export - 2024-04-08 10:25:41.303872+00:00
version: 1

@SpiderD555 I just tried to import this into my Authentik service, which only has one additional flow added for recovery email. Unfortunately it fails at the authentik.blueprints.v1.importer stage with an object does not exist error at the backends section.

Entry invalid: Serializer errors {'configure_flow': [ErrorDetail(string='Invalid pk "63204909-a37d-4dc1-ab3d-92fd141d6569" - object does not exist.', code='does_not_exist')]}

I was able to follow your earlier post's details though and get this set up and working within minutes, thanks. When exporting mine the pk's are different, that's the only thing. I'm new to Authentik so I'm not sure if there's anything that can be done to avoid the conflict in pks.

BeastleeUK commented 1 month ago

@SpiderD555 one issue I have found is that there's no way to log in with a password if you have an issue with the existing passkey for an account. In my case I set one up using Bitwarden on PC via the extension but on my Samsung phone I am prompted to use the passkey (that I don't have) on my device. There's no method for me to add an additional passkey on the device at this point as I can't get to the password entry. I suspect that adding 'Use a password instead' as a link won't work unless there's a way to pass a parameter to override the 'pending user supports webauthn' to it too.

EDIT: Actually this is an issue with the default implementation of webauthn. I cannot log into authentik with my phone if the first passkey I created was in Bitwarden on my PC. Bitwarden is bringing passkeys to Andorid soon so I'll figure out a workaround for now.