diff --git a/dev/rest.rst b/dev/rest.rst index 6a986f8b..94c7fb96 100644 --- a/dev/rest.rst +++ b/dev/rest.rst @@ -118,6 +118,15 @@ Debug Endpoints /rest/debug/... <../rest/debug.rst> +WebAuthn Endpoints +---------------- + +.. toctree:: + :maxdepth: 1 + :glob: + + ../rest/webauthn* + Noauth Endpoints ---------------- diff --git a/rest/noauth-webauthn.rst b/rest/noauth-webauthn.rst new file mode 100644 index 00000000..68d16555 --- /dev/null +++ b/rest/noauth-webauthn.rst @@ -0,0 +1,83 @@ +POST /rest/noauth/auth/webauthn-start +===================================== + +.. versionadded:: TODO 1.28.0 + +Begins a WebAuthn authentication ceremony. + +No parameters. The response is a JSON object +suitable as the parameter to ``navigator.credentials.get()`` +after base64url decoding binary values - i.e., +it contains a single attribute ``publicKey`` with the shape of +`PublicKeyCredentialRequestOptionsJSON +`_. + +Example response: + +.. code-block:: json + + { + "publicKey": { + "challenge": "tlGvyFeTIOEPWVJLWZuiRCBEl2dVnC0ZvWt4Epmk-rk", + "timeout": 120000, + "rpId": "localhost", + "allowCredentials": [ + { + "type": "public-key", + "id": "XW6tWsMNphd3rbESk4n9HEtd-h2MUdkHWQV6k2vuAzz8F9UoDTAVj3D-DWF_0z6q4R03mRJbtUPMDdNVr2Km-A", + "transports": ["usb"] + } + ], + "userVerification": "discouraged" + } + } + + +POST /rest/noauth/auth/webauthn-finish +====================================== + +.. versionadded:: TODO 1.28.0 + +Finishes a WebAuthn authentication ceremony, logging the user into the GUI if successful. + +The request body is a JSON object containing two attributes: +a required ``credential`` attribute with the shape of +`AuthenticationResponseJSON +`_ - i.e., +a ``PublicKeyCredential`` object +(the result of calling ``navigator.credentials.get()``) +with base64url encoded binary values, +and an optional Boolean ``stayLoggedIn`` attribute. +If ``stayLoggedIn`` is ``false`` or absent, the returned session cookie will expire with the browser session, +if ``true`` the cookie will persist for a time after the browser session ends. + +The response on success is status 204 (No content) with no response body +and a ``Set-Cookie`` header containing the session cookie. + +Example request: + +.. code-block:: json + + { + "credential": { + "type": "public-key", + "id": "XW6tWsMNphd3rbESk4n9HEtd-h2MUdkHWQV6k2vuAzz8F9UoDTAVj3D-DWF_0z6q4R03mRJbtUPMDdNVr2Km-A", + "rawId": "XW6tWsMNphd3rbESk4n9HEtd-h2MUdkHWQV6k2vuAzz8F9UoDTAVj3D-DWF_0z6q4R03mRJbtUPMDdNVr2Km-A", + "authenticatorAttachment": "cross-platform", + "response": { + "clientDataJSON": "eyJjaGFsbGVuZ2UiOiJ0bEd2eUZlVElPRVBXVkpMV1p1aVJDQkVsMmRWbkMwWnZXdDRFcG1rLXJrIiwib3JpZ2luIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6ODM4NCIsInR5cGUiOiJ3ZWJhdXRobi5nZXQifQ", + "authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MBAAABPA", + "signature": "MEUCIQDjTizDIioXQFPrMih8UaAAo9R6sdYCMedrBxpSeYkd2wIgIMI-5h_CHJHa04EFN4HPsFO4nLCW8XR3iu5cRu5X4-w", + "userHandle": null + }, + "clientExtensionResults": {} + }, + "stayLoggedIn": true + } + +Example response headers (other headers omitted): + +.. code-block:: + + HTTP/1.1 204 No Content + Set-Cookie: sessionid-STLYOU4=banm5zZNHRAzJXmHUwWLjZmoJ9p4huCGscVSxnbXjgSR6CLuES3vQr2u5uX3Zt43; Path=/; Max-Age=604800; Secure diff --git a/rest/webauthn.rst b/rest/webauthn.rst new file mode 100644 index 00000000..bed75c07 --- /dev/null +++ b/rest/webauthn.rst @@ -0,0 +1,285 @@ +.. _rest-webauthn-registration: + +POST /rest/webauthn/register-start +---------------------------------- + +.. versionadded:: TODO 1.28.0 + +``POST .../register-start`` begins a WebAuthn registration ceremony +and ``POST .../register-finish`` finishes it, +adding the newly created credential to a list of pending credentials. +Pending credentials may be persisted +by including them in a request to ``POST /rest/webauthn/state``. + +``POST .../register-start`` takes no parameters and returns a JSON object +suitable as the parameter to ``navigator.credentials.create()`` +after base64url decoding binary values - i.e., +it contains a single attribute ``publicKey`` with the shape of +`PublicKeyCredentialCreationOptionsJSON +`_. + +Example response: + +.. code-block:: json + + { + "publicKey": { + "rp": { + "name": "Syncthing @ DEVICENAME", + "id": "localhost" + }, + "user": { + "name": "asdf", + "displayName": "asdf", + "id": "4_KyxKWr6x2KvB3GGHYLkjmn1M6xTip5ITZQUgaUzJW5e023M0j4NBOkgR-4aQarM7RRCv7TGkmOD53kQBPhLQ" + }, + "challenge": "VopAfwRL52Jc1E_H0yi-kEmb59s4IfJ1UN2zSjY_5CA", + "pubKeyCredParams": [ + { "type": "public-key", "alg": -7 }, + { "type": "public-key", "alg": -35 }, + { "type": "public-key", "alg": -36 }, + { "type": "public-key", "alg": -257 }, + { "type": "public-key", "alg": -258 }, + { "type": "public-key", "alg": -259 }, + { "type": "public-key", "alg": -37 }, + { "type": "public-key", "alg": -38 }, + { "type": "public-key", "alg": -39 }, + { "type": "public-key", "alg": -8 } + ], + "timeout": 300000, + "authenticatorSelection": { + "requireResidentKey": false, + "userVerification": "preferred" + } + } + } + + +POST /rest/webauthn/register-finish +----------------------------------- + +.. versionadded:: TODO 1.28.0 + +``POST .../register-start`` begins a WebAuthn registration ceremony +and ``POST .../register-finish`` finshes it, +adding the newly created credential to a list of pending credentials. +Pending credentials may be persisted +by including them in a request to ``POST /rest/webauthn/state``. + +``POST .../register-finish`` takes a request body with the shape of +`RegistrationResponseJSON +`_ - i.e., +a ``PublicKeyCredential`` object +(the result of calling ``navigator.credentials.create()``) +with base64url encoded binary values. +It returns a JSON representation of the pending registered credential, +which can be added as an element of the ``credentials`` array +in the JSON body of a `/rest/webauthn/state `_ request. + + +.. note:: + WebAuthn credentials are "config-like" + and are managed in the "GUI" section of the settings GUI. + In order to follow the convention of changes being pending + until the user presses the "save" button, + this API call does not yet permanently save the returned credential. + To permanently save the credential and activate it as a login option, + the returned object must be saved by appending it to the ``credentials`` list + in the JSON body of a `/rest/webauthn/state `_ request. + + +Example request: + +.. code-block:: json + + { + "type": "public-key", + "id": "VxT1FCv2nrNwCTGmOnNDoUAY3p6RJyvBzF7y-dsD5Ll73Mve76m9okIX7C5cDf2elKxtBRRmcnMUuVnPk3TUuA", + "rawId": "VxT1FCv2nrNwCTGmOnNDoUAY3p6RJyvBzF7y-dsD5Ll73Mve76m9okIX7C5cDf2elKxtBRRmcnMUuVnPk3TUuA", + "authenticatorAttachment": "cross-platform", + "response": { + "clientDataJSON": "eyJjaGFsbGVuZ2UiOiJWb3BBZndSTDUySmMxRV9IMHlpLWtFbWI1OXM0SWZKMVVOMnpTallfNUNBIiwib3JpZ2luIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6ODM4NCIsInR5cGUiOiJ3ZWJhdXRobi5jcmVhdGUifQ", + "attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjESZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAABAAAAAAAAAAAAAAAAAAAAAAAQFcU9RQr9p6zcAkxpjpzQ6FAGN6ekScrwcxe8vnbA-S5e9zL3u-pvaJCF-wuXA39npSsbQUUZnJzFLlZz5N01LilAQIDJiABIVgg1ZEbVe7_o93_XuuRl98qhHa-cmsJrpL_Rw5wrpEqgqIiWCCpp0NlSL-xBR9lDc5Th5Y1WsGLs0vS5jgjxh_kS1D_0Q", + "transports": ["nfc", "usb"] + }, + "clientExtensionResults": {} + } + +Example response: + +.. code-block:: json + + { + "id": "VxT1FCv2nrNwCTGmOnNDoUAY3p6RJyvBzF7y-dsD5Ll73Mve76m9okIX7C5cDf2elKxtBRRmcnMUuVnPk3TUuA==", + "rpId": "localhost", + "nickname": "", + "publicKeyCose": "pQECAyYgASFYINWRG1Xu_6Pd_17rkZffKoR2vnJrCa6S_0cOcK6RKoKiIlggqadDZUi_sQUfZQ3OU4eWNVrBi7NL0uY4I8Yf5EtQ_9E=", + "signCount": 4, + "transports": ["nfc", "usb"], + "requireUv": false, + "createTime": "2024-07-21T15:24:01+02:00", + "lastUseTime": "2024-07-21T15:24:01+02:00" + } + + +.. _rest-webauthn-state: + +GET /rest/webauthn/state +------------------------ + +Returns the state of currently registered WebAuthn credentials. +The credential data model is described `below `_. + +.. versionadded:: TODO 1.28.0 + +Example ``GET`` response: + +.. code-block:: json + + { + "credentials": [ + { + "id": "cTVm-CWdvbMOX7v4QdUxJgPZ5TWpFuliLDWNcI9chOw02DBJcZjmvHDOwpGEwxS6Lk6H8eikYbystBghaJuq-g==", + "rpId": "localhost", + "nickname": "Security key", + "publicKeyCose": "pQECAyYgASFYIC9CP0p82dtJiRKYfUGSYeVaccOsNAmYgIz-EAl1GzbyIlggtcbhDVA8bUpjK_GH3QpGL9i_y9GfoTM1pg0jyEBf88M=", + "signCount": 644, + "transports": ["nfc", "usb"], + "requireUv": true, + "createTime": "2024-07-13T13:58:07Z", + "lastUseTime": "2024-07-21T12:55:43Z" + }, + { + "id": "4gvuaMwVUnv6a0cNRzUm4hkbeTgVsf7HUBbgXBoSB9A57AagRbZvWCUMaBjroYhnWBubRq_29uo4CGFtfWwpdg==", + "rpId": "localhost", + "nickname": "", + "publicKeyCose": "pQECAyYgASFYIGxYkCaHAkelm7Mu5JGtaQFdcAPqlWlhOFuGah4eom7KIlggtvPzU9tMFtxElKqr3zXO2YZAlIKAbUOvbTA93tx39Rc=", + "signCount": 115, + "transports": ["nfc", "usb"], + "requireUv": false, + "createTime": "2024-07-13T14:07:20Z", + "lastUseTime": "2024-07-13T15:36:44Z" + } + ] + } + + +POST /rest/webauthn/state +------------------------- + +.. versionadded:: TODO 1.28.0 + +Updates the WebAuthn state to match the body, +except the following rules are applied to the new ``credentials`` value: + +- Each item must have an ``id`` that already exists in the currently stored ``credentials`` value, + or in the list of pending credentials stored by `/rest/webauthn/register-finish `_. + Items with any other ``id`` are ignored. +- For each already existing item, all attributes except ``nickname`` and ``requireUv`` are ignored. + +The credential data model is described in the `next section `_. + +Assuming that ``id: "VxT1FCv2..."`` +was previously returned from `/rest/webauthn/register-finish `_ +as in the example above, +this example request would change the nickname of "Security key" to "My security key", +delete the credential with ``id: "4gvuaMwV..."`` +and persist the pending credential with the nickname "New security key": + +.. code-block:: json + + { + "credentials": [ + { + "id": "cTVm-CWdvbMOX7v4QdUxJgPZ5TWpFuliLDWNcI9chOw02DBJcZjmvHDOwpGEwxS6Lk6H8eikYbystBghaJuq-g==", + "rpId": "localhost", + "nickname": "My security key", + "publicKeyCose": "pQECAyYgASFYIC9CP0p82dtJiRKYfUGSYeVaccOsNAmYgIz-EAl1GzbyIlggtcbhDVA8bUpjK_GH3QpGL9i_y9GfoTM1pg0jyEBf88M=", + "signCount": 644, + "transports": ["nfc", "usb"], + "requireUv": true, + "createTime": "2024-07-13T13:58:07Z", + "lastUseTime": "2024-07-21T12:55:43Z" + }, + { + "id": "VxT1FCv2nrNwCTGmOnNDoUAY3p6RJyvBzF7y-dsD5Ll73Mve76m9okIX7C5cDf2elKxtBRRmcnMUuVnPk3TUuA==", + "rpId": "localhost", + "nickname": "New security key", + "publicKeyCose": "pQECAyYgASFYINWRG1Xu_6Pd_17rkZffKoR2vnJrCa6S_0cOcK6RKoKiIlggqadDZUi_sQUfZQ3OU4eWNVrBi7NL0uY4I8Yf5EtQ_9E=", + "signCount": 4, + "transports": ["nfc", "usb"], + "requireUv": false, + "createTime": "2024-07-21T15:24:01+02:00", + "lastUseTime": "2024-07-21T15:24:01+02:00" + } + ] + } + + +.. _rest-webauthn-credential: + +The credential data model +------------------------- + +Items in the ``credentials`` field of the `WebAuthn state `_ have the following attributes: + +- ``id`` + + The base64url-encoded `credential ID `_ of the credential. + This is created by the authenticator and cannot be changed. + +- ``rpId`` + + The value of the :stconf:opt:`gui.webauthnRpId` setting in effect at the time this credential was created. + This is set automatically and cannot be changed. + + If :stconf:opt:`gui.webauthnRpId` is changed after creating a credential, + the credential can no longer be used unless the :stconf:opt:`gui.webauthnRpId` value is restored. + This attribute is used in the settings GUI to highlight credentials that cannot currently be used + and show what :stconf:opt:`gui.webauthnRpId` to restore to in order to make them usable again. + +- ``publicKeyCose`` + + The base64url-encoded public key of the credential, in `COSE_Key format `_. + This is created by the authenticator and cannot be changed. + +- ``signCount`` + + The `signature counter `_ of the credential. + A decrease in the signature counter may indicate that the credential has been cloned. + Syncthing displays a warning if this happens, but does not otherwise act on it. + +- ``nickname`` + + A user-chosen nickname for the credential. + If empty or not set, the GUI will use the abbreviated credential ID (``id``) as the name of the credential. + This can be edited in the settings GUI. + +- ``requireUv`` + + If set to ``true``, this credential requires `User Verification (UV) `_, + for example a PIN or a biometric. + This means that logging in with this credential is two-factor authentication (2FA): + something you have (the credential private key) + combined with something you know (a PIN) or something you are (a biometric). + + This can be enabled or disabled in the settings GUI, see :ref:`webauthn-require2fa`. + +- ``transports`` + + A list of hints the browser may use to determine how to communicate with the authenticator + that holds the private key for this credential - + for example, this may be ``["nfc", "usb"]`` if the credential is stored on a USB security key + or ``["hybrid", "internal"]`` if the credential is stored on a smartphone or laptop. + + This is set automatically and cannot not be changed. + Changing it could make the credential unusable, + since the browser might conclude it has no way to communicate with the authenticator + if none of the transports listed here is available on the platform. + If this happens, you can attempt to make the credential usable again by deleting the attribute. + +- ``createTime`` and ``lastUseTime`` + + Timestamps recording when this credential was created and when it was last used to log in to the GUI. + Used only to help the user identify and distinguish credentials in the GUI; + not used for any security decisions. diff --git a/users/config.rst b/users/config.rst index 1d0c7d8f..5f915581 100644 --- a/users/config.rst +++ b/users/config.rst @@ -144,6 +144,9 @@ The following shows an example of a default configuration file (IDs will differ)
127.0.0.1:8384
k1dnz1Dd0rzTBjjFFh7CXPnrF12C49B1 default + 4_KyxKWr6x2KvB3GGHYLkjmn1M6xTip5ITZQUgaUzJW5e023M0j4NBOkgR-4aQarM7RRCv7TGkmOD53kQBPhLQ== + localhost + https://localhost:8384 @@ -842,6 +845,8 @@ set on the ``gui`` element: If not ``true``, the GUI and API will not be started. +.. _gui-tls: + .. option:: gui.tls :aliases: gui.useTLS @@ -943,6 +948,43 @@ The following child elements may be present: won't see browser popups prompting for username and password. +.. option:: gui.webauthnUserId + + .. versionadded:: TODO 1.28.0 + + The base64url-encoded `user handle `_ + to use when registering WebAuthn credentials (passkeys). + This is automatically set and should usually not need to be changed. + Authenticators may use this to overwrite existing credentials + with the same combination of user handle and RP ID when creating a new credential. + +.. option:: gui.webauthnRpId + + .. versionadded:: TODO 1.28.0 + + The `RP ID `_ + to use for WebAuthn (passkey) registration and authentication. + If not set, this defaults to ``localhost``. + + The RP ID is a domain name and must be the same as, or a parent domain of, the + domain where the Syncthing GUI is hosted. The RP ID cannot be a raw IP address. + + When you register a new WebAuthn credential (passkey), it gets tied to this RP ID. + If you change the RP ID, any existing keys tied to the previous RP ID will stop working. + +.. option:: gui.webauthnOrigin + + .. versionadded:: TODO 1.28.0 + + The scheme, host and port of the address where WebAuthn logins will take place. + If not set, this defaults to ``https://localhost:8384``. + WebAuthn registration and login will only work if the GUI is hosted at exactly this host address + (excluding the path, query string and hash fragment). + + In general, this should be set to ``https://:
``, + omitting the port if it is ``443``. + + LDAP Element ------------ diff --git a/users/index.rst b/users/index.rst index 4e7f0814..4c888c91 100644 --- a/users/index.rst +++ b/users/index.rst @@ -14,6 +14,7 @@ Usage introducer guilisten ldap + webauthn tuning metrics diff --git a/users/webauthn.rst b/users/webauthn.rst new file mode 100644 index 00000000..9de0f350 --- /dev/null +++ b/users/webauthn.rst @@ -0,0 +1,92 @@ +WebAuthn (Passkey) Authentication +================================= + +Syncthing can be configured to allow GUI authentication using `WebAuthn `_ (passkeys) +as an alternative to a password. +WebAuthn offers a passwordless login experience that some users may find preferable. + +To enable WebAuthn, the GUI must use HTTPS (see config :ref:`gui.tls `) +and must be served at exactly the address ``https://localhost:8384``, +unless configured otherwise as described in :ref:`webauthn-custom-gui-address`. + +WebAuthn authentication will be enabled if you have at least one `credential`, also called a `passkey`, registered. +A credential is a public-private key pair that is stored on an `authenticator`, +which could be an external security key, a smartphone, or built into your computer. +Some platforms might sync platform credentials between devices signed into the same cloud account. + +Use the settings GUI to register a new credential. + +.. note:: + We use the term "passkey" more inclusively here than usual. + A "passkey" is a credential that enables "username-less login", + which identifies the user automatically without needing them to enter a username first. + For technical reasons, this is incompatible with a cryptographic trick commonly used by external security keys + to support an unlimited number of credentials without consuming storage space. + Therefore, a "passkey" generally must consume storage space on the authenticator. + + However, because a Syncthing instance has only a single user account, + we can enable "username-less login" without preventing the unlimited storage trick. + We therefore sometimes refer to WebAuthn credentials in Syncthing as "passkeys", + even though they do not consume storage space on external security keys like passkeys generally do. + + +.. _webauthn-require2fa: + +The 2FA setting +--------------- + +Each credential (passkey) has a checkbox setting labeled "2FA" in the GUI. +When checked, Syncthing will enforce that this credential uses +`two-factor authentication (2FA) `_. +The technical name for this is `User Verification (UV) `_. + +For example: + +- If the credential is stored on a smartphone, + the phone may prompt for screen unlock to authenticate you to the phone before unlocking the passkey. + This could be a PIN, swipe pattern, fingerprint, face recognition + or something else, according to the phone's settings. + + Smartphones typically always require 2FA, + so this setting may not make a noticeable difference for smartphone-based credentials. + +- An external security key may prompt for a PIN configured on the security key, + or use a built-in fingerprint reader. + With the 2FA setting disabled, you would only need to plug in the security key + and usually press a button on it, + but would need no additional factor beyond possessing the security key. + + Some older models of security keys do not support 2FA. + +.. note:: + + No biometrics, PIN or other data is sent to the server - + Syncthing does **not** collect or store biometric information. + Instead, the second factor is only verified locally by your authenticator + (for example, a USB security key or a smartphone) before unlocking the passkey for login. + +If you have some credentials with 2FA enabled and some with 2FA disabled, +you might get prompted for 2FA even when using a credential that doesn't require it. +This is because Syncthing doesn't know beforehand which credential you're going to use, +so it needs to pessimistically request 2FA in case it is required for the credential you choose. + + +.. _webauthn-custom-gui-address: + +Customizing the GUI address +--------------------------- + +The GUI address can be customized via the advanced GUI settings +:stconf:opt:`gui.webauthnRpId` and :stconf:opt:`gui.webauthnOrigin`. + +If you access the GUI at some other address than ``https://localhost:``, +you'll need to set the ``webauthnRpId`` setting to the domain name or a parent domain name of that address +and ``webauthnOrigin`` to the full address including scheme and port (except the default port), but not path. +For example, if you serve the GUI at the address ``https://syncthing.mydomain.org:8443/syncthing/gui``, +set ``webauthnRpId`` to one of ``mydomain.org`` or ``syncthing.mydomain.org`` +and set ``webauthnOrigin`` to ``https://syncthing.mydomain.org:8443``. + +For hostnames other than ``localhost`` you will also need an HTTPS certificate your browser considers valid. +For guidance on how to create or obtain one, see for example +`OpenSSL Cookbook `_ +or `Let's Encrypt `_.