diff --git a/ecosystem/sep-0045.md b/ecosystem/sep-0045.md new file mode 100644 index 000000000..514b210e6 --- /dev/null +++ b/ecosystem/sep-0045.md @@ -0,0 +1,415 @@ +## Preamble + +``` +SEP: 0045 +Title: Stellar Web Authentication for Contracts +Author: +Status: Draft +Created: 2024-10-08 +Updated: 2024-10-08 +Version: 0.1.0 +``` + +## 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 also supports authenticating users of shared contract accounts. Clients can use [memos](#memos) to distinguish +users or sub-accounts of shared accounts. + +This SEP is based on SEP-0010, but only supports contract accounts and aims to provide a JWT that is fully compatible +with existing protocols that use SEP-0010. + +## Abstract + +This protocol is a variation of mutual challenge-response, which uses Soroban 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_ENDPOINT_SEP0045` (URL) + and `SIGNING_KEY` (`G...`). +- A **Server**: a server providing the `WEB_AUTH_ENDPOINT_SEP0045` 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. + - The `SIGNING_KEY` from the **Home Domain** is the **Server Account**. +- A **Client Account**: the account being authenticated. + - A Contract address (`C...`) that may be accompanied by a memo to scope the authentication to a user or sub-account + of the account. +- A **Web Auth Contract**: a contract that implements the `web_auth_verify` function. 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_ENDPOINT_SEP0045`, `WEB_AUTH_CONTRACT_ID` and `SIGNING_KEY` (i.e. **Server + Account**) 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 Soroban + authorization entries and the server's signatures represented as XDR strings +1. The **Server** responds with the challenge +1. The **Client** verifies each authorization entry does not include additional sub-invocations +1. The **Client** verifies each authorization entry has a corresponding signature that is signed by the **Server + Account** obtained through the discovery flow. +1. The **Client** verifies each authorization entry's root invocation function that: + 1. Contract addresses matches the `WEB_AUTH_CONTRACT_ID` from the **Server**'s `stellar.toml` + 1. Function name is `web_auth_verify` +1. The **Client** verifies each authorization entry if the root invocation function's first argument matches the + **Client Account** from the request +1. The **Client** verifies each authorization entry that the root invocation function's third argument is the **Home + Domain** +1. The **Client** verifies each authorization entry that the root invocation function's fourth argument is the + **Server**'s domain +1. If the client included a client domain in the request, the **Client** verifies each authorization entry that the root + invocation function's sixth argument is the **Client Account** +1. The **Client** signs the first authorization entry using the secret key(s) of the signer(s) for the **Client + Account**. +1. The **Client** obtains a signature from the **Client Domain Account** for the second authorization entry if the + **Client** included a client domain in the request. +1. The **Client** submits the credentials along with the original challenge back to the **Server** using + [`token`](#token) endpoint +1. The **Server** verifies the integrity of the authorization entries and server signatures returned by the **Client** +1. The **Server** verifies the last argument, the nonce, is the same across all authorization entries and is unique +1. The **Server** constructs a transaction with a single Invoke Host Function operation using the credentials and the + challenge authorization entries returned by the client and simulates the transaction. +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 **Server Account** +- The **Server** can verify that the **Client** holds the secret key(s) to signer(s) of the **Client Account** +- The **Server** can choose its own timeout for the authenticated session +- The **Server** can choose required signing threshold(s) that must be met, if any +- 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_ENDPOINT_SEP0045` 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 signed by the **Server Account**. The **Client** can then sign 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 **Server Account**, 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 | +| ------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `address` | `C...` string | The **Client Account**, which is a Contract address that the **Client** wishes to authenticate with the **Server**. | +| `memo` | string | (optional) The memo to attach to the challenge transaction. The memo must be of type `id`. Other memo types are not supported. See the [Memo](#memos) section for details. | +| `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. | + +Example: + +``` +GET https://auth.example.com/?address=CCIBUCGPOHWMMMFPFTDWBSVHQRT4DIBJ7AD6BZJYDITBK2LCVBYW7HUQ +``` + +#### Response + +##### Success + +On success the endpoint must return `200 OK` HTTP status code and a JSON object with these fields: + +- `authorization_entries`: a list of XDR-encoded `SorobanAuthorizationEntry`. The first entry is must be signed by the + **Client Account**. There is an optional second entry that must be signed by the **Client Domain Account** if the + **Client** included a client domain in the request. Each entry's `root_invocation` function is a `contract_fn` with + the following with no additional sub invocations: + - `contract_address` is the `WEB_AUTH_CONTRACT_ID` from the **Server**'s `stellar.toml` + - `function_name` is `web_auth_verify` + - `args`: + - `account` matches the **Client Account** from the request + - `memo` matches the `memo` from the request + - `home_domain` matches the `home_domain` from the request + - `web_auth_domain` matches the **Server**'s domain + - `client_domain` matches the **Client**'s domain + - `client_domain_address` is the **Client Domain**'s address +- `server_signatures`: a list of base64 hex encoded signatures of the `SorobanAuthorizationEntry` hashes by the **Server + Account**. The signature at index `i` corresponds to the `SorobanAuthorizationEntry` at index `i`. +- `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": [ + "AAAAAQAAAAHw6CVqzY+dCq3myVJBo1kb3nEGE7oO6obmJeUNvYQ0ukNc84Ms0ZvgAAAAAAAAAAEAAAAAAAAAAeA7wfSg10yaQYZDRmQeyqsepsS/Mb0rbMQxRgDoSVdWAAAAD3dlYl9hdXRoX3ZlcmlmeQAAAAAGAAAADgAAADhDRFlPUUpMS1pXSFoyQ1ZONDNFVkVRTkRMRU41NDRJR0NPNUE1MlVHNFlTNktETjVRUTJMVVdLWQAAAA4AAAADMTIzAAAAAA4AAAAcaHR0cDovL2xvY2FsaG9zdDo4MDgwL2MvYXV0aAAAAA4AAAAObG9jYWxob3N0OjgwODAAAAAAAA4AAAALZXhhbXBsZS5jb20AAAAAAQAAAAA=" + ], + "server_signatures": [ + "848FF1A729728A5AFF509CEE54AB4BAB1403634B74124A735E8D5F0A67AE45C294A846D24CDA358F139064A3781BFEAA717C98D832BFF2830A0D3ED2C4444C0C" + ], + "network_passphrase": "Public Global Stellar Network ; September 2015" +} +``` + +You can examine the example challenge transaction 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=AAAAAQAAAAHw6CVqzY+dCq3myVJBo1kb3nEGE7oO6obmJeUNvYQ0ukNc84Ms0ZvgAAAAAAAAAAEAAAAAAAAAAeA7wfSg10yaQYZDRmQeyqsepsS//Mb0rbMQxRgDoSVdWAAAAD3dlYl9hdXRoX3ZlcmlmeQAAAAAGAAAADgAAADhDRFlPUUpMS1pXSFoyQ1ZONDNFVkVRTkRMRU41NDRJR0NPNUE1MlVHNFlTNktETjVRUTJMVVdLWQAAAA4AAAADMTIzAAAAAA4AAAAcaHR0cDovL2xvY2FsaG9zdDo4MDgwL2MvYXV0aAAAAA4AAAAObG9jYWxob3N0OjgwODAAAAAAAA4AAAALZXhhbXBsZS5jb20AAAAAAQAAAAA=&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_ENDPOINT_SEP0045` using one of the following formats (both should be equally supported by the +server): + +- Content-Type: `application/x-www-form-urlencoded`, body: + `authorization_entries=,authorization_entries=,server_signatures=,server_signatures=,credentials=,credentials=`) +- Content-Type: `application/json`, body: + `{"authorization_entries": ["", ""], "server_signatures": ["", ""], "credentials": ["", ""]}` + +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. For each entry `i` in `authorization_entries`: + 1. decode the received input as a base64-urlencoded XDR representation of a `SorobanAuthorizationEntry`; + 1. verify that the `i`th signature corresponds to the `i`th authorization entry and was signed by the **Server + Account**; +1. Construct a transaction with a single Invoke Host Function operation using the credentials and the challenge + 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**. 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)) — there are several possible formats: + - If a memo was attached to the challenge transaction, the `sub` should be the stellar account appended with the memo, + separated by a colon (`C...:17509749319012223907`). + - Otherwise, the `sub` value should be Stellar account (`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`) + +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 | a list of base64 encoded authorization entry XDRs returned in the challenge response | +| `server_signatures` | string | a list of base64 hex encoded signatures of the `authorization_entry` hashes returned in the challenge response. The signature at index `i` corresponds to the `authorization_entries` at index `i` | +| `credentials` | list | a list of `SorobanCredentials` containing the signatures for each `authorization_entry`. The credentials at index `i` corresponds to the `authorization_entries` at index `i` | + +Example: + +``` +POST https://auth.example.com/ +Content-Type: application/json + +{"authorization_entries": ["AAAAAQAAAAHw6CVqzY+dCq3myVJBo1kb3nEGE7oO6obmJeUNvYQ0um6s4olInY8EAAAAAAAAAAEAAAAAAAAAAeA7wfSg10yaQYZDRmQeyqsepsS/Mb0rbMQxRgDoSVdWAAAAD3dlYl9hdXRoX3ZlcmlmeQAAAAAGAAAADgAAADhDRFlPUUpMS1pXSFoyQ1ZONDNFVkVRTkRMRU41NDRJR0NPNUE1MlVHNFlTNktETjVRUTJMVVdLWQAAAA4AAAADMTIzAAAAAA4AAAAcaHR0cDovL2xvY2FsaG9zdDo4MDgwL2MvYXV0aAAAAA4AAAAObG9jYWxob3N0OjgwODAAAAAAAAEAAAABAAAAAA=="],"server_signatures": ["0B6E3C5A1987A2459970A3F16551A34EB6756CEB97C2F7A8B8923BBB37671318003EA78A47C4C2484FC21303759A4560CF8815ED1B252DDBAE6772DEFD606F06"],"credentials":["AAAAAQAAAAHw6CVqzY+dCq3myVJBo1kb3nEGE7oO6obmJeUNvYQ0um6s4olInY8EAAWMHgAAABAAAAABAAAAAQAAABEAAAABAAAAAgAAAA8AAAAKcHVibGljX2tleQAAAAAADQAAACCLcZbWB5Tc+LwIlJMazXz6KECPC89cSo589hUfJAOrhwAAAA8AAAAJc2lnbmF0dXJlAAAAAAAADQAAAEC4vXPFDsGlKMFLfEvagmDYZ8x+A0VhH0RlFPgdri/PJHjjsC6bO2nusUPNCxcjt2mX4yxFbZs48d0dSAuXijEN"]} +``` + +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=AAAAAQAAAAHw6CVqzY+dCq3myVJBo1kb3nEGE7oO6obmJeUNvYQ0um6s4olInY8EAAWMHgAAABAAAAABAAAAAQAAABEAAAABAAAAAgAAAA8AAAAKcHVibGljX2tleQAAAAAADQAAACCLcZbWB5Tc+LwIlJMazXz6KECPC89cSo589hUfJAOrhwAAAA8AAAAJc2lnbmF0dXJlAAAAAAAADQAAAEC4vXPFDsGlKMFLfEvagmDYZ8x+A0VhH0RlFPgdri//PJHjjsC6bO2nusUPNCxcjt2mX4yxFbZs48d0dSAuXijEN&type=SorobanCredentials;;) + +#### 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.eyJzdWIiOiJHQTZVSVhYUEVXWUZJTE5VSVdBQzM3WTRRUEVaTVFWREpIREtWV0ZaSjJLQ1dVQklVNUlYWk5EQSIsImp0aSI6IjE0NGQzNjdiY2IwZTcyY2FiZmRiZGU2MGVhZTBhZDczM2NjNjVkMmE2NTg3MDgzZGFiM2Q2MTZmODg1MTkwMjQiLCJpc3MiOiJodHRwczovL2ZsYXBweS1iaXJkLWRhcHAuZmlyZWJhc2VhcHAuY29tLyIsImlhdCI6MTUzNDI1Nzk5NCwiZXhwIjoxNTM0MzQ0Mzk0fQ.8nbB83Z6vGBgC1X9r3N6oQCFTBzDiITAfCJasRft0z0" +} +``` + +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 transaction is not valid" +} +``` + +## Web Auth Contract + +The **Server** must deploy a contract at the `WEB_AUTH_CONTRACT_ID` address specified in the **Server**'s +`stellar.toml`. This allows the **Server** to customize the authentication logic such as client domain verification and +nonce verification. + +The contract must implement the `web_auth_verify` function with the following signature and call the `require_auth` +function on the `address`. Optionally, the contract can require a signature from the `client_domain_address` if the +**Server** supports client domain verification. The contract should ignore all other arguments. + +```rust +#[contract] +pub struct WebAuthContract; + +#[contractimpl] +impl WebAuthContract { + pub fn web_auth_verify( + _env: Env, + address: Address, + _memo: Option, // IGNORED + _home_domain: Option, // IGNORED + _web_auth_domain: Option, // IGNORED + _client_domain: Option, // IGNORED + client_domain_address: Option
, + _nonce: Option, // IGNORED, used by the Server to ensure challenge is unique + ) { + address.require_auth(); + // Optional: require a signature from the client domain address + if let Some(client_domain_address) = client_domain_address { + client_domain_address.require_auth(); + } + } +} +``` + +## 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 Account** on the +**Client Domain**). 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 with the +**Client Domain Account**, 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. + +### Memos + +Stellar transaction memos are used for a variety of purposes in the ecosystem, but in the context of this standard memos +attached to challenge transactions distinguish sessions that should be entirely separate, scoped, and detached from each +other. Two users who are authenticated with the same Stellar account but different memos should have the same level of +separation as two users who are authenticated with different Stellar accounts. Typically the sessions are unique users +who share a single Stellar account, sometimes called an omnibus or pooled account. + +The `memo` parameter supported in `GET ` API calls is used to communicate to the **Server** +that the client intends to authenticate a Stellar account with an additional claim that the account is shared and that +the user should be identified using the `address` and `memo` parameters, instead of only using `address`. The value of +the `memo` passed will ultimately be added to the decoded JWT's `sub` field, separated from the account address by a +colon (`:`). + +## 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 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