Express DID Auth - a challenge-response authentication model based in DIDs

This package is an implementation of the DID Auth protocol. It is designed to be easely integrated to any Express application.

Main features:

  • Automatically set up the needed endpoints to fullfil the full protocol specification (signup, auth, refresh token and logout)
  • Provides with an auth middleware to protect the desired business related endpoints
  • Allows to decide where to send the tokens (cookies, Authorization header or request body for refresh token)
  • Extensibility: it allows to add any specific business logic over the authentication/signup methods
  • Deterministic challenge generation
  • Limit requests per did per timeslot

Usage

Install

npm i @rsksmart/express-did-auth

Plug and play

This is the simplest approach. Just need to provide an express app and the desired configuration for the package and it will create the needed endpoints on your behalf.

import express from 'express'
import setupApp, { ExpressDidAuthConfig } from '@rsksmart/express-did-auth'

const config: ExpressDidAuthConfig = {
  // your config
}

const app = express()

const authMiddleware = setupApp(config)(app)

app.get('/not-protected', function (req, res) {
  res.send('This endpoint is not authenticating')
})

app.get('/protected', authMiddleware, function (req, res) {
  res.send('This endpoint is authenticating')
})

const port = process.env.PORT || 5000

app.listen(port, () => logger.info(`My express API with did-auth running in ${port}`))

Configure

All the configuration should be placed in just one object of type ExpressDidAuthConfig. That object may contain the following fields:

REQUIRED

challengeSecret: string: the secret that will be used to generate the deterministic challenge. See how we create deterministic challenges

serviceUrl: string: will be used as the audience of all the JWTs expected or emitted by this package. Should be a URI that identifies your service in the context where it is run

serviceDid: string: the did controlled by the servie. Will be used to sign JWTs.

serviceSigner: Signer: the signing function associated to the serviceDid. MUST implement ES256K algorithm, please find an example here

OPTIONAL

useCookies: boolean: determines if the access token and refresh token are saved in cookies or are returned in the body of the response. If true, the tokens will be extracted from the cookies. See how to send tokens for more information. Default: false

requestSignupPath: string: the request signup endpoint route. Default: /request-signup

signupPath: string: the signup endpoint route. Default: /signup

requestAuthPath: string: the request auth endpoint route. Default: /request-auth

authPath: string: the auth endpoint route. Default: /auth

logoutPath: string: the logout endpoint route. Default: /logout

refreshTokenPath: string: the refresh token endpoint route. Default: /refresh-token

challengeExpirationTimeInSeconds: number: the max expiration time for the generated challenge when requesting signup or auth. MUST be provided in seconds. Default: 300 (5 minutes)

maxRequestsPerTimeSlot: number: the max amount of requests per did per timeslot. Default: 20

timeSlotInSeconds: number: the amount of seconds that need to elapse before resetting the request counter. Default: 600 (10 minutes)

userSessionDurationInHours: number: the validity of each refresh token in hours. Default: 168 (one week)

rpcUrl: string: rpc url used to resolve Ethr DID identities. If not provided, will resolve using both RSK Mainnet and Testnet networks.

networkName: string: network name used to resolve Ethr DID identities. If not provided, will resolve using both RSK Mainnet and Testnet networks.

registry: string: DID Registry address used to resolve Ethr DID identities. Default: 0xdca7ef03e98e0dc2b855be647c39abe984fcf21b

accessTokenExpirationTimeInSeconds: number: the validity in seconds of each access token. Remember that it should be short because the long validity is for the refresh token. Default: 600 (10 minutes)

authenticationBusinessLogic: AuthenticationBusinessLogic: the business logic to execute when a DID tries to log in. Will be executed each time the /auth endpoint is invoked with a valid signature. If it throws an error, the error message will be returned as part of an HTTP 401 response. If not present, no business logic will be executed.

requiredCredentials: string[]: array of Verifiable Credential schemas that will be requested as part of the signup process. If neither requiredCredentials and requiredClaims are present, no sdr will be requested when a user signs up.

