{"id":13632330,"url":"https://github.com/StakeNow/SIWT-Deprecated","last_synced_at":"2025-04-18T02:32:35.255Z","repository":{"id":38473400,"uuid":"486620629","full_name":"StakeNow/SIWT-Deprecated","owner":"StakeNow","description":"Sign In With Tezos: Access Control Management using Tezos NFTs","archived":false,"fork":false,"pushed_at":"2023-10-31T10:44:41.000Z","size":386,"stargazers_count":32,"open_issues_count":0,"forks_count":5,"subscribers_count":6,"default_branch":"develop","last_synced_at":"2024-08-01T22:52:58.035Z","etag":null,"topics":["access-control","blockchain","nft","tezos"],"latest_commit_sha":null,"homepage":"https://siwtdemo.stakenow.fi/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/StakeNow.png","metadata":{"files":{"readme":"readme.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2022-04-28T14:08:17.000Z","updated_at":"2023-10-31T10:54:28.000Z","dependencies_parsed_at":"2024-01-22T01:16:11.600Z","dependency_job_id":"4ec7a3ce-883e-4b49-b6df-6b2d32db6a00","html_url":"https://github.com/StakeNow/SIWT-Deprecated","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/StakeNow%2FSIWT-Deprecated","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/StakeNow%2FSIWT-Deprecated/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/StakeNow%2FSIWT-Deprecated/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/StakeNow%2FSIWT-Deprecated/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/StakeNow","download_url":"https://codeload.github.com/StakeNow/SIWT-Deprecated/tar.gz/refs/heads/develop","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":223772173,"owners_count":17199968,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["access-control","blockchain","nft","tezos"],"created_at":"2024-08-01T22:03:00.039Z","updated_at":"2024-11-09T00:31:03.686Z","avatar_url":"https://github.com/StakeNow.png","language":"TypeScript","funding_links":[],"categories":["TypeScript"],"sub_categories":[],"readme":"# **SIWT DEPRECATED**\n\n\n----------------------------------------\n**ATTENTION:**\n\nThis repository is now deprecated and replaced by a Nx mono repository under [github.com/StakeNow/SIWT](https://github.com/StakeNow/SIWT). The change has been conducted as part of the [first milestone](https://forum.tezosagora.org/t/sign-in-with-tezos-siwt/4930) of the Tezos Foundation grant for SIWT. You can get in touch with the developers through the [StakeNow Discord](https://discord.gg/BbBQf9gEWF).\n\n---------------------------------------\n\nSign In With Tezos (SIWT) is a library that supports the development of your decentralized application (dApp) by\n- **proving** the users ownership of the private key to the address the user signs in with,\n- adding **permissions** to use your API or FrontEnd based on the **ownership** of a Non-Fungible Token (NFT).\n\n**Table of contents:**\n\n- [**SIWT**](#siwt)\n  - [Technical concepts](#technical-concepts)\n    - [**SIWT Message**](#siwt-message)\n    - [**Signing in the user**](#signing-in-the-user)\n    - [**Query access control**](#query-access-control)\n    - [**Tokens**](#tokens)\n  - [Getting started with your project](#getting-started-with-your-project)\n    - [**Implementing the ui**](#implementing-the-ui)\n      - [**Connecting the wallet**](#connecting-the-wallet)\n      - [**Creating the message**](#creating-the-message)\n      - [**Requesting the signature**](#requesting-the-signature)\n      - [**Signing the user into your dApp**](#signing-the-user-into-your-dapp)\n      - [**Token types**](#token-types)\n    - [**Implementing the server**](#implementing-the-server)\n      - [**Verifying the signature**](#verifying-the-signature)\n      - [**Creating tokens**](#creating-tokens)\n    - [**Putting it all together**](#putting-it-all-together)\n    - [**Implementing your authorization API**](#implementing-your-authorization-api)\n  - [Run the demo](#run-the-demo)\n    - [**Clone the project**](#clone-the-project)\n    - [**Add environment variables**](#add-environment-variables)\n    - [**Start the demo**](#start-the-demo)\n    - [**Start the server**](#start-the-server)\n    - [**Build and run the ui**](#build-and-run-the-ui)\n  - [Built with](#built-with)\n  - [Future outlook](#future-outlook)\n  - [Get in contact](#get-in-contact)\n\n## Technical concepts\n\n### **SIWT Message**\nThe message is constructed from the URL to your dApp and the user's wallet address more specifically the private key hash (pkh).\n\nCreate the message:\n```\nimport { createMessage } from '@stakenow/siwt'\n\n// constructing a message\nconst message = createMessage({\n  dappUrl: 'your-cool-app.xyz',\n  pkh: 'tz1',\n})\n```\n\nThe resulting message will look something like this:\n\n` 0501000000bc54657a6f73205369676e6564204d6573736167653a2055524c20323032322d30342d32385430383a34383a33332e3636345a2055524c20776f756c64206c696b6520796f7520746f207369676e20696e207769746820504b482e200a2020`\n\nDeconstructing this message will reveal the following format:\n\n- `05`: Indicates that this is a Micheline expression\n- `01`: Indicates it is converted to bytes\n- `000000bc`: Indicates the length of the message in hex\n- `54...`: Is the actual message in bytes\n\n__This message is now ready to be signed by the user.__\n\n### **Signing in the user**\n\nThe user specific signature derived from the signed message is used to sign the user into the dApp.\n\nTo successfully sign in you will need:\n- The original message that was created earlier using the `createMessage` function,\n- the signature itself and\n- the public key of the user.\n\n  (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.)\n\nWith 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.\n\n```\nimport { signin } from '@stakenow/siwt'\n\nconst API_URL = 'https://url-to-your-api.xyz'\nconst verification = signin(API_URL)({\n  message\n  signature,\n  pk,\n})\n```\n\n### **Query access control**\n\nNow 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. \nFor this you can use `queryAccessControl`.\n\nThe `queryAccessControl` function requires your NFT token contract, the pkh of the user and the ruleset to test against:\n\n```\n  {\n    contractAddress: 'CONTRACT_ADDRESS'\n    parameters: {\n      pkh: 'PKH'\n    }\n    test: {\n      comparator: '='\n      value: 1\n    }\n  }\n```\n\n### **Tokens**\n\nNow 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.\n\n\n## Getting started with your project\nThe SIWT library is available through `npm`. For contributions and building it locally see [contributing.md](./CONTRIBUTING.md).\n```\nnpm install @stakenow/siwt\n```\n\n### **Implementing the ui**\nSign 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]('https://www.walletbeacon.io/') to interact with the user's wallet.\n\n#### **Connecting the wallet**\n```\nconst walletPermissions = await dAppClient.requestPermissions()\n```\nThis 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.\n\n#### **Creating the message**\n```\nconst messagePayload = createMessagePayload({\n  dappUrl: 'siwt.stakenow.fi',\n  pkh: walletPermissions.address,\n})\n```\n\nThis will create a message payload that looks like this:\n```\n{\n  signingType: 'micheline',\n  payload: 'encoded message',\n  sourceAddress: 'The wallet address of the user signing in',\n}\n```\n\nThe human readable message presents as follows:\n\n```\nTezos Signed Message: DAPP_URL DATE DAPP_URL would like you to sign in with USER_ADDRESS.\n```\n\n#### **Requesting the signature**\n```\nconst signature = await dAppClient.requestSignPayload(messagePayload)\n```\n\n#### **Signing the user into your dApp**\n```\nconst signedIn = await signIn('API_URL')({\n  pk: walletPermissions.accountInfo.pk,\n  pkh: walletPermissions.address,\n  signature,\n})\n```\n\n#### **Token types**\nWith a successful sign in the server will return the following set of tokens:\n\n_Access Token:_\n\nUse the access token for authorization upon each protected API call. Add it as a bearer token in the `authorization` header of each API call.\n\n_Refresh Token:_\n\nIf you have implemented a refresh token strategy use this token to obtain a new access token.\n\n_ID Token:_\n\nThe 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.\n\n### **Implementing the server**\n#### **Verifying the signature**\nJust 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:\n\n```\nconst isValidSignature = verifySignature(message, pk, signature)\n```\n\n#### **Creating tokens**\n\nNow 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](https://jwt.io). You will use three different types of tokens:\n\n_Access Token:_\n\nThe 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.\n\n```\nimport { generateAccessToken } from '@stakenow/siwt'\n\nconst pkh = 'PKH'\nconst optionalClaims = {\n  claimKey: 'claimValue',\n}\n\nconst accessToken = generateAccessToken({\n  pkh,\n  claims: optionalClaims,\n})\n```\n\nOn 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:\n\n```\nconst accessToken = req.headers.authorization.split(' ')[1]\nconst pkh = verifyAccessToken(accessToken)\n\n```\nIf 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.\n\n_Refresh Token:_\n\nBy 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.\n\nCreating a refresh token:\n```\nimport { generateRefreshToken } from '@stakenow/siwt'\n\ngenerateRefreshToken('PKH OF THE USER')\n```\n\nVerifying the refresh token:\n```\nimport { verifyRefreshToken } from '@stakenow/siwt'\n\ntry {\n  verifyRefreshToken('REFRESH TOKEN')\n  // Refresh the access token for the user\n} catch {\n  // AccessToken cannot be renewed. Log your user out and request a new signed message to log in again.\n}\n```\n\nGet more information on refresh tokens in general [here](https://auth0.com/docs/secure/tokens/refresh-tokens).\n\n_ID Token:_\n\nThe 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:\n\n```\nimport { generateIdToken } from '@stakenow/siwt'\n\nconst pkh = 'PKH'\nconst optionalClaims = {\n  claimName: 'claimValue',\n}\nconst optionalUserInfo = {\n  tokenId: 'MEMBERSHIP_TOKEN_ID',\n}\n\ngenerateIdToken({\n  pkh,\n  claims: optionalClaims,\n  userInfo: optionalUserInfo,\n})\n```\n\n### **Putting it all together**\n\n*index.js*\n\n```\nimport { DAppClient } from '@airgap/beacon-sdk'\nimport jwt_decode from 'jwt-decode'\n\nimport * as siwt from '@stakenow/siwt'\n\nconst dAppClient = new DAppClient({ name: 'SIWT Demo' })\nconst state = { accessToken: '' }\n\nconst getProtectedData = () =\u003e {\n  fetch('http://localhost:3000/protected', {\n    method: 'GET',\n    headers: {\n      authorization: `Bearer ${state.accessToken}`,\n    },\n  })\n    .then(response =\u003e response.json())\n    .then(data =\u003e {\n      const protectedDataContainer = document.getElementsByClassName('protected-data-content-container')[0]\n      protectedDataContainer.innerHTML = data\n    })\n    .catch(error =\u003e {\n      const protectedDataContainer = document.getElementsByClassName('protected-data-content-container')[0]\n      protectedDataContainer.innerHTML = error.message\n    })\n}\n\nconst getPublicData = () =\u003e {\n  fetch('http://localhost:3000/public', {\n    method: 'GET',\n  })\n    .then(response =\u003e response.json())\n    .then(data =\u003e {\n      const publicDataContainer = document.getElementsByClassName('public-data-content-container')[0]\n      publicDataContainer.innerHTML = data\n    })\n    .catch(error =\u003e {\n      const publicDataContainer = document.getElementsByClassName('public-data-content-container')[0]\n      publicDataContainer.innerHTML = error.message\n    })\n}\n\nconst login = async () =\u003e {\n  try {\n    // request wallet permissions with Beacon dAppClient\n    const walletPermissions = await dAppClient.requestPermissions()\n\n    // create the message to be signed\n    const messagePayload = siwt.createMessagePayload({\n      dappUrl: 'siwt.stakenow.fi',\n      pkh: walletPermissions.address,\n    })\n\n    // request the signature\n    const signedPayload = await dAppClient.requestSignPayload(messagePayload)\n\n    // sign in the user to our app\n    const { data } = await siwt.signIn('http://localhost:3000')({\n      pk: walletPermissions.accountInfo.publicKey,\n      pkh: walletPermissions.address,\n      message: messagePayload.payload,\n      signature: signedPayload.signature,\n    })\n\n    const { accessToken, idToken } = data\n    state.accessToken = accessToken\n\n    const contentContainer = document.getElementsByClassName('content-container')[0]\n\n    if (idToken) {\n      const userIdInfo = jwt_decode(idToken)\n      contentContainer.innerHTML = `\u003ch3\u003eYou are logged in as ${userIdInfo.pkh}\u003c/h3\u003e`\n    }\n  } catch (error) {\n    const contentContainer = document.getElementsByClassName('content-container')[0]\n    contentContainer.innerHTML = error.message\n  }\n}\n\nconst init = () =\u003e {\n  const loginButton = document.getElementsByClassName('connect-button')[0]\n  const loadPublicDataButton = document.getElementsByClassName('load-public-data-button')[0]\n  const loadProtectedDataButton = document.getElementsByClassName('load-private-data-button')[0]\n  loginButton.addEventListener('click', login)\n  loadPublicDataButton.addEventListener('click', getPublicData)\n  loadProtectedDataButton.addEventListener('click', getProtectedData)\n}\n\nwindow.onload = init\n```\n\n*index.html*\n```\n\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"en\"\u003e\n  \u003chead\u003e\n    \u003cmeta charset=\"UTF-8\"\u003e\n    \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n    \u003cmeta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\"\u003e\n    \u003ctitle\u003eSign In with Tezos Demo\u003c/title\u003e\n  \u003c/head\u003e\n  \u003cbody\u003e\n    \u003cdiv\u003e\n        \u003ch1\u003eSign in with Tezos\u003c/h1\u003e\n        \u003cbutton class=\"connect-button\"\u003eConnect\u003c/button\u003e\n        \u003cdiv class=\"content-container\"\u003e\u003c/div\u003e\n        \u003cdiv\u003e\n          \u003cdiv\u003e\n            \u003ch2\u003ePublic data:\u003c/h2\u003e\n            \u003cdiv class=\"public-data-content-container\"\u003e\u003c/div\u003e\n            \u003cbutton class=\"load-public-data-button\"\u003eLoad public data\u003c/button\u003e\n          \u003c/div\u003e\n          \u003cdiv\u003e\n            \u003ch2\u003eProtected data:\u003c/h2\u003e\n            \u003cdiv class=\"protected-data-content-container\"\u003e\u003c/div\u003e\n            \u003cbutton class=\"load-private-data-button\"\u003eLoad private data\u003c/button\u003e\n          \u003c/div\u003e\n        \u003c/div\u003e\n      \u003c/div\u003e\n    \u003c/div\u003e\n    \u003cscript src=\"main.js\"\u003e\u003c/script\u003e\n  \u003c/body\u003e\n\u003c/html\u003e\n```\n\u003e For the full setup including the build process check out the demo folder.\n\n### **Implementing your authorization API**\n\nThe library relies in the backend on your signin endpoint to be called `/signin`, which is a `POST` request that takes the following body: \n```\n{\n  pk: 'USER PUBLIC KEY',\n  pkh: 'USER ADDRESS',\n  signature: 'MESSAGE SIGNATURE',\n}\n```\n\nFor this example you will write this endpoint in Node.js using Express:\n\n```\nconst express = require('express')\nconst bodyParser = require('body-parser')\nconst cors = require('cors')\n\nconst {\n  verifySignature,\n  generateAccessToken,\n  queryAccessControl,\n  generateRefreshToken,\n  generateIdToken,\n  verifyAccessToken,\n} = require('@stakenow/siwt')\n\nconst app = express()\nconst port = 3000\n\napp.use(cors())\napp.use(bodyParser.json())\n\nconst authenticate = async (req, res, next) =\u003e {\n  try {\n    // decode the access token\n    const accessToken = req.headers.authorization.split(' ')[1]\n    const pkh = verifyAccessToken(accessToken)\n    if (pkh) {\n      const accessControl = queryAccessControl({\n        contractAddress: 'KT1',\n        parameters: {\n          pkh,\n        },\n        test: {\n          comparator: '\u003e=',\n          value: 1,\n        },\n      })\n\n      if (accessControl.tokenId) {\n        return next()\n      }\n    }\n    return res.status(403).send('Forbidden')\n  } catch (e) {\n    console.log(e)\n    return res.status(403).send('Forbidden')\n  }\n}\n\napp.post('/signin', (req, res) =\u003e {\n  const { message, signature, pk, pkh } = req.body\n  try {\n    const isValidSignature = verifySignature(message, pk, signature)\n    if (isValidSignature) {\n      // when a user provided a valid signature, we can obtain and\n      // return the required information about the user.\n\n      // the usage of claims is supported but not required.\n      const claims = {\n        iss: 'https://api.siwtdemo.stakenow.fi',\n        aud: ['https://siwtdemo.stakenow.fi'],\n        azp: 'https://siwtdemo.stakenow.fi',\n      }\n\n      // the minimum we need to return is an access token that\n      // allows the user to access the API. The pkh is required,\n      // extra claims are optional.\n      const accessToken = generateAccessToken({ pkh, claims })\n\n      // we can use a refresh token to allow the access token to\n      // be refreshed without the user needing to log in again.\n      const refreshToken = generateRefreshToken(pkh)\n\n      // we can use a long-lived ID token to return some personal\n      // information about the user to the UI.\n      const access = queryAccessControl({\n        contractAddress: 'KT1',\n        parameters: {\n          pkh,\n        },\n        test: {\n          comparator: '\u003e=',\n          value: 1,\n        },\n      })\n\n      const idToken = generateIdToken({\n        claims,\n        userInfo: {\n          ...access,\n        },\n      })\n\n      return res.send({\n        accessToken,\n        refreshToken,\n        idToken,\n        tokenType: 'Bearer',\n      })\n    }\n    return res.status(403).send('Forbidden')\n  } catch (e) {\n    console.log(e)\n    return res.status(403).send('Forbidden')\n  }\n})\n\napp.get('/public', (req, res) =\u003e {\n  res.send(JSON.stringify('This data is public. Anyone can request it.'))\n})\n\napp.get('/protected', authenticate, (req, res) =\u003e {\n  res.send(JSON.stringify('This data is protected but you have the required NFT so you have access to it.'))\n})\n\napp.listen(port, () =\u003e {\n  console.log(`SIWT server app listening on port ${port}`)\n})\n```\n\n## Run the demo\n### **Clone the project**\n ```\ngit clone https://github.com/StakeNow/SIWT.git\ncd SIWT\n ```\n### **Add environment variables**\nFor 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](https://jwt.io) regarding their requirements.\n\nCreate an ```.env``` file in the root folder with the following content:\n```\nACCESS_TOKEN_SECRET=SECRET\nREFRESH_TOKEN_SECRET=SECRET\nID_TOKEN_SECRET=SECRET\n```\n\n### **Start the demo**\nBeginning from the root folder run:\n```\nnpm install\n```\n\n### **Start the server**\n```\nnpm run demo:server:start\n``` \n\nIf successful you should see the following message:\n\n```\nSIWT server app listening on port 3000\n```\n\n### **Build and run the ui**\nIn a new terminal window from the root folder run:\n```\nnpm run demo:ui:start\n```\nThe 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.\n\n__Happy Demo!__\n\n## Built with\n- TZKT API: https://tzkt.io\n\n## Future outlook\n\nThis 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:\n\n- Standardisation of the message to be signed when signing in\n- Standardisation of permission standards (ie. jwt claims/contents)\n- Creating specialized smart contract(s) that facilitate the derivation of a user's permissions, for instance by using views\n- Create a swap contract to directly obtain an NFT for a user to buy access to integrate in each individual project\n- Expanding the accessControlQuery to allow for more extensive requirements\n- Either remove or improve the use of indexers for retrieving accessControlQuery requirements\n- Improving the abstraction created by the SIWT Package\n\n## Get in contact\nIf you liked what you have seen here and want to get in touch with us just send us an email to info@stakenow.fi - any questions regarding this project can be initiated through the an Issue here on GitHub or by asking us directly in our [Discord](https://discord.com/invite/6J3bjhkpxm?utm_source=StakeNow+Discord+LP\u0026utm_medium=Landing+Page\u0026utm_campaign=StakeNow.Fi+Launch).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FStakeNow%2FSIWT-Deprecated","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FStakeNow%2FSIWT-Deprecated","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FStakeNow%2FSIWT-Deprecated/lists"}