Sign In With Tezos: Access Control Management using Tezos NFTs

Overview

SIWT

Sign In With Tezos (SIWT) is a library that supports the development of your decentralized application (dApp) by

  • proving the users ownership of the private key to the address the user signs in with,
  • adding permissions to use your API or FrontEnd based on the ownership of a Non-Fungible Token (NFT).

Table of content:

Technical concepts

SIWT Message

The message is constructed from the URL to your dApp and the user's wallet address more specifically the private key hash (pkh).

Create the message:

import { createMessage } from '@stakenow/siwt'

// constructing a message
const message = createMessage({
  dappUrl: 'your-cool-app.xyz',
  pkh: 'tz1',
})

The resulting message will look something like this:

0501000000bc54657a6f73205369676e6564204d6573736167653a2055524c20323032322d30342d32385430383a34383a33332e3636345a2055524c20776f756c64206c696b6520796f7520746f207369676e20696e207769746820504b482e200a2020

Deconstructing this message will reveal the following format:

  • 05: Indicates that this is a Micheline expression
  • 01: Indicates it is converted to bytes
  • 000000bc: Indicates the lenght of the message in hex
  • 54...: Is the actual message in bytes

This message is now ready to be signed by the user.

Signing in the user

The user specific signature derived from the signed message is used to sign the user into the dApp.

To successfully sign in you will need:

  • The original message that was created earlier using the createMessage function,

  • the signature itself and

  • the public key of the user.

    (Be aware that this is not the public key hash (pkh) also known as the address. This public key can be obtained when asking permissions from Beacon.)

With this you can verify the user is the actual owner of the address he/she is trying to sign in with. It is very similar to a user proving the ownership of their username by providing the correct password. This verification happens server side. This means you will have to set up a server that provides the API access. At this point the library looks for a signin endpoint. This is (for now) a hard requirement.

import { signin } from '@stakenow/siwt'

const API_URL = 'https://url-to-your-api.xyz'
const verification = signin(API_URL)({
  message
  signature,
  pk,
})

Query access control

Now that the user is signed in to your dApp, you can check whether your user has the required NFT to obtain permissions for your app. For this you can use queryAccessControl.

The queryAccessControl function requires your NFT token contract, the pkh of the user and the ruleset to test against:

  {
    contractAddress: 'CONTRACT_ADDRESS'
    parameters: {
      pkh: 'PKH'
    }
    test: {
      comparator: '='
      value: 1
    }
  }

Tokens

Now that we have permissions it is time to let your dApp know. For communicating information about your user, JWT tokens are being used. SIWT provides an abstraction to make it more convenenient to work with them. It does expect you to generate secure secrets and keep them in your .env file.

Getting started with your project

The SIWT library is available through npm. For contributions and building it locally see contributing.md.

npm install @stakenow/siwt

Implementing the ui

Sign In With Tezos will require a ui to interact with the user and an authentication API to make the necessary verifications and hand out permissions. On the ui we will make use of Beacon to interact with the user's wallet.

Connecting the wallet

const walletPermissions = await dAppClient.requestPermissions()

This will give your dApp permissions to interact with your user's wallet. It provides access to the user's information regarding public key, address and wallet.

Creating the message

const messagePayload = createMessagePayload({
  dappUrl: 'siwt.stakenow.fi',
  pkh: walletPermissions.address,
})

This will create a message payload that looks like this:

{
  signingType: 'micheline',
  payload: 'encoded message',
  sourceAddress: 'The wallet address of the user signing in',
}

The human readable message presents as follows:

Tezos Signed Message: DAPP_URL DATE DAPP_URL would like you to sign in with USER_ADDRESS.

Requesting the signature

const signature = await dAppClient.requestSignPayload(messagePayload)

Signing the user into your dApp

const signedIn = await signIn('API_URL')({
  pk: walletPermissions.accountInfo.pk,
  pkh: walletPermissions.address,
  signature,
})

Token types

With a successful sign in the server will return the following set of tokens:

Access Token:

Use the access token for authorization upon each protected API call. Add it as a bearer token in the authorization header of each API call.

Refresh Token:

If you have implemented a refresh token strategy use this token to obtain a new access token.

ID Token:

The ID token is used to obtain some information about the user that is signed in. Because it is a valid JWT token you can use any jwt decoding library to decode the token and use it's contents.

Implementing the server

Verifying the signature

