Repository: authelia/authelia
Pull request #9278: fix(webauthn): automatically upgrade passkeys
Author: login: james-d-elliott
Created: 2025-04-18T00:18:22Z | Merged: 2025-04-18T00:47:16Z

This is a workaround for non-compliant authenticators or browsers which do not properly communicate their resident key / discoverability / passkey state using the credential properties extension. This adjustment if enabled will allow the automatic upgrade of these WebAuthn Credentials which are in an invalid state.

Fixes #9094

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Introduced an experimental option to automatically upgrade WebAuthn credentials to Passkeys when not properly reported by browsers or authenticators.

- **Bug Fixes**
  - Improved validation to ensure experimental Passkey features are only enabled when Passkey login is active.

- **Documentation**
  - Updated WebAuthn configuration documentation to describe the new experimental Passkey upgrade option and improved consistency in configuration examples.
  - Adjusted date metadata in several CLI reference documentation files.

- **Tests**
  - Added tests to verify correct behavior and validation of Passkey-related configuration options.
  - Added tests covering WebAuthn credential loading and upgrade scenarios.

- **Chores**
  - Updated internal references and test mocks for loading WebAuthn credentials to use a consistent method.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Linked issue #9094: Passkey login error - this key does not look familiar
### Version


v4.39.1

### Deployment Method


Docker

### Reverse Proxy


Caddy

### Reverse Proxy Version


_No response_

### Description


I have 2 servers with 2 authelia instances having the same problem.

My yubikeys (I have 2) can be added and I can use them for 2fa login by entering my user and pass first, but when I try to use the `Sign in with a passkey` option, the keys are not recognized. I enter my pin, the key is blinking but the error pops:
`The security key doesn't look familiar. Please try a different one.`
There is no next step for entering the password (for 2fa).

I tried 2 different webauthn configs, none works:

```
webauthn:
  enable_passkey_login: true
  attestation_conveyance_preference: direct
  filtering:
    prohibit_backup_eligibility: true
  metadata:
    enabled: true
    validate_trust_anchor: true
    validate_entry: true
    validate_status: true
    validate_entry_permit_zero_aaguid: false
```

and

```
webauthn:
  disable: false
  enable_passkey_login: true
  display_name: 'Authelia'
  attestation_conveyance_preference: 'indirect'
  timeout: '60 seconds'
  filtering:
    permitted_aaguids: []
    prohibited_aaguids: []
    prohibit_backup_eligibility: false
  selection_criteria:
    attachment: ''
    discoverability: 'preferred'
    user_verification: 'preferred'
  metadata:
    enabled: false
    validate_trust_anchor: true
    validate_entry: true
    validate_entry_permit_zero_aaguid: false
    validate_status: true
    validate_status_permitted: []
    validate_status_prohibited:
      - 'REVOKED'
      - 'USER_KEY_PHYSICAL_COMPROMISE'
      - 'USER_KEY_REMOTE_COMPROMISE'
      - 'USER_VERIFICATION_BYPASS'
      - 'ATTESTATION_KEY_COMPROMISE'
```

### Reproduction


go to a protected service or to the auth.domain.com page and try to 
Sign in with a passkey
- I am promped for the pin
- I press the key 
- I am presented with the above error (key not familiar)

### Expectations


To be signed directly in (in case of a 1st factor) or to be presented the next screen to fill in my pass (for 2fa).

### Configuration (Authelia)


```yaml
theme: auto
default_2fa_method: totp
server:
  address: tcp://0.0.0.0:9091/
  endpoints:
    authz:
      forward-auth:
        implementation: 'ForwardAuth'
log:
  level: debug
  file_path: /config/log/authelia.log
  keep_stdout: true
totp:
  disable: false
  algorithm: sha512
  digits: 8
  period: 30
  skew: 1
  secret_size: 40
webauthn:
  enable_passkey_login: true
  attestation_conveyance_preference: direct
  filtering:
    prohibit_backup_eligibility: true
  metadata:
    enabled: true
    validate_trust_anchor: true
    validate_entry: true
    validate_status: true
    validate_entry_permit_zero_aaguid: false
authentication_backend:
  refresh_interval: 5m
  password_reset:
    disable: false
  file:
    path: /config/users_database.yml
    watch: false
    search:
      email: false
      case_insensitive: false
    password:
      algorithm: argon2
      argon2:
        variant: argon2id
        iterations: 3
        memory: 65536
        parallelism: 4
        key_length: 32
        salt_length: 16
password_policy:
  standard:
    enabled: false
    min_length: 8
    max_length: 0
    require_uppercase: true
    require_lowercase: true
    require_number: true
    require_special: true
access_control:
  default_policy: two_factor
session:
  name: authelia_session
  same_site: lax
  inactivity: 5m
  expiration: 1h
  remember_me: 1M
  cookies:
    - domain: {{ env "DOMAIN_A" }}
      authelia_url: https://auth.{{ env "DOMAIN_A" }}
      default_redirection_url: https://{{ env "DOMAIN_A" }}
  redis:
    host: authelia_redis
    port: 6379
regulation:
  max_retries: 3
  find_time: 2m
  ban_time: 30m
storage:
  local:
    path: /config/db.sqlite3
notifier:
  disable_startup_check: false
  template_path: /config/email_templates
  
identity_providers:
  oidc:
    jwks:
      - key: {{ secret "/config/jwks/rsa.2048.pem" | mindent 10 "|" | msquote }}
    lifespans:
      access_token: 1h
      authorize_code: 1m
      id_token: 1h
      refresh_token: 90m
    enable_client_debug_messages: false
    enforce_pkce: public_clients_only
    cors:
      endpoints:
        - authorization
        #- pushed-authorization-request
        - token
        - revocation
        - introspection
        - userinfo
      allowed_origins: "*"
      allowed_origins_from_client_redirect_uris: false
    clients:
      - client_id: portainer
        client_name: Portainer #generate secret: docker run authelia/authelia:latest authelia crypto hash generate pbkdf2 --variant sha512 --random --random.length 72
        client_secret: '$pbkdf2-sha512$310000$secret'
        public: false
        authorization_policy: two_factor
        redirect_uris:
          - https://portainer.domain.com
        scopes:
          - openid
          - groups
          - email
          - profile
        consent_mode: implicit
        userinfo_signed_response_alg: none 
        
      - client_id: grafana
        client_name: Grafana
        client_secret: '$pbkdf2-sha512$310000$secret'
        public: false
        authorization_policy: two_factor
        redirect_uris:
          - https://graph.domain.com/login/generic_oauth
        scopes:
          - openid
          - profile
          - groups
          - email
        consent_mode: implicit
        userinfo_signed_response_alg: none
     
      - client_id: miniflux
        client_name: Miniflux RSS Reader
        client_secret: '$pbkdf2-sha512$310000$secret'
        public: false
        authorization_policy: two_factor
        redirect_uris:
          - https://flux.domain.com/oauth2/oidc/callback
        scopes:
          - openid
          - profile
          - groups
          - email
        consent_mode: implicit
        userinfo_signed_response_alg: none
```

### Build Information


```shell
~/shared/pidocker » docker run authelia/authelia:latest authelia build-info                                                                                         ciss@rpi
Last Tag: v4.39.1
State: tagged clean
Branch: v4.39.1
Commit: 9a22f5c30b216b6d1aa1f80b6a36fac62e9f7727
Build Number: 41283
Build OS: linux
Build Arch: arm64
Build Compiler: gc
Build Date: Tue, 18 Mar 2025 14:49:44 +1100
Extra: 

Go:
    Version: go1.24.1 X:nosynchashtriemap
    Module Path: github.com/authelia/authelia/v4
    Executable Path: github.com/authelia/authelia/v4/cmd/authelia
```

### Logs (Authelia)


```shell
time=
2025-03-25T09:51:02+02:00
level=
debug
msg=
Loaded Configuration Sources
files=
[/config/configuration.yml]
filters=
[template]
time=
2025-03-25T09:51:02+02:00
level=
debug
msg=
Logging Initialized
fields.level=
debug
file=
/config/log/authelia.log
format=
keep_stdout=
true
time=
2025-03-25T09:51:02+02:00
level=
debug
msg=
Process user information
gid=
1000
uid=
1000
time=
2025-03-25T09:51:02+02:00
level=
warning
msg=
Configuration: access_control: no rules have been specified so the 'default_policy' of 'two_factor' is going to be applied to all requests
time=
2025-03-25T09:51:02+02:00
level=
info
msg=
Authelia v4.39.1 is starting
time=
2025-03-25T09:51:02+02:00
level=
info
msg=
Log severity set to debug
time=
2025-03-25T09:51:03+02:00
level=
debug
msg=
Registering OpenID Connect 1.0 client with client id 'portainer' and policy 'two_factor'
time=
2025-03-25T09:51:03+02:00
level=
debug
msg=
Registering OpenID Connect 1.0 client with client id 'grafana' and policy 'two_factor'
time=
2025-03-25T09:51:03+02:00
level=
debug
msg=
Registering OpenID Connect 1.0 client with client id 'miniflux' and policy 'two_factor'
time=
2025-03-25T09:51:03+02:00
level=
info
msg=
Storage schema is being checked for updates
time=
2025-03-25T09:51:03+02:00
level=
info
msg=
Storage schema is already up to date
time=
2025-03-25T09:51:05+02:00
level=
info
msg=
Startup complete
time=
2025-03-25T09:51:05+02:00
level=
info
msg=
Listening for non-TLS connections on '[::]:9091' path '/'
server=
main
service=
server
time=
2025-03-25T09:51:28+02:00
level=
debug
msg=
Mark 1FA authentication attempt made by user 'godmode'
method=
POST
path=
/api/firstfactor
remote_ip=
192.168.1.153
time=
2025-03-25T09:51:28+02:00
level=
debug
msg=
Successful 1FA authentication attempt made by user 'godmode'
method=
POST
path=
/api/firstfactor
remote_ip=
192.168.1.153
time=
2025-03-25T09:51:34+02:00
level=
debug
msg=
Mark TOTP authentication attempt made by user 'godmode'
method=
POST
path=
/api/secondfactor/totp
remote_ip=
192.168.1.153
time=
2025-03-25T09:51:34+02:00
level=
debug
msg=
Successful TOTP authentication attempt made by user 'godmode'
method=
POST
path=
/api/secondfactor/totp
remote_ip=
192.168.1.153
time=
2025-03-25T09:51:47+02:00
level=
debug
msg=
Sending an email to user to confirm identity for session elevation
id=
fe66aa77-34de-4d08-9a03-456255311f71
method=
POST
path=
/api/user/session/elevation
remote_ip=
192.168.1.153
signature=
0c71ebbae063b72c94e2bbeeb133850a121e0f00e97cf2bb60113ecb942258a1352f1f0f49fa8d0ac6a5884d703918b110c0f352fe59d7a2cbe5b8eaa97b0b09
username=
godmode
time=
2025-03-25T09:51:59+02:00
level=
debug
msg=
Getting user details for notification
method=
DELETE
path=
/api/secondfactor/webauthn/credential/1
remote_ip=
192.168.1.153
time=
2025-03-25T09:51:59+02:00
level=
debug
msg=
Getting user addresses for notification
method=
DELETE
path=
/api/secondfactor/webauthn/credential/1
remote_ip=
192.168.1.153
time=
2025-03-25T09:51:59+02:00
level=
debug
msg=
Sending an email to user godmode (\"status quo\" <hello@irvi.net>) to inform them of an important event.
method=
DELETE
path=
/api/secondfactor/webauthn/credential/1
remote_ip=
192.168.1.153
time=
2025-03-25T09:52:27+02:00
level=
debug
msg=
Getting user details for notification
method=
POST
path=
/api/secondfactor/webauthn/credential/register
remote_ip=
192.168.1.153
time=
2025-03-25T09:52:27+02:00
level=
debug
msg=
Getting user addresses for notification
method=
POST
path=
/api/secondfactor/webauthn/credential/register
remote_ip=
192.168.1.153
time=
2025-03-25T09:52:27+02:00
level=
debug
msg=
Sending an email to user godmode (\"status quo\" <hello@irvi.net>) to inform them of an important event.
method=
POST
path=
/api/secondfactor/webauthn/credential/register
remote_ip=
192.168.1.153
time=
2025-03-25T09:53:39+02:00
level=
debug
msg=
Mark 1FA authentication attempt made by user 'godmode'
method=
POST
path=
/api/firstfactor
remote_ip=
192.168.1.153
time=
2025-03-25T09:53:39+02:00
level=
debug
msg=
Successful 1FA authentication attempt made by user 'godmode'
method=
POST
path=
/api/firstfactor
remote_ip=
192.168.1.153
time=
2025-03-25T09:53:49+02:00
level=
debug
msg=
Mark WebAuthn authentication attempt made by user 'godmode'
method=
POST
path=
/api/secondfactor/webauthn
remote_ip=
192.168.1.153
time=
2025-03-25T09:53:49+02:00
level=
debug
msg=
Successful WebAuthn authentication attempt made by user 'godmode'
method=
POST
path=
/api/secondfactor/webauthn
remote_ip=
192.168.1.153
```

