Developers

Sign in with Atala PRISM

This guide provides a step-by-step explanation for integrating a Decentralized Identifier (DID) based login on your website using PRISM onboard and authenticate.

Assumptions

Before proceeding with the integration, we assume that

  1. You have tried out the demo available here.
  2. You have access to the PRISM Onboard and Authenticate Service and can use the PRISM agent's API.
  3. You understand how to invoke the PRISM agent in your backend code. Please familiarize yourself with the PRISM documentation for onboard and authenticate, available here.
  4. You have a mechanism to manage subsequent requests using a Cookie or Bearer Token. The sign-in process does not handle these subsequent requests. On this website we are using Microsoft asp.net identity in the backend and using cookies to detect a returning user.
  5. The blocktrust identity wallet is installed and you aim to trigger the onboard and authenticate flow on it.

This page implements the PRISM onboard and authenticate services, thus you may inspect the code used here for reference.

Wallet installation

The wallet injects code into your website after you click on the browser's Action icon for the wallet and have granted the wallet permission via a browser dialog. 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> 
                    

Starting the Onboarding Flow

1. Obtaining a DID Request

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.

2. Adding a Registration Button

Implement a button for user registration. Here is a simple example:

                    
<div id="prismOnboardRequestButton" type="button">
    Register
</div>
                    

3. Adding a Click Event Listener

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.

4. Adding a Listener for the Wallet's Response Message

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.

5. Calling the PRISM Agent and Answering the DID Request

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.

6. Adding a Registration Page

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.

Starting the Authentication Flow

Just like the onboarding flow, the authentication flow follows certain steps to ensure secure user login through the Blocktrust Identity Wallet.

1. Obtain a Challenge request

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"
}
                    

2. Set up a "Sign In" button

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>
                    

3. Add a click event listener

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.

4. Create a listener for wallet's response

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.

5. Call the PRISM agent to verify the 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.

6. Confirm user login

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.

Note

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.