OpenCred is a system designed to make it easy for organizations (verifiers) to check credentials from individuals (holders), with their consent, in a secure and verifiable way.
In other words, OpenCred is like a digital verification checkpoint where organizations can ask for proof of certain information, like a driver's license, and an individual can decide if they want to provide that information from their digital wallet.
OpenCred supports the following list of features:
The app is configured via a YAML file compatible with @bedrock/config-yaml. See configs/combined.example.yaml for an example.
Copy the example to the default config location cp configs/combined.example.yaml configs/combined.yaml
and edit the file. Configure the details for your relying
party and any of the OpenCred features below.
💡 Tip: When using VS Code with the YAML extension, you'll get type hints as you edit your
configs/combined.yaml
file.
If a BEDROCK_CONFIG
environment variable is set, the config specified in
the environment variable will supersede any file based configuration. The
environment variable must be a Base64 encoded string based on a YAML config
file. The environment variable may be set with the following command:
export BEDROCK_CONFIG=$(cat combined.yaml | base64)
If you're using VS Code as your editing environment, you can install an
extension and configure automatic schema validation for your combined.yaml
file. This will provide you with real-time feedback as you type in your
configuration file. Errors on missing required properties, descriptions and
example values for configuration fields, auto-complete of fields are supported.
To configure your VS Code workspace to use auto-completion, install the plugin
redhat.vscode-yaml
and add settings to a .vscode/settings.json
file at the root of this repo. If
the file does not exist, create it. Add the following content to the file:
{
"yaml.schemas": {
"./configs/combined.schema.json": "combined.yaml"
},
"yaml.format.enable": true,
"yaml.completion": true,
"yaml.validate": true,
"yaml.format.proseWrap": "preserve",
"yaml.format.printWidth": 80
}
Update the relyingParties
section of the config file to include a relying
party with a workflow of type native
. The native
workflow type is used to
implement an OID4VP or VC-API exchange on this instance of OpenCred. This results in a QR
code being displayed to the user or returned through the initiate exchange API
endpoint that can be scanned by a wallet app. The wallet app will then present
the user with a list of credentials that can be used to satisfy the request.
You can use OpenCred as a did:web endpoint by configuring the didWeb
section
of the config file. The following would result in a DID document being published
for the DID did:web:example.com
. The document would be available from OpenCred
at /.well-known/did.json
. If domain linkage is supported, you can find that
document at /.well-known/did-configuration.json
.
didWeb:
mainEnabled: true
linkageEnabled: true
mainDocument: >
{
"id": "did:web:example.com",
"@context": [
"https://www.w3.org/ns/did/v1",
{
"@base": "did:web:example.com"
}
],
"service": [
{
"id": "#linkeddomains",
"type": "LinkedDomains",
"serviceEndpoint": {
"origins": [
"https://example.com"
]
}
},
{
"id": "#hub",
"type": "IdentityHub",
"serviceEndpoint": {
"instances": [
"https://hub.did.msidentity.com/v1.0/test-instance-id"
]
}
}
],
"verificationMethod": [
{
"id": "test-signing-key",
"controller": "did:web:example.com",
"type": "EcdsaSecp256k1VerificationKey2019",
"publicKeyJwk": {
"crv": "secp256k1",
"kty": "EC",
"x": "test-x",
"y": "test-y"
}
}
],
"authentication": [
"test-signing-key"
],
"assertionMethod": [
"test-signing-key"
]
}
linkageDocument: >
{
"@context": "https://identity.foundation/.well-known/did-configuration/v1",
"linked_dids": ["eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa29USHNnTk5yYnk4SnpDTlExaVJMeVc1UVE2UjhYdXU2QUE4aWdHck1WUFVNI3o2TWtvVEhzZ05OcmJ5OEp6Q05RMWlSTHlXNVFRNlI4WHV1NkFBOGlnR3JNVlBVTSJ9.eyJleHAiOjE3NjQ4NzkxMzksImlzcyI6ImRpZDprZXk6ejZNa29USHNnTk5yYnk4SnpDTlExaVJMeVc1UVE2UjhYdXU2QUE4aWdHck1WUFVNIiwibmJmIjoxNjA3MTEyNzM5LCJzdWIiOiJkaWQ6a2V5Ono2TWtvVEhzZ05OcmJ5OEp6Q05RMWlSTHlXNVFRNlI4WHV1NkFBOGlnR3JNVlBVTSIsInZjIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIiwiaHR0cHM6Ly9pZGVudGl0eS5mb3VuZGF0aW9uLy53ZWxsLWtub3duL2RpZC1jb25maWd1cmF0aW9uL3YxIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmtleTp6Nk1rb1RIc2dOTnJieThKekNOUTFpUkx5VzVRUTZSOFh1dTZBQThpZ0dyTVZQVU0iLCJvcmlnaW4iOiJpZGVudGl0eS5mb3VuZGF0aW9uIn0sImV4cGlyYXRpb25EYXRlIjoiMjAyNS0xMi0wNFQxNDoxMjoxOS0wNjowMCIsImlzc3VhbmNlRGF0ZSI6IjIwMjAtMTItMDRUMTQ6MTI6MTktMDY6MDAiLCJpc3N1ZXIiOiJkaWQ6a2V5Ono2TWtvVEhzZ05OcmJ5OEp6Q05RMWlSTHlXNVFRNlI4WHV1NkFBOGlnR3JNVlBVTSIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJEb21haW5MaW5rYWdlQ3JlZGVudGlhbCJdfX0.aUFNReA4R5rcX_oYm3sPXqWtso_gjPHnWZsB6pWcGv6m3K8-4JIAvFov3ZTM8HxPOrOL17Qf4vBFdY9oK0HeCQ"]
}
You must configure a signing key by entering key information in the
signingKeys
section of the config, and the public keys will be published in
the ./well-known/jwks.json
endpoint for keys with the id_token
purpose as
well as in the .well-known/did.json
endpoint for keys with the
authorization_request
purpose.
Supported key types for JWT signing include:
JWT alg ES256
: generate a seed with npm run generate:prime256v1
.
signingKeys:
- type: ES256
id: 91705ba8b54357e00953b2d5cc2d805c25f86bbec4777ea4f0dc883dd84b4803
privateKeyPem: |
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgdU1KX0SdMjy4AzVm
5awy7B3tHz0y+mckq/x2V8fWwrmhRANCAARkJ4rsoMcdayGPTcAbgLfKRdqwN57I
n9CRsED9Yno+oC4R7xz6xXpT2CQAkioPDmou1DYYU+oMaV9lCjvw9vqs
-----END PRIVATE KEY-----
publicKeyPem: |
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEZCeK7KDHHWshj03AG4C3ykXasDee
yJ/QkbBA/WJ6PqAuEe8c+sV6U9gkAJIqDw5qLtQ2GFPqDGlfZQo78Pb6rA==
-----END PUBLIC KEY-----
purpose:
- id_token
- authorization_request
Within your relying party configuration, you may configure claims that will be
extracted from a credential and included in the id_token result of an Open ID
Connect login flow. The following example will extract the email
claim from a
credential that is presented by the user. The email
claim will be included in
the id_token that is returned to the relying party.
relyingParties:
- clientId: example
clientSecret: example
redirectUri: http://localhost:8080/oidc/callback
workflow:
...
claims:
- name: email
path: userEmail
This configuration will place an email
claim in the JWT, and the value of that
claim will be drawn from credentialSubject.userEmail
path in the credential
that is verified to match the workflow requirements, if successfully presented.
In the workflow, you can use the method appropriate to the workflow type to
specify which Verifiable Credential type, context, and/or issuers you will
accept. This enables the specification of a plaintext path
relative to
credentialSubject
to source the claim value from.
It is possible to include additional variables that will be passed along with an exchange. These can be passed through to the exchange creation process via query parameters or as JSON body properties. It is important to note that these params originate from the client side application and so should be treated as "untrusted".
While configuring a relying party workflow an untrustedVariableAllowList
property contains a list of variables that are allowed to be passed in this
manner. There is a default redirectPath
variable that will always be included.
relyingParties:
- clientId: example
workflow:
type: native
id: example-workflow
untrustedVariableAllowList:
- caseId
- color
A workflow step configures the specifics of how a presentation is requested.
The step contains a verifiablePresentationRequest
which uses a VPR to create a Presentation Exchange (PE) object to be included in the request. If for whatever
reason the constraints need to be overwritten that can be accomplished using the
constraintsOverride
property.
A step can also include a callback that will be sent an http POST request with
the id
, variables
and step
of the exchange. The callback URL can
optionally be protected by oauth2 and can include headers using a customizable
variable.
callback:
url: http://localhost:9000/callback
headersVariable: callbackHeaders
oauth:
issuer: http://example.com
token_url: http://example.com/token
client_secret: exampleClientSecret
client_id: exampleClientId
scope:
- default
OpenCred supports two methods for initiating an exchange with a wallet app,
Credential Handler API (CHAPI), and OpenID for Verifiable
Presentations(OID4VP).
Implementers may choose which of these protocols are supported by configuring
the options.exchangeProtocols
list in the config file. The order of the
protocols controls the order in which they are offered to the user.
options:
exchangeProtocols:
- chapi
- openid4vp
If this section is omitted, both protocols (openid4vp
and chapi
)
will be offered, with an OID4VP QR code offered to the user first.
The login page has text entries stored in the translations entries of the
config. To configure the text of the login page set the following entries with
the enabled languages as the first level of translations
:
translations:
en:
translations:
en: English
fr: French
translate: Translate
qrTitle: Login with your Wallet app
qrPageExplain: Scan the following QR Code using the Wallet app on your phone.
qrPageExplainHelp: (<a href="https://youtube.com">How do I do it?</a>)
qrFooter: "Note: Already on your phone with the Wallet app? Open the Wallet app, then come back and tap on the QR code above."
qrFooterHelp: Difficulty using the Wallet app to login? revert to using password <a href="#">here</a>
qrDisclaimer: If you don't have a Wallet app download it from the app store.
qrClickMessage: The Wallet app must be running in the background.
openid4vpAnotherWay: Want to try another way?
openid4vpQrAnotherWay: Use a wallet on this device
chapiPageAnotherWay: "Looking for a QR Code to scan with you wallet app instead?"
loginCta: "Login with your credential wallet"
loginExplain: "To login with your credential wallet, you will need to have the credential wallet app <with configurable URL to app stores> installed"
appInstallExplain: "If you don't have a credential wallet yet, you can get one by downloading the credential wallet app <with configurable URL to app stores>"
appCta: "Open wallet app"
copyright: "Powered by OpenCred"
pageTitle: "Login"
fr:
translations:
en: Anglais
fr: Français
translate: Traduire
qrTitle: Connectez-vous avec votre application CA DMV Wallet
...
It is also possible to use an embedded Google Translate widget that will enable
translations without including all of the translations in the configuration. To
enable this feature include a customTranslateScript
property (which will
override manual translations) in the config with a URL to a script that includes
a script for injecting the widget. To use the default Google Translate script
use the following config:
customTranslateScript: https://translate.google.com/translate_a/element.js?cb=googleTranslateElementInit
You can add auditing support to OpenCred to ensure that a VP token presented in the past was valid at the time it was presented. The VP token can be one of two formats: (1) JWT or (2) Data Integrity. In order to enable this feature, use the boolean field audit.enable
and the array field audit.fields
in the config file. Additionally, you may optionally configure the following fields: reCaptcha.enable
(boolean), reCaptcha.version
(number), reCaptcha.siteKey
(string), reCaptcha.secretKey
(string), and reCaptcha.pages
(array) (more on these later). Here is a sample audit configuration:
audit:
enable: true
fields:
- type: text
id: given_name
name: First Name
path: "$.credentialSubject.given_name"
required: true
- type: text
id: family_name
name: Last Name
path: "$.credentialSubject.family_name"
required: false
- type: date
id: birth_date
name: Date of Birth
path: "$.credentialSubject.birth_date"
required: true
- type: number
id: height
name: Height (cm)
path: "$.credentialSubject.height"
required: false
- type: dropdown
id: sex
name: Sex
path: "$.credentialSubject.sex"
required: false
options:
"Male": 1
"Female": 2
- type: dropdown
id: senior_citizen
name: Are you a senior citizen?
path: "$.credentialSubject.senior_citizen"
required: true
options:
"Yes": 1
"No": null
default: "No"
reCaptcha:
enable: true
version: 2
siteKey: 6LcNDSjdAAAAAAAAIe2uy0gavf0reiuhfer12345
secretKey: 6LcNDSjdAAAAAAAAIe3uy1gavf1reiuhfer67890
pages:
- audit
The audit.enable
field enables support for auditing in an OpenCred deployment (default: false
).
If you would also like to check for matching values in the token's credential
in a web interface, you can specify the following attributes for each
field of interest via the audit.fields
field and visit BASE_URL/audit-vp
in the browser:
type
- The field type (currently, supports text
, number
, date
, and dropdown
).id
- The field ID (can be anything, but must be unique among other fields).name
- The field name that appears in the web interface.path
- The field path in the credential (must be unique among other fields).required
- Whether the admin user is required to enter a value for the field in the web interface.options
- Data binding from user-friendly name to associated value for the field in the web interface. This property is used whenever a field can have one of multiple possible machine-readable values in a discrete set of options (e.g., Male
-> 1
, Female
-> 2
). The input for this field will be presented as a dropdown selection element. If one of the options is the absence of the field from the credential, you can represent this by binding the field to null
. For example, here are the expectations for each selection for the field named Are you a senior citizen?
in the sample snippet above:
Yes
- There exists a field with path $.credentialSubject.senior_citizen
containing value 1
in the credential.No
- There does not exist a field with path $.credentialSubject.senior_citizen
in the credential.default
- The default value for the field in the web interface (if not required). For a dropdown-type field, use the string label of the field, not the value.If you would like to enable reCAPTCHA in the audit web interface, you should specify the following fields after registering your OpenCred domain in the reCAPTCHA registration page (Note: you may register localhost
for local development):
reCaptcha.enable
- Whether to enable reCAPTCHA (default: false
).reCaptcha.version
- The version of reCAPTCHA that you registered for the domain (required if reCaptcha.enable
is true
). At the time of this writing, the only available versions are 2
and 3
.reCaptcha.siteKey
- The reCAPTCHA site key that you registered for the domain (required if reCaptcha.enable
is true
).reCaptcha.secretKey
- The reCAPTCHA secret key that you registered for the domain (required if reCaptcha.enable
is true
).reCaptcha.pages
- Array of page IDs for which to enable reCAPTCHA (audit
in the case of the audit web interface).If you want to test out the audit feature, follow these steps:
mongosh mongodb://localhost:27017/opencred_localhost
.db.Exchanges.find().pretty()
.vpToken
.cp test/fixtures/audit/vpTokenExample.json test/fixtures/audit/vpToken.json
.test/fixtures/audit/vpToken.json
and replace the value in the vpToken
field with the token from an earlier step.npm run audit-vp BASE_URL
, where BASE_URL
is the base URL of the running app, configured as app.server.baseUri
in the config.This app uses a @bedrock/express
server and a Vue 3 UI client application. It
supports hot reloading for UI changes during development.
Prerequisites:
Install dependencies and run the server:
$ npm i
$ npm run start
In order to interact with a wallet or resolve did:web
identifiers remotely, it
will be necessary to run the server over HTTPS from your local computer. You can
use localtunnel to set up a tunnel to your
local server.
First, you must install localtunnel globally.
npm i -g localtunnel
And then run the tunnel
npm run tunnel
The above command will output the domain of your remote tunnel URL. You will need to access that URL once to finish setting up the tunnel using the instructions on that page.
Set your app.server.baseUri
in your combined.yaml
with the above URL: baseUri: "https://evil-cows-return.loca.lt"
Then, you can run the server with the following:
npm run start
You can build and run the server via Docker mounting your local configuration
file with the following commands. $PWD
substitution is the expected format for
current working directory unix/bash/zsh, Substitute your actual project root
path for other systems.
$ docker build . -t opencred-platform
$ docker run -d -p 22443:22443 -v $PWD/configs:/etc/app-config opencred-platform
$ curl https://localhost:22443/health/live
OpenCred makes it easy to request a credential from a user and return information to a connected application or "relying party." This can either be done with OpenID Connect or calling OpenCred's HTTP API for more precise control.
id_token
that contains claims extracted
from the credential that the user presents.You can enable users to sign into a relying party application with a Verifiable
Credential using OpenCred as an identity provider connected over OAuth 2.0 /
OpenID Connect. OpenCred returns a signed id_token
that contains specific claims
There is an
openid-configuration
endpoint at /.well-known/openid-configuration
with
detailed information about the algorithm and protocol support that the server
has. It references a JWKS (keyset) endpoint at /.well-known/jwks.json
that
contains the signing key used to sign an id_token. Dynamic registration is not
supported, so you must configure clientId
and clientSecret
in the relying
party configuration manually, along with the credential exchange workflow that
you want to use for this client.
The OIDC workflow follows this process:
/login
endpoint with
appropriate query parameters client_id
, redirect_uri
, response_type
,
scope
, and state
.Notes:
id_token
purpose in the config to
use this method of integration. The public key will be published in the
/.well-known/jwks.json
endpoint.claims
of your relyingParty to specify which claims you
want to extract from the credential and include in the id_token
result.ES256
is the only supported signing algorithm for id_tokens to date.PKCE
not yet supported.userinfo
endpoint, the app only supports an id_token
result.Each time a relying party application requests a credential from a user,
OpenCred manages a credential "exchange" that lets the user present a Verifiable
Presentation containing a Verifiable Credential, which is verified and made
available to the relying party. The HTTP API is documented in the
OpenAPI format. You can view the API
documentation in a Swagger UI at the /api-docs
endpoint when the application
is running.
The HTTP API workflow follows this process:
clientId
, clientSecret
,
and a workflow.POST /workflows/{workflowId}/exchanges
. Authenticate this request using HTTP Basic
Auth using your client ID and client secret.OID4VP
URI and a QR
code as a Data URI that
you can present to your user to scan with a wallet app as well as a vcapi
value that you can use to initiate a CHAPI wallet flow. It contains an
exchangeId
that will be used to check status and an accessToken
that is a
short lived access token that allows you to authenticate the status check
request.GET /workflows/{workflowId}/exchanges/{exchangeId}
. Authenticate this request
with a Bearer token using Authorization: Bearer {accessToken}
with the
accessToken
from the exchange initiation. Or you may continue to use the
Basic method from the first request. The accessToken is short lived and will
expire after a 15 minutes and may be made available to a browser client,
whereas the clientId
should only be held server-side.exchange
object with a state
that is either
pending
, active
, complete
, or invalid
with additional results.Load testing can be performed using artillery.
To install artillery globally via npm
:
npm install -g artillery@latest
Ensure that there is a relyingParties configuration in config.yaml
for a
relying party with clientId: load-test
matching the configuration for that
client found in configs/config.example.yaml
. Load testing requires on this
configuration remaining congruent with hardcoded fixtures and credentials in
the load tests.
Run the load testing script:
npm run test:load
To run the load testing script against the QA environment:
With:
QA_BASIC_AUTH
variable in a .env
file which is the base64url encoding of client_id:client_secret
.QA_BASE_URL
variable in a .env
file which is the target base url.npm run test:load:qa