requiredClaims: Claim[]: array of Claims that will be requested as part of the signup process. If neither requiredCredentials and requiredClaims are present, no sdr will be requested when a user signs up.

signupBusinessLogic: SignupBusinessLogic: the business logic to execute when a DID tries to sign up. It receives the required sdr as part of the payload. Will be executed each time the /signup endpoint is invoked with a valid signature. If it throws an error, the error message will be returned as part of an HTTP 401 response. Should be used to validate the sdr against the business needings and/or to save users in any storage for future authentication validation. If not present, no business logic will be executed.

Included artifacts

Endpoints

GET /request-signup/:did

Expects the user did in the params of the request. Returns an HTTP 200 with a JSON containing { challenge, sdr? } in the body of the response. The sdr will be present if the service requires it to register (sign up) the user. See more information in the sign up protocol.

Possible error messages:

  • INVALID_DID (HTTP 401)
POST /signup

Expects the signed challenge and sdr (if needed) response in the body of the request as { response }. If using cookies, sets the cookies in the service and returns just an HTTP 200 with no content. If not using cookies, returns an HTTP 200 with a JSON containing { accessToken, refreshToken }.

Possible error messages (all HTTP 401):

  • NO_RESPONSE if response is empty or does not exist
  • INVALID_CHALLENGE if the JWT verification fails or the received challenge is invalid
  • UNAUTHORIZED_USER if signupBusinessLogic does not validate the user
  • If the signupBusinessLogic throws an error, it will be sent to the client as well.
GET /request-auth/:did: { challenge }

Expects the user did in the params of the requests. Returns an HTTP 200 with a JSON containing { challenge } in the body of the response.

Possible error messages:

  • INVALID_DID (HTTP 401)
POST /auth

Expects the signed challenge response in the body of the request as { response }. If using cookies, sets the cookies in the service and returns just an HTTP 200 with no content to the client. If not using cookies, returns an HTTP 200 with a JSON containing { accessToken, refreshToken }.

Possible error messages (all HTTP 401):

  • NO_RESPONSE if response is empty or does not exist
  • INVALID_CHALLENGE if the JWT verification fails or the received challenge is invalid
  • UNAUTHORIZED_USER if authenticationBusinessLogic does not validate the user
  • If the authenticationBusinessLogic throws an error, it will be sent to the client as well.
POST /refresh-token

If using cookies, will get the refresh token from a cookie. If not, expects the refresh token in the body of the request as { refreshToken }. Validates user session and:

  • If using cookies, sets the new tokens cookies in the service and returns just an HTTP 200 to the client.
  • If not using cookies, returns an HTTP 200 with a JSON containing the new tokens: { accessToken, refreshToken }.

Possible error messages (all HTTP 401):

  • NO_REFRESH_TOKEN if refreshToken could not be extracted from the cookies nor the body
  • INVALID_OR_EXPIRED_SESSION if invalid refresh token or expired session
POST /logout

This is a protected endpoint, so the auth middleware is executed before.

It invalidates the current user session and returns an HTTP 200 with no content.

It has not own validations, so the error messages it may respond with comes from the middleware.

Auth Middleware

Authenticates requests by checking the access token. If using cookies, will get it from a cookie. If not, expects the access token in the Authorization header of the request with the DID Auth scheme (DIDAuth ${accessToken}). It validates the extracted token and if it fullfils the protocol needs, it authenticates the request by injecting the user did in the request object so it is available to be used by the endpoint. The did will be injected under req.user.

Possible error messages (all HTTP 401):

  • INVALID_HEADER if not using cookies and the access token header does not follow the DIDAuth scheme
  • NO_ACCESS_TOKEN if access token could not be extracted from the cookies nor the Authorization header
  • EXPIRED_ACCESS_TOKEN if the access token has expired
  • INVALID_ACCESS_TOKEN if the access token nbf time is greater than current time

Run for development

The service source code is hosted in Github, so please refer directly to the README and check there the detailed guide to install and test the service locally.