Skip to content
This repository has been archived by the owner on May 17, 2024. It is now read-only.

Latest commit

 

History

History
428 lines (317 loc) · 13.4 KB

README.md

File metadata and controls

428 lines (317 loc) · 13.4 KB

Track 2.2 - Task 6: Issue credential for verified information

Progress

Description

In our previous issuing example, we issued a foobar credential to anyone who connects with us. However, this is not a likely real-world scenario. Probably the issuer wishes to issue some meaningful data that it knows to be valid.

Let's change our issuer so that it issues credentials for a verified email. The issuer displays a QR code as before, but when the connection is established, it will ask for the user's email address. It sends an email with a verification URL to this address. Only when a user opens the verification URL issuer will send the credential offer.

We must create a new schema and credential definition to issue email credentials. We also need to create logic for asking for the email address. In addition, a new endpoint needs to be added for the verification URL.

In this task, we will utilize SendGrid API for sending emails. You need an API key to access the SendGrid API. You will be provided one in the guided workshop.

🤠 Acquire SendGrid API key

Create a free account to SendGrid and acquire the API key: https://sendgrid.com/solutions/email-api/

Configure and verify also a sender identity for your email address.


Task sequence

App Overview

In this task:

We will create a new connection according to the steps in task 1 when the user reads the QR code in /issue-endpoint. We have already the most of the logic for that in place. In addition, we will add logic to the application to first verify user's email before issuing credential:

  1. Once the connection protocol is complete, the application is notified of the new connection.
  2. Application sends a basic message asking for the user's email address to the new connection.
  3. Application agent initiates the Aries basic message protocol.
  4. Wallet user gets a notification of the message.
  5. Wallet user replies with their email address.
  6. User agent initiates the Aries basic message protocol.
  7. Application gets a notification of the message.
  8. Application uses SendGrid API to send a verification email to the provided address.
  9. SendGrid handles the email sending.
  10. User receives the email and navigates to the provided URL.
  11. Application sends a credential offer to the wallet user.
  12. Application agent initiates the Aries issue credential protocol.
  13. Wallet user gets a notification of the offer.
  14. Wallet user accepts the offer.
  15. Issue credential protocol continues.
  16. Once the protocol is completed, the application is notified of the issuing success.
  17. Once the protocol is completed, the wallet user is notified of the received credential.
sequenceDiagram
    autonumber
    participant SendGrid API
    participant Client Application
    participant Application Agent
    participant User Agent
    actor Wallet User

    Note left of Wallet User: User reads QR-code from /issue-page
    Application Agent->>Client Application: <<New connection!>>
    Client Application->>Application Agent: "What's your email?"
    Note right of Application Agent: Aries Basic message protocol
    Application Agent->>User Agent: Send message
    User Agent->>Wallet User: <<Message received!>>
    Wallet User->>User Agent: "workshopper@example.com"
    User Agent->>Application Agent: Send message
    Application Agent->>Client Application: <<Message received!>>
    Client Application->>SendGrid API: Send email
    SendGrid API-->>Wallet User: <<email>>
    Wallet User->>Client Application: Navigate to http://localhost:3001/email/xxx
    Note right of Application Agent: Aries Issue credential protocol
    Client Application->>Application Agent: Send credential offer
    Application Agent->>User Agent: Send offer
    User Agent->>Wallet User: <<Offer received!>>
    Wallet User->>User Agent: Accept
    User Agent->>Application Agent: <<Protocol continues>
    Application Agent->>Client Application: <<Credential issued!>>
    User Agent->>Wallet User: <<Credential received!>>
Loading

1. Install SendGrid dependency

Stop your server (C-c).

Install the helper library:

go get github.com/sendgrid/sendgrid-go

2. Export environment variables for SendGrid API access

Open file .envrc. Add two new environment variables there:

