diff --git a/ecosystem/sep-0045.md b/ecosystem/sep-0045.md new file mode 100644 index 000000000..3fef0e137 --- /dev/null +++ b/ecosystem/sep-0045.md @@ -0,0 +1,490 @@ +## Preamble + +``` +SEP: 0045 +Title: Stellar Web Authentication for Contract Accounts +Author: Philip Liu <@philipliu>, Marcelo Salloum <@marcelosalloum>, Leigh McCulloch <@leighmcculloch> +Track: Standard +Status: Draft +Created: 2024-10-08 +Updated: 2024-10-08 +Version: 0.1.0 +Discussion: TBA +``` + +## Simple Summary + +This SEP defines the standard way for clients such as wallets or exchanges to create authenticated web sessions on +behalf of a user who holds a contract account. A wallet may want to authenticate with any web service which requires a +contract account ownership verification, for example, to upload KYC information to an anchor in an authenticated way as +described in [SEP-12](sep-0012.md). + +This SEP is based on [SEP-10](sep-0010.md), but does not replace it. This SEP only supports `C` (contract) accounts. +SEP-10 only `G` and `M` accounts. Services wishing to support all accounts should implement both SEPs. + +## Definitions + +- **Address**: An identifier representing a possible account on the Stellar network. Addresses begin with specific + prefixes like G for public keys (accounts controlled by secret keys), M for muxed accounts and C for contract + accounts. +- **Account**: A stateful entity on the Stellar network that can hold balances. This can refer to a Stellar account + (`G...` address, or the `G...` base address of an `M...` address) or Soroban contract that abstracts an account + (`C...` addresses). + +## Abstract + +This protocol is a variation of mutual challenge-response, which uses Stellar authorization entries to encode challenges +and responses. + +It involves the following components: + +- A **Home Domain**: a domain hosting a [SEP-1 stellar.toml](sep-0001.md) containing a `WEB_AUTH_FOR_CONTRACTS_ENDPOINT` + (URL), `WEB_AUTH_CONTRACT_ID`, and `SIGNING_KEY` (`G...`). + - The `SIGNING_KEY` from this domain is referred to as the **Home Domain Address** in this document. +- A **Server**: a server providing the `WEB_AUTH_FOR_CONTRACTS_ENDPOINT` that implements the GET and POST operations + discussed in this document. The server's domain may be the **Home Domain**, a sub-domain of the **Home Domain**, or a + different domain. +- A **Client Account**: the account being authenticated. + - A Stellar contract account (`C...`). +- A **Client**: the software used by the holder of the **Client Account** being authenticated by the **Server**. +- A **Client Domain** (optional): a domain hosting a [SEP-1 stellar.toml](sep-0001.md) containing a `SIGNING_KEY` used + for [Verifying the Client Domain](#verifying-the-client-domain) + - The `SIGNING_KEY` from this domain is referred to as the **Client Domain Address** in this document. +- A **Web Auth Contract**: a contract that implements the `web_auth_verify` function as described in this document. The + contract must be deployed at the `WEB_AUTH_CONTRACT_ID` address specified in the **Server**'s `stellar.toml`. + +The discovery flow is as follows: + +1. The **Client** retrieves the `stellar.toml` from the **Home Domain** in accordance with + [SEP-1 stellar.toml](sep-0001.md). +1. The **Client** looks up the `WEB_AUTH_FOR_CONTRACTS_ENDPOINT`, `WEB_AUTH_CONTRACT_ID` and `SIGNING_KEY` (i.e. **Home + Domain Address**) from the `stellar.toml`. + +The authentication flow is as follows: + +1. The **Client** requests a unique [`challenge`](#challenge) from the **Server** which includes a list of authorization + entries XDR-encoded strings +1. If the request contains a `client_domain` parameter, the **Server** may fetch the **Client Domain Address** and + generate the challenge with an additional authorization entry as described in the [Response](#response) section. +1. The **Server** responds with the challenge +1. The **Client** verifies that each authorization entry does not contain any sub-invocations +1. The **Client** verifies that the `contract_address` in each authorization entry matches the `WEB_AUTH_CONTRACT_ID` + from the **Server**'s `stellar.toml` +1. The **Client** verifies that the `function_name` in each authorization entry is `web_auth_verify` +1. The **Client** verifies that the `args` map in each authorization entry match the expected values and is the same + across all authorization entries: + 1. The `account` value matches the **Client Account** address + 1. The `home_domain` value matches the **Home Domain** + 1. The `home_domain_address` value matches the **Home Domain Address** + 1. The `web_auth_domain` value matches the **Server**'s domain + 1. If the **Client** included a `client_domain` in the request: + 1. The `client_domain` value matches the **Client**'s domain + 1. The `client_domain_address` value matches the **Client Domain Address** +1. The **Client** verifies that there is an authorization entry where `credentials.address.address` is the **Home Domain + Address** and contains a valid signature from the **Home Domain Address** +1. The **Client** signs the authorization entry where `credentials.address.address` is the **Client Account** using the + secret key(s) of the signer(s) for the **Client Account** + - Note: **Client** signatures may not be required if the contract's `__check_auth` implementation does not require + them +1. The **Client** obtains a signature from the **Client Domain Address** for the authorization entry where + `credentials.address.address` is the **Client Domain Address** if the **Client** included a client domain in the + request +1. The **Client** simulates the transaction with the signed authorization entries and verifies the following to ensure + the transaction does not have any unintended side effects: + - The transaction's ledger footprint `read_write` set contains only `contract_data` entries where: + - The `contract` is the **Client Account** address, and the `key` is `ledger_key_nonce`. + - The `contract` is the **Home Domain Address**, and the `key` is `ledger_key_nonce`. + - (Optional) if an authorization entry for the **Client Domain Address** was present in the challenge, the + `contract` is the **Client Domain Address**, and the `key` is `ledger_key_nonce`. +1. The **Client** submits the signed authorization entries back to the **Server** using [`token`](#token) endpoint +1. The **Server** extracts the arguments from the authorization entries returned by the client +1. The **Server** verifies that the `contract_address` in each authorization entry matches the `WEB_AUTH_CONTRACT_ID` + from the **Server**'s `stellar.toml` +1. The **Server** verifies that the `function_name` in each authorization entry is `web_auth_verify` +1. The **Server** verifies that the `args` map in each authorization entry match the expected values and are the same + across all authorization entries: + 1. The `account` value matches the **Client Account** address + 1. The `home_domain` value matches the **Home Domain** + 1. The `home_domain_address` value matches the **Home Domain Address** + 1. The `web_auth_domain` value matches the **Server**'s domain + 1. The `client_domain_address` value matches the **Client Domain Address** if the **Client** included a + `client_domain` in the request, otherwise it is not present +1. (Optional) The **Server** verifies that the `nonce` argument is the same across all authorization entries and is + unique +1. The **Server** verifies that there is an authorization entry where `credentials.address.address` is the **Home Domain + Address** and contains a valid signature from the **Home Domain Address** +1. The **Server** verifies that there is an authorization entry where `credentials.address.address` is the **Client + Account** address +1. The **Server** verifies that there is an authorization entry where `credentials.address.address` is the **Client + Domain Address** if the arguments included a `client_domain_address` +1. The **Server** constructs a transaction with a single Invoke Host Function operation where the contract address is + `WEB_AUTH_CONTRACT_ID` and the function is `web_auth_verify` using the previously extracted arguments and the + authorization entries returned by the client +1. If the simulation succeeds, the **Server** responds with a [JWT](https://jwt.io) that represents the authenticated + session + +The flow achieves several things: + +- Both **Client** and **Server** can be implemented using well-established Stellar libraries +- The **Client** can verify that the **Server** holds the secret key to the **Home Domain Address** +- The **Server** can verify that the **Client** is authorized by the contract account to create an authenticated session +- The **Server** can choose its own timeout for the authenticated session +- The **Server** can choose to include other application-specific claims + +## Authentication Endpoint + +The organization with a **Home Domain** indicates that it supports authentication via this protocol by specifying +`WEB_AUTH_FOR_CONTRACTS_ENDPOINT` in their [`stellar.toml`](sep-0001.md) file. This is how a wallet knows where to find +the **Server**. A **Server** is required to implement the following behavior for the web authentication endpoint: + +- [`GET `](#challenge): request a challenge (step 1) +- [`POST `](#token): exchange a signed challenge for session JWT (step 2) + +## Cross-Origin Headers + +Valid CORS headers are necessary to allow web clients from other sites to use the endpoints. The following HTTP header +must be set for all authentication endpoints, including error responses. + +``` +Access-Control-Allow-Origin: * +``` + +In order for browsers-based wallets to validate the CORS headers, as +[specified by W3C](https://www.w3.org/TR/cors/#preflight-request), the preflight request (OPTIONS request) must be +implemented in all the endpoints that support Cross-Origin. + +### Challenge + +This endpoint must respond with authorization entries to be signed by the **Client**. The **Client** signs the entries +using standard Stellar libraries and submit it to [`token`](#token) endpoint to prove that it controls the **Client +Account**. This approach is compatible with hardware wallets such as Ledger. The **Client Application** must also verify +the server's signature to be sure the challenge is signed by the **Home Domain Address**, that the home domain argument +in the function invocation is the **Home Domain**, and that the web auth domain argument in the function invocation is +the **Server** domain. + +#### Request + +``` +GET +``` + +##### Request Parameters: + +| Name | Type | Description | +| --------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `account` | `C...` string | The **Client Account** address (`C...`) that the **Client** wishes to authenticate with the **Server**. | +| `home_domain` | string | A **Home Domain**. Servers that generate tokens for multiple **Home Domain**s can use this parameter to identify which home domain the **Client** hopes to authenticate with. | +| `client_domain` | string | (optional) a **Client Domain**. Supplied by **Clients** that intend to verify their domain in addition to the **Client Account**. See [Verifying the Client Domain](#verifying-the-client-domain). **Servers** should ignore this parameter if the **Server** does not support **Client Domain** verification, or the **Server** does not support verification for the specific **Client Domain** included in the request. | + +Example: + +``` +GET https://auth.example.com/?account=CCIBUCGPOHWMMMFPFTDWBSVHQRT4DIBJ7AD6BZJYDITBK2LCVBYW7HUQ&home_domain=example.com +``` + +#### Response + +##### Success + +On success the endpoint must return `200 OK` HTTP status code and a JSON object with the following fields: + +- `authorization_entries`: an XDR-encoded array of `SorobanAuthorizationEntry`s. It contains an entry for the **Client + Account** in addition to a signed entry from the **Home Domain Address** and optionally the **Client Domain Address**. + The entries are not guaranteed to be in any specific order. + +Each entry's `root_invocation` function is a `contract_fn` with no sub-invocations and the following values: + +- `contract_address` is the `WEB_AUTH_CONTRACT_ID` from the **Server**'s `stellar.toml` +- `function_name` is `web_auth_verify` +- An `args` map containing the following Symbol to String pairs: + - `account` matches the `account` from the request + - `home_domain` matches the `home_domain` from the request + - `home_domain_address` matches the **Home Domain Address** + - `web_auth_domain` matches the **Server**'s domain + - `client_domain` matches the **Client**'s domain if the **Client** included a `client_domain` in the request and the + server supports [Verifying the Client Domain](#verifying-the-client-domain) + - `client_domain_address` matches the **Client Domain Address** if the **Client** included a `client_domain` in the + request and the server supports [Verifying the Client Domain](#verifying-the-client-domain) + - (Optional) `nonce` is a unique value that is the same across all authorization entries +- `network_passphrase`: (optional but recommended) Stellar network passphrase used by the **Server**. This allows a + **Client** to verify that it's using the correct passphrase when signing and is useful for identifying when a + **Client** or **Server** have been configured incorrectly. + +Example: + +```json +{ + "authorization_entries": "AAAAAQAAAAHDwFN8u4knndhlRcZmGC28sL8G7WJnadYNY88ZwAICiAhjuHNQ4DVQAAAAAAAAAAEAAAAAAAAAAX6lC8GUFNGjTHdg8uQyeJvi1taYAbI3H1Ss91/oUNawAAAAD3dlYl9hdXRoX3ZlcmlmeQAAAAABAAAAEQAAAAEAAAAFAAAADwAAAAdhY2NvdW50AAAAAA4AAAA4Q0RCNEFVMzRYT0VTUEhPWU1WQzRNWlFZRlc2TEJQWUc1VlJHTzJPV0JWUjQ2R09BQUlCSVE0R0QAAAAPAAAAC2hvbWVfZG9tYWluAAAAAA4AAAAObG9jYWxob3N0OjgwODAAAAAAAA8AAAATaG9tZV9kb21haW5fYWRkcmVzcwAAAAAOAAAAOEdESkxCWVlLTUNYTlZWTkFCT0U2Nk5ZWFFHSUE1QUM1RDIyM1oyS0Y2WkVZSzRVQkNBN0ZLTFRHAAAADwAAAAVub25jZQAAAAAAAA4AAAAKMjA2MDIxNDExNQAAAAAADwAAAA93ZWJfYXV0aF9kb21haW4AAAAADgAAAA5sb2NhbGhvc3Q6ODA4MAAAAAAAAAAAAAEAAAAAAAAAANKw4wpgrtrVoAuJ7zcXgZAOgF0etbzpRfZJhXKBED5VHSLrhXQ/QVwAAaLlAAAAEAAAAAEAAAABAAAAEQAAAAEAAAACAAAADwAAAApwdWJsaWNfa2V5AAAAAAANAAAAINKw4wpgrtrVoAuJ7zcXgZAOgF0etbzpRfZJhXKBED5VAAAADwAAAAlzaWduYXR1cmUAAAAAAAANAAAAQDUYC2mj/RW0NcGfY66p+eltjwflRSmwc8ZZ7as2HAUvu4k/bY2ZHIhhS6M7Ufx74XmV9QlxHPv6vXUA/j9uRAwAAAAAAAAAAX6lC8GUFNGjTHdg8uQyeJvi1taYAbI3H1Ss91/oUNawAAAAD3dlYl9hdXRoX3ZlcmlmeQAAAAABAAAAEQAAAAEAAAAFAAAADwAAAAdhY2NvdW50AAAAAA4AAAA4Q0RCNEFVMzRYT0VTUEhPWU1WQzRNWlFZRlc2TEJQWUc1VlJHTzJPV0JWUjQ2R09BQUlCSVE0R0QAAAAPAAAAC2hvbWVfZG9tYWluAAAAAA4AAAAObG9jYWxob3N0OjgwODAAAAAAAA8AAAATaG9tZV9kb21haW5fYWRkcmVzcwAAAAAOAAAAOEdESkxCWVlLTUNYTlZWTkFCT0U2Nk5ZWFFHSUE1QUM1RDIyM1oyS0Y2WkVZSzRVQkNBN0ZLTFRHAAAADwAAAAVub25jZQAAAAAAAA4AAAAKMjA2MDIxNDExNQAAAAAADwAAAA93ZWJfYXV0aF9kb21haW4AAAAADgAAAA5sb2NhbGhvc3Q6ODA4MAAAAAAAAA==", + "network_passphrase": "Test SDF Network ; September 2015" +} +``` + +You can examine the example challenge in the +[XDR Viewer](https://lab.stellar.org/xdr/view?$=network$id=testnet&label=Testnet&horizonUrl=https:////horizon-testnet.stellar.org&rpcUrl=https:////soroban-testnet.stellar.org&passphrase=Test%20SDF%20Network%20/;%20September%202015;&xdr$blob=AAAAAQAAAAHDwFN8u4knndhlRcZmGC28sL8G7WJnadYNY88ZwAICiAhjuHNQ4DVQAAAAAAAAAAEAAAAAAAAAAX6lC8GUFNGjTHdg8uQyeJvi1taYAbI3H1Ss91//oUNawAAAAD3dlYl9hdXRoX3ZlcmlmeQAAAAABAAAAEQAAAAEAAAAFAAAADwAAAAdhY2NvdW50AAAAAA4AAAA4Q0RCNEFVMzRYT0VTUEhPWU1WQzRNWlFZRlc2TEJQWUc1VlJHTzJPV0JWUjQ2R09BQUlCSVE0R0QAAAAPAAAAC2hvbWVfZG9tYWluAAAAAA4AAAAObG9jYWxob3N0OjgwODAAAAAAAA8AAAATaG9tZV9kb21haW5fYWRkcmVzcwAAAAAOAAAAOEdESkxCWVlLTUNYTlZWTkFCT0U2Nk5ZWFFHSUE1QUM1RDIyM1oyS0Y2WkVZSzRVQkNBN0ZLTFRHAAAADwAAAAVub25jZQAAAAAAAA4AAAAKMjA2MDIxNDExNQAAAAAADwAAAA93ZWJfYXV0aF9kb21haW4AAAAADgAAAA5sb2NhbGhvc3Q6ODA4MAAAAAAAAAAAAAEAAAAAAAAAANKw4wpgrtrVoAuJ7zcXgZAOgF0etbzpRfZJhXKBED5VHSLrhXQ//QVwAAaLlAAAAEAAAAAEAAAABAAAAEQAAAAEAAAACAAAADwAAAApwdWJsaWNfa2V5AAAAAAANAAAAINKw4wpgrtrVoAuJ7zcXgZAOgF0etbzpRfZJhXKBED5VAAAADwAAAAlzaWduYXR1cmUAAAAAAAANAAAAQDUYC2mj//RW0NcGfY66p+eltjwflRSmwc8ZZ7as2HAUvu4k//bY2ZHIhhS6M7Ufx74XmV9QlxHPv6vXUA//j9uRAwAAAAAAAAAAX6lC8GUFNGjTHdg8uQyeJvi1taYAbI3H1Ss91//oUNawAAAAD3dlYl9hdXRoX3ZlcmlmeQAAAAABAAAAEQAAAAEAAAAFAAAADwAAAAdhY2NvdW50AAAAAA4AAAA4Q0RCNEFVMzRYT0VTUEhPWU1WQzRNWlFZRlc2TEJQWUc1VlJHTzJPV0JWUjQ2R09BQUlCSVE0R0QAAAAPAAAAC2hvbWVfZG9tYWluAAAAAA4AAAAObG9jYWxob3N0OjgwODAAAAAAAA8AAAATaG9tZV9kb21haW5fYWRkcmVzcwAAAAAOAAAAOEdESkxCWVlLTUNYTlZWTkFCT0U2Nk5ZWFFHSUE1QUM1RDIyM1oyS0Y2WkVZSzRVQkNBN0ZLTFRHAAAADwAAAAVub25jZQAAAAAAAA4AAAAKMjA2MDIxNDExNQAAAAAADwAAAA93ZWJfYXV0aF9kb21haW4AAAAADgAAAA5sb2NhbGhvc3Q6ODA4MAAAAAAAAA==&type=SorobanAuthorizationEntry;;) + +##### Error + +Every other HTTP status code will be considered an error. For example: + +```json +{ + "error": "The provided account has requested too many challenges recently. Try again later." +} +``` + +### Token + +This endpoint accepts a signed challenge transaction, validates it and responds with a session +[JSON Web Token](https://jwt.io/) authenticating the account. + +The **Client** submits a challenge (that was previously returned by the [`challenge`](#challenge) endpoint) as a HTTP +POST request to `WEB_AUTH_FOR_CONTRACTS_ENDPOINT` using one of the following formats (both should be equally supported +by the server): + +- Content-Type: `application/x-www-form-urlencoded`, body: `authorization_entries=`) +- Content-Type: `application/json`, body: `{"authorization_entries": """}` + +To validate the challenge transaction the following steps are performed by the **Server**. If any of the listed steps +fail, then the authentication request must be rejected — that is, treated by the **Server** as an invalid input. + +1. Extract the arguments from the authorization entries returned by the client; +1. Verify that the `contract_address` in each authorization entry matches the `WEB_AUTH_CONTRACT_ID` from the + **Server**'s `stellar.toml`; +1. Verify that the `function_name` in each authorization entry is `web_auth_verify`; +1. Verify that the `args` in each authorization entry match the expected values and is the same across all authorization + entries: + 1. The `home_domain` value matches the **Home Domain**; + 1. The `home_domain_address` value matches the **Home Domain Address**; + 1. The `web_auth_domain` value matches the **Server**'s domain; + 1. The `client_domain` is present if `client_domain_address` is present; + 1. The `client_domain_address` value matches the **Client Domain Address** if `client_domain` is present; +1. (Optional) Verify that the `nonce` argument is the same across all authorization entries and is valid; +1. Verify that there is an authorization entry where `credentials.address.address` is the **Home Domain Address** and + contains a valid signature from the **Home Domain Address**; +1. Verify that there is an authorization entry where `credentials.address.address` is the **Client Account** address +1. Verify that there is an authorization entry where `credentials.address.address` is the **Client Domain Address** if + the arguments included a `client_domain_address`. +1. Construct a transaction with a single Invoke Host Function operation where the contract address is + `WEB_AUTH_CONTRACT_ID` and the function is `web_auth_verify` using the previously extracted arguments and the + authorization entries returned by the client; +1. Simulate the transaction and verify that it succeeds. + +The verification process confirms that the **Client** controls the **Client Account** and that the challenge was not +tampered with. Depending on your application this may mean complete signing authority, some threshold of control, or +being a signer of the account. See [Verification](#verification) for examples. + +Upon successful verification, **Server** responds with a session JWT, containing the following claims: + +- `iss` (the principal that issued a token, [RFC7519, Section 4.1.1](https://tools.ietf.org/html/rfc7519#section-4.1.1)) + — a [Uniform Resource Identifier (URI)] for the issuer (`https://example.com` or `https://example.com/G...`) +- `sub` (the principal that is the subject of the JWT, + [RFC7519, Section 4.1.2](https://tools.ietf.org/html/rfc7519#section-4.1.2)) — the **Client Account**'s address + (`C...`). +- `iat` (the time at which the JWT was issued + [RFC7519, Section 4.1.6](https://tools.ietf.org/html/rfc7519#section-4.1.6)) — current timestamp (`1530644093`) +- `exp` (the expiration time on or after which the JWT must not be accepted for processing, + [RFC7519, Section 4.1.4](https://tools.ietf.org/html/rfc7519#section-4.1.4)) — a server can pick its own expiration + period for the token (`1530730493`) +- `client_domain` - (optional) a nonstandard JWT claim containing the client home domain, included if the challenge + transaction contained a `client_domain` (see [Verifying the Client Domain](#verifying-the-client-domain)) + +The JWT may contain other claims specific to your application, see [RFC7519]. + +The **Server** should not provide more than one JWT for a specific challenge transaction. + +[Uniform Resource Identifier (URI)]: https://en.wikipedia.org/wiki/Uniform_Resource_Identifier +[RFC7519]: https://tools.ietf.org/html/rfc7519 + +#### Request + +``` +POST +``` + +Request Parameters: + +| Name | Type | Description | +| ----------------------- | ------ | ----------------------------------------------------------- | +| `authorization_entries` | string | an XDR-encoded array of signed `SorobanAuthorizationEntry`s | + +Example: + +```json +POST https://auth.example.com/ +Content-Type: application/json + +{ + "authorization_entries": "AAAAAQAAAAHDwFN8u4knndhlRcZmGC28sL8G7WJnadYNY88ZwAICiAhjuHNQ4DVQAAGi7gAAABAAAAABAAAAAQAAABEAAAABAAAAAgAAAA8AAAAKcHVibGljX2tleQAAAAAADQAAACCLcZbWB5Tc+LwIlJMazXz6KECPC89cSo589hUfJAOrhwAAAA8AAAAJc2lnbmF0dXJlAAAAAAAADQAAAEDIHVoFpnEUUmqOzTCzsYqu3naPvxUh1BmreIjoBbgXcbsCulvxQQdWKq40JWxNuacxOnoiOy1qyRD2ylTbirgBAAAAAAAAAAF+pQvBlBTRo0x3YPLkMnib4tbWmAGyNx9UrPdf6FDWsAAAAA93ZWJfYXV0aF92ZXJpZnkAAAAAAQAAABEAAAABAAAABQAAAA8AAAAHYWNjb3VudAAAAAAOAAAAOENEQjRBVTM0WE9FU1BIT1lNVkM0TVpRWUZXNkxCUFlHNVZSR08yT1dCVlI0NkdPQUFJQklRNEdEAAAADwAAAAtob21lX2RvbWFpbgAAAAAOAAAADmxvY2FsaG9zdDo4MDgwAAAAAAAPAAAAE2hvbWVfZG9tYWluX2FkZHJlc3MAAAAADgAAADhHREpMQllZS01DWE5WVk5BQk9FNjZOWVhRR0lBNUFDNUQyMjNaMktGNlpFWUs0VUJDQTdGS0xURwAAAA8AAAAFbm9uY2UAAAAAAAAOAAAACjIwNjAyMTQxMTUAAAAAAA8AAAAPd2ViX2F1dGhfZG9tYWluAAAAAA4AAAAObG9jYWxob3N0OjgwODAAAAAAAAAAAAABAAAAAAAAAADSsOMKYK7a1aALie83F4GQDoBdHrW86UX2SYVygRA+VR0i64V0P0FcAAGi5QAAABAAAAABAAAAAQAAABEAAAABAAAAAgAAAA8AAAAKcHVibGljX2tleQAAAAAADQAAACDSsOMKYK7a1aALie83F4GQDoBdHrW86UX2SYVygRA+VQAAAA8AAAAJc2lnbmF0dXJlAAAAAAAADQAAAEA1GAtpo/0VtDXBn2OuqfnpbY8H5UUpsHPGWe2rNhwFL7uJP22NmRyIYUujO1H8e+F5lfUJcRz7+r11AP4/bkQMAAAAAAAAAAF+pQvBlBTRo0x3YPLkMnib4tbWmAGyNx9UrPdf6FDWsAAAAA93ZWJfYXV0aF92ZXJpZnkAAAAAAQAAABEAAAABAAAABQAAAA8AAAAHYWNjb3VudAAAAAAOAAAAOENEQjRBVTM0WE9FU1BIT1lNVkM0TVpRWUZXNkxCUFlHNVZSR08yT1dCVlI0NkdPQUFJQklRNEdEAAAADwAAAAtob21lX2RvbWFpbgAAAAAOAAAADmxvY2FsaG9zdDo4MDgwAAAAAAAPAAAAE2hvbWVfZG9tYWluX2FkZHJlc3MAAAAADgAAADhHREpMQllZS01DWE5WVk5BQk9FNjZOWVhRR0lBNUFDNUQyMjNaMktGNlpFWUs0VUJDQTdGS0xURwAAAA8AAAAFbm9uY2UAAAAAAAAOAAAACjIwNjAyMTQxMTUAAAAAAA8AAAAPd2ViX2F1dGhfZG9tYWluAAAAAA4AAAAObG9jYWxob3N0OjgwODAAAAAAAAA=" +} +``` + +You can examine the example credentials in the +[XDR Viewer](https://lab.stellar.org/xdr/view?$=network$id=testnet&label=Testnet&horizonUrl=https:////horizon-testnet.stellar.org&rpcUrl=https:////soroban-testnet.stellar.org&passphrase=Test%20SDF%20Network%20/;%20September%202015;&xdr$blob=AAAAAQAAAAHDwFN8u4knndhlRcZmGC28sL8G7WJnadYNY88ZwAICiAhjuHNQ4DVQAAGi7gAAABAAAAABAAAAAQAAABEAAAABAAAAAgAAAA8AAAAKcHVibGljX2tleQAAAAAADQAAACCLcZbWB5Tc+LwIlJMazXz6KECPC89cSo589hUfJAOrhwAAAA8AAAAJc2lnbmF0dXJlAAAAAAAADQAAAEDIHVoFpnEUUmqOzTCzsYqu3naPvxUh1BmreIjoBbgXcbsCulvxQQdWKq40JWxNuacxOnoiOy1qyRD2ylTbirgBAAAAAAAAAAF+pQvBlBTRo0x3YPLkMnib4tbWmAGyNx9UrPdf6FDWsAAAAA93ZWJfYXV0aF92ZXJpZnkAAAAAAQAAABEAAAABAAAABQAAAA8AAAAHYWNjb3VudAAAAAAOAAAAOENEQjRBVTM0WE9FU1BIT1lNVkM0TVpRWUZXNkxCUFlHNVZSR08yT1dCVlI0NkdPQUFJQklRNEdEAAAADwAAAAtob21lX2RvbWFpbgAAAAAOAAAADmxvY2FsaG9zdDo4MDgwAAAAAAAPAAAAE2hvbWVfZG9tYWluX2FkZHJlc3MAAAAADgAAADhHREpMQllZS01DWE5WVk5BQk9FNjZOWVhRR0lBNUFDNUQyMjNaMktGNlpFWUs0VUJDQTdGS0xURwAAAA8AAAAFbm9uY2UAAAAAAAAOAAAACjIwNjAyMTQxMTUAAAAAAA8AAAAPd2ViX2F1dGhfZG9tYWluAAAAAA4AAAAObG9jYWxob3N0OjgwODAAAAAAAAAAAAABAAAAAAAAAADSsOMKYK7a1aALie83F4GQDoBdHrW86UX2SYVygRA+VR0i64V0P0FcAAGi5QAAABAAAAABAAAAAQAAABEAAAABAAAAAgAAAA8AAAAKcHVibGljX2tleQAAAAAADQAAACDSsOMKYK7a1aALie83F4GQDoBdHrW86UX2SYVygRA+VQAAAA8AAAAJc2lnbmF0dXJlAAAAAAAADQAAAEA1GAtpo//0VtDXBn2OuqfnpbY8H5UUpsHPGWe2rNhwFL7uJP22NmRyIYUujO1H8e+F5lfUJcRz7+r11AP4//bkQMAAAAAAAAAAF+pQvBlBTRo0x3YPLkMnib4tbWmAGyNx9UrPdf6FDWsAAAAA93ZWJfYXV0aF92ZXJpZnkAAAAAAQAAABEAAAABAAAABQAAAA8AAAAHYWNjb3VudAAAAAAOAAAAOENEQjRBVTM0WE9FU1BIT1lNVkM0TVpRWUZXNkxCUFlHNVZSR08yT1dCVlI0NkdPQUFJQklRNEdEAAAADwAAAAtob21lX2RvbWFpbgAAAAAOAAAADmxvY2FsaG9zdDo4MDgwAAAAAAAPAAAAE2hvbWVfZG9tYWluX2FkZHJlc3MAAAAADgAAADhHREpMQllZS01DWE5WVk5BQk9FNjZOWVhRR0lBNUFDNUQyMjNaMktGNlpFWUs0VUJDQTdGS0xURwAAAA8AAAAFbm9uY2UAAAAAAAAOAAAACjIwNjAyMTQxMTUAAAAAAA8AAAAPd2ViX2F1dGhfZG9tYWluAAAAAA4AAAAObG9jYWxob3N0OjgwODAAAAAAAAA=&type=SorobanAuthorizationEntry;;) + +#### Response + +If the **Server** successfully validates the submitted challenge transaction, the endpoint should return `200 OK` HTTP +status code and a JSON object with the following fields: + +| Name | Type | Description | +| ------- | ------ | ------------------------------------------------------------------------------ | +| `token` | string | The JWT that can be used to authenticate future endpoint calls with the anchor | + +Example: + +```json +{ + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJsb2NhbGhvc3Q6ODA4MCIsInN1YiI6IkNEQjRBVTM0WE9FU1BIT1lNVkM0TVpRWUZXNkxCUFlHNVZSR08yT1dCVlI0NkdPQUFJQklRNEdEIiwiaWF0IjoxNzM0Mzg3NzkyLCJleHAiOjE3MzQzODgwOTIsImp0aSI6ImMzZDhiZjc3MGZjNTVkZGZiYTYzYmM5Njg3NjkwNTg4ZWFjNWNiNmVmYTY1ZTQwNzRhY2UzODM4YTk4MTM5ZmMiLCJob21lX2RvbWFpbiI6ImxvY2FsaG9zdDo4MDgwIn0.DyQQ75Kv9sKlXbj6K1L3xoNGcLL9lUmWaLrwHxp3Z9A" +} +``` + +Check the example session token on +[JWT.IO](https://jwt.io/#debugger-io?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJHQTZVSVhYUEVXWUZJTE5VSVdBQzM3WTRRUEVaTVFWREpIREtWV0ZaSjJLQ1dVQklVNUlYWk5EQSIsImp0aSI6IjE0NGQzNjdiY2IwZTcyY2FiZmRiZGU2MGVhZTBhZDczM2NjNjVkMmE2NTg3MDgzZGFiM2Q2MTZmODg1MTkwMjQiLCJpc3MiOiJodHRwczovL2ZsYXBweS1iaXJkLWRhcHAuZmlyZWJhc2VhcHAuY29tLyIsImlhdCI6MTUzNDI1Nzk5NCwiZXhwIjoxNTM0MzQ0Mzk0fQ.8nbB83Z6vGBgC1X9r3N6oQCFTBzDiITAfCJasRft0z0). + +Every other HTTP status code will be considered an error. For example: + +```json +{ + "error": "The provided authorization entries are not valid" +} +``` + +## Web Authentication Contract + +The **Server** must deploy a contract at the `WEB_AUTH_CONTRACT_ID` address specified in the **Server**'s +`stellar.toml`. This contract allows the **Server** to customize authentication logic, including client domain +verification and nonce handling. + +The contract must implement the `web_auth_verify` function with the following signature. Within this function, the +contract should call `require_auth` on the address to ensure the client has authorized the operation, and call +`require_auth` on the home domain address so that the **Server** can verify that the challenge has not been tampered. + +If the **Server** supports client domain verification, the contract can optionally require a signature from the +`client_domain_address`. All other arguments are ignored by the contract. + +The server can optionally include a `nonce` in the `web_auth_verify` function arguments. The `nonce` is a random string +generated by the **Server** and included in the challenge transaction. The `nonce` is verified when the **Client** +submits signed challenge to the **Server**. The `nonce` is used to prevent replay attacks. + +Below is an example implementation of the `web_auth_verify` function in Rust: + +```rust +#[contracterror] +pub enum WebAuthError { + MissingArgument = 1, +} + +#[contractimpl] +impl WebAuthContract { + pub fn __constructor(env: Env, admin: Address) -> () { + env.storage().instance().set(&DataKey::Admin, &admin); + } + + /// Verifies the client is authorized to authenticate with the server + /// + /// Arguments: + /// - account: The client account address + /// - home_domain_address: The home domain address + /// - web_auth_domain: The server's domain + /// - client_domain: The client domain (optional) + /// - client_domain_address: The client domain address (optional) + /// - nonce: A random string generated by the server to prevent replay attacks (optional) + pub fn web_auth_verify(env: Env, args: Map) -> Result<(), WebAuthError> { + if let Some(address) = args.get(Symbol::new(&env, "account")) { + let addr = Address::from_string(&address); + addr.require_auth(); + } else { + return Err(WebAuthError::MissingArgument); + } + + if let Some(home_domain_address) = args.get(Symbol::new(&env, "home_domain_address")) { + let home_domain_addr = Address::from_string(&home_domain_address); + home_domain_addr.require_auth(); + } else { + return Err(WebAuthError::MissingArgument); + } + + // Optional client domain verification + if let Some(client_domain_address) = args.get(Symbol::new(&env, "client_domain_address")) { + let client_domain_addr = Address::from_string(&client_domain_address); + client_domain_addr.require_auth(); + } + + Ok(()) + } +} +``` + +## Client Recommendations + +To prevent unintended side effects in case the `web_auth_verify` invocation operation is executed on the network, the +**Client** should consider implementing the following recommendations in addition to verifying the function arguments +and sub-invocations. + +### Signature Expiration Ledger + +When signing the authorization entries, the **Client** should set the signature expiration ledger to the current ledger +plus a buffer. This ensures the signature remains valid for an appropriate duration while preventing replay attacks. +Setting this to the current ledger plus 1 is a reasonable default. + +## Verification + +### Verifying the Client Domain + +A web service requiring SEP-45 authentication may want to attribute each HTTP request made to it to a specific +**Client** software. For example, a web service may want to offer reduced fees for the users of a specific **Client**. + +In order to use this optional feature, the organization that provides the **Client** must host a +[SEP-1 stellar.toml](sep-0001.md) file containing a `SIGNING_KEY` attribute (i.e. the **Client Domain Address**). The +`SIGNING_KEY` attribute must be a Stellar public key in the form of a `G` address. The secret key paired with the +`SIGNING_KEY` should be protected as anyone in possession of the secret can verify the **Client Domain**. + +This setup allows the **Server** to verify that the challenge returned by the **Client** is also signed by the **Client +Domain Address**, proving that the **Client** is associated with the **Client Domain**. Web services requiring SEP-45 +authentication can now attribute requests made with the resulting JWT to the **Client Domain** that signed the +challenge. + +**Servers** may chose which **Client Domains** to verify. If the **Client** requests verification of its domain but the +**Server** has no use for verifying that domain, the **Server** should proceed as if the **Client** did not provide the +domain in the request for the challenge transaction. If the **Server** attempts but is unable to fetch the `SIGNING_KEY` +from the provided **Client Domain**, the **Server** should return a `400 Bad Request` HTTP status code. + +## JWT Expiration + +Servers should select an expiration time for the JWT that is appropriate for the assumptions and risk of the +interactions the **Client** can perform with it. A **Client** may be in control of an account at the time the JWT is +issued but they may lose control of the account through a change in signers. Expiration times that are too long increase +the risk that control on the account has changed. Expiration times that are too short increase the number of times +authentication must reoccur, and a user using a hardware signing device or who must complete a complex signing process +could have a poor user experience. + +## A convention for signatures + +Signatures in Stellar involve both the secret key of the signer and the passphrase of the network. SEP-45 clients and +servers must use the following convention when deciding what network passphrase to use for signing and verifying +signatures in SEP-45: + +- If the server is for testing purposes or interacts with the Stellar testnet, use the Stellar testnet passphrase. +- Otherwise, use the Stellar pubnet passphrase. + +This convention ensures that SEP-45 clients and servers can use the same passphrase as they're using for interacting +with the Stellar network. + +The client can examine the `network_passphrase` (if defined) that the server includes in its response from the challenge +endpoint to be sure it's using the correct passphrase and is connecting to the server that it expected. + +## JWT best practices + +When generating and validating JWTs it's important to follow best practices. The IETF is in the process of producing a +set of best current practices when using JWTs: [IETF JWT BCP]. + +[IETF JWT BCP]: https://tools.ietf.org/wg/oauth/draft-ietf-oauth-jwt-bcp/ +[SEP-1]: sep-0001.md + +## Implementations + +- None + +## Changelog + +- `0.1.0`: Initial draft