Streamlining Email Verification with the Email Verification Protocol (EVP)

Web Security

The Email Verification Protocol (EVP) offers a modern approach to verify email addresses without sending emails or redirects, enhancing privacy and user experience for web applications.

Verifying control of an email address is a common and critical activity on the web, used both to confirm a user has provided a valid email and as a method for authenticating returning users. However, existing verification methods present significant challenges.

Traditional Email-Based Verification Users are sent a link to click or a verification code. This process often requires switching applications, waiting for the email, and then performing the verification action. This 'friction' frequently leads to user drop-off. Furthermore, there are privacy implications as email transmission reveals which applications a user is engaging with and when.

Social Login Providers Another approach involves users logging in with providers like Apple or Google, which supply a verified email address. This method necessitates applications establishing relationships with multiple social providers and requires users to be registered with and willing to share additional profile information via the OpenID Connect flow.

These existing approaches create practical hurdles for developers. Email-based flows battle deliverability issues, delays, and user abandonment. Social login requires complex integrations, key management, and user reliance on specific providers. In both scenarios, developers have limited options to verify an email without sending mail or redirecting users, and cannot prevent email providers from knowing about verification attempts.

Introducing the Email Verification Protocol (EVP) The Email Verification Protocol offers a novel solution, enabling a web application to obtain a verified email address without sending an email or requiring the user to leave the current web page. This functionality is achieved by delegating email verification from the mail domain to an 'Issuer' that holds authentication cookies for the user.

When a user enters an email into an HTML form field, the browser interacts with the Issuer, passing authentication cookies. The Issuer returns a token, which the browser verifies and then provides to the web application. The web application then verifies this token, confirming the user's email address. User privacy is enhanced as the browser mediates the request, preventing the Issuer from learning which specific web application initiated the verification.

Key Concepts

SD-JWT+KB Token

The Selective Disclosure JSON Web Token with Key Binding (SD-JWT+KB) is specified in Selective Disclosure for JWT (note: this protocol primarily leverages the key binding feature, not selective disclosure itself). This feature enables a clear separation between token issuance and token presentation. An SD-JWT+KB is comprised of two JWTs, concatenated with a ~ character:

  1. SD-JWT (Issuance Token): Signed by the Issuer, this JWT contains email and email_verified claims for the user, along with the public key used by the browser in its request.
  2. KB Token (Key Binding Token): Signed by the browser, this JWT includes a hash of the first JWT. The combined SD-JWT+KB forms the presentation token, allowing the application to verify the email address provided by the Issuer without the Issuer knowing the specific application.

Issuer

The Issuer is a service responsible for verifying a user's control over an email address. A DNS record for the email domain explicitly delegates email verification to this Issuer. The Issuer hosts a .well-known/email-verification metadata file, which contains:

  • issuance_endpoint: The API endpoint for obtaining an issuance token.
  • jwks_uri: The URL pointing to the JWKS (JSON Web Key Set) file, containing public keys used to verify the SD-JWT.

The Issuer is identified by its domain, an eTLD+1 (e.g., issuer.example). All URLs within the Issuer's metadata must have hostnames that end with the Issuer's domain. This identifier links the SD-JWT, DNS delegation, and the Issuer.

User Experience

When a user navigates to a website requiring a verified email address and focuses on the email input field, the browser can suggest one or more verified emails previously known to it. The user simply selects a verified email, and the application proceeds without further verification steps.

  • User Interface Enhancement: Should verifiable emails be visibly differentiated (e.g., 'decorated') in the browser's autocomplete UI?
  • Verification Status Feedback: How should the user be informed that an email provided to the app is already verified?

Processing Steps

The Email Verification Protocol involves a detailed interaction sequence between the User, Browser, Relying Party (RP) Page, RP Server, Issuer, and DNS, spanning six main steps:

sequenceDiagram
    participant U as User
    participant B as Browser
    participant RP as RP Page
    participant RPS as RP Server
    participant I as Issuer
    participant DNS as DNS

    Note over U,DNS: Step 1: Email Request
    U->>RP: Navigate to site
    RP->>RPS: Nonce request
    RPS->>RPS: Generate nonce, bind to session
    RPS->>RP: Nonce
    RP->>B: Display page 

    Note over U,DNS: Step 2: Email Selection
    U->>RP: Focus on email input field
    RP->>B: Input field focused
    B->>U: Display email address list
    U->>B: Select email address

    Note over U,DNS: Step 3: Token Request
    B->>DNS: DNS TXT lookup<br/>_email-verification.$EMAIL_DOMAIN
    DNS->>B: Return iss=issuer.example
    B->>I: GET /.well-known/email-verification
    I->>B: Return metadata
    B->>B: Generate key pair<br/>Create request token
    B->>I: POST request_token=JWT...

    Note over U,DNS: Step 4: Token Issuance
    I->>I: Verify request
    I->>I: Generate SD-JWT
    I->>B: {"issuance_token":"SD-JWT"}

    Note over U,DNS: Step 5: Token Presentation
    B->>B: Verify SD-JWT
    B->>I: GET jwks_uri for public keys
    I->>B: Return JWKS
    B->>B: Create KB
    B->>RP: Provide SD-JWT+KB

    Note over U,DNS: Step 6: Token Verification
    RP->>RPS: Send SD-JWT+KB 
    RPS->>RPS: Parse SD-JWT+KB
    RPS->>DNS: DNS TXT lookup for email domain
    DNS->>RPS: Return iss=issuer.example
    RPS->>I: GET /.well-known/email-verification
    I->>RPS: Return metadata with jwks_uri
    RPS->>I: GET jwks_uri
    I->>RPS: Return JWKS public keys
    RPS->>RPS: Verify SD-JWT
    RPS->>RPS: Verify KB-JWT
    RPS->>RP: Email verification complete

1. Email Request

The user navigates to a Relying Party (RP) site.

1.1 The RP Server generates a nonce (a unique, single-use token) and binds it to the user's session.

1.2 The RP Server returns an HTML page containing an input field with the autocomplete property set to "email" and a nonce property set to the generated nonce. If the browser subsequently receives an issuance_token (per step 4.4), it dispatches an emailverified event with a presentationToken property. Below is an example HTML snippet:

<input id="email" type="email" autocomplete="email" nonce="12345677890..random">
<script>
const input = document.getElementById('email');
input.addEventListener('emailverified', e => {
  // e.presentationToken is SD-JWT+KB
  console.log({ presentationToken: e.presentationToken });
});
</script>

Authors are exploring alternative HTML and JavaScript API approaches.

2. Email Selection

2.1 The user focuses on the email input field.

2.2 The browser displays a list of email addresses it has for the user.

Question: Should verifiable emails be visually distinguished for the user?

2.3 The user either selects an email address from the browser's suggestions or types one into the field.

Future Considerations: Allow users to type new emails to be learned by the browser, or to use the protocol even if they don't want the browser to remember emails. In the future, passkey authentication to the issuer could enable verified email provision to a web application from a public computer without entering any secrets.

3. Token Request

If the RP has completed Step 1:

3.1 The browser parses the email domain ($EMAIL_DOMAIN) from the email address and performs a DNS TXT record lookup for _email-verification.$EMAIL_DOMAIN. The record's content must begin with iss= followed by the issuer identifier. There must be only one such TXT record.

Example Record: _email-verification.email-domain.example TXT iss=issuer.example

This record signifies that email-domain.example has delegated email verification to issuer.example. If the email domain and issuer are the same, the record would be _email-verification.issuer.example TXT iss=issuer.example.

This DNS delegation provides assurance of issuer authorization, as access to DNS records is typically separate from website deployments, preventing an insider with only website access from improperly setting up an issuer for their domain.

