Blockstack authentication server-side (node.js)

Zinc is a work identity platform that shares Blockstack’s vision for decentralisation and privacy. We’ve entered the app mining programme and written about our experience adding the blockstack authentication flow below. Find out more: zinc.work

Intro

The Blockstack authentication flow enables users to authenticate themselves in an entirely client-side fashion. This way, users can port their identity and private data without the need for third party authentication. 

But… what if you need to authenticate with a server? For one reason or another, users may need to authenticate with third parties in order to access services that they trust. Luckily, Blockstack has all the methods required to support this use case. 

Client set-up

The first half of the flow works exactly the same as the client-only flow described in the Blockstack guide. Your app will generate an authRequest with the necessary scopes and redirect your user to the blockstack browser. Your user will return to the callback URL with an authResponse (JWT) which contains all the information needed to authenticate the user. You’ll need to POST the authResponse to your server for authentication. 

Server set-up

The authResponse token contains all the information needed to authenticate your user including a unique identifier, email address, signature and the URL needed to fetch the user’s profile data. 

  • First, we can use the blockstack.verifyAuthResponse() method to verify that the authResponse is valid.
  • Then, decode the JWT to reveal the user’s unique identifier, email and profile URL.
  • Then, perform a GET request to the profile URL to retrieve the user’s name.
** FOR DEMONSTRATION PURPOSES ONLY **
import request from "request-promise"
import { verifyAuthResponse } from "blockstack"
import { decodeToken } from "jsontokens"
const LOOKUP_URL = "https://core.blockstack.org/v1/names"
async function blockstackAuth(req: Request, res: Response) { 
const authResponse = req.body.authResponse
let token: any
try {
const valid = await verifyAuthResponse(authResponse, LOOKUP_URL)
token = decodeToken(authResponse).payload
if (!valid || !token) {
throw new Error("invalid authResponse.")
}
}
catch (e) {
return res.status(400).send(e)
}
const userJSON = await request(token.profile_url)
const userToken = JSON.parse(userProfile)[0].decodedToken
const userData = userToken.payload.claim
const userUniqueIdentifier = token.iss
const userEmail = token.email
const userFullName = userData.name

// Some logic relating to the now authenticated user e.g. sign-up
  res.status(200).send({ message: "all good!" })
}

Catch — clock skew 

Time sensitive JWT authentication can suffer from clock skew, where the issuer’s time and the authenticator’s time is out of sync.

If you find that a blockstack authResponse is deemed invalid by your server, it might be because your server’s time is behind that of the server which issued the authResponse. The blockstack.verifyAuthResponse() method will deem the futuristic issuance time as invalid.

To resolve this, at Zinc, we omitted the issuance validation of the blockstack.verifyAuthResponse() method in favour of a custom method which allowed for time discrepancy (or “leeway” by the JWT standard). We made use of the Blockstack component functions to re-construct our own authResponse validation:

// blockstack.js methods 
await blockstack.isExpirationDateValid(authResponse)
await blockstack.doSignaturesMatchPublicKeys(authResponse)
await blockstack.doPublicKeysMatchIssuer(authResponse)
await blockstack.doPublicKeysMatchUsername(authResponse, LOOKUP_URL)
// custom issuance date validation method
await isIssuanceDateValid(authResponse, {leeway: 30})

If you have any questions or suggestions for improving this article then please contact george@zinc.work