Just having the user sign this message is not enough. We also have to make sure the signature is valid before allowing the user to use our dApp. This happens on the server and requires only the following statement:

const isValidSignature = verifySignature(message, pk, signature)

Creating tokens

Now that you have verified the identity, you can let your application know all is good in the world. You do this using JSON Web Tokens or JWT for short. For more information about JWT check the official website. You will use three different types of tokens:

Access Token:

The access token will be used for token based authentication for the API. To create an access token the user's pkh is required, but more claims are supported by supplying a claims object. The access token is valid for 15 minutes.

import { generateAccessToken } from '@stakenow/siwt'

const pkh = 'PKH'
const optionalClaims = {
  claimKey: 'claimValue',
}

const accessToken = generateAccessToken({
  pkh,
  claims: optionalClaims,
})

On each protected API route you will have to verify if the access token is still valid. Therefore the token should be sent with each call to the API in an authorization header as a bearer token and be verified:

const accessToken = req.headers.authorization.split(' ')[1]
const pkh = verifyAccessToken(accessToken)

If the access token is valid, you will receive the pkh of the valid user. Validate this with the account data that is being requested. If everything checks out, supply the user with the requested API information. If the access token is invalid, the pkh will be false. Thus the user should not get an API response.

Refresh Token:

By default the access token is only valid for 15 minutes. After this time the user will no longer be able to request information from the API. To make sure you will not need to make the user sign another message to retrieve a valid access token, you can implement a refresh token flow.

Creating a refresh token:

import { generateRefreshToken } from '@stakenow/siwt'

generateRefreshToken('PKH OF THE USER')

Verifying the refresh token:

import { verifyRefreshToken } from '@stakenow/siwt'

try {
  verifyRefreshToken('REFRESH TOKEN')
  // Refresh the access token for the user
} catch {
  // AccessToken cannot be renewed. Log your user out and request a new signed message to log in again.
}

Get more information on refresh tokens in general here.

ID Token:

The ID token is an optional token, used for some extra information about your user. It is long lived and can be used to maintain some information about the user in your application. It requires the user's pkh, and takes claims and extra optionalUserInfo:

import { generateIdToken } from '@stakenow/siwt'

const pkh = 'PKH'
const optionalClaims = {
  claimName: 'claimValue',
}
const optionalUserInfo = {
  tokenId: 'MEMBERSHIP_TOKEN_ID',
}

generateIdToken({
  pkh,
  claims: optionalClaims,
  userInfo: optionalUserInfo,
})

Putting it all together

index.js

import { DAppClient } from '@airgap/beacon-sdk'
import jwt_decode from 'jwt-decode'

import * as siwt from '@stakenow/siwt'

const dAppClient = new DAppClient({ name: 'SIWT Demo' })
const state = { accessToken: '' }

const getProtectedData = () => {
  fetch('http://localhost:3000/protected', {
    method: 'GET',
    headers: {
      authorization: `Bearer ${state.accessToken}`,
    },
  })
    .then(response => response.json())
    .then(data => {
      const protectedDataContainer = document.getElementsByClassName('protected-data-content-container')[0]
      protectedDataContainer.innerHTML = data
    })
    .catch(error => {
      const protectedDataContainer = document.getElementsByClassName('protected-data-content-container')[0]
      protectedDataContainer.innerHTML = error.message
    })
}

const getPublicData = () => {
  fetch('http://localhost:3000/public', {
    method: 'GET',
  })
    .then(response => response.json())
    .then(data => {
      const publicDataContainer = document.getElementsByClassName('public-data-content-container')[0]
      publicDataContainer.innerHTML = data
    })
    .catch(error => {
      const publicDataContainer = document.getElementsByClassName('public-data-content-container')[0]
      publicDataContainer.innerHTML = error.message
    })
}

const login = async () => {
  try {
    // request wallet permissions with Beacon dAppClient
    const walletPermissions = await dAppClient.requestPermissions()

    // create the message to be signed
    const messagePayload = siwt.createMessagePayload({
      dappUrl: 'siwt.stakenow.fi',
      pkh: walletPermissions.address,
    })

    // request the signature
    const signedPayload = await dAppClient.requestSignPayload(messagePayload)

    // sign in the user to our app
    const { data } = await siwt.signIn('http://localhost:3000')({
      pk: walletPermissions.accountInfo.publicKey,
      pkh: walletPermissions.address,
      message: messagePayload.payload,
      signature: signedPayload.signature,
    })

    const { accessToken, idToken } = data
    state.accessToken = accessToken

    const contentContainer = document.getElementsByClassName('content-container')[0]

    if (idToken) {
      const userIdInfo = jwt_decode(idToken)
      contentContainer.innerHTML = `<h3>You are logged in as ${userIdInfo.pkh}</h3>`
    }
  } catch (error) {
    const contentContainer = document.getElementsByClassName('content-container')[0]
    contentContainer.innerHTML = error.message
  }
}