3.2 If an issuer is found, the browser loads https://$ISSUER$/.well-known/email-verification and must follow redirects to the same path but potentially a different subdomain of the Issuer (e.g., https://issuer.example/.well-known/email-verification might redirect to https://accounts.issuer.example/.well-known/email-verification).

3.3 The browser confirms that the .well-known/email-verification file contains JSON with the following properties, each with a hostname rooted in the issuer domain:

  • issuance_endpoint: The API endpoint the browser calls to obtain an SD-JWT.
  • jwks_uri: The URL where the issuer provides its public keys for SD-JWT verification.
  • signing_alg_values_supported (OPTIONAL): A JSON array listing JWS signing algorithms (alg values) supported by both the browser for request tokens and the issuer for issued tokens. The same algorithm must be used for both request_token and issuance within a single flow. Algorithm identifiers must be from the IANA registry. If omitted, "EdDSA" is the default and should be included if present. The value "none" must not be used.

Example .well-known/email-verification file:

{
  "issuance_endpoint": "https://accounts.issuer.example/email-verification/issuance",
  "jwks_uri": "https://accounts.issuer.example/email-verification/jwks",
  "signing_alg_values_supported": ["EdDSA", "RS256"]
}

3.4 The browser generates a fresh private/public key pair and signs a JWT with the private key. This JWT includes the public key in JWK format within its header as a jwk claim, and the following claims in the payload:

  • aud: The issuer.
  • iat: Time when the JWT was signed.
  • jti: Unique identifier for the token.
  • email: Email address to be verified.

The browser should select an algorithm from the issuer's signing_alg_values_supported array or default to "EdDSA".

Example JWT header:

{
  "alg": "EdDSA",
  "typ": "JWT",
  "jwk": {
    "kty": "OKP",
    "crv": "Ed25519",
    "x": "11qYAYdk9E6z7mT6rk6j1QnXb6pYq4v9wXb6pYq4v9w"
  }
}

Example payload:

{
  "aud": "issuer.example",
  "iat": 1692345600,
  "email": "user@example.com"
}

3.5 The browser then sends a POST request to the issuance_endpoint of the issuer, including 1st-party cookies. The Content-Type header is application/x-www-form-urlencoded, containing a request_token parameter set to the signed JWT, and the Sec-Fetch-Dest header set to email-verification.

Example POST request:

POST /email-verification/issuance HTTP/1.1
Host: accounts.issuer.example
Cookie: session=...
Content-Type: application/x-www-form-urlencoded
Sec-Fetch-Dest: email-verification

request_token=eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVC...

4. Token Issuance

Upon receiving a token request:

4.1 The issuer must verify the request headers:

  • Content-Type is application/x-www-form-urlencoded.
  • Sec-Fetch-Dest is email-verification.

4.2 The issuer must verify the request_token by:

  • Parsing the JWT into its header, payload, and signature components.
  • Confirming the presence of, and extracting, the jwk and alg fields from the JWT header, and the aud, iat, and email claims from the payload.
  • Verifying the JWT signature using the jwk with the alg algorithm.
  • Verifying the aud claim precisely matches the issuer's identifier.
  • Verifying the iat claim is within 60 seconds of the current time.
  • Verifying the email claim contains a syntactically valid email address.

