Streamlining Email Verification with the Email Verification Protocol (EVP)
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:
- SD-JWT (Issuance Token): Signed by the Issuer, this JWT contains
emailandemail_verifiedclaims for the user, along with the public key used by the browser in its request. - 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 (algvalues) supported by both the browser for request tokens and the issuer for issued tokens. The same algorithm must be used for bothrequest_tokenandissuancewithin 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-Typeisapplication/x-www-form-urlencoded.Sec-Fetch-Destisemail-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
jwkandalgfields from the JWT header, and theaud,iat, andemailclaims from the payload. - Verifying the JWT signature using the
jwkwith thealgalgorithm. - Verifying the
audclaim precisely matches the issuer's identifier. - Verifying the
iatclaim is within 60 seconds of the current time. - Verifying the
emailclaim 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 matchrequest_token's),kid(key identifier), andtypset to"evp+sd-jwt". - Payload: Must contain
iss(issuer identifier),iat(issued at time),cnf(confirmation claim with the public key from therequest_token'sjwkfield),email(email address fromrequest_token), andemail_verified(set totrue, per OpenID Connect 1.0). - Signature: Must be signed with the issuer's private key corresponding to a public key in the
jwks_uriidentified bykid.
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 TypeifContent-Typeis notapplication/x-www-form-urlencoded. - 4.5.2 Invalid Sec-Fetch-Dest Header:
HTTP 400 Bad Requestwith{"error":"invalid-request", "error_description":"Missing or invalid Sec-Fetch-Dest header"}. - 4.5.3 Authentication Required:
HTTP 401 Unauthorizedwith{"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 Requestwith{"error":"invalid_request", "error_description":"Invalid or malformed request_token"}ifrequest_tokenis malformed, missing claims, or has invalid values. - 4.5.5 Invalid Token:
HTTP 400 Bad Requestwith{"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 Errorwith{"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
algandkidfields from the SD-JWT header, and theiss,iat,cnf,email, andemail_verifiedclaims from the payload. - Parsing the email domain from the
emailclaim and performing a DNSTXTrecord lookup for_email-verification.$EMAIL_DOMAINto verify that theissclaim matches the issuer identifier in the DNS record. - Fetching the issuer's public keys from the
jwks_urispecified in the.well-known/email-verificationfile. - Verifying the SD-JWT signature using the public key identified by
kidfrom the JWKS with thealgalgorithm. - Verifying the
iatclaim is within 60 seconds of the current time. - Verifying the
emailclaim matches the email address the user selected. - Verifying the
email_verifiedclaim istrue.
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 originalnavigator.credentials.get()call),iat(current time),sd_hash(SHA-256 hash of the SD-JWT).
- Header:
- 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
algfield from the KB-JWT header, and theaud,nonce,iat, andsd_hashclaims from the payload. - Verifying the
audclaim matches the RP's origin. - Verifying the
nonceclaim matches the nonce from the RP's session with the web page. - Verifying the
iatclaim is within a reasonable time window. - Computing the SHA-256 hash of the SD-JWT and verifying it matches the
sd_hashclaim.
6.4 Verifying the SD-JWT by:
- Parsing the SD-JWT into header, payload, and signature components.
- Confirming the presence of, and extracting, the
algandkidfields from the SD-JWT header, and theiss,iat,cnf,email, andemail_verifiedclaims from the payload. - Parsing the email domain from the
emailclaim and performing a DNSTXTrecord lookup for_email-verification.$EMAIL_DOMAINto verify that theissclaim matches the issuer identifier in the DNS record. - Fetching the issuer's public keys from the
jwks_urispecified in the.well-known/email-verificationfile. - Verifying the SD-JWT signature using the public key identified by
kidfrom the JWKS with thealgalgorithm. - Verifying the
issclaim exactly matches the issuer identifier from the DNS record. - Verifying the
iatclaim is within a reasonable time window. - Verifying the
email_verifiedclaim istrue.
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-knownfor Mail Domain Delegation to Issuer: Instead of a DNSTXTrecord, the mail domain would host a JSON file in the.well-knowndomain. 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.