const init = () => {
  const loginButton = document.getElementsByClassName('connect-button')[0]
  const loadPublicDataButton = document.getElementsByClassName('load-public-data-button')[0]
  const loadProtectedDataButton = document.getElementsByClassName('load-private-data-button')[0]
  loginButton.addEventListener('click', login)
  loadPublicDataButton.addEventListener('click', getPublicData)
  loadProtectedDataButton.addEventListener('click', getProtectedData)
}

window.onload = init

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Sign In with Tezos Demo</title>
  </head>
  <body>
    <div>
        <h1>Sign in with Tezos</h1>
        <button class="connect-button">Connect</button>
        <div class="content-container"></div>
        <div>
          <div>
            <h2>Public data:</h2>
            <div class="public-data-content-container"></div>
            <button class="load-public-data-button">Load public data</button>
          </div>
          <div>
            <h2>Protected data:</h2>
            <div class="protected-data-content-container"></div>
            <button class="load-private-data-button">Load private data</button>
          </div>
        </div>
      </div>
    </div>
    <script src="main.js"></script>
  </body>
</html>

For the full setup including the build process check out the demo folder.

Implementing your authorization API

The library relies in the backend on your signin endpoint to be called /signin, which is a POST request that takes the following body:

{
  pk: 'USER ADDRESS',
  pkh: 'USER PUBLIC KEY',
  signature: 'MESSAGE SIGNATURE',
}

For this example you will write this endpoint in Node.js using Express:

const express = require('express')
const bodyParser = require('body-parser')
const cors = require('cors')

const {
  verifySignature,
  generateAccessToken,
  queryAccessControl,
  generateRefreshToken,
  generateIdToken,
  verifyAccessToken,
} = require('@stakenow/siwt')

const app = express()
const port = 3000

app.use(cors())
app.use(bodyParser.json())

const authenticate = async (req, res, next) => {
  try {
    // decode the access token
    const accessToken = req.headers.authorization.split(' ')[1]
    const pkh = verifyAccessToken(accessToken)
    if (pkh) {
      const accessControl = queryAccessControl({
        contractAddress: 'KT1',
        parameters: {
          pkh,
        },
        test: {
          comparator: '>=',
          value: 1,
        },
      })

      if (accessControl.tokenId) {
        return next()
      }
    }
    return res.status(403).send('Forbidden')
  } catch (e) {
    console.log(e)
    return res.status(403).send('Forbidden')
  }
}

app.post('/signin', (req, res) => {
  const { message, signature, pk, pkh } = req.body
  try {
    const isValidSignature = verifySignature(message, pk, signature)
    if (isValidSignature) {
      // when a user provided a valid signature, we can obtain and
      // return the required information about the user.

      // the usage of claims is supported but not required.
      const claims = {
        iss: 'https://api.siwtdemo.stakenow.fi',
        aud: ['https://siwtdemo.stakenow.fi'],
        azp: 'https://siwtdemo.stakenow.fi',
      }

      // the minimum we need to return is an access token that
      // allows the user to access the API. The pkh is required,
      // extra claims are optional.
      const accessToken = generateAccessToken({ pkh, claims })

      // we can use a refresh token to allow the access token to
      // be refreshed without the user needing to log in again.
      const refreshToken = generateRefreshToken(pkh)

      // we can use a long-lived ID token to return some personal
      // information about the user to the UI.
      const access = queryAccessControl({
        contractAddress: 'KT1',
        parameters: {
          pkh,
        },
        test: {
          comparator: '>=',
          value: 1,
        },
      })

      const idToken = generateIdToken({
        claims,
        userInfo: {
          ...access,
        },
      })

      return res.send({
        accessToken,
        refreshToken,
        idToken,
        tokenType: 'Bearer',
      })
    }
    return res.status(403).send('Forbidden')
  } catch (e) {
    console.log(e)
    return res.status(403).send('Forbidden')
  }
})