export SENDGRID_API_KEY='<this_value_will_be_provided_for_you_in_the_workshop>'
export SENDGRID_SENDER='<this_value_will_be_provided_for_you_in_the_workshop>'

Save the file and type direnv allow.

🤠 Configure your own SendGrid account

Create API key with SendGrid UI and replace the value to SENDGRID_API_KEY variable. Configure the verified sender email to SENDGRID_SENDER variable.


3. Create new credential definition

In task 3, we created a schema and credential definition for foobar-credentials. Now we need another schema and credential definition for our email credential.

Let's modify our code for creating the schema and the credential definition.

Open file agent/prepare.go.

Modify function PrepareIssuing. Change the schemaName to "email" and attributes list to "email":

func (a *AgencyClient) PrepareIssuing() (credDefID string, err error) {
  defer err2.Handle(&err)

  const credDefIDFileName = "CRED_DEF_ID"
  const schemaName = "email"
  schemaAttributes := []string{"email"}

  ...

Then delete (or rename) file CRED_DEF_ID from the workspace root. This ensures that the schema and credential definition creation code is executed on server startup, as there is no cached credential definition id.

mv CRED_DEF_ID foobar_CRED_DEF_ID

Start your server go run ..

4. Ensure credential definition for email schema is created

Note! It will take a while for the agency to create a new credential definition. Wait patiently.

Server logs

5. Modify issuer for email changes

Open file handlers/issuer.go.

Add following rows to imports:

import (

  ...

  "github.com/sendgrid/sendgrid-go"
  "github.com/sendgrid/sendgrid-go/helpers/mail"

  ...
)

Create new global variable for SendGrid client using the API key:

var (
  sgClient = sendgrid.NewSendClient(os.Getenv("SENDGRID_API_KEY"))
)

Add new fields to email and verified to Connection-interface:

type connection struct {
  id string
  email string
  verified bool
}

Add new utility functions askForEmail and sendEmail for sending messages:

func (i *Issuer) askForEmail(connectionID string) (err error) {
  defer err2.Handle(&err)

  // Ask for user email via basic message
  pw := async.NewPairwise(i.conn, connectionID)
  try.To1(pw.BasicMessage(context.TODO(), "Please enter your email to get started."))

  return err
}

func (i *Issuer) sendEmail(content, email string) (err error) {
  defer err2.Handle(&err)

  from := mail.NewEmail("Issuer example", os.Getenv("SENDGRID_SENDER"))
  subject := "Email verification"
  to := mail.NewEmail(email, email) // Change to your recipient
  message := mail.NewSingleEmail(from, subject, to, content, content)

  log.Printf("Sending email %s to %s", content, email)
  try.To1(sgClient.Send(message))

  return err
}

Instead of issuing the credential when a new connection is established, we want to ask the user for their email and send the credential offer only after verifying their email. So when a new connection is established, we send a basic message asking for the email. Replace the contents of HandleNewConnection with following:

func (i *Issuer) HandleNewConnection(
  notification *agency.Notification,
  status *agency.ProtocolStatus_DIDExchangeStatus,
) {
  defer err2.Catch(err2.Err(func(err error) {
    log.Printf("Error handling new connection: %v", err)
  }))

  conn := i.getConnection(notification.ConnectionID)

  if conn == nil {
    // Connection was not for issuing, skip
    return
  }

  if conn.email == "" {
    i.askForEmail(conn.id)
  }
}

Add new function HandleBasicMesssageDone. This function will handle the basic messages the user is sending from the other end. If the user replies with an email address, a verification email is sent to the provided address.

func (i *Issuer) HandleBasicMesssageDone(
  notification *agency.Notification,
  status *agency.ProtocolStatus_BasicMessageStatus,
) {
  defer err2.Catch(err2.Err(func(err error) {
    log.Printf("Error handling basic message: %v", err)
  }))

  conn := i.getConnection(notification.ConnectionID)

  // Skip handling if
  // 1. Connection was not for issuing
  // 2. Message was sent by us
  // 3. Email has been already asked
  if conn == nil || status.SentByMe || conn.email != "" {
    return
  }

  msg := status.Content
  msgValid := len(strings.Split(msg, " ")) == 1 && strings.Contains(msg, "@")

  log.Printf("Basic message %s with protocol id %s completed with %s",
    msg, notification.ProtocolID, conn.id)

  if msgValid {
    i.connections.Store(conn.id, &connection{id: conn.id, email: msg})

    // Create simple verification link
    // Note: in real-world we should use some random value instead of the connection id
    content := fmt.Sprintf("Please verify your email by clicking the following link:\n http://localhost:3001/email?value=%s", conn.id)
    i.sendEmail(content, msg)

    // Send confirmation via basic message
    pw := async.NewPairwise(i.conn, conn.id)
    try.To1(pw.BasicMessage(context.TODO(), "Email is on it's way! Please check your mailbox 📫."))

  } else {
    // If email is invalid, ask again
    i.askForEmail(conn.id)
  }
}

Add new function SetEmailVerified. This function will send a credential offer of a verified email when the user has clicked the verification link.

func (i *Issuer) SetEmailVerified(connectionID string) (err error) {
  defer err2.Handle(&err)

  conn := i.getConnection(connectionID)

  // Skip handling if
  // 1. Connection was not for issuing
  // 2. Email has not been saved
  // 3. Credential has been already issued
  if conn == nil || conn.email == "" || conn.verified {
    return
  }

  i.connections.Store(conn.id, &connection{id: conn.id, email: conn.email, verified: true})

  // Create credential content
  attributes := make([]*agency.Protocol_IssuingAttributes_Attribute, 1)
  attributes[0] = &agency.Protocol_IssuingAttributes_Attribute{
    Name:  "email",
    Value: conn.email,
  }

  log.Printf(
    "Offer credential, conn id: %s, credDefID: %s, attrs: %v",
    conn.id,
    i.credDefID,
    attributes,
  )

  // Send credential offer to the other agent
  pw := async.NewPairwise(i.conn, conn.id)
  res := try.To1(pw.IssueWithAttrs(
    context.TODO(),
    i.credDefID,
    &agency.Protocol_IssuingAttributes{
      Attributes: attributes,
    }),
  )

  log.Printf("Credential offered: %s", res.GetID())
  return nil
}

5. Add endpoint for email verification

Open file main.go.

Add a new endpoint that handles email URL clicks. The function asks the issuer to send a credential offer if the connection is valid and found.

// Email verification
func (a *app) emailHandler(response http.ResponseWriter, r *http.Request) {
  defer err2.Catch(err2.Err(func(err error) {
    log.Println(err)
    http.Error(response, err.Error(), http.StatusInternalServerError)
  }))

  values := r.URL.Query()
  connID := values.Get("value")

  var html = `<html><h1>Error</h1></html>`
  if a.issuer.SetEmailVerified(connID) == nil {
    html = `<html>
    <h1>Offer sent!</h1>
    <p>Please open your wallet application and accept the credential.</p>
    <p>You can close this window.</p></html>`
  }
  try.To1(response.Write([]byte(html)))
}

Add new endpoint to router:

  router.HandleFunc("/email", myApp.emailHandler)

6. Testing

Now you should have the needed bits and pieces in place. Let's test if it works.

Navigate to page http://localhost:3001/issue and create a new pairwise connection to your web wallet user. The app should ask you for your email. Input a valid email address that you have access to.

Input email

The application sends the verification email and basic message telling the user to check their inbox.

Email sent

Check your inbox and navigate to the verification link with your desktop browser.

Email inbox

Check the web wallet view and accept the credential offer.

Credential received

Review server logs.

Server logs

7. Continue with task 7

Congratulations, you have completed task 6 and now know a little more about how to build the application logic for issuers! To revisit what happened, check the sequence diagram.

You can now continue with task 7.