4.3 The issuer checks if the provided cookies represent a logged-in user and if that user controls the email address from the request_token. If so, the issuer generates an SD-JWT with the following properties:

  • Header: Must contain alg (signing algorithm, should match request_token's), kid (key identifier), and typ set to "evp+sd-jwt".
  • Payload: Must contain iss (issuer identifier), iat (issued at time), cnf (confirmation claim with the public key from the request_token's jwk field), email (email address from request_token), and email_verified (set to true, per OpenID Connect 1.0).
  • Signature: Must be signed with the issuer's private key corresponding to a public key in the jwks_uri identified by kid.

Example header:

{
  "alg": "EdDSA",
  "kid": "2024-08-19",
  "typ": "evp+sd-jwt"
}

Example payload:

{
  "iss": "issuer.example",
  "iat": 1724083200,
  "cnf": {
    "jwk": {
      "kty": "OKP",
      "crv": "Ed25519",
      "x": "11qYAYdk9E6z7mT6rk6j1QnXb6pYq4v9wXb6pYq4v9w"
    }
  },
  "email": "user@example.com",
  "email_verified": true
}

The resulting JWT has ~ appended, making it a valid SD-JWT.

4.4 The issuer returns the SD-JWT to the browser as the value of issuance_token in an application/json response.

Example response:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "issuance_token": "eyJhbGciOiJFZERTQSIsImtpZCI6IjIwMjQtMDgtMTkiLCJ0eXAiOiJ3ZWItaWRlbnRpdHkrc2Qtand0In0..."
}

4.5 Error Responses

If the issuer fails to process the token request, it must return an appropriate HTTP status code with a JSON error response containing an error field and an optional error_description field.

  • 4.5.1 Invalid Content-Type Header: HTTP 415 Unsupported Media Type if Content-Type is not application/x-www-form-urlencoded.
  • 4.5.2 Invalid Sec-Fetch-Dest Header: HTTP 400 Bad Request with {"error":"invalid-request", "error_description":"Missing or invalid Sec-Fetch-Dest header"}.
  • 4.5.3 Authentication Required: HTTP 401 Unauthorized with {"error":"authentication_required", "error_description":"User must be authenticated and have control of the requested email address"}.
  • 4.5.4 Invalid Parameters: HTTP 400 Bad Request with {"error":"invalid_request", "error_description":"Invalid or malformed request_token"} if request_token is malformed, missing claims, or has invalid values.
  • 4.5.5 Invalid Token: HTTP 400 Bad Request with {"error":"invalid_token", "error_description":"Token signature verification failed or token structure is invalid"} if signature verification fails or token structure is invalid.
  • 4.5.6 Server Errors: HTTP 500 Internal Server Error with {"error":"server_error", "error_description":"Temporary server error, please try again later"} for internal issues.

A future version of this specification may allow the issuer to prompt the user to log in via a URL or a Passkey request.

5. Token Presentation

Upon receiving the issuance_token:

5.1 The browser must verify the SD-JWT (as per SD-JWT specification) by:

  • Parsing the SD-JWT into header, payload, and signature components.
  • Confirming the presence of, and extracting, the alg and kid fields from the SD-JWT header, and the iss, iat, cnf, email, and email_verified claims from the payload.
  • Parsing the email domain from the email claim and performing a DNS TXT record lookup for _email-verification.$EMAIL_DOMAIN to verify that the iss claim matches the issuer identifier in the DNS record.
  • Fetching the issuer's public keys from the jwks_uri specified in the .well-known/email-verification file.
  • Verifying the SD-JWT signature using the public key identified by kid from the JWKS with the alg algorithm.
  • Verifying the iat claim is within 60 seconds of the current time.
  • Verifying the email claim matches the email address the user selected.
  • Verifying the email_verified claim is true.

5.2 The browser then creates an SD-JWT+KB by:

  • Taking the verified SD-JWT from step 5.1 as the base token.
  • Creating a Key Binding JWT (KB-JWT) with the following structure:
    • Header: alg (same signing algorithm used by the browser's private key), typ ("kb+jwt").
    • Payload: aud (the RP's origin), nonce (from the original navigator.credentials.get() call), iat (current time), sd_hash (SHA-256 hash of the SD-JWT).
  • Signing the KB-JWT with the browser's private key (the same key pair generated in step 3.4).
  • Concatenating the SD-JWT and the KB-JWT, separated by a tilde (~), to form the SD-JWT+KB.

Example KB-JWT header:

{
  "alg": "EdDSA",
  "typ": "kb+jwt"
}

Example KB-JWT payload:

{
  "aud": "https://rp.example",
  "nonce": "259c5eae-486d-4b0f-b666-2a5b5ce1c925",
  "salt": "kR7fY9mP3xQ8wN2vL5jH6tZ1cB4nM9sD8fG3hJ7kL2p",
  "iat": 1724083260,
  "sd_hash": "X9yH0Ajrdm1Oij4tWso9UzzKJvPoDxwmuEcO3XAdRC0"
}

5.3 The browser sets a TBD hidden field and fires a TBD event. (Details TBD)

6. Token Verification

The RP web page receives the SD-JWT+KB from the event and passes it to the RP server (or the token was posted directly). (Details TBD)

The RP server must verify the SD-JWT+KB by:

6.1 Receiving the SD-JWT+KB from the web page.

6.2 Parsing the SD-JWT+KB by separating the SD-JWT and KB-JWT components (separated by ~).

6.3 Verifying the KB-JWT by:

  • Parsing the KB-JWT into header, payload, and signature components.
  • Confirming the presence of, and extracting, the alg field from the KB-JWT header, and the aud, nonce, iat, and sd_hash claims from the payload.
  • Verifying the aud claim matches the RP's origin.
  • Verifying the nonce claim matches the nonce from the RP's session with the web page.
  • Verifying the iat claim is within a reasonable time window.
  • Computing the SHA-256 hash of the SD-JWT and verifying it matches the sd_hash claim.

6.4 Verifying the SD-JWT by:

  • Parsing the SD-JWT into header, payload, and signature components.
  • Confirming the presence of, and extracting, the alg and kid fields from the SD-JWT header, and the iss, iat, cnf, email, and email_verified claims from the payload.
  • Parsing the email domain from the email claim and performing a DNS TXT record lookup for _email-verification.$EMAIL_DOMAIN to verify that the iss claim matches the issuer identifier in the DNS record.
  • Fetching the issuer's public keys from the jwks_uri specified in the .well-known/email-verification file.
  • Verifying the SD-JWT signature using the public key identified by kid from the JWKS with the alg algorithm.
  • Verifying the iss claim exactly matches the issuer identifier from the DNS record.
  • Verifying the iat claim is within a reasonable time window.
  • Verifying the email_verified claim is true.

6.5 Verifying the KB-JWT signature using the public key from the cnf claim in the SD-JWT with the alg algorithm from the KB-JWT header.

Privacy Considerations

Discussions around privacy implications include:

  • Enhanced User Privacy: The email domain operator no longer learns which applications a user is verifying their email address to, as applications no longer send email verification codes. By using an SD-JWT+KB, the browser intermediates requests and responses, ensuring the Issuer does not learn the identity of the Relying Party.
  • RP Inference: The Relying Party can infer if a user is logged into the Issuer (receiving an SD-JWT if logged in, otherwise not).
  • Issuer Learning New Emails: The Issuer might learn that a user has an email at a mail domain it is authoritative for, which it previously did not know about.

Alternatives Under Consideration

  • JavaScript API for Providing the Email: A web page could call a JS API, passing the email address and nonce. This API, callable after a user gesture (e.g., clicking a 'verify' button), would return a promise resolving to an SD-JWT or an error. This offers greater flexibility for web pages to gather email addresses (e.g., displaying a list of previously used emails).
  • Passkey Authentication: In addition to, or as an alternative to, the browser sending cookies, the Issuer could initiate a WebAuthn request to the browser if it has credentials for the user identified by the email address. The browser would then handle user interaction and provide the WebAuthn response to the Issuer, authenticating the user and prompting the Issuer to return the SD-JWT.

Alternatives Considered

  • Using .well-known for Mail Domain Delegation to Issuer: Instead of a DNS TXT record, the mail domain would host a JSON file in the .well-known domain. This approach was deemed problematic for smaller, individually owned domains, as it would require an email-only domain to support a web server. Additionally, apex domains (common for mail domains) do not support CNAME records, complicating website hosting.