Verifying control of an email address is a frequent activity on the web today and is used both to prove the user has provided a valid email address, and as a means of authenticating the user when returning to an application.
Verification is performed by either:
-
Sending the user a link they click on or a verification code. This requires the user to switch from the application they are using to their email address and having to wait for the email arrive, and then perform the verification action. This friction often causes drop off in users completing the task. There are privacy implications as the email transmission informs the mail service the applications the user is using and when they used them.
-
The user logs in with a social login provider such as Apple or Google that provide a verified email address. This requires the application to have set up a relationship with each social provider, and the user to be using one of those services and wanting to share the additional profile information that is also provided in the OpenID Connect flow.
The Email Verification Protocol enables a web application to obtain a verified email address without sending an email, and without the user leaving the web page they are on. To enable the functionality, the mail domain delegates email verification to an issuer that has authentication cookies for the user. When the user provides an email to the HTML form field, the browser calls the issuer passing authentication cookies, the issuer returns a token, which the browser verifies and updates and provides to the web application. The web application then verifies the token and has a verified email address for the user.
User privacy is enhanced as the issuer does not learn which web application is making the request as the request is mediated by the browser.
-
SD-JWT+KB token: The selective disclosure json web token with key binding is specified in Selective Disclosure for JWT. This protocol does not use the selective disclosure features, it uses the key binding feature which enables a separation of token issuance and token presentation. The SD-JWT+KB is a token composed of two JWTs separated by the
~
character. The first JWT is an SD-JWT aka the issuance token and is signed by the issuer and contains theemail
andemail_verified
claims for the user, and the public key used by the browser to make the request. The second JWT is a KB token and is signed by the browser and contains a hash of the first JWT. The resulting SD-JWT+KB is the presentation token, and enables the application to verify the issuer provided the email address for the user without the issuer learning about the specific application -
Issuer: The service that verifies the user controls an email address. A DNS record for the email domain delegates email verification to the issuer. The issuer serves a
.well-known/email-verification
metadata file that contains itsissuance_endpoint
that is called to obtain an issuance token, and itsjwks_uri
that points to the JWKS file containing the public keys used to verify the SD-JWT. The issuer is identified by its domain, an eTLD+1 (egissuer.example
). The hostname in all URLs from the issuer's metadata MUST end with the issuer's domain. This identifier is what binds the SD-JWT, the DNS delegation, with the issuer.
Verified Email Release: The user navigates to any website that requires a verified email address and an input field to enter the email address. The user focusses on the input field and the browser provides one or emails for the user to select based on emails the user has provided previously to the browser. The user selects a verified email and the app proceeds having obtained the verified email.
Are emails that can be verified decorated by the browser in the autocomplete UI? What UX is presented to the user when the app gets a verified email so the user knows it is already verified?
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
User navigates to a site that will act as the RP.
-
1.1 - the RP Server generates a nonce and binds the nonce to the session.
-
1.2 - the RP Server returns a page that has an input field with the
autocomplete
property set to"email"
and thenonce
property set the the nonce. If the browser receives anissuance_token
per 4.4 below, then it sends aemailverifed
event that has apresentationToken
property. Following is an example of the HTML in the page:
<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 JS API approaches
-
2.1 - User focusses on email input field
-
2.2 - The browser displays the list of email addresses it has for the user.
Q: Are emails that could be verified decorated for user to understand?
- 2.3 - User selects an email address from browser selection, or the user types an email into the field.
Future: allow user to type in a field so we learn about new emails, or if the user does not want the browser to remember emails, the Email Verification Protocol is still available. In the future when we allow the user to use a passkey to authenticate to the issuer, the user can provide a verified email to a web application using a public computer by authenticating with their passkey and not enter any secrets into the public computer.
If the RP has performed (1):
- 3.1 - the browser parses the email domain ($EMAIL_DOMAIN) from the email address, looks up the
TXT
record for_email-verification.$EMAIL_DOMAIN
. The contents of the record MUST start withiss=
followed by the issuer identifier. There MUST be only oneTXT
record for_email-verification.$EMAIL_DOMAIN
.
example record
_email-verification.email-domain.example TXT iss=issuer.example
This record states that email-domain.example
has delegated email verification to the issuer issuer.example
.
If the email domain and the issuer are the same domain, then the record would be:
_email-verification.issuer.example TXT iss=issuer.example
Access to DNS records and email is often independent of website deployments. This provides assurance that an issuer is truly authorized as an insider with only access to websites on
issuer.example
could setup an issuer that would grant them verified emails for any email atissuer.example
.
- 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 with a different subdomain of the Issuer.
For example, https://issuer.example/.well-known/email-verification
may redirect to https://accounts.issuer.example/.well-known/email-verification
.
-
3.3 - the browser confirms that the
.well-known/email-verification
file contains JSON that includes the following properties: -
issuance_endpoint - the API endpoint the browser calls to obtain an SD-JWT
-
jwks_uri - the URL where the issuer provides its public keys to verify the SD-JWT
-
signing_alg_values_supported - OPTIONAL. JSON array containing a list of the 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 the
request_token
andissuance
within a single issuance flow. Algorithm identifiers MUST be from the IANA "JSON Web Signature and Encryption Algorithms" registry. If omitted, "EdDSA" is the default. "EdDSA" SHOULD be included in the supported algorithms list. The value "none" MUST NOT be used.
Each of these properties MUST include the issuer domain as the root of their hostname.
Following is an 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 and signs a JWT with the private key that has the public key in the JWT header in the JWK format as a
jwk
claim that contains 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 use "EdDSA" if the property is not present.
An example JWT header:
{
"alg": "EdDSA",
"typ": "JWT",
"jwk": {
"kty": "OKP",
"crv": "Ed25519",
"x": "11qYAYdk9E6z7mT6rk6j1QnXb6pYq4v9wXb6pYq4v9w" // base64url-encoded public key
}
}
do we want to register a new JWT
typ
An example payload
{
"aud": "issuer.example",
"iat": 1692345600,
"email": "[email protected]"
}
- 3.5 - the browser POSTs to the
issuance_endpoint
of the issuer with 1P cookies with a content-type ofapplication/x-www-form-urlencoded
containing arequest_token
parameter set to the signed JWT and theSec-Fetch-Dest
header set toemail-verification
.
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...
On receipt of a token request:
-
4.1 - the issuer MUST verify the request headers:
Content-Type
isapplication/x-www-form-urlencoded
Sec-Fetch-Dest
isemail-verification
-
4.2 - the issuer MUST verify the request_token by:
- parsing the JWT into header, payload, and signature components
- confirming the presence of, and extracting the
jwk
andalg
fields from the JWT header, and theaud
,iat
, andemail
, claims from the payload - verifying the JWT signature using the
jwk
with thealg
algorithm - verifying the
aud
claim exactly 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 cookies sent represent a logged in user, and if the logged in user has control of the email provided in the request_token. If so the issuer generates an SD-JWT with the following properties:
- Header: MUST contain
alg
: signing algorithm (SHOULD match the algorithm from the request_token)kid
: key identifier of key used to signtyp
set to "evp+sd-jwt"
- Payload: MUST contain the following claims:
iss
: the issuer identifieriat
: issued at timecnf
: confirmation claim containing the public key from the request_token'sjwk
fieldemail
: claim containing the email address from the request_tokenemail_verified
: claim that email is verified 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 bykid
- Header: MUST contain
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": "[email protected]",
"email_verified": true
}
The resulting JWT has the ~
appended to it, making it a valid SD-JWT.
- 4.4 - the issuer returns the SD-JWT to the browser as the value of
issuance_token
in anapplication/json
response.
Example:
HTTP/1.1 200 OK
Content-Type: application/json
{"issuance_token":"eyJhbGciOiJFZERTQSIsImtpZCI6IjIwMjQtMDgtMTkiLCJ0eXAiOiJ3ZWItaWRlbnRpdHkrc2Qtand0In0..."}
If the issuer cannot process the token request successfully, it MUST return an appropriate HTTP status code with a JSON error response containing an error
field and optionally an error_description
field.
When the request does not include the required Content-Type: application/x-www-form-urlencoded
header, the server MUST return the 415 HTTP response code
When the request does not include the required Sec-Fetch-Dest: email-verification
header:
HTTP 400 Bad Request
{
"error": "invalid-request",
"error_description": "Missing or invalid Sec-Fetch-Dest header"
}
The error_description
SHOULD specify that the Sec-Fetch-Dest header is missing or invalid.
When the request lacks valid authentication cookies, contains expired/invalid cookies, or the authenticated user does not have control of the requested email address:
HTTP 401 Unauthorized
{
"error": "authentication_required",
"error_description": "User must be authenticated and have control of the requested email address"
}
When the request_token
is malformed, missing required claims, or contains invalid values:
HTTP 400 Bad Request
{
"error": "invalid_request",
"error_description": "Invalid or malformed request_token"
}
When the request_token
signature verification fails or the token structure is invalid:
HTTP 400 Bad Request
{
"error": "invalid_token",
"error_description": "Token signature verification failed or token structure is invalid"
}
For internal server errors or temporary unavailability:
HTTP 500 Internal Server Error
{
"error": "server_error",
"error_description": "Temporary server error, please try again later"
}
In a future version of this spec, the issuer could prompt the user to login via a URL or with a Passkey request.
On receiving the issuance_token
:
-
5.1 - the browser MUST verify the SD-JWT per (SD-JWT spec) by:
- parsing the SD-JWT into header, payload, and signature components
- confirming the presence of, and extracting the
alg
andkid
fields from the SD-JWT header, and theiss
,iat
,cnf
,email
, andemail_verified
claims from the payload - parsing the email domain from the
email
claim and looking up theTXT
record for_email-verification.$EMAIL_DOMAIN
to verify theiss
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 thealg
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 keytyp
: "kb+jwt"
- Payload:
aud
: the RP's originnonce
: the nonce from the originalnavigator.credentials.get()
calliat
: current time when creating the KB-JWTsd_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 the TBD event ...
details TBD
The RP web page now has the SD-JWT+KB from the event, and passes it to the RP server, or the token was posted to the RP server.
details TBD
The RP server MUST verify the SD-JWT+KB by:
-
6.1 - the RP server receives the SD-JWT+KB from the web page
-
6.2 - the RP parses the SD-JWT+KB by separating the SD-JWT and KB-JWT components (separated by tilde ~)
-
6.3 - the RP verifies 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 theaud
,nonce
,iat
, andsd_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 - the RP verifies the SD-JWT by:
- parsing the SD-JWT into header, payload, and signature components
- confirming the presence of, and extracting the
alg
andkid
fields from the SD-JWT header, and theiss
,iat
,cnf
,email
, andemail_verified
claims from the payload - parsing the email domain from the
email
claim and looking up theTXT
record for_email-verification.$EMAIL_DOMAIN
to verify theiss
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 thealg
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 - the RP verifies the KB-JWT signature using the public key from the
cnf
claim in the SD-JWT with thealg
algorithm from the KB-JWT header
Below are notes capturing some discussions of potential privacy implications.
-
The email domain operator no longer learns which applications the user is verifying their email address to as the applications are no longer sending an email verification code to the user. By using an SD-JWT+KB, the browser intermediates the request and response so that the issuer does not learn the identity of the RP.
-
The RP can infer if a user is logged into the issuer as the RP receives a SD-JWT when the user is logged in, and does not when the user is not logged in.
-
The issuer may learn the user has email at a mail domain it is authoritative for that it did not know the user had.
The web page would call an API passing the email address and nonce. It would return a promise that resolves to the SD_JWT or an error response. The API would only be callable after a user gesture such as clicking a button labelled verify on the web page. This provides the web page in more flexibility in how to gather the email address. For example, if the web page is using EVP for login, and the user has used different emails for login and those are stored in cookies, the page can display the list of emails and an option to provide a different one. The user can then select the email they want to use rather than having to type it into a text field.
In addition to, or instead of the browser sending cookies to the Issuer, the Issuer could return a WebAuthN request to the browser if it has credentials for the user identified by the email address. The browser would then interact with the user and provide the WebAuthN response to the Issuer, authenticating the user, and the Issuer would then return the SD-JWT.
Rather than the DNS TXT record, the Mail Domain would host a JSON file in the .wellknown domain. This creates challenges for the long tail of individually owned domains:
- would require a domain that is used just for email to now have to support a web server
- the mail domain is usually an apex domain, which does not support CNAME, complicating hosting a web site