Identity Hub service APIs
Updated: June 2, 2020
Identity hubs expose a set of interfaces for reading and writing data to a DID's personal data store. This article describes the steps necessary for sending your first identity hub requests.
We generally recommend using DIF's identity hub SDK to read and write to identity hubs. Only continue with the article if you are an advanced user and you are sure you don't want to use an available SDK.
Register a DID
Before you can proceed with this article, you'll need a DID that represents a test user, and an associated private key. If you don't have one already, the steps for registering a DID are described in this registration article.
A user always has complete access to their own identity hub. In this article, we'll assume your app has access to the user's private key. Having access to such a key allows us to read, write, and modify any data in an identity hub.
Discovering hub endpoints
The first step to integrating with an identity hub is to get the user's DID. Typically the user would disclose their DID to you as part of some authentication process, as described in our sign-in tutorial. For this article, we'll assume the user's DID is did:example:abc123
.
You then need to look up the URL(s) for the user's identity hub service. Users will have different hub providers with different URLs. The correct URL for the user can be looked up via the following process:
- Resolve the user's DID to get the DID of their hub service provider.
- Resolve the hub's DID to get the HTTP endpoint for the identity hub.
- If necessary, look up the hub's interface map to get interface-specific HTTP endpoints.
- Send a hub request to the correct HTTP endpoint.
First, send a DID Discovery Request to fetch the DID document, which will have the form:
{
"document": {
"@context": "https://w3id.org/did/v1",
"publicKey": [
{
"id": "#key-1",
"type": "Secp256k1VerificationKey2018",
"publicKeyJwk": {
"kty": "EC",
"kid": "#key-1",
"crv": "P-256K",
"x": "Y4ezHen9MPuJcowKwhc9jT1owEzNb65BMUqtS7NH_C8",
"y": "wWDGd0PHYjIGRcP9owNvsSLYWzSbFLuCKE8KX75KFRY",
"use": "verify",
"defaultEncryptionAlgorithm": "none",
"defaultSignAlgorithm": "ES256K"
}
}
],
"service": [
{
"id": "IdentityHub",
"type": "IdentityHub",
"serviceEndpoint": {
"@context": "schema.identity.foundation/hub",
"@type": "UserServiceEndpoint",
"instance": [
"did:test:hub.id"
]
}
}
],
"id": "did:ion-test:EiDDNR0RyVI4rtKFeI8GpaSougQ36mr1ZJb8u6vTZOW6Vw"
},
"resolverMetadata": {
"driverId": "did:ion-test",
"driver": "HttpDriver",
"retrieved": "2019-05-10T20:07:17.489Z",
"duration": "152.6719ms"
}
}
If the user has an identity hub, the service
object will contain an entry of type IdentityHub
with a serviceEndpoint
object containing the DIDs for all identity hub services in the instances
property.
The next step is to resolve one of these hub DIDs into its HTTP endpoint(s). You can do so by sending another DID Discovery Request, this time for the hub's DID, did:example:abc456
:
"service": [{
"type": "IdentityHub",
"publicKey": "did:btcs:456#key-1",
"serviceEndpoint": {
"@context": "https://schema.identity.foundation/1.0/hub",
"@type": "HostServiceEndpoint",
"locations": ["https://hub.azure.com/.well-known/hub-configuration"]
}
}]
The response will contain another service
object with an entry of type IdentityHub
which also has a serviceEndpoint
object. A hub's serviceEndpoint
object will contain a locations
property with the URL for the hub service.
The value of the locations
endpoint may directly contain the URL that can be used for sending and receiving identity hub requests. However, if a value contains the /.well-known/hub-configuration
suffix, an additional resolution step is required.
The extra step is to send an HTTP GET request to the URL listed to fetch the hub's configuration document. This document contains additional hub metadata that describes the features and APIs supported by a given identity hub:
GET /.well-known/hub-configuration HTTP/1.1
Host: hub.azure.com
Accept: application/json
HTTP/1.1 200 OK
Content-Length: 301
Content-Type: application/json
{
"@context": "https://schema.identity.foundation/1.0/hub",
"@type": "IdentityHubConfiguration",
"endpoints": [{
"@context": "http://schema.identity.foundation/1.0/hub",
"@type": "InterfaceMap",
"interfaces": {
"Collections": "https://hub.azure.com/Collections",
"Profile": "https://www.foo.com/.identity/Profile",
"Actions": "https://id.bar.org/.identity/Actions",
...
}
},
{
"@context": "http://schema.identity.foundation/1.4/hub",
"@type": "InterfaceMap",
"interfaces": {
"Collections": "https://hub.azure.com/Collections",
"Profile": "https://www.foo.com/whatever-path/Profile",
"Actions": "https://id.bar.org/.identity/Actions",
...
}
}],
"signature": {
"kid": "did:btcs:456#key-1",
"value": "jv9323lavjsdav0d9ada2...",
"timestamp": "2018-10-24T18:39:10.10:00Z"
}
}
From this discovery document, extract the URLs that should be used for hub communication from the endpoints
array. Each value of the endpoints
array is an InterfaceMap
. The version of the identity hub API standards supported by the hub is indicated in the @context
field. The HTTP endpoints for each of the identity hub's interfaces are listed in the interfaces
object.
Finally, you can cache the interface map for a given identity hub service, and use its URLs to send and receive hub requests. For the remainder of this article, we will assume the URL for the user's hub is:
https://beta.personal.hub.microsoft.com
Understanding commits
Now that we know the URL for the identity hub, we can proceed with reading and writing data.
All data in identity hubs is represented as a series of "commits". A commit is similar to a git commit; it represents a change to an object. To write data to an identity hub, we need to construct and send a new commit to the hub. To read data from an identity hub, we need to fetch all commits from the hub. An object's current value can be constructed by applying all its commits in order.
To learn more about commits in identity hubs, refer to the hub overview article.
Constructing a commit
Let's write our first piece of data to the identity hub. The data we'll write is a simple to-do item, or task. It will have the following contents:
{
"@context": "https://schema.org",
"@type": "TodoItem",
"text": "Get milk from the grocery store",
"completed": false
}
Note, the @context
and @type
properties here are not required. They are included simply to indicate the intended semantic structure of the object. To learn more about the identity hub's semantic data model visit the hub overview.
Now, construct a commit for the new to-do item. A commit is a JSON web token (JWT) that is signed by the committer. The JWT for a commit must have the following structure:
// JWT headers
{
"alg": "RS256",
"kid": "did:example:abc123#key-abc",
"interface": "Collections",
"context": "https://schema.org",
"type": "TodoItem",
"operation": "create",
"committed_at": "2018-10-24T18:39:10.10:00Z",
"commit_strategy": "basic",
"sub": "did:example:abc123"
}
// JWT body
{
"@context": "https://schema.org",
"@type": "TodoItem",
"text": "Get milk from the grocery store",
"completed": false
}
// JWT signature
uQRqsaky-SeP3m9QPZmTGtRtMoKzyg6wwWF...
The JWT body is just our to-do item. The header values must be the following:
Header | Description |
---|---|
alg |
Standard JWT header. Indicates the algorithm used to sign the JWT. |
kid |
Standard JWT header. The value should take the form {did}#{key-id} . Another app can take this value, resolve the DID, and find the indicated public key that can be used for signature validation of the commit. Here, we have used did:example:abc123 , because the commit is signed with the user's private key. |
interface |
Must be one of Collections , Profile , Actions , or Permissions . |
context |
The context of the object. This, along with type , indicates the type of object that is being created. |
type |
The type of the object. This, along with context , indicates the type of object that is being created. |
operation |
Must be one of create , update , or delete . |
committed_at |
The time at which the commit is taking place. |
commit_strategy |
Must be basic . See the hub overview for more details. |
sub |
The DID of the user whose hub is being accessed; the hub owner's DID. |
Once you've created and signed the JWT representing the commit, you can construct a hub write request.
Constructing a write operation
A hub write request contains the commit from above as well as a few other properties necessary for data storage. A basic hub write request is a JSON object with the following format:
{
"@context": "https://schema.identity.foundation/0.1",
"@type": "WriteRequest",
"iss": "did:example:abc123",
"aud": "did:example:abc456",
"sub": "did:example:abc123",
"commit": {
"payload": "SW4gb3VyIHZpbGxhZ2UsIGZvbGtzIHNheS...",
"protected": "eyJhbGciOiJFUzI1...",
"header": {
"rev": "3a9de008f526d239...",
"iss": "did:example:abc123"
},
"signature": "b7V2UpDPytr-kMnM_YjiQ3E0J2..."
}
}
Property | Description |
---|---|
@context |
Must be https://schema.identity.foundation/0.1 . |
@type |
Must be WriteRequest . |
iss |
Issuer. The DID of the party sending the request. In this case, it is the user's DID, since we are using the user's private keys directly. The keys for this DID will be used to encrypt responses from the hub. |
aud |
Audience. The DID of the hub receiving the request. We got this DID during the hub endpoint discovery step above. The keys for this DID will be used to encrypt requests to the hub. |
sub |
Subject. The DID of the user who owns the hub where the data will be stored. |
commit |
The commit we created above, in JWT JSON Serialization format. This is an alternative representation of a JWT. It can be easily constructed by splitting the parts (header, body, signature) of a conventional compact serialized JWT and placing them in the relevant properties (protected, payload, signature). The additional header property must have the values in the rows below. |
commit.payload |
The body of the JWT. |
commit.protected |
The header of the JWT. |
commit.header.rev |
The unique ID for this commit, which can be constructed by taking the SHA-256 hash of the protected and payload values of the commit concatenated by a . character. |
commit.header.iss |
The DID of the issuer of the commit. Must match the DID used in the kid property of the commit protected headers. |
commit.signature |
The signature of the JWT. |
Once you've formed a write request JSON object, you're ready to send the request to the user's identity hub.
Authenticating a hub request
Identity hub requests and responses are signed and encrypted using the DID keys of the sender and the recipient. This protects the message over any transportation medium. All encrypted requests and responses follow the JSON Web Encryption (JWE) standard.
The steps to construct a JWE are as follows. Full documentation on the DID authentication scheme used for hub messages can be found on GitHub here.
First, construct another JWT. This JWT will be signed by the sender of the hub request; the iss
from above. This JWT must have the following form:
// JWT headers
{
"alg": "RS256",
"kid": "did:example:abc123#key-abc",
"did-requester-nonce": "randomized-string",
"did-access-token": "eyJhbGciOiJSUzI1N...",
}
// JWT body
{
"@context": "https://schema.identity.foundation/0.1",
"@type": "WriteRequest",
"iss": "did:example:abc123",
...
}
// JWT signature
uQRqsaky-SeP3m9QPZmTGtRtMoKzyg6wwWF...
The JWT body is just the hub WriteRequest
we constructed above. The header values must be the following:
Header | Description |
---|---|
alg |
Standard JWT header. Indicates the algorithm used to sign the JWT. |
kid |
Standard JWT header. The value should take the form {did}#{key-id} . Another app can take this value, resolve the DID, and find the indicated public key that can be used for signature validation of the commit. Here, we have used did:example:abc123 , because the request is signed with the user's private key. |
did-requester-nonce |
A randomly generated string that must be cached on the client side. This string will be used to verify the response from the hub in the sections below. |
did-access-token |
A token that should be cached on the client side and included in each request sent to the hub. Since we do not have an access token yet, leave this property out on the initial request. Sections below explain how to get an access token. |
This JWT must use the typical JWT compact serialization format.
We can now use this JWT as the plaintext used to construct our JWE. The JWE must have the following format:
// JWE protected header
{
"alg": "RSA-OAEP-256",
"kid": "did:example:abc456#abc-123",
"enc": "A128GCM",
}
// JWE encrypted content encryption key
OKOawDo13gRp2ojaHV7LFpZcgV7T6DVZKTyKOM...
// JWE initialization vector
48V1_ALb6US04U3b...
// JWE plaintext (the JWT from above)
eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3R...
// JWE authentication tag
XFBoMYUZodetZdv...
We strongly recommend using a JWT library to produce the above JWE. Using a library, you should only need to provide the protected headers and the plaintext. The plaintext value should be the JWT constructed above. The header values are:
Header | Description |
---|---|
alg |
Standard JWT header. Indicates the algorithm used to encrypt the JWE content encryption key. |
kid |
Standard JWT header. The value should take the form {did}#{key-id} . Indicates the hub's key that is used to encrypt the JWE content encryption key. Here, we have used did:example:abc456 , since that is the DID used by the hub. The DID used here should match the aud value in the hub WriteRequest . |
enc |
Standard JWT header. Indicates the algorithm used to encrypt the plaintext using the content encryption key to produce the ciphertext and authentication tag. |
Finally, you have a signed and encrypted hub request that can be transmitted to the user's identity hub for secure storage.
Sending a hub request
When the user's identity hub service is exposed over HTTP, you can send the hub WriteRequest
using the HTTP POST method as follows:
POST / HTTP/1.1
Host: beta.personal.hub.microsoft.com
Content-Type: application/jwt
Content-Length: 1283
eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZHQ00ifQ.
OKOawDo13gRp2ojaHV7LFpZcgV7T6DVZKTyKOMTYUmKoTCVJRgckCL9kiMT03JGe
ipsEdY3mx_etLbbWSrFr05kLzcSr4qKAq7YN7e9jwQRb23nfa6c9d-StnImGyFDb
Sv04uVuxIp5Zms1gNxKKK2Da14B8S4rzVRltdYwam_lDp5XnZAYpQdb76FdIKLaV
mqgfwX7XWRxv2322i-vDxRfqNzo_tETKzpVLzfiwQyeyPGLBIO56YJ7eObdv0je8
1860ppamavo35UgoRdbYaBcoh9QcfylQr66oc6vFWXRcZ_ZT2LawVCWTIy3brGPi
6UklfCpIMfIjf7iGdXKHzg.
48V1_ALb6US04U3b.
5eym8TW_c8SuK0ltJ3rpYIzOeDQz7TALvtu6UG9oMo4vpzs9tX_EFShS8iB7j6ji
SdiwkIr3ajwQzaBtQD_A.
XFBoMYUZodetZdvTiFvSkQ
The URL used should be the URL discovered above. The body of the POST should be the JWE produced in the above section.
Caching the access token
To send a successful request to an identity hub, you need to include an access token in the did-access-token
header of the JWE. The access token is a short-lived JWT that can be used across many hub requests until it expires.
Because the request above did not include this header, the hub will reject the request. Instead, the hub will return a JWE response (as described in the next section) whose payload is an access token. You should extract the access token from the response and cache it somewhere safe. The access token can be used for subsequent requests.
Once you've cached the access token, include it in each request in the did-access-token
JWE header as described above.
Eventually, the access token will expire. Its expiry time can be found in the exp
claim inside the access token. If you attempt to use an expired access token, the identity hub will return an error indicating a new access token is required. To get a new access token, repeat the above process sending a hub request without a did-access-token
JWE header.
Receiving a hub response
The hub will receive the request and attempt to decrypt the WriteRequest
and store the commit in the user's hub.
If the request was formatted properly and no unexpected errors occurred, the hub will respond with another JWE:
HTTP/1.1 200 OK
Date: Mon, 27 Jul 2018 12:28:53 GMT
Content-Length: 1421
Content-Type: application/jwt
Connection: Closed
eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZHQ00ifQ...
This JWE can be decrypted with the user's private key following the JWE standard to reproduce the response's plaintext.
The contents of the JWE will either be a valid hub response, described below, or a new access token. A new access token will only be included if the did-access-token
header was omitted in the request.
The hub may also respond with a different HTTP status code, which typically indicates that an error has occurred. An example is:
HTTP/1.1 400 Bad Request
Date: Mon, 27 Jul 2018 12:28:53 GMT
Content-Length: 324
Content-Type: application/json
Connection: Closed
{
"error_code": "authentication_failed",
"error_url": "https://mydocumentation.com/errors/ABC123",
"developer_message": "Unable
to verify signature of JWE in request",
"inner_error": {
"request_id": "622c9254-d0c2-45d8-8cd9-47e80153eb05",
"timestamp": "Mon, 27 Jul 2018 12:28:53 GMT",
"trace": null,
}
}
More details on the possible HTTP responses are available in the identity hub service API reference.
Write operation responses
Assuming the hub returned a 200 OK
status, and the response JWE has been successfully decrypted into its plaintext, we now have a valid hub response.
A successful response to an identity hub WriteRequest
will have the following structure:
{
"@context": "https://schema.identity.foundation/0.1",
"@type": "WriteResponse",
"revisions": ["3a9de008f526d239..."]
}
Property | Description |
---|---|
@context |
Must be https://schema.identity.foundation/0.1 . |
@type |
Must be WriteResponse . |
revisions |
An array of rev values, each of which represents a commit for the object that has been modified. Because we have just created this object, there is only one commit in the array. |
If you receive a WriteResponse
like this, your commit has been accepted and stored in the user's identity hub.
You may also receive a JWE plaintext that has the following structure:
{
"@context": "https://schema.identity.foundation/0.1",
"@type": "ErrorResponse",
"error_code": "validation_failed",
"developer_message": "An invalid value for a property was given.",
"inner_error": {
"request_id": "622c9254-d0c2-45d8-8cd9-47e80153eb05",
"timestamp": "Mon, 27 Jul 2018 12:28:53 GMT",
"trace": null,
}
}
An ErrorResponse
like this indicates that the WriteRequest
was a failure, and will need to be retried.
Constructing an object query
Now that we've written some data into the user's identity hub, we need to read it out. Reading data in identity hubs is a three step process:
- Step 1: Query for objects of a given kind, or a particular object by ID.
- Step 2: Fetch the contents of each commit for each object of interest.
- Step 3: Merge all commits for each object to produce its current value.
To query for objects, construct an identity hub ObjectQueryRequest
and send it to the user's identity hub. This can be done the same way as the WriteRequest
above; a JWE sent via HTTP to the user's hub endpoint.
The payload of the JWE for an ObjectQueryRequest
should be the following:
{
"@context": "https://schema.identity.foundation/0.1",
"@type": "ObjectQueryRequest",
"iss": "did:example:abc123",
"aud": "did:example:abc456",
"sub": "did:example:abc123",
"query": {
"interface": "Collections",
"context": "http://schema.org",
"type": "TodoItem",
// Optional
"object_id": ["3a9de008f526d239..."]
}
}
Property | Description |
---|---|
@context |
Must be https://schema.identity.foundation/0.1 . |
@type |
Must be ObjectQueryRequest . |
iss |
Issuer. The DID of the party sending the request. In this case, it is the user's DID, since we are using the user's private keys directly. The keys for this DID will be used to encrypt responses from the hub. |
aud |
Audience. The DID of the hub receiving the request. We got this DID during the hub endpoint discovery step above. The keys for this DID will be used to encrypt requests to the hub. |
sub |
Subject. The DID of the user who owns the hub where the data will be stored. |
query.interface |
One of Collections , Actions , Permissions , or Profile . Collections is most commonly used for data storage. |
query.context |
This value, along with type , indicate the type of object that is being queried for. |
query.type |
This value, along with context , indicate the type of object that is being queried for. |
query.object_id |
This optional value can be provided if the query should be restricted to a single object, instead of all objects of the provided type. |
Object query responses
A successful response to an object query will contain a list of objects in the following format:
{
"@context": "https://schema.identity.foundation/0.1",
"@type": "ObjectQueryResponse",
"objects": [
{
"interface": "Collections",
"context": "http://schema.org",
"type": "TodoItem",
"id": "3a9de008f526d239...",
"created_by": "did:example:abc123",
"created_at": "2018-10-24T18:39:10.10:00Z",
"sub": "did:example:abc123",
"commit_strategy": "basic",
},
...
]
}
Property | Description |
---|---|
@context |
Must be https://schema.identity.foundation/0.1 . |
@type |
Must be ObjectQueryResponse . |
objects[i].interface |
The hub interface where the object was retrieved from. |
objects[i].context |
This value, along with type , indicate the type of the object. |
objects[i].type |
This value, along with context , indicate the type of the object. |
objects[i].id |
The unique object_id for this object. The object_id for an object is the commit, or rev , that initally created the object. |
objects[i].created_by |
The DID of the party that created the object. |
objects[i].created_at |
The time at which the object was initally created. |
objects[i].sub |
The DID that owns the object. |
objects[i].commit_strategy |
Must be basic . See the hub overview for more details. |
An error response to an ObjectQueryRequest
will take the same ErrorResponse
format that is described above.
Constructing a read operation
Once you've queried for the object(s) of interest and have gotten all object_id
values, you can read the object by fetching all commits from the user's identity hub.
A CommitQueryRequest
for a single object can be submitted in the same way as an ObjectQueryRequest
or a WriteRequest
. All requests are JWEs sent over HTTP to the user's hub endpoint.
The structure of a CommitQueryRequest
is as follows:
{
"@context": "https://schema.identity.foundation/0.1",
"@type": "CommitQueryRequest",
"iss": "did:example:abc123",
"aud": "did:example:abc456",
"sub": "did:example:abc123",
"query": {
"object_id": ["3a9de008f526d239..."],
}
}
Property | Description |
---|---|
@context |
Must be https://schema.identity.foundation/0.1 . |
@type |
Must be CommitQueryRequest . |
iss |
Issuer. The DID of the party sending the request. In this case, it is the user's DID, since we are using the user's private keys directly. The keys for this DID will be used to encrypt responses from the hub. |
aud |
Audience. The DID of the hub receiving the request. We got this DID during the hub endpoint discovery step above. The keys for this DID will be used to encrypt requests to the hub. |
sub |
Subject. The DID of the user who owns the hub where the data will be stored. |
query.object_id |
The object_id of the object whose commits we want to read. |
Read operation responses
A successful response to a commit query will contain a list of relevant commits in the following structure:
{
"@context": "https://schema.identity.foundation/0.1",
"@type": "CommitQueryResponse",
"commits": [
{
"payload": "SW4gb3VyIHZpbGxhZ2UsIGZvbGtzIHNheS...",
"protected": "eyJhbGciOiJFUzI1...",
"header": {
"rev": "3a9de008f526d239...",
"iss": "did:example:abc123"
},
"signature": "b7V2UpDPytr-kMnM_YjiQ3E0J2..."
},
...
]
}
Property | Description |
---|---|
@context |
Must be https://schema.identity.foundation/0.1 . |
@type |
Must be CommitQueryResponse . |
commits |
An array of commits, in JSON serialized JWT format, that modify the object. The format of each commit is described in the above sections. |
An error response to an CommitQueryRequest
will take the same ErrorResponse
format that is described above.
Merge commits using the basic strategy
The object(s) queried will have many commits, each of which somehow modifies the object. Each commit's payload
contains the data that describes each modification. It's up to you to know how to merge these commits to arrive at a final object state.
Currently, the identity hub only supports one commit merge strategy, known as basic
. The basic strategy is very simple. Each commit contains the entire object. When updates are made to the object, the entire object is re-submitted in a new commit.
To determine the current value of an object, you simply need to find the commit with the most recent committed_at
value. This commit's payload contains the current value for the object.
Future versions of identity hubs may employ more advanced commit merging logic and commit representations. These advanced strategies will be indicated by a different commit_strategy
value rather than basic
.
Congratulations! You now know how to read and write data to a user's personal identity hub. You can now build applications and features that integrate with identity hubs to give users control over their sensitive data and respect user's privacy. If you have any questions or suggestions on how this document could be improved, please reach out to us.
See something missing? We'd love your feedback and input on the Verifiable Credentials preview. Please contact us. When you use Microsoft DID Services, you agree to the DID Preview Agreement and the Microsoft Privacy Statement.