app.get('/public', (req, res) => {
  res.send(JSON.stringify('This data is public. Anyone can request it.'))
})

app.get('/protected', authenticate, (req, res) => {
  res.send(JSON.stringify('This data is protected but you have the required NFT so you have access to it.'))
})

app.listen(port, () => {
  console.log(`SIWT server app listening on port ${port}`)
})

Run the demo

Clone the project

git clone https://github.com/StakeNow/SIWT.git
cd SIWT

Add environment variables

For the demo you will need to create your personal SECRETS which should be sufficently long, random and not easy to guess. For the demo it is not safety relevant but for your project please refer to this documentation regarding their requirements.

Create an .env file in the root folder with the following content:

ACCESS_TOKEN_SECRET=SECRET
REFRESH_TOKEN_SECRET=SECRET
ID_TOKEN_SECRET=SECRET

Start the demo

Beginning from the root folder run:

npm install

Start the server

npm run demo:server:start

If successful you should see the following message:

SIWT server app listening on port 3000

Build and run the ui

In a new terminal window from the root folder run:

npm run demo:ui:start

The browser should open automatically. If not just open http://localhost:8080. Note that if port 8080 is already in use the application increments to 8081.

Happy Demo!

Future outlook

This demo proves that the concept of Signing In With Tezos to verify ownership of your pkh (public key hash aka address), and requiring ownership of certain assets (e.g. NFTs) to gain access to protected resources, works efficiently. This however is just the start of a larger discussion we would love to continue building with the Tezos community regarding these follow up topics:

  • Standardisation of the message to be signed when signing in
  • Standardisation of permission standards (ie. jwt claims/contents)
  • Creating specialized smart contract(s) that facilitate the derivation of a user's permissions, for instance by using views
  • Create a swap contract to directly obtain an NFT for a user to buy access to integrate in each individual project
  • Expanding the accessControlQuery to allow for more extensive requirements
  • Either remove or improve the use of indexers for retrieving accessControlQuery requirements
  • Improving the abstraction created by the SIWT Package

Get in contact

If you liked what you have seen here and want to get in touch with us just send us an email to [email protected] - any questions regarding this project can be initiated through the an Issue here on GitHub or by asking us directly in our Discord.

You might also like...

✍️ Easily sign any message using your Ethereum wallet

wallet-sign Easily sign any message using your Ethereum wallet Use the app here: https://marcusmolchany.github.io/wallet-sign Depolyment yarn deploy D

Nov 26, 2022

The Frontend of Escobar's Inventory Management System, Employee Management System, Ordering System, and Income & Expense System

The Frontend of Escobar's Inventory Management System, Employee Management System, Ordering System, and Income & Expense System

Usage Create an App # with npx $ npx create-nextron-app my-app --example with-javascript # with yarn $ yarn create nextron-app my-app --example with-

Jan 2, 2023

Sample code for ETH Sign In

Sign in with Ethereum Sample code for ETH Sign In at https://acik-kaynak.org/oauth-guzel-peki-ethereumu-denediniz-mi/ The related code for Sign in wit

Jan 2, 2023

Demodal is a browser extension that automatically removes content blocking modals including paywalls, discount offers, promts to sign up or enter your email address and more.

Demodal Demodal is a browser extension that automatically removes content blocking modals including paywalls, discount offers, promts to sign up or en

Jan 4, 2023

Firebase SDK 9 + Google Sign In + Chrome Extension Manifest Version 3 + Webpack

Firebase SDK 9 + Google Sign In + Chrome Extension Manifest Version 3 + Webpack Demo Find this Chrome Extension Setup and working demo here or on Yout

Dec 28, 2022

Development of a Sign Up page form in HTML

Project: Trybewarts Project developed studying in Trybe. Technologies and tools used HTML CSS FlexBox JavaScript Kanban / Scrum Project objective Deve

Jun 14, 2022

Front-end for FireNearby service. View recent fires and sign up to receive alerts: caseymm.github.io/fire-nearby

fire-nearby (firenearby service front-end) This application is composed of three pages: Map of recent fires Sign up form to receive alerts About this

Mar 30, 2022

Create, sign & decode Solana transactions with minimum deps

micro-sol-signer Create, sign & decode Solana transactions with minimum deps. Tiny: 674 LOC, 3K LOC with all deps bundled No network code in main pack

Nov 23, 2022

Create, sign & decode BTC transactions with minimum deps.

