Developers
This guide provides a step-by-step explanation for integrating a Decentralized Identifier (DID) based login on your website using PRISM onboard and authenticate.
Before proceeding with the integration, we assume that
This page implements the PRISM onboard and authenticate services, thus you may inspect the code used here for reference.
The wallet injects code into your website. You can verify if the wallet is installed using this JavaScript code snippet:
<a id="walletInstallation" href="https://github.com/bsandmann/blocktrust-identity-wallet">identity wallet</a>
<script>
// if the wallet is not installed show it
if (typeof blocktrust !== 'undefined') {
document.getElementById("walletInstallation").style.display = "none";
}
else {
document.getElementById("walletInstallation").style.display = "inline-block";
}
</script>
When loading your site on the backend, invoke the PRISM agent to get a DID request. This can also be done in JavaScript if you don't need to conceal the API key. The PRISM agent will return a response that looks like this:
{
"self": "https://demo.atalaprism.io/did-requests/did-request-1234",
"kind": "DidRequestState",
"id": "did-request-1234",
"didRequest": {
"type": "https://atalaprism.io/did-request",
"onboardEndpoint": "https://demo.atalaprism.io:8085/request-id-1234",
"from": "Government Issuer"
},
"did": "did:peer:12345",
"state": "pending",
"createdAt": "2021-10-31T09:22:23Z",
"updatedAt": "2021-12-31T13:59:59Z"
}
The inner part of the response is what we need to forward to the wallet. We also have to provide it as information on the page (preferably hidden), as seen also in step 3.
Implement a button for user registration. Here is a simple example:
<div id="prismOnboardRequestButton" type="button">
Register
</div>
Add an event listener that triggers when the user clicks on the registration button.
// information set on the page, so that the event-listener can grab them.
<div style="display:none">
<span>ONBOARD: </span>
<span id="prismOnboardDidRequestFrom">@Model.DidRequestState.Value.DidRequest.From</span>
<span id="prismOnboardDidRequestType">@Model.DidRequestState.Value.DidRequest.Type</span>
<span id="prismOnboardDidRequestOnboardEndpoint">@Model.DidRequestState.Value.DidRequest.OnboardEndpoint</span>
</div>
...
<script>
if (document.getElementById("prismOnboardRequestButton")) {
document.getElementById("prismOnboardRequestButton").addEventListener(
// Sends a message to the content-script
"click",
() => {
// Reads the contents of the html-elements generated from the DIDRequest
var prismOnboardDidRequestType = document.getElementById("prismOnboardDidRequestType").innerHTML;
var prismOnboardDidRequestFrom = document.getElementById("prismOnboardDidRequestFrom").innerHTML;
var prismOnboardDidRequestOnboardEndpoint = document.getElementById("prismOnboardDidRequestOnboardEndpoint").innerHTML;
// Basic validation
if (prismOnboardDidRequestType != 'https://atalaprism.io/did-request') {
console.log('invalid prism-onboard-type')
return;
}
if (!prismOnboardDidRequestFrom || prismOnboardDidRequestFrom.trim() === '') {
console.log('invalid prism-onboard-from')
return;
}
if (!prismOnboardDidRequestOnboardEndpoint.includes('.atalaprism.io/enterprise/onboard/')) {
console.log('invalid prism-onboard-endpoint')
return;
}
sendWalletRequest({
type: "PRISMONBOARD_REQUEST",
content: {
'type': prismOnboardDidRequestType,
'from': prismOnboardDidRequestFrom,
'onboardEndpoint': prismOnboardDidRequestOnboardEndpoint
},
iframeMode: false
}, callbackPrismOnboardRequest)
},
false
);
}
function callbackPrismOnboardRequest(result) {
//optional logging
}
</script>
This JavaScript function reads the response values from the previous DID requests, conducts basic validation (though similar validation is also executed in the wallet, this step can be skipped), and sends the request to the Wallet using the type PRISMONBOARD_REQUEST. You can also register a callback to get an immediate result from the wallet, which can be useful for debugging.
The next step is to handle the wallet's return message. Register an event listener and attach it to the window object, so all messages are parsed.
<script>
...
(() => {
window.addEventListener(
"message",
(event) => {
if (event.data && event.data.messageType === 'WALLET_RESPONSE_MSG') {
if (window.location.origin.toLowerCase() !== event.data.content.sourceOrigin.toLowerCase()) {
console.log("invalid source. Expected " + event.data.content.sourceOrigin + " , but found " + window.location.origin)
} else if (event.data.timestamp < 10000) {
console.log("invalid timestamp")
} else {
var walletResponseType = event.data.content.walletResponseType;
if (walletResponseType === "PRISMONBOARD_RESPONSE") {
console.log("HTML received onboard response:")
var onboardedDid = event.data.content.onboardedDid;
var onboardEndpoint = event.data.content.onboardEndpoint;
console.log(onboardedDid)
console.log(onboardEndpoint)
let parts = onboardEndpoint.split('.atalaprism.io/enterprise/onboard/');
let requestId = parts[1];
// send message to the backend to initiate the registration in the database
fetch('/api/prism/onboardCompletion', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({ "onboardedDid": onboardedDid, "onboardEndpoint": onboardEndpoint })
})
.then(response => location.assign('/identity/prismRegistration/' + requestId));
}
}
}
}
,
false
)
})();
</script>
This code filters messages by type, performs basic validation, and decodes the inner content of the message. The returned objects include the onboardedDID and onboardedEndpoint. The information is then submitted to the backend of the website.
In the backend, call the PRISM agent as specified in the PRISM onboarding documentation. Be aware that you don't need an API key for this step.
First, the PRISM agent is called to onboard the DID from the wallet. Next, a new entry is created in the database to store the DID, current date/time, and requestId. Once completed, the user is redirected to the registration page with the previously stored requestId.
The registration page retrieves the entry from the database based on the requestId and provides options to finalize registration. After the user accepts the terms of service, a flag for this agreement is set in the database.
While the onboarding was completed from the perspective from the PRISM agent already after the completion of step 5, our own state of the onboarding is only complete after the user agreed to the terms of service.
Just like the onboarding flow, the authentication flow follows certain steps to ensure secure user login through the Blocktrust Identity Wallet.
Start by calling the PRISM agent to get a Challenge request. You will get a response similar to the following code. Note that the inner part of the response needs to be sent to the wallet. Hence, it is recommended to store these details in a hidden DOM element on the page for subsequent use (discussed further in step 3).
{
"self": "https://demo.atalaprism.io/did-authentication/challenges/challenge-id-1234",
"kind": "AuthenticationChallengeState",
"id": "challenge-id-1234",
"did": null,
"challenge": {
"type": "https://atalaprism.io/authentication-challenge",
"submissionEndpoint": "https://demo.atalaprism.io:8085/did-authentication/challenge-submissions/request-id-1234",
"nonce": "authenticate-NzIxZTZmNjQtOGY0Ni00ODQ4LWFhYjAtZGYzNDJmYzNlMjM2",
"from": "The App",
"expireAt": "2021-10-31T09:22:23Z"
},
"state": "pending",
"createdAt": "2021-10-31T09:22:23Z",
"updatedAt": "2021-12-31T13:59:59Z"
}
Similar to the registration process, you need to create a "Sign In" button to trigger the authentication flow. Here's an example:
<div id="prismOnboardRequestButton" type="button">
Register
</div>
Once the "Sign In" button is created, add an event listener to detect when someone clicks on that button. The event listener will trigger the authentication process upon the user action and send the request to the wallet.
if (document.getElementById("prismAuthenticateRequestButton")) {
document.getElementById("prismAuthenticateRequestButton").addEventListener(
// Sends a message to the content-script
"click",
() => {
// Reads the contents of the html-elements generated from the Challange
var prismAuthenticateChallengeType = document.getElementById("prismAuthenticateChallengeType").innerHTML;
var prismAuthenticateChallengeFrom = document.getElementById("prismAuthenticateChallengeFrom").innerHTML;
var prismAuthenticateChallengeEndpoint = document.getElementById("prismAuthenticateChallengeEndpoint").innerHTML;
var prismAuthenticateChallengeNonce = document.getElementById("prismAuthenticateChallengeNonce").innerHTML;
var prismAuthenticateChallengeExpireAt = document.getElementById("prismAuthenticateChallengeExpireAt").innerHTML;
// Basic validation
if (prismAuthenticateChallengeType != 'https://atalaprism.io/authentication-challenge') {
console.log('invalid prism-authenticate-type')
return;
}
if (!prismAuthenticateChallengeFrom || prismAuthenticateChallengeFrom.trim() === '') {
console.log('invalid prism-autenticate-from')
return;
}
if (!prismAuthenticateChallengeEndpoint.includes('.atalaprism.io/enterprise/did-authentication/challenge-submissions/')) {
console.log('invalid prism-authenticate-endpoint')
return;
}
if (!prismAuthenticateChallengeNonce || prismAuthenticateChallengeNonce.trim() === '') {
console.log('invalid prism-autenticate-nonce')
return;
}
if (!prismAuthenticateChallengeExpireAt || prismAuthenticateChallengeExpireAt.trim() === '') {
console.log('invalid prism-autenticate-expiresAt')
return;
}
sendWalletRequest({
type: "PRISMAUTHENTICATE_REQUEST",
content: {
'type': prismAuthenticateChallengeType,
'from': prismAuthenticateChallengeFrom,
'submissionEndpoint': prismAuthenticateChallengeEndpoint,
'nonce': prismAuthenticateChallengeNonce,
'expireAt': prismAuthenticateChallengeExpireAt,
},
iframeMode: false
}, callbackPrismOnboardRequest)
},
false
);
}
The callback function is again optional, and might be used for logging.
Next, establish an event listener to handle the response message coming from the wallet. This involves parsing the response to understand whether the user has been authenticated successfully.
...
(() => {
window.addEventListener(
"message",
(event) => {
if (event.data && event.data.messageType === 'WALLET_RESPONSE_MSG') {
if (window.location.origin.toLowerCase() !== event.data.content.sourceOrigin.toLowerCase()) {
console.log("invalid source. Expected " + event.data.content.sourceOrigin + " , but found " + window.location.origin)
} else if (event.data.timestamp < 10000) {
console.log("invalid timestamp")
} else {
var walletResponseType = event.data.content.walletResponseType;
if (walletResponseType === "PRISMAUTHENTICATE_RESPONSE") {
console.log("HTML received authenticate response:")
var authenticatedDid = event.data.content.authenticatedDid;
var authenticateEndpoint = event.data.content.authenticateEndpoint;
var authenticateSignature = event.data.content.authenticateSignature;
console.log(authenticatedDid)
console.log(authenticateEndpoint)
console.log(authenticateSignature)
let parts = authenticateEndpoint.split('.atalaprism.io/enterprise/did-authentication/challenge-submissions/');
let challengeSubmissionId = parts[1];
// send message to the backend to initiate the registration in the database
fetch('/api/prism/authenticateCompletion', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({ "authenticatedDid": authenticatedDid, "authenticateEndpoint": authenticateEndpoint, "authenticateSignature": authenticateSignature })
})
.then(response => location.assign('/'));
}
}
}
}
,
false
)
})();
With a successful return message from the wallet, the script is now calling end endpoint on the website to then also complete the PRISM challenge.
Implement a function in your backend to call the PRISM agent and verify the challenge. This involves matching the response from the wallet with the challenge initially provided by the PRISM agent.
Once the challenge is verified and matches the initial request, the user is considered as successfully logged in. From this point, the authenticated session for the user begins.
Keep in mind that the challenge provided by the PRISM agent has an expiration date. You should renew the challenge as necessary or only ask for it when required. Also, remember that logging out does not delete the account from the contacts.