Credential Handler API (CHAPI) polyfill for browsers
The CHAPI polyfill provides a number of features that enable the issuance, holding, presentation, and general management of Verifiable Credentials, Authorization Capabilities, and a variety of other cross-origin credentials.
For more information on CHAPI and links to updated demos and a playground, check out chapi.io!
See the feature videos for more animations of CHAPI in action.
Credential Handler API (CHAPI) is:
- a browser API
- lets web apps securely
get()
andstore()
credentials, without having to roll their own wallet infrastructure - provides a secure trusted UI for users to manage those credentials
- gives users ability to choose service providers for wallets
Read more: CHAPI Motivation and Background.
Take a look at the following websites to try out a minimal CHAPI implementation:
Before you can get and store credentials, you need to load the polyfill library.
If you're loading the polyfill from a <script>
tag, you will have
access to the navigator.credentials
and credentialHandlerPolyfill
globals.
const polyfill = window.credentialHandlerPolyfill;
// must be run from an async function if top-level await is unavailable
await polyfill.loadOnce();
console.log('Ready to work with credentials!');
Otherwise (if you're developing on Node.js and using Webpack, for example), import it in the usual manner:
import * as polyfill from 'credential-handler-polyfill';
// must be run from an async function if top-level await is unavailable
await polyfill.loadOnce();
console.log('Ready to work with credentials!');
A web application can get()
and store()
credentials without knowing anything
about the user's wallet. This is intentional; for privacy reasons, the client
app must not be able to query any information (without user consent) about which wallets or
credential handlers a user may have installed (otherwise, fingerprinting and
other attacks would be possible).
A web app (a Relying Party or verifier) can request a credential using
credentials.get()
during a user gesture event, for example when the user
pushes a button on a page that requires identity attributes or authentication.
const credentialQuery = {
// "web" means to ask the user to select a credential handler, aka
// "wallet", that can provide Web-based credentials (as opposed to
// asking for a local stored password or 2FA credential)
web: {
// one type of Web-based credential that can be asked for is a
// "VerifiablePresentation" that contains Verifiable Credentials
VerifiablePresentation: {
// this data is not read or understood by the credential mediator;
// it must be understood by the user-selected credential handler;
// this example uses the Verifiable Presentation Request (VPR) format;
// any other JSON-based format can also be used
query: [{
type: "QueryByExample",
credentialQuery: {
reason: "A university degree is required to complete your application.",
example: {
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://www.w3.org/2018/credentials/examples/v1"
],
"type": ["UniversityDegreeCredential"]
}
}
]
},
// these are optional credential handler origins that can be recommended to
// the user if they don't have a credential handler, aka "wallet", they
// want to use already
recommendedHandlerOrigins: [
"https://wallet.example.chapi.io"
],
// these are optional protocol URLs that will be passed to credential
// handlers that have registered their "credential_handler.acceptedInput"
// type as "url" in their web app manifest (instead of the default
// "event"); these credential handlers do not communicate with the mediator
// via events, but rather instead receive these "protocols" as a query
// parameter appended to their registered "credential_handler.url";
// additionally, the registered credential handler must have specified
// a matching protocol via "credential_handler.acceptedProtocols", e.g.,
// `"credential_handler": {"acceptedProtocols": ["OID4VCI"], ...}`
protocols: {
OID4VCI: 'openid-initiate-issuance://?issuer=https%3A%2F%2Fexample.edu%2Foid4vci-example&credential_type=https%3A%2F%2Fexample.org%2Fexamples%23UniversityDegreeCredential&pre-authorized_code=not_real_12345',
vcapi: 'https://vcapi.example/exchanges/zc32763fhjsdfa2wf32/exchange/zcx6wef73f632f23f23f'
}
}
};
const webCredential = await navigator.credentials.get(credentialQuery);
if(!webCredential) {
console.log('no credentials received');
}
// the `webCredential.data` property will hold the query-specific
// response such as a `VerifiablePresentation`
A web app (for example, a credential issuer such as a university or institution) can ask to store a credential during a user gesture event, for example when the user pushes a button to receive a credential.
TODO: Expand on WebCredential
object
const result = await navigator.credentials.store(webCredential);
if(!result) {
console.log('store credential operation did not succeed');
}
TODO: Discuss creating and receiving WebCredential instances
When working with VerifiableCredentials (just one type of credential supported by CHAPI), a VerifiablePresentation is used to both store or present VerifiableCredentials. When storing a VerifiableCredential, the VerifiablePresentation does not need to be signed.
const presentation = {
"@context": [
"https://www.w3.org/2018/credentials/v1"
],
"type": "VerifiablePresentation",
"verifiableCredential": [{
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://www.w3.org/2018/credentials/examples/v1"
],
"id": "http://example.edu/credentials/1872",
"type": ["VerifiableCredential", "AlumniCredential"],
"issuer": "https://example.edu/issuers/565049",
"issuanceDate": "2010-01-01T19:73:24Z",
"credentialSubject": {
"id": "did:example:ebfeb1f712ebc6f1c276e12ec21",
"alumniOf": {
"id": "did:example:c276e12ec21ebfeb1f712ebc6f1",
"name": {
"value": "Example University",
"lang": "en"
}
}
},
"proof": {
"type": "RsaSignature2018",
"created": "2017-06-18T21:19:10Z",
"proofPurpose": "assertionMethod",
"verificationMethod": "https://example.edu/issuers/keys/1",
"jws": "eyJhbGciOiJSUzI1NiIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..TCYt5XsITJX1CxPCT8yAV-TVkIEq_PbChOMqsLfRoPsnsgw5WEuts01mq-pQy7UJiN5mgRxD-WUcX16dUEMGlv50aqzpqh4Qktb3rk-BuQy72IFLOqV0G_zS245-kronKb78cPN25DGlcTwLtjPAYuNzVBAh4vGHSrQyHUdBBPM"
}
}]
};
const options = {
// optionally include `recommendedHandlerOrigins` so the user can choose an
// applicable wallet if they don't have one yet:
recommendedHandlerOrigins: [
'https://wallet.example.chapi.io'
],
// these are optional protocol URLs that will be passed to credential
// handlers that have registered their "credential_handler.acceptedInput"
// type as "url" in their web app manifest (instead of the default "event");
// these credential handlers do not communicate with the mediator via events,
// but rather instead receive these "protocols" as a query parameter appended
// to their registered "credential_handler.url"; additionally, the registered
// credential handler must have specified a matching protocol via
// "credential_handler.acceptedProtocols", e.g.,
// `"credential_handler": {"acceptedProtocols": ["OID4VCI"], ...}`
protocols: {
OID4VCI: 'openid-initiate-issuance://?issuer=https%3A%2F%2Fexample.edu%2Foid4vci-example&credential_type=https%3A%2F%2Fexample.org%2Fexamples%23UniversityDegreeCredential&pre-authorized_code=not_real_12345',
vcapi: 'https://vcapi.example/exchanges/zc32763fhjsdfa2wf32/exchange/zcx6wef73f632f23f23f'
}
};
const webCredential = new WebCredential(
'VerifiablePresentation', presentation, options);
If the get()
or store()
operation resolves to a null
value, this means one
of two things:
- The user has denied or canceled the request
- The user does not have a wallet (credential handler service) installed
As mentioned previously, there is (intentionally) no way for the client to know which of these is the case.
As an app developer, the recommended way to handle this situation depends on your specific use case. This dilemma is familiar to mobile app developers asking for specific phone permissions (to access the camera or location, for example). It is up to you to decide whether your app has fallback mechanisms, or whether the operation is required and things come to a halt without it.
Typical ways of handling empty results may include:
- Invite the user to install a wallet if they haven't already (and provide a link/recommendation)
- (In case the user denied the request) Invite the user to retry the operation, after explaining why you're asking to get or store the credential
- (If possible/applicable) Provide an alternate path to the user (the conceptual equivalent of allowing "Guest Checkout" if the user has refused to register for an ecommerce account).
For most web app developers, your only interaction with the Credential Handler
API will be through the get
and store
operations.
However, if you're a service provider aiming to offer users a credential management service or a wallet, you will need the advanced API to prompt the user for permission to install.
In order to register a credential handler, the credential handler website must serve a "manifest.json" file from its root path ("/manifest.json"). This file must also be CORS-enabled.
The "manifest.json" file must, at a minimum, contain a "credential_handler" field that expresses the location of the credential handler and which types of Web credentials are supported. An example minimal "manifest.json" is:
{
"credential_handler": {
"url": "/wallet-worker.html",
"enabledTypes": ["VerifiablePresentation"]
}
}
A better "manifest.json" that would include display hints is:
{
"icons": [
{
"sizes": "48x48 64x64",
"src": "demo-wallet.png",
"type": "image/png"
}
],
"name": "Demo Wallet",
"short_name": "Demo Wallet",
"credential_handler": {
"url": "/wallet-worker.html",
"enabledTypes": ["VerifiablePresentation"]
}
}
If a Web app manifest with a proper "credential_handler" field cannot be
retrieved from /manifest.json
, then any permission request to allow the
site to manage credentials for a user will be denied.
An example wallet worker page (the url
for the credential handler)
can be found here:
const {CredentialManager, CredentialHandlers} = polyfill;
// if permission is granted, the handler URL in `credential_handler` in
// `/manifest.json` will be installed and made available as a choice whenever
// the `enabledTypes` in `credential_handler` in `/manifest.json` match a
// credential `get` or `store` request
const result = await CredentialManager.requestPermission();
if(result !== 'granted') {
throw new Error('Permission denied.');
}
Adding the following <script>
makes the navigator.credentials
and
credentialHandlerPolyfill
globals available to your code.
<script src="https://unpkg.com/credential-handler-polyfill@3/dist/credential-handler-polyfill.min.js"></script>
To install as a dependency of another project, add this to your package.json
:
"credential-handler-polyfill": "^3.0.0"
If you plan to develop or modify this polyfill, install it from Github:
git clone https://github.com/credential-handler/credential-handler-polyfill.git
cd credential-handler-polyfill
npm install
The CHAPI polyfill provides a number of features that enable the issuance, holding, presentation, and general management of Verifiable Credentials, Authorization Capabilities, and a variety of other cross-origin credentials.
You can add a Credential Handler by calling the
CredentialManager.requestPermission()
API. This call will ensure that the
individual using the browser explicitly confirms that they want to use the
website as a credential handler. This call must be called immediately
following a user interaction, such as a button click or tap in order to
ensure that the permission prompt can be shown to the user.
CHAPI supports storing credentials via the navigator.credentials.store()
API.
Storage of credentials prompts the individual using the browser to confirm
that they want to store the credential in their digital wallet.
CHAPI supports the presentation of credentials via the
navigator.credentials.get()
API. CHAPI is agnostic to the presentation
request query language and passes the query directly through to the credential
handler. When presenting credentials, the individual is shown what they will
be sharing and must provide explicit consent before the credentials are
shared with the requesting party.
Multiple credential handlers may be registered. If an individual has multiple credential handlers registered, they are given the option of selecting between the handlers or setting one as the default on a per-website basis.
When an individual desires to not use a credential handler anymore, they can hide that credential handler via the interface. If they accidentally click the hide button, they have several seconds to undo the action. Credential Handlers that are hidden can be added again by going to the registration website.
If an individual has no credential handlers registered, the website that uses CHAPI can suggest up to three credential handlers that can be "just in time" installed so that the original storage operation can complete.
CHAPI is designed to run on desktop, tablet, and mobile form factors. The interface is responsive to provide the best experience for each form factor.
This polyfill makes use of a UI that emulates secure browser UI (also known as "browser chrome"). This polyfill UI is an emulation and IS NOT implemented by the browser. Support for the Credential Handler API could make this UI (or most likely a much better one!) a reality in browsers in the future.
See the contribute file!
PRs accepted.
Note: If editing the README, please conform to the standard-readme specification.
Commercial support for this library is available upon request from Digital Bazaar: [email protected]
New BSD License (3-clause) © Digital Bazaar