micro-btc-signer Create, sign & decode BTC transactions with minimum deps. 🪶 Small: ~2.2K lines Create transactions, inputs, outputs, sign them No ne

Dec 30, 2022
Comments
  • Discord Bot

    Discord Bot

    I have tested the Discord invite link and noticed the new channel #verification.

    1. The messages are clear to the user. I am asked to verify my account and cautioned to not share my keys. good.
    2. If I click on Can't verify I am redirected to https://siwt.xyz/ which is an invalid address. I assume this is for now intended.
    3. If I click Verify a new message pops up telling me toLet's go
    4. I am redirected and asked if I trust the link. It is hard to determine if I trust it as the ID is auto generated:
      • Proposal: Explain the user that there will be an ID in the link and update the message with the steps that the user will have to expect and follow through.
    5. The website opens and I see a very clean interface. nice.
    6. I connect with my wallet (tested with kukai) and it tells me that the verification worked and I can close the tab.
    7. At first I was now expecting a visible change in Discord and was starting to write this ticket. While reproducing the steps I clicked on Verify again and received a new message that You are verified!
      • Question: Can we tell the user to click the button again as it will likely to be possible to push the event of successful verification to Discord?
    8. I see the channel #pass-holder now - grewat work!
    question 
    opened by jdsika 0
  • Readme

    Readme

    The readme's seem auto generated. Is it allowed to change them here on GitHub or is it managed elsewhere? Can and should we copy the readme from the deprecated repository? link to old readme

    question 
    opened by jdsika 0
Owner
StakeNow
StakeNow.fi gives you a 360º view of your investments and lets you manage your Tezos assets in one place.
StakeNow
A plugin for Strapi Headless CMS that provides ability to sign-in/sign-up to an application by link had sent to email.

Strapi PasswordLess Plugin A plugin for Strapi Headless CMS that provides ability to sign-in/sign-up to an application by link had sent to email. A pl

Andrey Kucherenko 51 Dec 12, 2022
A comprehensive user sign-up/sign-in system

Nodejs Login System You want a comprehensive user sign-up/sign-in system I strongly suggest you take a look at this repo. The System Includes Welcome

Furkan Gulsen 8 Aug 1, 2022
The LMS (Life Management System) is a free tool for personal knowledge management and goal management based on Obsidian.md.

README Documentation | 中文帮助 The LMS (Life Management System) is a tool for personal knowledge management and goal management based on Obsidian.md. It

null 27 Dec 21, 2022
Web based application that uses playerctl in it backend to control remotely your audio using the frontend as remote control.

Linux Remote This is a web based application that uses playerctl in it backend to control remotely your audio using the frontend as remote control. Do

Gabriel Guerra 4 Jul 6, 2022
A three.js and roslibjs powered web-control for zju fast-drone-250 for laptop-free flight control

Web Control for ZJU Fast-Drone-250 A three.js and roslibjs powered web-control for zju fast-drone-250 for laptop-free flight control (tested on Xiaomi

null 6 Nov 11, 2022
Type Identity - a powerful and highly customizable authentication and authrozation and access-control framework

Type Identity is a powerful and highly customizable authentication and authrozation and access-control framework. It is the de-facto standard for securing Type Script api beta release

Saeed Mohammed Al-abidi 2 Jan 1, 2023
An authorization library that supports access control models like ACL, RBAC, ABAC in modern JavaScript platforms

Casbin-Core ?? Looking for an open-source identity and access management solution like Okta, Auth0, Keycloak ? Learn more about: Casdoor News: still w

Casbin 6 Oct 20, 2022
a cobbled together alternative UI to launchdarkly, allowing read/write access via LD API access token

discount-launchdarkly a cobbled together alternative UI to launchdarkly, allowing read/write access via LD API access token setup make sure you have a

null 9 Oct 19, 2022
An indexer that aggregates and normalizes NFT related data on the Tezos Blockchain and provides a GraphQL API for developers.

TezTok Token Indexer An indexer that aggregates and normalizes NFT related data on the Tezos Blockchain and provides a GraphQL API for developers. Not

null 29 Dec 23, 2022
A robust and light-weight inventory management application designed to help businesses maintain perfect control over every unit of stock.

Inventory Buddy Access inventory anytime on web, tablet or mobile. Inventory Buddy is a robust and light-weight inventory management application desig

Brynn Smith 7 Nov 5, 2022