credential-handler / credential-handler-polyfill

Credential Handler API polyfill
https://chapi.io
BSD 3-Clause "New" or "Revised" License
36 stars 13 forks source link

Credential Handler API polyfill (credential-handler-polyfill)

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!

Animation showing selection of Credential Handler

See the feature videos for more animations of CHAPI in action.

Table of Contents

Background

Credential Handler API (CHAPI) is:

Read more: CHAPI Motivation and Background.

Demo

Take a look at the following websites to try out a minimal CHAPI implementation:

  1. https://wallet.example.chapi.io
  2. https://issuer.example.chapi.io
  3. https://verifier.example.chapi.io

Usage

Loading the Polyfill

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!');

Requesting and Storing 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).

get()

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`

store()

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');
}

WebCredential

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);

Handling Empty Results

If the get() or store() operation resolves to a null value, this means one of two things:

  1. The user has denied or canceled the request
  2. 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:

Advanced Operations for Wallet Providers

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.

Serving a manifest.json Web app manifest

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:

https://github.com/credential-handler/chapi-demo-wallet/blob/master/wallet-worker.html

Requesting Permission to Register the Handler

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.');
}

Install

Including Credential Handler API in browser scripts

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>

Installing using Node.js (for development)

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

Features

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.

Add Credential Handler

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.

Animation showing addition of a Credential Handler

Store Credentials

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.

Animation showing storage of Credentials

Present Credentials

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.

Animation showing request for a Credential

Select Credential Handler

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.

Animation showing selection of Credential Handler

Hide Credential Handler

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.

Animation showing hiding of Credential Handler

Just-In-Time Install of Credential Handler

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.

Animation showing Just-In-Time Addition of Credential Handler

Works on Mobile

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.

Animation showing Credential Handler working on Mobile

Security

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.

Contribute

See the contribute file!

PRs accepted.

Note: If editing the README, please conform to the standard-readme specification.

Commercial Support

Commercial support for this library is available upon request from Digital Bazaar: support@digitalbazaar.com

License

New BSD License (3-clause) © Digital Bazaar