### Logs (Proxy / Application)


```shell

```

### Documentation


_No response_

### Generative AI


No

### Pre-Submission Checklist


- [x] I agree to follow the [Code of Conduct](http://www.authelia.com/code-of-conduct)

- [x] This is a bug report and not a support request

- [x] I have read the security policy and this bug report is not a security issue or security related issue

- [x] I have either included the complete configuration file or I am sure it's unrelated to the configuration

- [x] I have either included the complete debug / trace logs or the output of the build-info command if the logs are not relevant

- [x] I have provided all of the required information in full with the only alteration being reasonable sanitization in accordance with the [Troubleshooting Sanitization](https://www.authelia.com/r/sanitize) reference guide

- [x] I have checked for related proxy or application logs and included them if available

- [x] I have checked for related issues and checked the documentation

Linked issue comments: user: login: james-d-elliott
created_at: 2025-03-25T08:24:54Z
body: What value is the discoverable attribute in the information panel? How did you register them? What browser? What OS?
author_association: MEMBER
user: login: C8opmBM
created_at: 2025-03-25T09:28:27Z
body: Hey James,
Indeed the value is NOT discoverable.
I tried to pass the `discoverability: 'preferred' for selection_criteria`, but it has no effect, it remains not discoverable.

```
Description: yubikey2
Relying Party ID: auth.xxxx.net
Authenticator GUID: xxxxx-8113-47ea-b116-xxxx
Attestation Type: packed
Attachment: Cross-Platform
Discoverable: No
User Verified: Yes
Backup State: Not Eligible
Transports: NFC, USB
Clone Warning: No
Usage Count: 26
Added: March 21, 2025 at 2:04 PM
Last Used: March 21, 2025 at 2:32 PM
```

I added the keys via Brave browser in my pc with Windows10.
The 2 servers are running debian (one raspberrrypi, one normal cpu).

![Image](https://github.com/user-attachments/assets/75267d6d-47c0-47b9-befc-98c004658c8c)
author_association: NONE
user: login: james-d-elliott
created_at: 2025-03-25T09:41:01Z
body: The authenticator is not affirmatively reporting that it is a passkey when queried for the credential properties, thus should be assumed to be **_not discoverable_** according to the webauthn spec, i.e. the single most important characteristic of passkey.
author_association: MEMBER
user: login: C8opmBM
created_at: 2025-03-25T09:42:58Z
body: Therefore, is there something to be done at my end to fix this?
Thank you!
author_association: NONE
user: login: james-d-elliott
created_at: 2025-03-25T09:50:16Z
body: So realistically there may be nothing you can do directly. You can try setting the discoverability requirement to `required` or you can try another browser potentially. There effectively are only two reasonable solutions to this, either the browsers which are not reporting the credential properties need to remedy this,or we need to implement a feature in the form of a flow where users are given an opportunity to "upgrade" the credential to a passkey.

Essentially it boils down to ensuring only legitimate passkeys can be used as passkeys.
author_association: MEMBER
user: login: james-d-elliott
created_at: 2025-03-25T10:00:45Z
body: To be clear, registering with a separate browser may allow the discoverable bit. One other thing to check is if the credentials even appear as passkeys on the authenticator via ykman.
author_association: MEMBER
user: login: C8opmBM
created_at: 2025-03-25T10:49:57Z
body: Thanks, will follow your advice.
author_association: NONE
user: login: C8opmBM
created_at: 2025-03-25T11:02:55Z
body: For the time being I will disable passkeys as it seems neither another browser or the `required `flag can fix this.
`enable_passkey_login: false`

I will keep an eye on further related issues, it's not something I would definitely use, but it seems nice to have.
Thanks for developing this awesome authelia thingie and have a great day mr James!
author_association: NONE
user: login: vayakovlev
created_at: 2025-04-12T21:21:07Z
body: I have the similar problem, but I my case yubico key was rejected by windows hello with message: This key cannot be used.

Key was registered on this system and browser, two factor auth is working fine. So maybe my case is little bit different
author_association: NONE
user: login: james-d-elliott
created_at: 2025-04-18T00:30:04Z
body: We're going to implement the above/below workaround for these non-compliant implementations. We will revisit it in the future to determine the correct way to implement it.
author_association: MEMBER

Diff:
diff --git a/docs/content/configuration/second-factor/webauthn.md b/docs/content/configuration/second-factor/webauthn.md
index ef31e21cfc52f..4a75d8185c41b 100644
--- a/docs/content/configuration/second-factor/webauthn.md
+++ b/docs/content/configuration/second-factor/webauthn.md
@@ -69,6 +69,8 @@ be prompted for their password by default if the request requires multi-factor a
 
 ### experimental_enable_passkey_uv_two_factors
 
+{{< confkey type="boolean" default="false" required="no" >}}
+
 {{< callout context="danger" title="Stability and Security Notice" icon="outline/alert-octagon" >}}
 This option is not considered supported. It is completely experimental and will be replaced by
 [custom policies](../../roadmap/active/granular-authorization.md#custom-policy-flows) that can be defined in the access
@@ -81,6 +83,22 @@ time we add the flow to replace this) that this option will cause a startup fail
 This option allows for authenticators that enforce user verification (PIN entry, biometric proof, etc) and reports they
 have performed user verification, to satisfy the `two_factor` policy for access control rules.
 
+### experimental_enable_passkey_upgrade
+
+{{< confkey type="boolean" default="false" required="no" >}}
+
+{{< callout context="danger" title="Stability Notice" icon="outline/alert-octagon" >}}
+This option is not considered supported. It is completely experimental and will be removed in the future. It will either
+become a deliberate flow within the UI, be automatic, or be entirely removed.
+{{< /callout >}}
+
+Some authenticators or browsers have issues with reporting if a WebAuthn credential is actually a Passkey or not. The
+relevant specifications instruct implementers to assume they are not a Passkey in this instance. However this presents
+some user frustration. This option attempts to alleviate some of that frustration.
+
+When true this will attempt to automatically upgrade a WebAuthn credential to a Passkey when the browser or
+authenticator does not report a newly registered WebAuthn credential to be a Passkey.
+
 ### display_name
 
 {{< confkey type="string" default="Authelia" required="no" >}}
diff --git a/docs/content/reference/cli/authelia-gen/authelia-gen_misc.md b/docs/content/reference/cli/authelia-gen/authelia-gen_misc.md
index e851523729378..3cb58866c80fd 100644
--- a/docs/content/reference/cli/authelia-gen/authelia-gen_misc.md
+++ b/docs/content/reference/cli/authelia-gen/authelia-gen_misc.md
@@ -2,7 +2,7 @@
 title: "authelia-gen misc"
 description: "Reference for the authelia-gen misc command."
 lead: ""
-date: 2025-04-17T21:31:57+10:00
+date: 2022-09-16T14:21:05+10:00
 draft: false
 images: []
 weight: 915
diff --git a/docs/content/reference/cli/authelia-gen/authelia-gen_misc_oidc.md b/docs/content/reference/cli/authelia-gen/authelia-gen_misc_oidc.md
index 422b6057ed849..07960dbb3042b 100644
--- a/docs/content/reference/cli/authelia-gen/authelia-gen_misc_oidc.md
+++ b/docs/content/reference/cli/authelia-gen/authelia-gen_misc_oidc.md
@@ -2,7 +2,7 @@
 title: "authelia-gen misc oidc"
 description: "Reference for the authelia-gen misc oidc command."
 lead: ""
-date: 2025-04-17T21:31:57+10:00
+date: 2022-09-16T14:21:05+10:00
 draft: false
 images: []
 weight: 915
diff --git a/docs/content/reference/cli/authelia-gen/authelia-gen_misc_oidc_conformance.md b/docs/content/reference/cli/authelia-gen/authelia-gen_misc_oidc_conformance.md
index d429ddc8c63cc..c2b491ccb5dfb 100644
--- a/docs/content/reference/cli/authelia-gen/authelia-gen_misc_oidc_conformance.md
+++ b/docs/content/reference/cli/authelia-gen/authelia-gen_misc_oidc_conformance.md
@@ -2,7 +2,7 @@
 title: "authelia-gen misc oidc conformance"
 description: "Reference for the authelia-gen misc oidc conformance command."
 lead: ""
-date: 2025-04-17T21:31:57+10:00
+date: 2022-06-15T17:51:47+10:00
 draft: false
 images: []
 weight: 915
diff --git a/docs/data/configkeys.json b/docs/data/configkeys.json
index a996db014aa09..51f24524ddf08 100644
--- a/docs/data/configkeys.json
+++ b/docs/data/configkeys.json
@@ -1254,6 +1254,11 @@
         "secret": false,
         "env": "AUTHELIA_WEBAUTHN_ENABLE_PASSKEY_LOGIN"
     },
+    {
+        "path": "webauthn.experimental_enable_passkey_upgrade",
+        "secret": false,
+        "env": "AUTHELIA_WEBAUTHN_EXPERIMENTAL_ENABLE_PASSKEY_UPGRADE"
+    },
     {
         "path": "webauthn.experimental_enable_passkey_uv_two_factors",
         "secret": false,
diff --git a/docs/static/schemas/latest/json-schema/configuration.json b/docs/static/schemas/latest/json-schema/configuration.json
index bfbadf0a4ba82..408c785fa2444 100644
--- a/docs/static/schemas/latest/json-schema/configuration.json
+++ b/docs/static/schemas/latest/json-schema/configuration.json
@@ -4268,6 +4268,12 @@
           "description": "This option is experimental and WILL be removed. When true this will consider a Passkey login where the WebAuthn user verification requirement is met as being a single event 2FA login.",
           "default": false
         },
+        "experimental_enable_passkey_upgrade": {
+          "type": "boolean",
+          "title": "Experimental: Enable User Verified Passkey Second Factor",
+          "description": "When true this will attempt to automatically upgrade a WebAuthn credential to a Passkey when the browser or authenticator does not report a newly registered WebAuthn credential to be a Passkey.",
+          "default": false
+        },
         "display_name": {
           "type": "string",
           "title": "Display Name",
diff --git a/docs/static/schemas/v4.39/json-schema/configuration.json b/docs/static/schemas/v4.39/json-schema/configuration.json
index bfbadf0a4ba82..408c785fa2444 100644
--- a/docs/static/schemas/v4.39/json-schema/configuration.json
+++ b/docs/static/schemas/v4.39/json-schema/configuration.json
@@ -4268,6 +4268,12 @@
           "description": "This option is experimental and WILL be removed. When true this will consider a Passkey login where the WebAuthn user verification requirement is met as being a single event 2FA login.",
           "default": false
         },
+        "experimental_enable_passkey_upgrade": {
+          "type": "boolean",
+          "title": "Experimental: Enable User Verified Passkey Second Factor",
+          "description": "When true this will attempt to automatically upgrade a WebAuthn credential to a Passkey when the browser or authenticator does not report a newly registered WebAuthn credential to be a Passkey.",
+          "default": false
+        },
         "display_name": {
           "type": "string",
           "title": "Display Name",
diff --git a/internal/configuration/schema/keys.go b/internal/configuration/schema/keys.go
index a22b8ec16db7f..9c25fc8b2ad05 100644
--- a/internal/configuration/schema/keys.go
+++ b/internal/configuration/schema/keys.go
@@ -468,6 +468,7 @@ var Keys = []string{
 	"webauthn.disable",
 	"webauthn.display_name",
 	"webauthn.enable_passkey_login",
+	"webauthn.experimental_enable_passkey_upgrade",
 	"webauthn.experimental_enable_passkey_uv_two_factors",
 	"webauthn.filtering.permitted_aaguids",
 	"webauthn.filtering.prohibit_backup_eligibility",
diff --git a/internal/configuration/schema/webauthn.go b/internal/configuration/schema/webauthn.go
index 6e78c1a5a6399..41ba920d6edc6 100644
--- a/internal/configuration/schema/webauthn.go
+++ b/internal/configuration/schema/webauthn.go
@@ -10,10 +10,11 @@ import (
 
 // WebAuthn represents the webauthn config.
 type WebAuthn struct {
-	Disable            bool   `koanf:"disable" yaml:"disable" toml:"disable" json:"disable" jsonschema:"default=false,title=Disable" jsonschema_description:"Disables the WebAuthn 2FA functionality."`
-	EnablePasskeyLogin bool   `koanf:"enable_passkey_login" yaml:"enable_passkey_login" toml:"enable_passkey_login" json:"enable_passkey_login" jsonschema:"default=false,title=Enable Passkey Logins" jsonschema_description:"Allows users to sign in via Passkeys."`
-	EnablePasskey2FA   bool   `koanf:"experimental_enable_passkey_uv_two_factors" yaml:"experimental_enable_passkey_uv_two_factors" toml:"experimental_enable_passkey_uv_two_factors" json:"experimental_enable_passkey_uv_two_factors" jsonschema:"default=false,title=Experimental: Enable User Verified Passkey Second Factor" jsonschema_description:"This option is experimental and WILL be removed. When true this will consider a Passkey login where the WebAuthn user verification requirement is met as being a single event 2FA login."`
-	DisplayName        string `koanf:"display_name" yaml:"display_name,omitempty" toml:"display_name,omitempty" json:"display_name,omitempty" jsonschema:"default=Authelia,title=Display Name" jsonschema_description:"The display name attribute for the WebAuthn relying party."`
+	Disable              bool   `koanf:"disable" yaml:"disable" toml:"disable" json:"disable" jsonschema:"default=false,title=Disable" jsonschema_description:"Disables the WebAuthn 2FA functionality."`
+	EnablePasskeyLogin   bool   `koanf:"enable_passkey_login" yaml:"enable_passkey_login" toml:"enable_passkey_login" json:"enable_passkey_login" jsonschema:"default=false,title=Enable Passkey Logins" jsonschema_description:"Allows users to sign in via Passkeys."`
+	EnablePasskey2FA     bool   `koanf:"experimental_enable_passkey_uv_two_factors" yaml:"experimental_enable_passkey_uv_two_factors" toml:"experimental_enable_passkey_uv_two_factors" json:"experimental_enable_passkey_uv_two_factors" jsonschema:"default=false,title=Experimental: Enable User Verified Passkey Second Factor" jsonschema_description:"This option is experimental and WILL be removed. When true this will consider a Passkey login where the WebAuthn user verification requirement is met as being a single event 2FA login."`
+	EnablePasskeyUpgrade bool   `koanf:"experimental_enable_passkey_upgrade" yaml:"experimental_enable_passkey_upgrade" toml:"experimental_enable_passkey_upgrade" json:"experimental_enable_passkey_upgrade" jsonschema:"default=false,title=Experimental: Enable User Verified Passkey Second Factor" jsonschema_description:"When true this will attempt to automatically upgrade a WebAuthn credential to a Passkey when the browser or authenticator does not report a newly registered WebAuthn credential to be a Passkey."`
+	DisplayName          string `koanf:"display_name" yaml:"display_name,omitempty" toml:"display_name,omitempty" json:"display_name,omitempty" jsonschema:"default=Authelia,title=Display Name" jsonschema_description:"The display name attribute for the WebAuthn relying party."`
 
 	ConveyancePreference protocol.ConveyancePreference `koanf:"attestation_conveyance_preference" yaml:"attestation_conveyance_preference,omitempty" toml:"attestation_conveyance_preference,omitempty" json:"attestation_conveyance_preference,omitempty" jsonschema:"default=indirect,enum=none,enum=indirect,enum=direct,title=Conveyance Preference" jsonschema_description:"The default conveyance preference for all WebAuthn credentials."`
 
diff --git a/internal/configuration/validator/const.go b/internal/configuration/validator/const.go
index f65208f8c5845..d3a16ccb46e16 100644
--- a/internal/configuration/validator/const.go
+++ b/internal/configuration/validator/const.go
@@ -327,6 +327,7 @@ const (
 	errFmtWebAuthnSelectionCriteria      = "webauthn: selection_criteria: option '%s' must be one of %s but it's configured as '%s'"
 	errFmtWebAuthnPasskeyDiscoverability = "webauthn: selection_criteria: option 'discoverability' should generally be configured as '%s' or '%s' when passkey logins are enabled" //nolint:gosec
 	errFmtWebAuthnFiltering              = "webauthn: filtering: option 'permitted_aaguids' and 'prohibited_aaguids' are mutually exclusive however both have values"
+	errFmtWebAuthnBoolean                = "webauthn: option '%s' is %t but it must be %t when '%s' is %t"
 )
 
 // Access Control error constants.
diff --git a/internal/configuration/validator/webauthn.go b/internal/configuration/validator/webauthn.go
index 83cda82cdacbf..ce5a5c2196a16 100644
--- a/internal/configuration/validator/webauthn.go
+++ b/internal/configuration/validator/webauthn.go
@@ -10,7 +10,9 @@ import (
 	"github.com/authelia/authelia/v4/internal/utils"
 )
 
-// ValidateWebAuthn validates and update WebAuthn configuration.
+// ValidateWebAuthn validates and updates WebAuthn configuration.
+//
+//nolint:gocyclo
 func ValidateWebAuthn(config *schema.Configuration, validator *schema.StructValidator) {
 	if config.WebAuthn.DisplayName == "" {
 		config.WebAuthn.DisplayName = schema.DefaultWebAuthnConfiguration.DisplayName
@@ -20,6 +22,20 @@ func ValidateWebAuthn(config *schema.Configuration, validator *schema.StructVali
 		config.WebAuthn.Timeout = schema.DefaultWebAuthnConfiguration.Timeout
 	}
 
+	if config.WebAuthn.EnablePasskeyLogin {
+		if config.WebAuthn.Disable {
+			validator.Push(fmt.Errorf(errFmtWebAuthnBoolean, "enable_passkey_login", config.WebAuthn.EnablePasskeyLogin, false, "disable", config.WebAuthn.Disable))
+		}
+	} else {
+		if config.WebAuthn.EnablePasskey2FA {
+			validator.Push(fmt.Errorf(errFmtWebAuthnBoolean, "experimental_enable_passkey_uv_two_factors", config.WebAuthn.EnablePasskey2FA, false, "enable_passkey_login", config.WebAuthn.EnablePasskeyLogin))
+		}
+
+		if config.WebAuthn.EnablePasskeyUpgrade {
+			validator.Push(fmt.Errorf(errFmtWebAuthnBoolean, "experimental_enable_passkey_upgrade", config.WebAuthn.EnablePasskeyUpgrade, false, "enable_passkey_login", config.WebAuthn.EnablePasskeyLogin))
+		}
+	}
+
 	switch {
 	case config.WebAuthn.ConveyancePreference == "":
 		config.WebAuthn.ConveyancePreference = schema.DefaultWebAuthnConfiguration.ConveyancePreference
diff --git a/internal/configuration/validator/webauthn_test.go b/internal/configuration/validator/webauthn_test.go
index e6990a384ee82..40104f0092c9d 100644
--- a/internal/configuration/validator/webauthn_test.go
+++ b/internal/configuration/validator/webauthn_test.go
@@ -29,6 +29,118 @@ func TestWebAuthnShouldSetDefaultValues(t *testing.T) {
 	assert.Equal(t, schema.DefaultWebAuthnConfiguration.SelectionCriteria.Attachment, config.WebAuthn.SelectionCriteria.Attachment)
 }
 
+func TestWebAuthnPasskeyBooleans(t *testing.T) {
+	testCases := []struct {
+		name     string
+		disable  bool
+		passkey  bool
+		mfa      bool
+		upgrade  bool
+		expected []string
+	}{
+		{
+			"ShouldNotErrorOnDefaultValues",
+			false,
+			false,
+			false,
+			false,
+			nil,
+		},
+		{
+			"ShouldNotErrorDisabled",
+			true,
+			false,
+			false,
+			false,
+			nil,
+		},
+		{
+			"ShouldNotErrorPasskeys",
+			false,
+			true,
+			false,
+			false,
+			nil,
+		},
+		{
+			"ShouldNotErrorPasskeysAllEnabled",
+			false,
+			true,
+			true,
+			true,
+			nil,
+		},
+		{
+			"ShouldErrorPasskeysWebAuthnDisabled",
+			true,
+			true,
+			false,
+			false,
+			[]string{
+				"webauthn: option 'enable_passkey_login' is true but it must be false when 'disable' is true",
+			},
+		},
+		{
+			"ShouldErrorNoPasskeysWithPasskeyOptionUpgrade",
+			false,
+			false,
+			false,
+			true,
+			[]string{
+				"webauthn: option 'experimental_enable_passkey_upgrade' is true but it must be false when 'enable_passkey_login' is false",
+			},
+		},
+		{
+			"ShouldErrorNoPasskeysWithPasskeyOptionMFA",
+			false,
+			false,
+			true,
+			false,
+			[]string{
+				"webauthn: option 'experimental_enable_passkey_uv_two_factors' is true but it must be false when 'enable_passkey_login' is false",
+			},
+		},
+		{
+			"ShouldErrorNoPasskeysWithPasskeyOptionMultiple",
+			false,
+			false,
+			true,
+			true,
+			[]string{
+				"webauthn: option 'experimental_enable_passkey_uv_two_factors' is true but it must be false when 'enable_passkey_login' is false",
+				"webauthn: option 'experimental_enable_passkey_upgrade' is true but it must be false when 'enable_passkey_login' is false",
+			},
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			config := &schema.Configuration{
+				WebAuthn: schema.WebAuthn{
+					Disable:              tc.disable,
+					EnablePasskeyLogin:   tc.passkey,
+					EnablePasskey2FA:     tc.mfa,
+					EnablePasskeyUpgrade: tc.upgrade,
+				},
+			}
+
+			val := schema.NewStructValidator()
+
+			ValidateWebAuthn(config, val)
+
+			assert.False(t, val.HasWarnings())
+
+			errs := val.Errors()
+
+			require.Len(t, errs, len(tc.expected))
+
+			for i, err := range errs {
+				assert.EqualError(t, err, tc.expected[i])
+			}
+		})
+	}
+}
+
 func TestWebAuthnShouldSetDefaultTimeoutWhenNegative(t *testing.T) {
 	validator := schema.NewStructValidator()
 	config := &schema.Configuration{
diff --git a/internal/handlers/handler_firstfactor_passkey.go b/internal/handlers/handler_firstfactor_passkey.go
index dfac80fdfff36..bf18a9ab5cbbe 100644
--- a/internal/handlers/handler_firstfactor_passkey.go
+++ b/internal/handlers/handler_firstfactor_passkey.go
@@ -223,6 +223,12 @@ func FirstFactorPasskeyPOST(ctx *middlewares.AutheliaCtx) {
 		if bytes.Equal(credential.KID.Bytes(), c.ID) {
 			credential.UpdateSignInInfo(w.Config, ctx.Clock.Now().UTC(), c.Authenticator)
 
+			if !credential.Discoverable {
+				credential.Discoverable = true
+
+				ctx.Logger.WithFields(map[string]any{"kid": credential.KID.String(), "rpid": credential.RPID, "aaguid": credential.AAGUID.UUID.String(), "username": credential.Username, "description": credential.Description}).Debug("WebAuthn Credential Passively Upgraded to a Passkey")
+			}
+
 			ok = true
 
 			if err = ctx.Providers.StorageProvider.UpdateWebAuthnCredentialSignIn(ctx, credential); err != nil {
diff --git a/internal/handlers/handler_firstfactor_passkey_test.go b/internal/handlers/handler_firstfactor_passkey_test.go
index 1ef6efdaade94..99b74953f62cd 100644
--- a/internal/handlers/handler_firstfactor_passkey_test.go
+++ b/internal/handlers/handler_firstfactor_passkey_test.go
@@ -427,6 +427,111 @@ func TestFirstFactorPasskeyPOST(t *testing.T) {
 				assert.Nil(t, us.WebAuthn)
 			},
 		},
+		{
+			name:   "ShouldSuccessUpgrade",
+			config: &schema.DefaultWebAuthnConfiguration,
+			setup: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
+				mock.Ctx.Configuration.WebAuthn.EnablePasskeyUpgrade = true
+
+				us, err := mock.Ctx.GetSession()
+
+				require.NoError(t, err)
+
+				us.WebAuthn = &session.WebAuthn{
+					SessionData: &webauthn.SessionData{
+						Challenge:        "in1cL-oWfSjSd7uuwUvv2ndOAmRXb0cOAbUoTtAqvGE",
+						Expires:          time.Now().Add(time.Minute),
+						UserVerification: "preferred",
+					},
+				}
+
+				require.NoError(t, mock.Ctx.SaveSession(us))
+
+				credential := model.WebAuthnCredential{
+					ID:              1,
+					CreatedAt:       time.Now(),
+					LastUsedAt:      sql.NullTime{Time: mock.Clock.Now().UTC().Add(time.Second * -10), Valid: true},
+					RPID:            "login.example.com",
+					Username:        testUsername,
+					Description:     "test",
+					KID:             model.NewBase64(decode("rwOwV8WCh1hrE0M6mvaoRGpGHidqK6IlhkDJ2xERhPU=")),
+					AAGUID:          uuid.NullUUID{UUID: uuid.Must(uuid.Parse("01020304-0506-0708-0102-030405060708")), Valid: true},
+					AttestationType: "packed",
+					Attachment:      "cross-platform",
+					Transport:       "usb",
+					SignCount:       2,
+					CloneWarning:    false,
+					Discoverable:    true,
+					Present:         true,
+					Verified:        true,
+					BackupEligible:  false,
+					BackupState:     false,
+					PublicKey:       []byte{165, 1, 2, 3, 38, 32, 1, 33, 88, 32, 184, 17, 198, 170, 14, 81, 23, 237, 100, 218, 123, 122, 48, 76, 56, 148, 23, 111, 173, 245, 67, 239, 176, 229, 199, 205, 213, 46, 239, 91, 222, 183, 34, 88, 32, 171, 141, 116, 74, 68, 180, 81, 66, 81, 127, 81, 41, 236, 173, 38, 7, 9, 34, 128, 167, 101, 51, 25, 84, 239, 100, 10, 124, 117, 165, 178, 179},
+				}
+
+				updated := model.WebAuthnCredential{
+					ID:              1,
+					CreatedAt:       credential.CreatedAt,
+					LastUsedAt:      sql.NullTime{Time: mock.Clock.Now().UTC(), Valid: true},
+					RPID:            "login.example.com",
+					Username:        testUsername,
+					Description:     "test",
+					KID:             model.NewBase64(decode("rwOwV8WCh1hrE0M6mvaoRGpGHidqK6IlhkDJ2xERhPU=")),
+					AAGUID:          uuid.NullUUID{UUID: uuid.Must(uuid.Parse("01020304-0506-0708-0102-030405060708")), Valid: true},
+					AttestationType: "packed",
+					Attachment:      "cross-platform",
+					Transport:       "usb",
+					SignCount:       3,
+					CloneWarning:    false,
+					Discoverable:    true,
+					Present:         true,
+					Verified:        true,
+					BackupEligible:  false,
+					BackupState:     false,
+					PublicKey:       []byte{165, 1, 2, 3, 38, 32, 1, 33, 88, 32, 184, 17, 198, 170, 14, 81, 23, 237, 100, 218, 123, 122, 48, 76, 56, 148, 23, 111, 173, 245, 67, 239, 176, 229, 199, 205, 213, 46, 239, 91, 222, 183, 34, 88, 32, 171, 141, 116, 74, 68, 180, 81, 66, 81, 127, 81, 41, 236, 173, 38, 7, 9, 34, 128, 167, 101, 51, 25, 84, 239, 100, 10, 124, 117, 165, 178, 179},
+				}
+
+				gomock.InOrder(
+					mock.StorageMock.EXPECT().
+						LoadWebAuthnUserByUserID(mock.Ctx, gomock.Eq("login.example.com"), gomock.Eq("example")).
+						Return(&model.WebAuthnUser{UserID: "example", Username: testUsername}, nil),
+					mock.StorageMock.EXPECT().
+						LoadWebAuthnCredentialsByUsername(mock.Ctx, gomock.Eq("login.example.com"), gomock.Eq(testUsername)).
+						Return([]model.WebAuthnCredential{credential}, nil),
+					mock.StorageMock.EXPECT().
+						UpdateWebAuthnCredentialSignIn(mock.Ctx, updated).
+						Return(nil),
+					mock.UserProviderMock.EXPECT().
+						GetDetails(gomock.Eq(testUsername)).
+						Return(&authentication.UserDetails{Username: testUsername}, nil),
+					mock.StorageMock.EXPECT().
+						LoadBannedIP(mock.Ctx, gomock.Eq(model.NewIP(mock.Ctx.RemoteIP()))).
+						Return(nil, nil),
+					mock.StorageMock.EXPECT().
+						LoadBannedUser(mock.Ctx, gomock.Eq(testUsername)).
+						Return(nil, nil),
+					mock.StorageMock.EXPECT().
+						AppendAuthenticationLog(gomock.Eq(mock.Ctx), gomock.Eq(model.AuthenticationAttempt{
+							Time:       mock.Ctx.Clock.Now(),
+							Successful: true,
+							Banned:     false,
+							Username:   testUsername,
+							Type:       regulation.AuthTypePasskey,
+							RemoteIP:   model.NullIP{IP: net.ParseIP("0.0.0.0")},
+						})).
+						Return(nil),
+				)
+			},
+			have:           dataReqGood,
+			expectedStatus: fasthttp.StatusOK,
+			expectedf: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
+				us, err := mock.Ctx.GetSession()
+
+				require.NoError(t, err)
+
+				assert.Nil(t, us.WebAuthn)
+			},
+		},
 		{
 			name:   "ShouldNotAllowBannedUserToUsePasskey",
 			config: &schema.DefaultWebAuthnConfiguration,
diff --git a/internal/handlers/webauthn.go b/internal/handlers/webauthn.go
index 455c56659efa1..3b563d2d14087 100644
--- a/internal/handlers/webauthn.go
+++ b/internal/handlers/webauthn.go
@@ -55,8 +55,14 @@ func handlerWebAuthnDiscoverableLogin(ctx *middlewares.AutheliaCtx, rpid string)
 			return nil, fmt.Errorf("user not found")
 		}
 
-		if u.Credentials, err = ctx.Providers.StorageProvider.LoadWebAuthnPasskeyCredentialsByUsername(ctx, rpid, u.Username); err != nil {
-			return nil, err
+		if ctx.Configuration.WebAuthn.EnablePasskeyUpgrade {
+			if u.Credentials, err = ctx.Providers.StorageProvider.LoadWebAuthnCredentialsByUsername(ctx, rpid, u.Username); err != nil {
+				return nil, err
+			}
+		} else {
+			if u.Credentials, err = ctx.Providers.StorageProvider.LoadWebAuthnPasskeyCredentialsByUsername(ctx, rpid, u.Username); err != nil {
+				return nil, err
+			}
 		}
 
 		return u, nil
diff --git a/internal/handlers/webauthn_test.go b/internal/handlers/webauthn_test.go
index f6a0ec777e6e8..2be1f7d5ea986 100644
--- a/internal/handlers/webauthn_test.go
+++ b/internal/handlers/webauthn_test.go
@@ -412,6 +412,8 @@ func TestWebAuthnGetUserWithErr(t *testing.T) {
 		Username: testUsername,
 	}
 
+	assert.False(t, ctx.Ctx.Configuration.WebAuthn.EnablePasskeyUpgrade)
+
 	ctx.StorageMock.EXPECT().
 		LoadWebAuthnUser(ctx.Ctx, exampleDotCom, testUsername).
 		Return(&model.WebAuthnUser{ID: 1, RPID: exampleDotCom, Username: testUsername, UserID: "john123"}, nil)
@@ -479,6 +481,23 @@ func TestWebAuthnHandlerDiscoverableLogin(t *testing.T) {
 			nil,
 			"bad credentials",
 		},
+		{
+			"ShouldHandleCredentialsErrorUpgrade",
+			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
+				mock.Ctx.Configuration.WebAuthn.EnablePasskeyUpgrade = true
+
+				gomock.InOrder(
+					mock.StorageMock.EXPECT().
+						LoadWebAuthnUserByUserID(mock.Ctx, "https://example.com", "example").
+						Return(&model.WebAuthnUser{ID: 1, Username: "john"}, nil),
+					mock.StorageMock.EXPECT().
+						LoadWebAuthnCredentialsByUsername(mock.Ctx, "https://example.com", "john").
+						Return(nil, fmt.Errorf("bad credentials")),
+				)
+			},
+			nil,
+			"bad credentials",
+		},
 		{
 			"ShouldHandleUserError",
 			func(t *testing.T, mock *mocks.MockAutheliaCtx) {


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>docs/content/configuration/second-factor/webauthn.md (2)</summary><blockquote>

`95-96`: **Add a comma after 'However', fool!**

When starting a sentence with 'However', you need a comma after it.

```diff
-Some authenticators or browsers have issues with reporting if a WebAuthn credential is actually a Passkey or not. The
-relevant specifications instruct implementers to assume they are not a Passkey in this instance. However this presents
+Some authenticators or browsers have issues with reporting if a WebAuthn credential is actually a Passkey or not. The
+relevant specifications instruct implementers to assume they are not a Passkey in this instance. However, this presents
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[style] ~95-~95: In contexts where ‘if’ is followed by ‘or’, using ‘whether’ may be more appropriate (and formal).
Context: ... or browsers have issues with reporting if a WebAuthn credential is actually a Pas...

(IF_WHETHER)

---

[uncategorized] ~96-~96: A comma may be missing after the conjunctive/linking adverb ‘However’.
Context: ...hey are not a Passkey in this instance. However this presents some user frustration. Th...

(SENT_START_CONJUNCTIVE_LINKING_ADVERB_COMMA)

</details>

</details>

---

`99-100`: **Fix that split infinitive, sucka!**

The current wording splits the infinitive "to upgrade" with "automatically". This ain't good style!

```diff
-When true this will attempt to automatically upgrade a WebAuthn credential to a Passkey when the browser or
+When true this will attempt to upgrade a WebAuthn credential automatically to a Passkey when the browser or
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[style] ~99-~99: Style-wise, it’s not ideal to insert an adverb in the middle of an infinitive construction.
Context: ... that frustration.  When true this will attempt to automatically upgrade a WebAuthn credential to a Passkey when the browser or authen...

(SPLIT_INFINITIVE)

</details>

</details>

</blockquote></details>
<details>
<summary>internal/configuration/validator/webauthn_test.go (1)</summary><blockquote>

`83-93`: **I pity the test that doesn't cover isolated cases, fool!**

The test case "ShouldErrorNoPasskeysWithPasskeyOptions" tests both MFA and Upgrade options enabled at the same time. Consider adding separate test cases for each option individually to ensure they're validated correctly in isolation.

Consider adding these test cases:

```go
{
    "ShouldErrorNoPasskeysWithMFAOption",
    false,
    false,
    true,
    false,
    []string{
        "webauthn: option 'experimental_enable_passkey_uv_two_factors' is true but it must be false when 'enable_passkey_login' is false",
    },
},
{
    "ShouldErrorNoPasskeysWithUpgradeOption",
    false,
    false,
    false,
    true,
    []string{
        "webauthn: option 'experimental_enable_passkey_upgrade' is true but it must be false when 'enable_passkey_login' is false",
    },
},
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between f7f541a1310452d41d6519a528ef7620291cd298 and bbf25d3d9f4003810cc92e0f2db923775c78d85b.

</details>

<details>
<summary>⛔ Files ignored due to path filters (3)</summary>

* `docs/data/configkeys.json` is excluded by `!**/*.json`
* `docs/static/schemas/latest/json-schema/configuration.json` is excluded by `!**/*.json`
* `docs/static/schemas/v4.39/json-schema/configuration.json` is excluded by `!**/*.json`

</details>

<details>
<summary>📒 Files selected for processing (13)</summary>

* `docs/content/configuration/second-factor/webauthn.md` (2 hunks)
* `docs/content/reference/cli/authelia-gen/authelia-gen_misc.md` (1 hunks)
* `docs/content/reference/cli/authelia-gen/authelia-gen_misc_oidc.md` (1 hunks)
* `docs/content/reference/cli/authelia-gen/authelia-gen_misc_oidc_conformance.md` (1 hunks)
* `internal/configuration/schema/keys.go` (1 hunks)
* `internal/configuration/schema/webauthn.go` (1 hunks)
* `internal/configuration/validator/const.go` (1 hunks)
* `internal/configuration/validator/webauthn.go` (1 hunks)
* `internal/configuration/validator/webauthn_test.go` (1 hunks)
* `internal/handlers/handler_firstfactor_passkey.go` (1 hunks)
* `internal/handlers/handler_firstfactor_passkey_test.go` (12 hunks)
* `internal/handlers/webauthn.go` (1 hunks)
* `internal/handlers/webauthn_test.go` (2 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (2)</summary>

<details>
<summary>internal/configuration/validator/webauthn_test.go (2)</summary><blockquote>

<details>
<summary>internal/configuration/schema/webauthn.go (1)</summary>

* `WebAuthn` (12-26)

</details>
<details>
<summary>internal/configuration/validator/webauthn.go (1)</summary>

* `ValidateWebAuthn` (14-63)

</details>

</blockquote></details>
<details>
<summary>internal/configuration/validator/webauthn.go (1)</summary><blockquote>

<details>
<summary>internal/configuration/schema/webauthn.go (1)</summary>

* `WebAuthn` (12-26)

</details>

</blockquote></details>

</details><details>
<summary>🪛 LanguageTool</summary>

<details>
<summary>docs/content/configuration/second-factor/webauthn.md</summary>

[style] ~95-~95: In contexts where ‘if’ is followed by ‘or’, using ‘whether’ may be more appropriate (and formal).
Context: ... or browsers have issues with reporting if a WebAuthn credential is actually a Pas...

(IF_WHETHER)

---

[uncategorized] ~96-~96: A comma may be missing after the conjunctive/linking adverb ‘However’.
Context: ...hey are not a Passkey in this instance. However this presents some user frustration. Th...

(SENT_START_CONJUNCTIVE_LINKING_ADVERB_COMMA)

---

[style] ~99-~99: Style-wise, it’s not ideal to insert an adverb in the middle of an infinitive construction.
Context: ... that frustration.  When true this will attempt to automatically upgrade a WebAuthn credential to a Passkey when the browser or authen...

(SPLIT_INFINITIVE)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (2)</summary>

* GitHub Check: Analyze (go)
* GitHub Check: Analyze (javascript)

</details>

<details>
<summary>🔇 Additional comments (15)</summary><blockquote>

<details>
<summary>docs/content/reference/cli/authelia-gen/authelia-gen_misc.md (1)</summary>

`5-5`: **Metadata date updated properly**  
Listen up, fool! This front matter `date` was rolled back to `2022-09-16T14:21:05+10:00` to keep the CLI docs in sync. I pity the fool who forgets!

</details>
<details>
<summary>docs/content/reference/cli/authelia-gen/authelia-gen_misc_oidc_conformance.md (1)</summary>

`5-5`: **Consistent date alignment**  
I pity the fool who overlooks consistency! The `date` metadata is now `2022-06-15T17:51:47+10:00`, matching sibling CLI reference files perfectly.

</details>
<details>
<summary>docs/content/reference/cli/authelia-gen/authelia-gen_misc_oidc.md (1)</summary>

`5-5`: **Front matter date synced**  
Listen up, fool! Updated the front matter `date` to `2022-09-16T14:21:05+10:00` for harmony with the other docs. I pity the fool who doesn’t!

</details>
<details>
<summary>internal/configuration/validator/const.go (1)</summary>

`330-330`: **I pity the fool who doesn't document this new error constant!**

This new error format constant `errFmtWebAuthnBoolean` is for reporting boolean option mismatches in WebAuthn configuration. Good addition for validating those passkey upgrade settings, fool!

</details>
<details>
<summary>internal/handlers/webauthn_test.go (2)</summary>

`454-455`: **Method rename lookin' good, sucka!**

Changed from `LoadWebAuthnPasskeyCredentialsByUsername` to the more general `LoadWebAuthnCredentialsByUsername`. This unifies the credential loading method across the codebase. I ain't got time for duplicate methods!

---

`474-476`: **Same method update here too! Consistency is key, fool!**

Another instance of updating to `LoadWebAuthnCredentialsByUsername` for the error case. Keeping it all uniform. That's how Mr. T likes his code!

</details>
<details>
<summary>internal/configuration/schema/keys.go (1)</summary>

`471-471`: **New configuration key added, and I pity the fool who forgets it!**

This experimental feature key for passkey upgrades is properly positioned between other related WebAuthn keys. Good job, sucka! Makes sure browsers that don't properly report passkey status can still use the feature.

</details>
<details>
<summary>internal/handlers/handler_firstfactor_passkey_test.go (3)</summary>

`394-395`: **Test mockup method name updated! No pain, no gain!**

Updated method call from `LoadWebAuthnPasskeyCredentialsByUsername` to `LoadWebAuthnCredentialsByUsername` to match the unified approach in the handler code. Ain't nobody got time for inconsistent mocks!

---

`497-498`: **Same method rename in this test case too! Mr. T approves!**

Another instance of the method rename to maintain consistency. This approach makes the code cleaner and removes duplication. That's how we roll!

---

`602-603`: **All these method updates are GOLDEN, just like my chains!**

Multiple instances of the same method rename throughout the test file. This unified approach to credential loading is smart - makes the code more maintainable and ready for that automatic passkey upgrade feature!



Also applies to: 704-705, 798-799, 902-903, 1004-1005, 1106-1107, 1180-1181, 1272-1273, 1325-1326, 1530-1531

</details>
<details>
<summary>internal/handlers/webauthn.go (1)</summary>

`58-58`: **Method call upgraded to handle all WebAuthn credential types!**

I pity the fool who doesn't use consistent credential loading methods! This change from `LoadWebAuthnPasskeyCredentialsByUsername` to `LoadWebAuthnCredentialsByUsername` is good, fool! It aligns with the broader passkey upgrade feature and unifies credential loading across handlers.

</details>
<details>
<summary>internal/configuration/schema/webauthn.go (1)</summary>

`16-16`: **New experimental flag added for passkey upgrades!**

I like what you're doin' here, fool! Adding this `EnablePasskeyUpgrade` option to automatically fix those crazy non-compliant browsers and authenticators that don't properly report passkey status. Smart move!

</details>
<details>
<summary>internal/handlers/handler_firstfactor_passkey.go (1)</summary>

`226-230`: **Passive passkey upgrade looks good!**

Listen up, fool! This is a solid implementation that checks if a credential ain't discoverable, then upgrades it and logs the details. Just what the PR ordered - fixing those non-compliant authenticators! The debug logging is proper with all the necessary fields too.

</details>
<details>
<summary>docs/content/configuration/second-factor/webauthn.md (1)</summary>

`86-101`: **I pity the fool who doesn't document experimental features properly!**

The documentation for the new experimental feature looks good. It clearly explains the purpose, limitations, and stability warning for this passkey upgrade option.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[style] ~95-~95: In contexts where ‘if’ is followed by ‘or’, using ‘whether’ may be more appropriate (and formal).
Context: ... or browsers have issues with reporting if a WebAuthn credential is actually a Pas...

(IF_WHETHER)

---

[uncategorized] ~96-~96: A comma may be missing after the conjunctive/linking adverb ‘However’.
Context: ...hey are not a Passkey in this instance. However this presents some user frustration. Th...

(SENT_START_CONJUNCTIVE_LINKING_ADVERB_COMMA)

---

[style] ~99-~99: Style-wise, it’s not ideal to insert an adverb in the middle of an infinitive construction.
Context: ... that frustration.  When true this will attempt to automatically upgrade a WebAuthn credential to a Passkey when the browser or authen...

(SPLIT_INFINITIVE)

</details>

</details>

</details>
<details>
<summary>internal/configuration/validator/webauthn_test.go (1)</summary>

`32-122`: **Lookin' good, fool! Comprehensive test cases for the new passkey upgrade feature!**

This test function thoroughly validates the expected behavior of the WebAuthn passkey boolean configuration options. The test cases cover all important scenarios:
- Default values
- Disabled WebAuthn
- Enabled passkeys
- All passkey options enabled
- Invalid combinations with error verification

I like that you're testing the exact error messages too. That's how you write robust tests!

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T00:21:03Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>docs/content/configuration/second-factor/webauthn.md (3)</summary><blockquote>

`90-93`: **Consider using a "warning" callout context, fool!** Since this is a stability notice rather than a security-critical alert, switching `context="danger"` to `context="warning"` would better reflect the nature of the message and maintain semantic clarity.

---

`95-98`: **Refine wording for clarity, fool!** You could replace "if a WebAuthn credential is actually a Passkey or not" with "whether a WebAuthn credential is a Passkey", and add a comma after "However" to improve readability. For example:
```diff
- Some authenticators or browsers have issues with reporting if a WebAuthn credential is actually a Passkey or not. The relevant specifications instruct implementers to assume they are not a Passkey in this instance. However this presents some user frustration. This option attempts to alleviate some of that frustration.
+ Some authenticators or browsers have issues with reporting whether a WebAuthn credential is a Passkey. The relevant specifications instruct implementers to assume they are not a Passkey in this instance. However, this presents some user frustration. This option attempts to alleviate that frustration.
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[style] ~95-~95: In contexts where ‘if’ is followed by ‘or’, using ‘whether’ may be more appropriate (and formal).
Context: ... or browsers have issues with reporting if a WebAuthn credential is actually a Pas...

(IF_WHETHER)

---

[uncategorized] ~96-~96: A comma may be missing after the conjunctive/linking adverb ‘However’.
Context: ...hey are not a Passkey in this instance. However this presents some user frustration. Th...

(SENT_START_CONJUNCTIVE_LINKING_ADVERB_COMMA)

</details>

</details>

---

`99-101`: **Add missing comma after introductory phrase, fool!** Insert a comma after "When true" to align with the style used elsewhere for clarity. For example:
```diff
- When true this will attempt to automatically upgrade a WebAuthn credential to a Passkey when the browser or authenticator does not report a newly registered WebAuthn credential to be a Passkey.
+ When true, this will attempt to automatically upgrade a WebAuthn credential to a Passkey when the browser or authenticator does not report a newly registered WebAuthn credential to be a Passkey.
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[uncategorized] ~99-~99: A comma might be missing here.
Context: ...leviate some of that frustration.  When true this will attempt to automatically upgr...

(AI_EN_LECTOR_MISSING_PUNCTUATION_COMMA)

---

[style] ~99-~99: Style-wise, it’s not ideal to insert an adverb in the middle of an infinitive construction.
Context: ... that frustration.  When true this will attempt to automatically upgrade a WebAuthn credential to a Passkey when the browser or authen...

(SPLIT_INFINITIVE)

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between bbf25d3d9f4003810cc92e0f2db923775c78d85b and 5a07add9ce35d1b77c1dbec03484c2f123f21327.

</details>

<details>
<summary>⛔ Files ignored due to path filters (3)</summary>

* `docs/data/configkeys.json` is excluded by `!**/*.json`
* `docs/static/schemas/latest/json-schema/configuration.json` is excluded by `!**/*.json`
* `docs/static/schemas/v4.39/json-schema/configuration.json` is excluded by `!**/*.json`

</details>

<details>
<summary>📒 Files selected for processing (13)</summary>

* `docs/content/configuration/second-factor/webauthn.md` (2 hunks)
* `docs/content/reference/cli/authelia-gen/authelia-gen_misc.md` (1 hunks)
* `docs/content/reference/cli/authelia-gen/authelia-gen_misc_oidc.md` (1 hunks)
* `docs/content/reference/cli/authelia-gen/authelia-gen_misc_oidc_conformance.md` (1 hunks)
* `internal/configuration/schema/keys.go` (1 hunks)
* `internal/configuration/schema/webauthn.go` (1 hunks)
* `internal/configuration/validator/const.go` (1 hunks)
* `internal/configuration/validator/webauthn.go` (1 hunks)
* `internal/configuration/validator/webauthn_test.go` (1 hunks)
* `internal/handlers/handler_firstfactor_passkey.go` (1 hunks)
* `internal/handlers/handler_firstfactor_passkey_test.go` (1 hunks)
* `internal/handlers/webauthn.go` (1 hunks)
* `internal/handlers/webauthn_test.go` (2 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (4)</summary>

* docs/content/reference/cli/authelia-gen/authelia-gen_misc_oidc_conformance.md
* docs/content/reference/cli/authelia-gen/authelia-gen_misc_oidc.md
* docs/content/reference/cli/authelia-gen/authelia-gen_misc.md
* internal/configuration/schema/keys.go

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (8)</summary>

* internal/handlers/webauthn.go
* internal/configuration/validator/webauthn.go
* internal/configuration/validator/const.go
* internal/configuration/validator/webauthn_test.go
* internal/configuration/schema/webauthn.go
* internal/handlers/webauthn_test.go
* internal/handlers/handler_firstfactor_passkey_test.go
* internal/handlers/handler_firstfactor_passkey.go

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 LanguageTool</summary>

<details>
<summary>docs/content/configuration/second-factor/webauthn.md</summary>

[style] ~95-~95: In contexts where ‘if’ is followed by ‘or’, using ‘whether’ may be more appropriate (and formal).
Context: ... or browsers have issues with reporting if a WebAuthn credential is actually a Pas...

(IF_WHETHER)

---

[uncategorized] ~96-~96: A comma may be missing after the conjunctive/linking adverb ‘However’.
Context: ...hey are not a Passkey in this instance. However this presents some user frustration. Th...

(SENT_START_CONJUNCTIVE_LINKING_ADVERB_COMMA)

---

[uncategorized] ~99-~99: A comma might be missing here.
Context: ...leviate some of that frustration.  When true this will attempt to automatically upgr...

(AI_EN_LECTOR_MISSING_PUNCTUATION_COMMA)

---

[style] ~99-~99: Style-wise, it’s not ideal to insert an adverb in the middle of an infinitive construction.
Context: ... that frustration.  When true this will attempt to automatically upgrade a WebAuthn credential to a Passkey when the browser or authen...

(SPLIT_INFINITIVE)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: Analyze (go)

</details>

<details>
<summary>🔇 Additional comments (2)</summary><blockquote>

<details>
<summary>docs/content/configuration/second-factor/webauthn.md (2)</summary>

`72-73`: **Great consistency update, fool!** The `confkey` snippet for `experimental_enable_passkey_uv_two_factors` has been added to match other options' formatting in this doc.

---

`86-89`: **Nice addition of the new experimental option, fool!** The `### experimental_enable_passkey_upgrade` header and its `confkey` snippet are structured consistently with the rest of the configuration reference.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T00:31:07Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>docs/content/configuration/second-factor/webauthn.md (2)</summary><blockquote>

`72-73`: **I pity the fool who forgets to update the example config**  
You added the `confkey` macro for `experimental_enable_passkey_uv_two_factors`, but the primary YAML example (lines 23–51) doesn’t include this option. Mirror the new snippet in the sample configuration so users can see all available flags in context.

---

`86-101`: **I pity the fool who writes docs without proper punctuation and clarity**  
A few nitpicks to polish readability and formality:

```diff
- issues with reporting if a WebAuthn credential is actually a Passkey or not.
+ issues with reporting whether a WebAuthn credential is actually a Passkey or not.

- However this presents some user frustration.
+ However, this presents some user frustration.

- When true this will attempt to automatically upgrade a WebAuthn credential to a Passkey when the browser or authenticator does not report a newly registered WebAuthn credential to be a Passkey.
+ When true, this will attempt to automatically upgrade a WebAuthn credential to a Passkey when the browser or authenticator does not report a newly registered WebAuthn credential to be a Passkey.
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[style] ~95-~95: In contexts where ‘if’ is followed by ‘or’, using ‘whether’ may be more appropriate (and formal).
Context: ... or browsers have issues with reporting if a WebAuthn credential is actually a Pas...

(IF_WHETHER)

---

[uncategorized] ~96-~96: A comma may be missing after the conjunctive/linking adverb ‘However’.
Context: ...hey are not a Passkey in this instance. However this presents some user frustration. Th...

(SENT_START_CONJUNCTIVE_LINKING_ADVERB_COMMA)

---

[uncategorized] ~99-~99: A comma might be missing here.
Context: ...leviate some of that frustration.  When true this will attempt to automatically upgr...

(AI_EN_LECTOR_MISSING_PUNCTUATION_COMMA)

---

[style] ~99-~99: Style-wise, it’s not ideal to insert an adverb in the middle of an infinitive construction.
Context: ... that frustration.  When true this will attempt to automatically upgrade a WebAuthn credential to a Passkey when the browser or authen...

(SPLIT_INFINITIVE)

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 5a07add9ce35d1b77c1dbec03484c2f123f21327 and 2bfd7df22509eed4bb7036f63b210c8faafc4bf1.

</details>

<details>
<summary>⛔ Files ignored due to path filters (3)</summary>

* `docs/data/configkeys.json` is excluded by `!**/*.json`
* `docs/static/schemas/latest/json-schema/configuration.json` is excluded by `!**/*.json`
* `docs/static/schemas/v4.39/json-schema/configuration.json` is excluded by `!**/*.json`

</details>

<details>
<summary>📒 Files selected for processing (13)</summary>

* `docs/content/configuration/second-factor/webauthn.md` (2 hunks)
* `docs/content/reference/cli/authelia-gen/authelia-gen_misc.md` (1 hunks)
* `docs/content/reference/cli/authelia-gen/authelia-gen_misc_oidc.md` (1 hunks)
* `docs/content/reference/cli/authelia-gen/authelia-gen_misc_oidc_conformance.md` (1 hunks)
* `internal/configuration/schema/keys.go` (1 hunks)
* `internal/configuration/schema/webauthn.go` (1 hunks)
* `internal/configuration/validator/const.go` (1 hunks)
* `internal/configuration/validator/webauthn.go` (2 hunks)
* `internal/configuration/validator/webauthn_test.go` (1 hunks)
* `internal/handlers/handler_firstfactor_passkey.go` (1 hunks)
* `internal/handlers/handler_firstfactor_passkey_test.go` (1 hunks)
* `internal/handlers/webauthn.go` (1 hunks)
* `internal/handlers/webauthn_test.go` (2 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (3)</summary>

* docs/content/reference/cli/authelia-gen/authelia-gen_misc_oidc.md
* docs/content/reference/cli/authelia-gen/authelia-gen_misc.md
* internal/configuration/schema/webauthn.go

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (9)</summary>

* docs/content/reference/cli/authelia-gen/authelia-gen_misc_oidc_conformance.md
* internal/configuration/schema/keys.go
* internal/handlers/handler_firstfactor_passkey.go
* internal/configuration/validator/const.go
* internal/configuration/validator/webauthn.go
* internal/handlers/webauthn_test.go
* internal/handlers/webauthn.go
* internal/handlers/handler_firstfactor_passkey_test.go
* internal/configuration/validator/webauthn_test.go

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 LanguageTool</summary>

<details>
<summary>docs/content/configuration/second-factor/webauthn.md</summary>

[style] ~95-~95: In contexts where ‘if’ is followed by ‘or’, using ‘whether’ may be more appropriate (and formal).
Context: ... or browsers have issues with reporting if a WebAuthn credential is actually a Pas...

(IF_WHETHER)

---

[uncategorized] ~96-~96: A comma may be missing after the conjunctive/linking adverb ‘However’.
Context: ...hey are not a Passkey in this instance. However this presents some user frustration. Th...

(SENT_START_CONJUNCTIVE_LINKING_ADVERB_COMMA)

---

[uncategorized] ~99-~99: A comma might be missing here.
Context: ...leviate some of that frustration.  When true this will attempt to automatically upgr...

(AI_EN_LECTOR_MISSING_PUNCTUATION_COMMA)

---

[style] ~99-~99: Style-wise, it’s not ideal to insert an adverb in the middle of an infinitive construction.
Context: ... that frustration.  When true this will attempt to automatically upgrade a WebAuthn credential to a Passkey when the browser or authen...

(SPLIT_INFINITIVE)

</details>

</details>

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T00:33:09Z

Review comments:
diff_hunk: @@ -20,6 +20,20 @@
 		config.WebAuthn.Timeout = schema.DefaultWebAuthnConfiguration.Timeout
 	}
 
+	if config.WebAuthn.EnablePasskeyLogin {
+		if config.WebAuthn.Disable {
+			validator.Push(fmt.Errorf(errFmtWebAuthnBoolean, "enable_passkey_login", config.WebAuthn.EnablePasskeyLogin, false, "disable", config.WebAuthn.Disable))
+		}
+	} else {
+		if config.WebAuthn.EnablePasskey2FA {
+			validator.Push(fmt.Errorf(errFmtWebAuthnBoolean, "experimental_enable_passkey_uv_two_factors", config.WebAuthn.EnablePasskey2FA, false, "enable_passkey_login", config.WebAuthn.EnablePasskeyLogin))
+		}
+
+		if config.WebAuthn.EnablePasskeyUpgrade {
+			validator.Push(fmt.Errorf(errFmtWebAuthnBoolean, "experimental_enable_passkey_upgrade", config.WebAuthn.EnablePasskey2FA, false, "enable_passkey_login", config.WebAuthn.EnablePasskeyLogin))
+		}
+	}
path: internal/configuration/validator/webauthn.go
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Validation logic is solid, but contains a bug!**

I pity the fool who uses the wrong variable in error messages! On line 33, you're using `config.WebAuthn.EnablePasskey2FA` instead of `config.WebAuthn.EnablePasskeyUpgrade` in the error formatting. That's gonna confuse users with the wrong error message, fool!


Fix this error message by using the right variable:

```diff
-validator.Push(fmt.Errorf(errFmtWebAuthnBoolean, "experimental_enable_passkey_upgrade", config.WebAuthn.EnablePasskey2FA, false, "enable_passkey_login", config.WebAuthn.EnablePasskeyLogin))
+validator.Push(fmt.Errorf(errFmtWebAuthnBoolean, "experimental_enable_passkey_upgrade", config.WebAuthn.EnablePasskeyUpgrade, false, "enable_passkey_login", config.WebAuthn.EnablePasskeyLogin))
```

The rest of the validation logic looks good - making sure these flags are consistent is important, fool!


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
	if config.WebAuthn.EnablePasskeyLogin {
		if config.WebAuthn.Disable {
			validator.Push(fmt.Errorf(
				errFmtWebAuthnBoolean,
				"enable_passkey_login",
				config.WebAuthn.EnablePasskeyLogin,
				false,
				"disable",
				config.WebAuthn.Disable,
			))
		}
	} else {
		if config.WebAuthn.EnablePasskey2FA {
			validator.Push(fmt.Errorf(
				errFmtWebAuthnBoolean,
				"experimental_enable_passkey_uv_two_factors",
				config.WebAuthn.EnablePasskey2FA,
				false,
				"enable_passkey_login",
				config.WebAuthn.EnablePasskeyLogin,
			))
		}

		if config.WebAuthn.EnablePasskeyUpgrade {
			validator.Push(fmt.Errorf(
				errFmtWebAuthnBoolean,
				"experimental_enable_passkey_upgrade",
-				config.WebAuthn.EnablePasskey2FA,
+				config.WebAuthn.EnablePasskeyUpgrade,
				false,
				"enable_passkey_login",
				config.WebAuthn.EnablePasskeyLogin,
			))
		}
	}
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-18T00:21:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -29,6 +29,98 @@ func TestWebAuthnShouldSetDefaultValues(t *testing.T) {
 	assert.Equal(t, schema.DefaultWebAuthnConfiguration.SelectionCriteria.Attachment, config.WebAuthn.SelectionCriteria.Attachment)
 }
 
+func TestWebAuthnPasskeyBooleans(t *testing.T) {
+	testCases := []struct {
+		name     string
+		disable  bool
+		passkey  bool
+		mfa      bool
+		upgrade  bool
+		expected []string
+	}{
+		{
+			"ShouldNotErrorOnDefaultValues",
+			false,
+			false,
+			false,
+			false,
+			nil,
+		},
+		{
+			"ShouldNotErrorDisabled",
+			true,
+			false,
+			false,
+			false,
+			nil,
+		},
+		{
+			"ShouldNotErrorPasskeys",
+			false,
+			true,
+			false,
+			false,
+			nil,
+		},
+		{
+			"ShouldNotErrorPasskeysAllEnabled",
+			false,
+			true,
+			true,
+			true,
+			nil,
+		},
+		{
+			"ShouldErrorPasskeysWebAuthnDisabled",
+			true,
+			true,
+			false,
+			false,
+			[]string{
+				"webauthn: option 'enable_passkey_login' is true but it must be false when 'disable' is true",
+			},
+		},
+		{
+			"ShouldErrorNoPasskeysWithPasskeyOptions",
+			false,
+			false,
+			true,
+			true,
+			[]string{
+				"webauthn: option 'experimental_enable_passkey_uv_two_factors' is true but it must be false when 'enable_passkey_login' is false",
+				"webauthn: option 'experimental_enable_passkey_upgrade' is true but it must be false when 'enable_passkey_login' is false",
path: internal/configuration/validator/webauthn_test.go
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Check your error message consistency, fool!**

In the ValidateWebAuthn function (line 27 in the relevant code snippet), you're checking EnablePasskey2FA when EnablePasskeyUpgrade is true, but the error message mentions EnablePasskey2FA. This looks like a copy-paste error.

```diff
-				"webauthn: option 'experimental_enable_passkey_upgrade' is true but it must be false when 'enable_passkey_login' is false",
+				"webauthn: option 'experimental_enable_passkey_upgrade' is true but it must be false when 'enable_passkey_login' is false",
```

Check the ValidateWebAuthn function implementation - it looks like there might be a copy-paste error there as well:

```shell
#!/bin/bash
# Check if the error message in ValidateWebAuthn matches the correct variable

grep -A 3 "EnablePasskeyUpgrade" internal/configuration/validator/webauthn.go
```

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-18T00:21:03Z
author_association: CONTRIBUTOR

PR comments:
user: login: authelia[bot]
created_at: 2025-04-18T00:18:26Z
body: ## Artifacts
These changes are published for testing on Buildkite, DockerHub and GitHub Container Registry.

### Docker Container
* `docker pull authelia/authelia:fix-passkey-upgrade`
* `docker pull ghcr.io/authelia/authelia:fix-passkey-upgrade`
author_association: NONE
user: login: coderabbitai[bot]
created_at: 2025-04-18T00:18:30Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

Listen up, fool! This change introduces a new experimental configuration option called `experimental_enable_passkey_upgrade` to the WebAuthn second-factor setup. It updates the schema, validation logic, and documentation to support this new flag, which aims to upgrade WebAuthn credentials to passkeys automatically if the browser or authenticator doesn't report them correctly. The change also adds validation checks to ensure all passkey-related features are enabled only when the main passkey login is active, and updates tests and documentation for consistency. Some internal method names for credential loading were unified across handlers and tests.

## Changes

| Files/Paths                                                                                  | Change Summary                                                                                                    |
|---------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------|
| docs/content/configuration/second-factor/webauthn.md                                        | Documented new `experimental_enable_passkey_upgrade` option and updated related config snippets.                  |
| docs/content/reference/cli/authelia-gen/authelia-gen_misc*.md                               | Updated only the date metadata in the front matter.                                                              |
| internal/configuration/schema/keys.go<br>internal/configuration/schema/webauthn.go           | Added new config key and struct field for `experimental_enable_passkey_upgrade`.                                  |
| internal/configuration/validator/const.go                                                   | Added new error format constant for boolean WebAuthn option validation.                                           |
| internal/configuration/validator/webauthn.go                                                | Enhanced validation logic for cross-field passkey feature dependencies.                                           |
| internal/configuration/validator/webauthn_test.go                                           | Added new test cases for passkey-related boolean flag validation.                                                 |
| internal/handlers/handler_firstfactor_passkey.go                                            | Added logic to passively upgrade credentials to passkeys during authentication and log the upgrade.               |
| internal/handlers/handler_firstfactor_passkey_test.go<br>internal/handlers/webauthn.go<br>internal/handlers/webauthn_test.go | Replaced calls to `LoadWebAuthnPasskeyCredentialsByUsername` with `LoadWebAuthnCredentialsByUsername` for consistency and added related tests. |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant User
    participant Handler
    participant CredentialStore
    participant Logger

    User->>Handler: Attempt passkey authentication
    Handler->>CredentialStore: Load WebAuthn credentials by username
    CredentialStore-->>Handler: Return credentials
    Handler->>Handler: Check credential Discoverable flag
    alt Not Discoverable
        Handler->>Handler: Set Discoverable = true
        Handler->>Logger: Log credential upgrade to passkey
    end
    Handler->>CredentialStore: Update credential sign-in info
    Handler-->>User: Authentication result
```

## Assessment against linked issues

| Objective                                                                                                                          | Addressed | Explanation                                                                                  |
|------------------------------------------------------------------------------------------------------------------------------------|-----------|----------------------------------------------------------------------------------------------|
| Passkey login should recognize registered security keys and allow direct login or prompt for password for 2FA (#9094)               | ✅        |                                                                                              |
| Passkey upgrade should handle cases where authenticators/browsers do not report credentials as passkeys (#9094)                     | ✅        |                                                                                              |
| Validation and documentation of new configuration options for passkey upgrade (#9094)                                               | ✅        |                                                                                              |
| Consistent credential loading logic and updated tests for passkey features (#9094)                                                  | ✅        |                                                                                              |

<!-- walkthrough_end -->


---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 2bfd7df22509eed4bb7036f63b210c8faafc4bf1 and cb43f9c7063461346d868193e3a1a9ebb15e8fff.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `internal/configuration/validator/webauthn.go` (2 hunks)
* `internal/configuration/validator/webauthn_test.go` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (2)</summary>

* internal/configuration/validator/webauthn.go
* internal/configuration/validator/webauthn_test.go

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: Analyze (go)

</details>

</details>
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=authelia/authelia&utm_content=9278):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` or `@coderabbitai title` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: netlify[bot]
created_at: 2025-04-18T00:19:42Z
body: ### <span aria-hidden="true">✅</span> Deploy Preview for *authelia-staging* ready!


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | bbf25d3d9f4003810cc92e0f2db923775c78d85b |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/authelia-staging/deploys/68019a50de37a50008d2d01b |
|<span aria-hidden="true">😎</span> Deploy Preview | [https://deploy-preview-9278--authelia-staging.netlify.app](https://deploy-preview-9278--authelia-staging.netlify.app) |
|<span aria-hidden="true">📱</span> Preview on mobile | <details><summary> Toggle QR Code... </summary><br /><br />![QR Code](https://app.netlify.com/qr-code/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL2RlcGxveS1wcmV2aWV3LTkyNzgtLWF1dGhlbGlhLXN0YWdpbmcubmV0bGlmeS5hcHAifQ.4wBFD7yTMboCMvWu8fSOioHx6oCKhf-PI61xm1rzjiA)<br /><br />_Use your smartphone camera to open QR code link._</details> |
---
<!-- [authelia-staging Preview](https://deploy-preview-9278--authelia-staging.netlify.app) -->
_To edit notification comments on pull requests, go to your [Netlify site configuration](https://app.netlify.com/sites/authelia-staging/configuration/notifications#deploy-webhooks)._
author_association: NONE
user: login: codecov[bot]
created_at: 2025-04-18T00:25:00Z
body: ## [Codecov](https://app.codecov.io/gh/authelia/authelia/pull/9278?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=authelia) Report
Attention: Patch coverage is `79.16667%` with `5 lines` in your changes missing coverage. Please review.
> Project coverage is 70.16%. Comparing base [(`f7f541a`)](https://app.codecov.io/gh/authelia/authelia/commit/f7f541a1310452d41d6519a528ef7620291cd298?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=authelia) to head [(`cb43f9c`)](https://app.codecov.io/gh/authelia/authelia/commit/cb43f9c7063461346d868193e3a1a9ebb15e8fff?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=authelia).
> Report is 1 commits behind head on master.

| [Files with missing lines](https://app.codecov.io/gh/authelia/authelia/pull/9278?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=authelia) | Patch % | Lines |
|---|---|---|
| [internal/handlers/handler\_firstfactor\_passkey.go](https://app.codecov.io/gh/authelia/authelia/pull/9278?src=pr&el=tree&filepath=internal%2Fhandlers%2Fhandler_firstfactor_passkey.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=authelia#diff-aW50ZXJuYWwvaGFuZGxlcnMvaGFuZGxlcl9maXJzdGZhY3Rvcl9wYXNza2V5Lmdv) | 0.00% | [4 Missing and 1 partial :warning: ](https://app.codecov.io/gh/authelia/authelia/pull/9278?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=authelia) |

<details><summary>Additional details and impacted files</summary>


[![Impacted file tree graph](https://app.codecov.io/gh/authelia/authelia/pull/9278/graphs/tree.svg?width=650&height=150&src=pr&token=DaM9iyKU4C&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=authelia)](https://app.codecov.io/gh/authelia/authelia/pull/9278?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=authelia)

```diff
@@            Coverage Diff             @@
##           master    #9278      +/-   ##
==========================================
+ Coverage   70.14%   70.16%   +0.01%     
==========================================
  Files         400      400              
  Lines       37526    37547      +21     
  Branches     1098     1098              
==========================================
+ Hits        26324    26343      +19     
- Misses       9998    10000       +2     
  Partials     1204     1204              
```

| [Flag](https://app.codecov.io/gh/authelia/authelia/pull/9278/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=authelia) | Coverage Δ | |
|---|---|---|
| [backend](https://app.codecov.io/gh/authelia/authelia/pull/9278/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=authelia) | `71.74% <79.16%> (+0.01%)` | :arrow_up: |
| [frontend](https://app.codecov.io/gh/authelia/authelia/pull/9278/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=authelia) | `56.93% <ø> (-0.03%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=authelia#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Files with missing lines](https://app.codecov.io/gh/authelia/authelia/pull/9278?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=authelia) | Coverage Δ | |
|---|---|---|
| [internal/configuration/validator/webauthn.go](https://app.codecov.io/gh/authelia/authelia/pull/9278?src=pr&el=tree&filepath=internal%2Fconfiguration%2Fvalidator%2Fwebauthn.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=authelia#diff-aW50ZXJuYWwvY29uZmlndXJhdGlvbi92YWxpZGF0b3Ivd2ViYXV0aG4uZ28=) | `100.00% <100.00%> (ø)` | |
| [internal/handlers/webauthn.go](https://app.codecov.io/gh/authelia/authelia/pull/9278?src=pr&el=tree&filepath=internal%2Fhandlers%2Fwebauthn.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=authelia#diff-aW50ZXJuYWwvaGFuZGxlcnMvd2ViYXV0aG4uZ28=) | `100.00% <100.00%> (ø)` | |
| [internal/handlers/handler\_firstfactor\_passkey.go](https://app.codecov.io/gh/authelia/authelia/pull/9278?src=pr&el=tree&filepath=internal%2Fhandlers%2Fhandler_firstfactor_passkey.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=authelia#diff-aW50ZXJuYWwvaGFuZGxlcnMvaGFuZGxlcl9maXJzdGZhY3Rvcl9wYXNza2V5Lmdv) | `71.86% <0.00%> (-1.24%)` | :arrow_down: |

... and [4 files with indirect coverage changes](https://app.codecov.io/gh/authelia/authelia/pull/9278/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=authelia)

</details>
<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE