AutomationFlowsGeneral › Host Your Own Jwt Authentication System with Data Tables and Token Management

Host Your Own Jwt Authentication System with Data Tables and Token Management

ByLuka Zivkovic @zivkovic58 on n8n.io

A production-ready authentication workflow implementing secure user registration, login, token verification, and refresh token mechanisms. Perfect for adding authentication to any application without needing a separate auth service.

Webhook trigger★★★★★ complexity87 nodesCryptoData TableExecute Workflow Trigger
General Trigger: Webhook Nodes: 87 Complexity: ★★★★★ Added:

This workflow corresponds to n8n.io template #9660 — we link there as the canonical source.

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "nodes": [
    {
      "id": "d4d5e7a8-c447-4239-a570-2c705868974d",
      "name": "Generate Salt",
      "type": "n8n-nodes-base.crypto",
      "position": [
        -848,
        -1248
      ],
      "parameters": {
        "action": "generate",
        "encodingType": "hex",
        "dataPropertyName": "salt"
      },
      "typeVersion": 1
    },
    {
      "id": "8c643b0d-574b-4c8a-a00a-f8587dfed883",
      "name": "Hash Password",
      "type": "n8n-nodes-base.crypto",
      "position": [
        -576,
        -1248
      ],
      "parameters": {
        "type": "SHA512",
        "value": "={{ $json.passwordWithSalt }}",
        "dataPropertyName": "password_hash"
      },
      "typeVersion": 1
    },
    {
      "id": "01a0c5fa-d902-4ee3-9116-240a1bc54d71",
      "name": "Process login webhook",
      "type": "n8n-nodes-base.set",
      "position": [
        560,
        -1280
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "0e50cdb0-f300-4d82-a81c-054dffbc9cfb",
              "name": "email",
              "type": "string",
              "value": "={{ $json.body.email }}"
            },
            {
              "id": "e5fffb05-c66c-4e35-811b-8718ea43645b",
              "name": "password",
              "type": "string",
              "value": "={{ $json.body.password}}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "cd32f96d-a552-4522-86c6-c35010193be0",
      "name": "Get User",
      "type": "n8n-nodes-base.dataTable",
      "position": [
        992,
        -1200
      ],
      "parameters": {
        "limit": 1,
        "filters": {
          "conditions": [
            {
              "keyName": "email",
              "keyValue": "={{ $json.email }}"
            }
          ]
        },
        "operation": "get",
        "dataTableId": {
          "__rl": true,
          "mode": "list",
          "value": "YbPI3l04vLMH8qga",
          "cachedResultUrl": "/projects/AT3bgmtmB45S5nQf/datatables/YbPI3l04vLMH8qga",
          "cachedResultName": "review_users"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "c91e3b78-b71a-4fde-92c6-41cc99e75c0c",
      "name": "Extract Salt & Hash",
      "type": "n8n-nodes-base.code",
      "position": [
        1360,
        -1200
      ],
      "parameters": {
        "jsCode": "const user = $input.item.json;\nconst inputPassword = $('Verify Input').item.json.password;\nconst [salt, storedHash] = user.password_hash.split(':');\n\nif (!salt || !storedHash) {\n  throw new Error('Invalid password format in database');\n}\n\nreturn {\n  json: {\n    userId: user.id,\n    email: user.email,\n    username: user.username,\n    salt: salt,\n    storedHash: storedHash,\n    passwordWithSalt: inputPassword + salt\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "03a90300-9b8c-43cd-b51d-fcdc4a2abc85",
      "name": "Hash Input Password",
      "type": "n8n-nodes-base.crypto",
      "position": [
        1536,
        -1200
      ],
      "parameters": {
        "type": "SHA512",
        "value": "={{ $json.passwordWithSalt }}",
        "dataPropertyName": "inputHash"
      },
      "typeVersion": 1
    },
    {
      "id": "b70a3266-04fc-4398-ac19-7fb2cbf005f7",
      "name": "Sign Access Token",
      "type": "n8n-nodes-base.crypto",
      "position": [
        2176,
        -1328
      ],
      "parameters": {
        "type": "SHA256",
        "value": "={{ $json.accessSignatureInput }}",
        "action": "hmac",
        "secret": "={{ $('SET ACCESS AND REFRESH SECRET').item.json.ACCESS_SECRET }}",
        "encoding": "base64",
        "dataPropertyName": "accessSignature"
      },
      "typeVersion": 1
    },
    {
      "id": "a17a5545-1961-48e5-8e5a-4cc51f16d32f",
      "name": "Sign Refresh Token",
      "type": "n8n-nodes-base.crypto",
      "position": [
        2176,
        -1104
      ],
      "parameters": {
        "type": "SHA256",
        "value": "={{ $json.refreshSignatureInput }}",
        "action": "hmac",
        "secret": "={{ $('SET ACCESS AND REFRESH SECRET').item.json.REFRESH_SECRET }}",
        "encoding": "base64",
        "dataPropertyName": "refreshSignature"
      },
      "typeVersion": 1
    },
    {
      "id": "9c400b40-273f-43ac-b6db-81ef5dd410cb",
      "name": "Format JWT Tokens",
      "type": "n8n-nodes-base.code",
      "position": [
        2560,
        -1216
      ],
      "parameters": {
        "jsCode": "const header = $('Create JWT Payload').first().json.header;\nconst accessPayload = $input.item.json.accessPayload;\nconst refreshPayload = $input.item.json.refreshPayload;\nconst accessSignature = $input.item.json.accessSignature;\nconst refreshSignature = $input.item.json.refreshSignature;\n\nconst accessSigBase64url = Buffer.from(accessSignature, 'base64')\n  .toString('base64url');\nconst refreshSigBase64url = Buffer.from(refreshSignature, 'base64')\n  .toString('base64url');\n\nconst accessJWT = `${header}.${accessPayload}.${accessSigBase64url}`;\nconst refreshJWT = `${header}.${refreshPayload}.${refreshSigBase64url}`;\n\nreturn {\n  json: {\n    userId: $input.item.json.userId,\n    email: $input.item.json.email,\n    username: $input.item.json.username,\n    accessToken: accessJWT,\n    refreshToken: refreshJWT\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "638d49ae-5b15-49a8-b7f0-e5c32308e0e6",
      "name": "Merge JWT Tokens",
      "type": "n8n-nodes-base.merge",
      "position": [
        2400,
        -1216
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "joinMode": "keepEverything",
        "fieldsToMatchString": "userId"
      },
      "typeVersion": 3.2
    },
    {
      "id": "a1f01174-648e-45cc-9bd0-cb4294f8dccc",
      "name": "Hash Refresh Token for Storage",
      "type": "n8n-nodes-base.crypto",
      "position": [
        2752,
        -1216
      ],
      "parameters": {
        "type": "SHA256",
        "value": "={{ $json.refreshToken }}",
        "dataPropertyName": "refreshTokenHash"
      },
      "typeVersion": 1
    },
    {
      "id": "ca71f487-e4c7-4f33-a6f8-b7a13a3f90b5",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        368,
        -1360
      ],
      "parameters": {
        "color": 5,
        "width": 3344,
        "height": 608,
        "content": "## Login Flow"
      },
      "typeVersion": 1
    },
    {
      "id": "4f8558f6-0a62-49e4-a04d-85cba870e6ab",
      "name": "Parse JWT",
      "type": "n8n-nodes-base.code",
      "position": [
        832,
        160
      ],
      "parameters": {
        "jsCode": "const token = $input.first().json.access_token;\n\nif (!token) {\n  throw new Error('Token is required');\n}\n\nconst parts = token.split('.');\n\nif (parts.length !== 3) {\n  throw new Error('Invalid token format');\n}\n\nconst [header, payload, signature] = parts;\n\nconst decodedPayload = JSON.parse(\n  Buffer.from(payload, 'base64url').toString()\n);\n\nconst now = Math.floor(Date.now() / 1000);\nif (decodedPayload.exp < now) {\n  throw new Error('Token expired');\n}\n\nif (decodedPayload.type === 'refresh') {\n  throw new Error('Invalid token type');\n}\n\nreturn {\n  json: {\n    header,\n    payload,\n    access_token: token,\n    providedSignature: signature,\n    signatureInput: `${header}.${payload}`,\n    userId: decodedPayload.userId,\n    email: decodedPayload.email\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "3ed751a2-bf2d-4e65-a13e-487b117f8e01",
      "name": "Verify HMAC Signature",
      "type": "n8n-nodes-base.crypto",
      "position": [
        1280,
        160
      ],
      "parameters": {
        "type": "SHA256",
        "value": "={{ $('Parse JWT').item.json.access_token }}",
        "action": "hmac",
        "secret": "={{ $('SET ACCESS AND REFRESH SECRET2').item.json.ACCESS_SECRET }}",
        "encoding": "base64",
        "dataPropertyName": "expectedSignature"
      },
      "typeVersion": 1
    },
    {
      "id": "e503f741-5c7e-4bf2-bc0f-14a9484c6a80",
      "name": "Compare Signatures",
      "type": "n8n-nodes-base.code",
      "onError": "continueErrorOutput",
      "position": [
        1472,
        160
      ],
      "parameters": {
        "jsCode": "const providedSignature = $('Parse JWT').first().json.providedSignature;\nconst expectedSignature = $input.item.json.expectedSignature;\n\nconst providedBase64 = Buffer.from(providedSignature, 'base64url')\n  .toString('base64');\n\nif (providedBase64 !== expectedSignature) {\n  throw new Error('Invalid token signature');\n}\n\nreturn {\n  json: {\n    valid: true,\n    userId: $input.item.json.userId,\n    email: $input.item.json.email\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "76d82b8d-a879-41cc-8358-af930333ad19",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        368,
        -16
      ],
      "parameters": {
        "color": 6,
        "width": 1616,
        "height": 496,
        "content": "## Verify Token Flow"
      },
      "typeVersion": 1
    },
    {
      "id": "5761b1d1-22fd-43c8-85a7-ae967630c44a",
      "name": "Create User",
      "type": "n8n-nodes-base.dataTable",
      "onError": "continueErrorOutput",
      "position": [
        -304,
        -1248
      ],
      "parameters": {
        "columns": {
          "value": {
            "email": "={{ $json.email }}",
            "username": "={{ $json.username }}",
            "password_hash": "={{ $json.password_hash }}"
          },
          "schema": [
            {
              "id": "email",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "email",
              "defaultMatch": false
            },
            {
              "id": "username",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "username",
              "defaultMatch": false
            },
            {
              "id": "password_hash",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "password_hash",
              "defaultMatch": false
            },
            {
              "id": "refresh_token",
              "type": "string",
              "display": true,
              "removed": true,
              "readOnly": false,
              "required": false,
              "displayName": "refresh_token",
              "defaultMatch": false
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "dataTableId": {
          "__rl": true,
          "mode": "list",
          "value": "YbPI3l04vLMH8qga",
          "cachedResultUrl": "/projects/AT3bgmtmB45S5nQf/datatables/YbPI3l04vLMH8qga",
          "cachedResultName": "review_users"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "469e109c-a981-49be-bbe7-b24bd0467f04",
      "name": "Registration Webhook",
      "type": "n8n-nodes-base.webhook",
      "position": [
        -2240,
        -1232
      ],
      "parameters": {
        "path": "register-user",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 2.1
    },
    {
      "id": "82ca8cb5-4eda-45ee-96bf-dfa520a44726",
      "name": "Format Password & Salt",
      "type": "n8n-nodes-base.code",
      "position": [
        -848,
        -960
      ],
      "parameters": {
        "jsCode": "const password = $input.item.json.password;\n   const salt = $input.item.json.salt;\n   \n   return {\n     json: {\n       ...items[0].json,\n       passwordWithSalt: password + salt\n     }\n   };"
      },
      "typeVersion": 2
    },
    {
      "id": "07149331-060d-4d01-a418-4f1a2abe4813",
      "name": "Format User Data",
      "type": "n8n-nodes-base.code",
      "position": [
        -592,
        -944
      ],
      "parameters": {
        "jsCode": "return {\n     json: {\n       email: $input.item.json.email,\n       username: $input.item.json.username,\n       password_hash: `${$input.item.json.salt}:${$input.item.json.password_hash}`\n     }\n   };"
      },
      "typeVersion": 2
    },
    {
      "id": "b460d06f-759c-47ca-9cc4-b33e49489563",
      "name": "Error Registration Response",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        -64,
        -1024
      ],
      "parameters": {
        "options": {
          "responseCode": 500
        },
        "respondWith": "json",
        "responseBody": "{\n   \"success\": false,\n   \"message\": \"Internal Server Error\"\n}"
      },
      "typeVersion": 1.4
    },
    {
      "id": "c0751a62-a1af-441a-a8db-583eddabd3f3",
      "name": "Validate Registration Request",
      "type": "n8n-nodes-base.code",
      "onError": "continueErrorOutput",
      "position": [
        -1968,
        -1232
      ],
      "parameters": {
        "jsCode": "const { email, username, password } = $input.item.json;\n   \n   if (!email || !username || !password) {\n     throw new Error('Email, username, and password are required');\n   }\n   \n   if (password.length < 8) {\n     throw new Error('Password must be at least 8 characters');\n   }\n   \n   return {\n     json: {\n       email: email.toLowerCase().trim(),\n       username: username.trim(),\n       password: password\n     }\n   };"
      },
      "typeVersion": 2
    },
    {
      "id": "1ecf5f63-5099-4aaa-ab89-de65c7e6dbcd",
      "name": "Registration Successful",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        -64,
        -1264
      ],
      "parameters": {
        "options": {
          "responseCode": 200
        },
        "respondWith": "json",
        "responseBody": "={\n    \"success\": true,\n    \"message\": \"User registered successfully\",\n    \"user\": {\n        \"id\": \"{{ $json.id }}\",\n        \"email\": \"{{ $json.email }}\",\n        \"username\": \"{{ $json.username }}\"\n    }\n}"
      },
      "typeVersion": 1.4
    },
    {
      "id": "5439ebb6-4ec7-4fe5-be49-f44b0c45d889",
      "name": "If User Exists",
      "type": "n8n-nodes-base.if",
      "position": [
        1152,
        -1216
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "36861ab1-365a-4a88-b50c-9949932f6d01",
              "operator": {
                "type": "object",
                "operation": "notEmpty",
                "singleValue": true
              },
              "leftValue": "={{ $('Get User').item.json }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "083ce4f7-f48a-4d28-8265-4aa75b87d642",
      "name": "User Not Found",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        1136,
        -960
      ],
      "parameters": {
        "options": {
          "responseCode": 401
        },
        "respondWith": "json",
        "responseBody": "{\n   \"success\": false,\n   \"message\": \"User Not Found\"\n}"
      },
      "typeVersion": 1.4
    },
    {
      "id": "53f40d35-f65e-4a17-9041-910a31b3d652",
      "name": "Code in JavaScript",
      "type": "n8n-nodes-base.code",
      "position": [
        2928,
        -1216
      ],
      "parameters": {
        "jsCode": "return {\n     json: {\n       user_id: $input.item.json.userId,\n       token_hash: $input.item.json.refreshTokenHash,\n       expires_at: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),\n       created_at: new Date().toISOString(),\n       accessToken: $input.item.json.accessToken,\n       refreshToken: $input.item.json.refreshToken,\n       userId: $input.item.json.userId,\n       email: $input.item.json.email,\n       username: $input.item.json.username\n     }\n   };"
      },
      "typeVersion": 2
    },
    {
      "id": "04d49b68-8955-498e-a60f-4ed4e89e55ef",
      "name": "Update User Refresh Token",
      "type": "n8n-nodes-base.dataTable",
      "position": [
        3136,
        -1344
      ],
      "parameters": {
        "columns": {
          "value": {
            "refresh_token": "={{ $json.refreshToken }}"
          },
          "schema": [
            {
              "id": "email",
              "type": "string",
              "display": true,
              "removed": true,
              "readOnly": false,
              "required": false,
              "displayName": "email",
              "defaultMatch": false
            },
            {
              "id": "username",
              "type": "string",
              "display": true,
              "removed": true,
              "readOnly": false,
              "required": false,
              "displayName": "username",
              "defaultMatch": false
            },
            {
              "id": "password_hash",
              "type": "string",
              "display": true,
              "removed": true,
              "readOnly": false,
              "required": false,
              "displayName": "password_hash",
              "defaultMatch": false
            },
            {
              "id": "refresh_token",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "refresh_token",
              "defaultMatch": false
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "filters": {
          "conditions": [
            {
              "keyValue": "={{ $('Get User').item.json.id }}"
            }
          ]
        },
        "operation": "update",
        "dataTableId": {
          "__rl": true,
          "mode": "list",
          "value": "YbPI3l04vLMH8qga",
          "cachedResultUrl": "/projects/AT3bgmtmB45S5nQf/datatables/YbPI3l04vLMH8qga",
          "cachedResultName": "review_users"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "c91111d8-9657-44c0-be37-07bce3b60fd6",
      "name": "Store Refresh Token",
      "type": "n8n-nodes-base.dataTable",
      "position": [
        3136,
        -1104
      ],
      "parameters": {
        "columns": {
          "value": {
            "user_id": "={{ $('Get User').item.json.id }}",
            "expires_at": "={{ $json.expires_at }}",
            "token_hash": "={{ $json.token_hash }}"
          },
          "schema": [
            {
              "id": "user_id",
              "type": "number",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "user_id",
              "defaultMatch": false
            },
            {
              "id": "token_hash",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "token_hash",
              "defaultMatch": false
            },
            {
              "id": "expires_at",
              "type": "dateTime",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "expires_at",
              "defaultMatch": false
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "filters": {
          "conditions": [
            {
              "keyName": "user_id",
              "keyValue": "={{ $('Get User').item.json.id }}"
            }
          ]
        },
        "operation": "upsert",
        "dataTableId": {
          "__rl": true,
          "mode": "list",
          "value": "wACP3KxHplgwFrJ1",
          "cachedResultUrl": "/projects/AT3bgmtmB45S5nQf/datatables/wACP3KxHplgwFrJ1",
          "cachedResultName": "refresh_tokens"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "032cfc35-65e3-4b0c-a326-d4ea61228cc5",
      "name": "Merge",
      "type": "n8n-nodes-base.merge",
      "position": [
        3344,
        -1216
      ],
      "parameters": {
        "mode": "chooseBranch"
      },
      "typeVersion": 3.2
    },
    {
      "id": "9112cefa-951d-4e30-9148-6f83625f78fd",
      "name": "Verify Access Token",
      "type": "n8n-nodes-base.webhook",
      "position": [
        416,
        32
      ],
      "parameters": {
        "path": "verify-token",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 2.1
    },
    {
      "id": "38b357fd-91a3-4e3a-82de-62b71a556fbb",
      "name": "When Executed by Another Workflow",
      "type": "n8n-nodes-base.executeWorkflowTrigger",
      "position": [
        448,
        320
      ],
      "parameters": {
        "workflowInputs": {
          "values": [
            {
              "name": "token"
            }
          ]
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "8cc94ddc-6bbd-44a7-b7f0-afbad69d7daf",
      "name": "Refresh Access Token",
      "type": "n8n-nodes-base.webhook",
      "position": [
        -2336,
        -16
      ],
      "parameters": {
        "path": "refresh",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 2.1
    },
    {
      "id": "f9946939-4efe-43e4-8626-69ddb41b618a",
      "name": "Parse Refresh Token",
      "type": "n8n-nodes-base.code",
      "position": [
        -1904,
        0
      ],
      "parameters": {
        "jsCode": "const refreshToken = $('Process Refresh Token').first().json.refresh_token;\n\nif (!refreshToken) {\n  throw new Error('Refresh token is required');\n}\n\nconst parts = refreshToken.split('.');\n\nif (parts.length !== 3) {\n  throw new Error('Invalid token format');\n}\n\nconst [header, payload, signature] = parts;\n\nconst decodedPayload = JSON.parse(\n  Buffer.from(payload, 'base64url').toString()\n);\n\nconst now = Math.floor(Date.now() / 1000);\nif (decodedPayload.exp < now) {\n  throw new Error('Refresh token expired');\n}\n\nreturn {\n  json: {\n    header,\n    payload,\n    providedSignature: signature,\n    signatureInput: `${header}.${payload}`,\n    userId: decodedPayload.userId,\n    refreshToken: refreshToken\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "b0c044ef-13cc-4711-b7dd-3db0d176f202",
      "name": "Verify Signature",
      "type": "n8n-nodes-base.crypto",
      "position": [
        -1904,
        272
      ],
      "parameters": {
        "type": "SHA256",
        "value": "={{ $json.signatureInput }}",
        "action": "hmac",
        "secret": "={{ $('SET ACCESS AND REFRESH SECRET1').item.json.REFRESH_SECRET }}",
        "encoding": "base64",
        "dataPropertyName": "expectedSignature"
      },
      "typeVersion": 1
    },
    {
      "id": "d426f5e3-dd5c-485e-bd8e-ef9c8b0a028d",
      "name": "Compare Refresh Token Signature",
      "type": "n8n-nodes-base.code",
      "position": [
        -1648,
        0
      ],
      "parameters": {
        "jsCode": "const providedSignature = $input.item.json.providedSignature;\nconst expectedSignature = $input.item.json.expectedSignature;\n\nconst providedBase64 = Buffer.from(providedSignature, 'base64url')\n  .toString('base64');\n\nif (providedBase64 !== expectedSignature) {\n  throw new Error('Invalid token signature');\n}\n\nreturn {\n  json: {\n    userId: $input.item.json.userId,\n    refreshToken: $input.item.json.refreshToken\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "597d6c4f-0b0e-4777-867c-6a883d128dcc",
      "name": "Hash Refresh Token For DB Lookup",
      "type": "n8n-nodes-base.crypto",
      "position": [
        -1648,
        272
      ],
      "parameters": {
        "type": "SHA256",
        "value": "={{ $json.refreshToken }}",
        "dataPropertyName": "tokenHash"
      },
      "typeVersion": 1
    },
    {
      "id": "56f2e5a6-460d-4a6c-ade3-f7e99d9ae99e",
      "name": "If Refresh Token Exists",
      "type": "n8n-nodes-base.dataTable",
      "position": [
        -1392,
        16
      ],
      "parameters": {
        "limit": 1,
        "filters": {
          "conditions": [
            {
              "keyName": "token_hash",
              "keyValue": "={{ $json.tokenHash }}"
            },
            {
              "keyName": "user_id",
              "keyValue": "={{ $json.userId }}"
            }
          ]
        },
        "matchType": "allConditions",
        "operation": "get",
        "dataTableId": {
          "__rl": true,
          "mode": "list",
          "value": "wACP3KxHplgwFrJ1",
          "cachedResultUrl": "/projects/AT3bgmtmB45S5nQf/datatables/wACP3KxHplgwFrJ1",
          "cachedResultName": "refresh_tokens"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "98469d0f-cebc-4cf0-b775-1d72769d0560",
      "name": "If Refresh Token Is Valid",
      "type": "n8n-nodes-base.if",
      "position": [
        -1200,
        16
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "c14943c0-fa2b-4681-b9f1-a9953da78aed",
              "operator": {
                "type": "object",
                "operation": "notEmpty",
                "singleValue": true
              },
              "leftValue": "={{ $('If Refresh Token Exists').item.json }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "8d1c5fee-ca4a-4e5d-8efd-b9196d1cf9d1",
      "name": "Create Access Token Payload",
      "type": "n8n-nodes-base.code",
      "position": [
        -992,
        0
      ],
      "parameters": {
        "jsCode": "const userId = $('Parse Refresh Token').item.json.userId;\nconst now = Math.floor(Date.now() / 1000);\n\nconst accessPayload = {\n  userId: userId,\n  iat: now,\n  exp: now + (15 * 60)\n};\n\nconst header = Buffer.from(JSON.stringify({alg: 'HS256', typ: 'JWT'}))\n  .toString('base64url');\n\nconst accessPayloadEncoded = Buffer.from(JSON.stringify(accessPayload))\n  .toString('base64url');\n\nconst signatureInput = `${header}.${accessPayloadEncoded}`;\n\nreturn {\n  json: {\n    header,\n    payload: accessPayloadEncoded,\n    signatureInput\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "860e8b13-759e-4c81-861e-44c8c8e3bd51",
      "name": "Format Access Token JWT",
      "type": "n8n-nodes-base.code",
      "position": [
        -624,
        0
      ],
      "parameters": {
        "jsCode": "const header = $input.item.json.header;\nconst payload = $input.item.json.payload;\nconst signature = $input.item.json.accessSignature;\n\nconst signatureBase64url = Buffer.from(signature, 'base64')\n  .toString('base64url');\n\nconst accessToken = `${header}.${payload}.${signatureBase64url}`;\n\nreturn {\n  json: {\n    accessToken\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "7ec78f50-e2bc-4f9e-a063-f5cee4e05562",
      "name": "Return New Access Token",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        -448,
        0
      ],
      "parameters": {
        "options": {
          "responseCode": 200
        },
        "respondWith": "json",
        "responseBody": "={\n  \"success\": \"true\",\n  \"accessToken\": \"{{ $json.accessToken }}\"\n}"
      },
      "typeVersion": 1.4
    },
    {
      "id": "fa47661a-acb6-42b2-8783-b6c21fedf42b",
      "name": "Session Expired",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        -992,
        272
      ],
      "parameters": {
        "options": {
          "responseCode": 403
        },
        "respondWith": "json",
        "responseBody": "{\n  \"success\": false,\n  \"message\": \"Session Expired\"\n}"
      },
      "typeVersion": 1.4
    },
    {
      "id": "ffea7a05-b838-44cc-b9b5-2177cf31c97f",
      "name": "Parse Register Request",
      "type": "n8n-nodes-base.set",
      "position": [
        -2256,
        -976
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "a3b6e7b7-b2e7-42fb-8832-b62f1e53dca7",
              "name": "email",
              "type": "string",
              "value": "={{ $json.body.email }}"
            },
            {
              "id": "c388e803-f70b-44e2-b7dc-c272669fa261",
              "name": "username",
              "type": "string",
              "value": "={{ $json.body.username }}"
            },
            {
              "id": "12504a6d-e779-4768-9572-d4a9d02a522e",
              "name": "password",
              "type": "string",
              "value": "={{ $json.body.password }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "9edc4ba9-11b5-4705-8846-0623eec15b35",
      "name": "Login Successful",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        3520,
        -1216
      ],
      "parameters": {
        "options": {
          "responseCode": 200
        },
        "respondWith": "json",
        "responseBody": "={\n    \"accessToken\": \"{{ $('Hash Refresh Token for Storage').item.json.accessToken }}\",\n    \"refreshToken\": \"{{ $('Hash Refresh Token for Storage').item.json.refreshToken }}\",\n    \"user\": {\n        \"id\": \"{{ $json.id }}\",\n        \"email\": \"{{ $json.email }}\",\n        \"username\": \"{{ $json.username }}\"\n    }\n}"
      },
      "typeVersion": 1.4
    },
    {
      "id": "74bb0bc8-75c7-444d-b396-a797598a95ef",
      "name": "Login Credentials Invalid Response",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        1696,
        -976
      ],
      "parameters": {
        "options": {
          "responseCode": 400
        },
        "respondWith": "json",
        "responseBody": "{\n  \"success\": false,\n  \"message\": \"Credentials Invalid\"\n}"
      },
      "typeVersion": 1.4
    },
    {
      "id": "91534fa0-06f0-466b-a751-cd736e38e212",
      "name": "Process Verify Token Webhook",
      "type": "n8n-nodes-base.set",
      "position": [
        592,
        32
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "0e50cdb0-f300-4d82-a81c-054dffbc9cfb",
              "name": "access_token",
              "type": "string",
              "value": "={{ $json.body.access_token }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "ba27471d-e369-4ae3-af7d-11c6f6375dc6",
      "name": "Verify Input",
      "type": "n8n-nodes-base.code",
      "onError": "continueErrorOutput",
      "position": [
        784,
        -1184
      ],
      "parameters": {
        "jsCode": "const email = $('Process login webhook').first().json.email;\nconst password = $('Process login webhook').first().json.password;\n\nif (!email || !password) {\n  throw new Error('Email and password are required');\n}\n\nreturn {\n  json: {\n    email: email.toLowerCase().trim(),\n    password: password\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "207cace5-a58b-4b4d-bf5b-5bc2516f8ae1",
      "name": "Verify Password",
      "type": "n8n-nodes-base.code",
      "onError": "continueErrorOutput",
      "position": [
        1696,
        -1200
      ],
      "parameters": {
        "jsCode": "const inputHash = $input.item.json.inputHash;\nconst storedHash = $input.item.json.storedHash;\n\nif (inputHash !== storedHash) {\n  throw new Error('Invalid credentials');\n}\n\nreturn {\n  json: {\n    userId: $input.item.json.userId,\n    email: $input.item.json.email,\n    username: $input.item.json.username\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "8307d145-aaec-459c-9709-f60c41bb355e",
      "name": "Create JWT Payload",
      "type": "n8n-nodes-base.code",
      "position": [
        1904,
        -1216
      ],
      "parameters": {
        "jsCode": "const userId = $input.item.json.userId;\nconst email = $input.item.json.email;\nconst username = $input.item.json.username;\n\nconst now = Math.floor(Date.now() / 1000);\n\nconst accessPayload = {\n  userId: userId,\n  email: email,\n  iat: now,\n  exp: now + (15 * 60)\n};\n\nconst refreshPayload = {\n  userId: userId,\n  type: 'refresh',\n  iat: now,\n  exp: now + (7 * 24 * 60 * 60)\n};\n\nconst header = Buffer.from(JSON.stringify({alg: 'HS256', typ: 'JWT'}))\n  .toString('base64url');\n\nconst accessPayloadEncoded = Buffer.from(JSON.stringify(accessPayload))\n  .toString('base64url');\nconst refreshPayloadEncoded = Buffer.from(JSON.stringify(refreshPayload))\n  .toString('base64url');\n\nreturn {\n  json: {\n    userId,\n    email,\n    username,\n    header,\n    accessPayload: accessPayloadEncoded,\n    refreshPayload: refreshPayloadEncoded,\n    accessSignatureInput: `${header}.${accessPayloadEncoded}`,\n    refreshSignatureInput: `${header}.${refreshPayloadEncoded}`\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "d55ad76d-58a1-405b-a916-10896e6aacef",
      "name": "Process Refresh Token",
      "type": "n8n-nodes-base.set",
      "position": [
        -2160,
        -16
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "0e50cdb0-f300-4d82-a81c-054dffbc9cfb",
              "name": "refresh_token",
              "type": "string",
              "value": "={{ $json.body.refresh_token }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "e1e60536-b002-44f6-af55-4382c343e4fd",
      "name": "Sign New Access Token",
      "type": "n8n-nodes-base.crypto",
      "position": [
        -816,
        0
      ],
      "parameters": {
        "type": "SHA256",
        "value": "={{ $json.signatureInput }}",
        "action": "hmac",
        "secret": "={{ $('SET ACCESS AND REFRESH SECRET1').item.json.ACCESS_SECRET }}",
        "encoding": "base64",
        "dataPropertyName": "accessSignature"
      },
      "typeVersion": 1
    },
    {
      "id": "1c20777d-fbd2-45ab-846a-11664df3a0a3",
      "name": "If Username Is Available",
      "type": "n8n-nodes-base.if",
      "position": [
        -1568,
        -1264
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "36861ab1-365a-4a88-b50c-9949932f6d01",
              "operator": {
                "type": "object",
                "operation": "empty",
                "singleValue": true
              },
              "leftValue": "={{ $('Get User By Username').item.json }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "2057c030-5dc4-4adb-9c35-18cf15f3d282",
      "name": "Get User By Username",
      "type": "n8n-nodes-base.dataTable",
      "position": [
        -1760,
        -1248
      ],
      "parameters": {
        "limit": 1,
        "filters": {
          "conditions": [
            {
              "keyName": "username",
              "keyValue": "={{ $json.username }}"
            }
          ]
        },
        "operation": "get",
        "dataTableId": {
          "__rl": true,
          "mode": "list",
          "value": "YbPI3l04vLMH8qga",
          "cachedResultUrl": "/projects/AT3bgmtmB45S5nQf/datatables/YbPI3l04vLMH8qga",
          "cachedResultName": "review_users"
        }
      },
      "typeVersion": 1,
      "alwaysOutputData": true
    },
    {
      "id": "d2cb987b-183d-4ea7-be00-8b0181df6e89",
      "name": "Username Taken Error",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        -1568,
        -960
      ],
      "parameters": {
        "options": {
          "responseCode": 400
        },
        "respondWith": "json",
        "responseBody": "={\n  \"success\": false,\n  \"message\": \"That username is taken. Try a different one!\"\n}"
      },
      "typeVersion": 1.4
    },
    {
      "id": "2dc548d9-838f-4f7b-9076-768d1ae0bbbe",
      "name": "Registration Request Invalid Error",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        -1984,
        -960
      ],
      "parameters": {
        "options": {
          "responseCode": 400
        },
        "respondWith": "json",
        "responseBody": "={\n  \"success\": false,\n  \"message\": \"{{ $json.error }}\"\n}"
      },
      "typeVersion": 1.4
    },
    {
      "id": "c5f15368-adbc-426a-afe7-c418ebdbc810",
      "name": "If Email Is Available",
      "type": "n8n-nodes-base.if",
      "position": [
        -1120,
        -1264
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "36861ab1-365a-4a88-b50c-9949932f6d01",
              "operator": {
                "type": "object",
                "operation": "empty",
                "singleValue": true
              },
              "leftValue": "={{ $('Get User By Email').item.json }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "da039f08-ad18-4783-9fae-ab35ddaeab93",
      "name": "Get User By Email",
      "type": "n8n-nodes-base.dataTable",
      "position": [
        -1328,
        -1248
      ],
      "parameters": {
        "limit": 1,
        "filters": {
          "conditions": [
            {
              "keyName": "email",
              "keyValue": "={{ $json.email }}"
            }
          ]
        },
        "operation": "get",
        "dataTableId": {
          "__rl": true,
          "mode": "list",
          "value": "YbPI3l04vLMH8qga",
          "cachedResultUrl": "/projects/AT3bgmtmB45S5nQf/datatables/YbPI3l04vLMH8qga",
          "cachedResultName": "review_users"
        }
      },
      "typeVersion": 1,
      "alwaysOutputData": true
    },
    {
      "id": "551765c6-9690-4044-9767-81d280101024",
      "name": "Email Taken Error",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        -1120,
        -960
      ],
      "parameters": {
        "options": {
          "responseCode": 400
        },
        "respondWith": "json",
        "responseBody": "={\n  \"success\": false,\n  \"message\": \"That email is already registered. Try a different one!\"\n}"
      },
      "typeVersion": 1.4
    },
    {
      "id": "926df073-ee05-45a4-8221-1eeae39095ec",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2352,
        -1360
      ],
      "parameters": {
        "color": 5,
        "width": 2496,
        "height": 608,
        "content": "## Registration Flow"
      },
      "typeVersion": 1
    },
    {
      "id": "beef3c9d-e920-4038-9003-6fa317457706",
      "name": "Login Webhook",
      "type": "n8n-nodes-base.webhook",
      "position": [
        400,
        -1280
      ],
      "parameters": {
        "path": "login",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 2.1
    },
    {
      "id": "707614cc-c896-40ef-97c9-2a433182ad59",
      "name": "Bad Request",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        784,
        -960
      ],
      "parameters": {
        "options": {
          "responseCode": 400
        },
        "respondWith": "json",
        "responseBody": "{\n  \"success\": false,\n  \"message\": \"{{ $json.error }}\"\n}"
      },
      "typeVersion": 1.4
    },
    {
      "id": "c0d24010-60fa-445b-881e-1bce33fd3e66",
      "name": "Access Token Valid",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        1728,
        16
      ],
      "parameters": {
        "options": {},
        "respondWith": "json",
        "responseBody": "{\n    \"success\": true,\n    \"message\": \"Token Is Valid\"\n}"
      },
      "typeVersion": 1.4
    },
    {
      "id": "3078f280-cdf6-47c9-b7e2-b06801ac0245",
      "name": "Access Token Invalid",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        1744,
        272
      ],
      "parameters": {
        "options": {
          "responseCode": 403
        },
        "respondWith": "json",
        "responseBody": "{\n   \"success\": false,\n   \"message\": \"Access Denied\"\n}"
      },
      "typeVersion": 1.4
    },
    {
      "id": "4607d4be-8a77-4b1b-9d74-1a435acbc98e",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2368,
        -80
      ],
      "parameters": {
        "color": 5,
        "width": 2176,
        "height": 560,
        "content": "## Refresh Token Flow"
      },
      "typeVersion": 1
    },
    {
      "id": "a8f6dfcf-9f47-4153-a472-da54b44d2a8b",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2352,
        -1664
      ],
      "parameters": {
        "color": 7,
        "width": 432,
        "height": 272,
        "content": "## \ud83d\udcdd REGISTRATION FLOW\n\nChecks username \u2192 Checks email \u2192 Creates user\n\nDuplicate Prevention:\n- Username must be unique\n- Email must be unique\n- Checks happen BEFORE password hashing (saves resources)\n\nPassword stored as: \"salt:hash\"\nExample: \"a4f9c8:9f86d0...\""
      },
      "typeVersion": 1
    },
    {
      "id": "175cc098-cf21-427c-885b-aa8c01056d10",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1888,
        -1664
      ],
      "parameters": {
        "color": 7,
        "width": 432,
        "height": 272,
        "content": "## \ud83d\udd10 PASSWORD SECURITY\n\n1. Random salt generated (unique per user)\n2. Password + Salt combined\n3. Hashed with SHA-512 (irreversible)\n4. Stored as \"salt:hash\"\n\nWhy salt? Makes every password hash unique,\neven if two users have same password."
      },
      "typeVersion": 1
    },
    {
      "id": "ab2930d7-7130-4876-acb6-79956f83ed0c",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        368,
        -1680
      ],
      "parameters": {
        "color": 7,
        "width": 432,
        "height": 272,
        "content": "## \ud83d\udd11 LOGIN FLOW\n\nValidates input \u2192 Finds user \u2192 Verifies password\n\u2192 Generates tokens \u2192 Stores refresh token \u2192 Returns tokens\n\nPassword Verification:\n- Extracts salt from stored \"salt:hash\"\n- Hashes input password with SAME salt\n- Compares hashes (never plain text!)"
      },
      "typeVersion": 1
    },
    {
      "id": "cc909ef8-b87e-48d1-9f2e-d06b2704558f",
      "name": "Sticky Note7",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2000,
        -1776
      ],
      "parameters": {
        "color": 7,
        "width": 448,
        "height": 384,
        "content": "## \ud83c\udfab TWO TOKEN SYSTEM\n\nAccess Token (15 min):\n- For API requests\n- Short lifespan = more secure\n- Not stored in DB\n\nRefresh Token (7 days):\n- Gets new access tokens\n- Long lifespan = better UX\n- Stored in DB (can be revoked)\n\nBoth tokens signed with HMAC-SHA256"
      },
      "typeVersion": 1
    },
    {
      "id": "719b318f-746a-40f5-b3ec-b7ef4ddaad5b",
      "name": "Sticky Note8",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        368,
        -320
      ],
      "parameters": {
        "color": 7,
        "width": 432,
        "height": 272,
        "content": "\u2705 TOKEN VERIFICATION\n\n1. Parse token into parts\n2. Check expiration time\n3. Recreate signature with secret key\n4. Compare signatures\n\nIf signature doesn't match \u2192 Token was tampered with\nIf expired \u2192 Token no longer valid"
      },
      "typeVersion": 1
    },
    {
      "id": "85218220-90b2-481b-aef8-7fa9a5d6db31",
      "name": "Sticky Note9",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2368,
        -384
      ],
      "parameters": {
        "color": 7,
        "width": 432,
        "height": 272,
        "content": "## \ud83d\udd04 REFRESH PROCESS\n\nClient sends refresh token \u2192\nVerify signature \u2192 Check expiration \u2192\nLookup in database \u2192 Generate new access token\n\nDatabase Check:\nAllows token revocation (logout all devices)\nDetects stolen/compromised tokens\n\nRefresh token is hashed before DB lookup\n(extra security if database leaks)"
      },
      "typeVersion": 1
    },
    {
      "id": "4f2b4bff-9e34-4a92-8427-766ae54fa997",
      "name": "Sticky Note10",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1904,
        -384
      ],
      "parameters": {
        "color": 7,
        "width": 432,
        "height": 272,
        "content": "## \ud83d\udd12 DEFENSE IN DEPTH\n\nRefresh token is hashed (SHA-256) before storing.\n\nWhy? If database leaks:\n- Attacker can't use tokens directly\n- Must crack SHA-256 (very hard)\n- Adds security layer\n\nUser sends full token \u2192 We hash it \u2192 Lookup by hash"
      },
      "typeVersion": 1
    },
    {
      "id": "564f1dd3-11c1-4e00-9139-6d81404b821c",
      "name": "Sticky Note11",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -4224,
        -688
      ],
      "parameters": {
        "color": 7,
        "width": 416,
        "height": 336,
        "content": "## \ud83d\udd11 CRITICAL SECURITY\n\nTwo secret keys used:\n- ACCESS_SECRET: Signs access tokens\n- REFRESH_SECRET: Signs refresh tokens\n\n\u26a0\ufe0f MUST be identical everywhere:\n- Login workflow (signing)\n- Verify workflow (verification)\n- Refresh workflow (both)\n\nIf keys don't match \u2192 Signatures fail!"
      },
      "typeVersion": 1
    },
    {
      "id": "e9cf5472-0061-48e2-95e4-b65ed512fd3c",
      "name": "Sticky Note12",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -3744,
        -688
      ],
      "parameters": {
        "color": 3,
        "width": 416,
        "height": 336,
        "content": "## \u26a0\ufe0f TROUBLESHOOTING\n\n\"Invalid signature\":\n\u2192 Check secret keys match everywhere\n\n\"Token not found in DB\":\n\u2192 Check hash type (SHA256 + HEX encoding)\n\n\"Cannot read property\":\n\u2192 Check node references $('Node Name')\n\nUser can't login:\n\u2192 Check password hash format: \"salt:hash\"\n\nRefresh not working:\n\u2192 Check token stored in refresh_tokens table"
      },
      "typeVersion": 1
    },
    {
      "id": "2382a6af-4e25-4495-be93-22f9cb945203",
      "name": "Sticky Note13",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -3152,
        -1520
      ],
      "parameters": {
        "color": 7,
        "width": 416,
        "height": 336,
        "content": "## \ud83e\uddea TEST SEQUENCE\n\n1. Register new user\n2. Login with credentials\n3. Copy accessToken and refreshToken\n4. Verify access token\n5. Wait 15+ min OR manually test refresh\n6. Use refresh token to get new access token\n\nIf any step fails \u2192 Check error response\nand review corresponding workflow section."
      },
      "typeVersion": 1
    },
    {
      "id": "0d27d432-abbe-42b1-9a9a-b70434e4aa38",
      "name": "Sticky Note14",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -3616,
        -1520
      ],
      "parameters": {
        "color": 7,
        "width": 416,
        "height": 576,
        "content": "# \ud83d\udcbe TABLE SETUP\n\n## **Use n8n Data Tables Feature**\n\n### Table 'users':\n- **email** string (login identifier)\n- **username** string (login identifier)\n- **password_hash** string (as \"salt:hash\")\n- **refresh_token** string (latest token)\n\n### Table 'refresh_tokens':\n- **token_hash** - string (SHA-256 hash)\n- **user_id** number (which user owns it)\n- **expires_at** datetime (when it expires)\n\n**Access tokens**: NOT stored (stateless, fast)"
      },
      "typeVersion": 1
    },
    {
      "id": "4cc02f8c-a459-40a1-981d-247e9e311e14",
      "name": "Sticky Note15",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2496,
        -1776
      ],
      "parameters": {
        "color": 7,
        "width": 384,
        "height": 384,
        "content": "\u2699\ufe0f CUSTOMIZATION POINTS\n\nChange token lifespan:\n- Find: exp: now + (15 * 60)\n- Adjust: (30 * 60) = 30 minutes\n\nChange hash algorithm:\n- Update Crypto nodes (SHA256 \u2192 SHA512)\n- Must update ALL instances!\n\nAdd fields to JWT:\n- Modify \"Create JWT Payload\" code\n- Add to payload object\n\nRevoke all user tokens:\n- Delete from refresh_tokens table\n- Or update token_hash to invalid value"
      },
      "typeVersion": 1
    },
    {
      "id": "71e19da5-0fdb-466e-bea1-42dead1f983e",
      "name": "SET ACCESS AND REFRESH SECRET",
      "type": "n8n-nodes-base.set",
      "position": [
        496,
        -976
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "4b5a0988-b8bb-46ff-b33b-ddabe2d006ba",
              "name": "ACCESS_SECRET",
              "type": "string",
              "value": ""
            },
            {
              "id": "5721f756-ce20-4bd6-a4db-b364bffb6cab",
              "name": "REFRESH_SECRET",
              "type": "string",
              "value": ""
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "f5267c80-05c8-4378-aad1-c8ff115d1d82",
      "name": "Sticky Note32",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        432,
        -1024
      ],
      "parameters": {
        "color": 3,
        "height": 256,
        "content": ""
      },
      "typeVersion": 1
    },
    {
      "id": "2d74c194-e5a8-489e-a90b-f21e2740e173",
      "name": "Sticky Note33",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        368,
        -736
      ],
      "parameters": {
        "color": 3,
        "width": 416,
        "height": 192,
        "content": "## !! ATTENTION !!\nYou can use this node to set the **ACCESS_SECRET** and **REFRESH_SECRET**, but ideally you will use Variables to do this:\n\nYou will need to handle authenticating requests in different workflows, so having these variables available globally is **crucial**"
      },
      "typeVersion": 1
    },
    {
      "id": "682c87b6-9388-43bf-8981-c2d07fcb1996",
      "name": "Sticky Note34",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        816,
        -736
      ],
      "parameters": {
        "color": 7,
        "width": 448,
        "height": 240,
        "content": "## Migrating to Variables\n### You'll need to change following nodes to use Variables instead of Set Node:\n- **'Sign Access Token'** and **'Sign Refresh Token'** nodes to use the Variables\n- **Verify Signature** and **Sign New Access Token** nodes to use the Variables\n- **Verify HMAC Signature** node to use the Variable"
      },
      "typeVersion": 1
    },
    {
      "id": "85544cc1-ee32-45e7-9257-17a7a1e3924b",
      "name": "Sticky Note35",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -4224,
        -1520
      ],
      "parameters": {
        "color": 7,
        "width": 560,
        "height": 640,
        "content": "# \ud83d\udd10 COMPLETE AUTH WORKFLOW OVERVIEW\n\nImplement a complete auth process using n8n workflow. This template will enable you to easily add authentication to any application you need. Real production apps have a more extensive authentication system, but this is a nice overview of how that system looks like\n\n### \ud83d\udcdd REGISTRATION\n- User submits: email, username, password\n\n### \ud83d\udd11 LOGIN\n- User submits: email, password\n\n### \u2705 VERIFY TOKEN\n- Client submits: access_token\n\n### \ud83d\udd04 REFRESH TOKEN\n- Client submits: refresh_token\n\n### \ud83d\udcca CLIENT FLOW:\nRegister \u2192 Login (get tokens) \u2192 Use access token for requests\n\u2192 When expired (401) \u2192 Use refresh token \u2192 Get new access token\n\u2192 When refresh expires \u2192 Login again"
      },
      "typeVersion": 1
    },
    {
      "id": "0c46dac3-dad7-4aa9-ba02-ca1f3f59e4e2",
      "name": "Sticky Note37",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -4240,
        -1664
      ],
      "parameters": {
        "color": 7,
        "width": 1616,
        "height": 80,
        "content": "# Workflow Overview And Setup"
      },
      "typeVersion": 1
    },
    {
      "id": "d5b86a70-6837-4664-acfd-62538f6c7066",
      "name": "Sticky Note38",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -4256,
        -816
      ],
      "parameters": {
        "color": 7,
        "width": 1616,
        "height": 80,
        "content": "# Important Notes"
      },
      "typeVersion": 1
    },
    {
      "id": "9064bdba-5fcf-43d0-a3e9-3d7eab89b319",
      "name": "SET ACCESS AND REFRESH SECRET1",
      "type": "n8n-nodes-base.set",
      "position": [
        -2240,
        272
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "4b5a0988-b8bb-46ff-b33b-ddabe2d006ba",
              "name": "ACCESS_SECRET",
              "type": "string",
              "value": ""
            },
            {
              "id": "5721f756-ce20-4bd6-a4db-b364bffb6cab",
              "name": "REFRESH_SECRET",
              "type": "string",
              "value": ""
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "363e999b-554f-4396-b33a-846889a8cd4c",
      "name": "Sticky Note36",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2320,
        208
      ],
      "parameters": {
        "color": 3,
        "height": 256,
        "content": ""
      },
      "typeVersion": 1
    },
    {
      "id": "4f228c6f-9a05-4085-9c17-d980b2b0f3e5",
      "name": "SET ACCESS AND REFRESH SECRET2",
      "type": "n8n-nodes-base.set",
      "position": [
        1072,
        160
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "4b5a0988-b8bb-46ff-b33b-ddabe2d006ba",
              "name": "ACCESS_SECRET",
              "type": "string",
              "value": ""
            },
            {
              "id": "5721f756-ce20-4bd6-a4db-b364bffb6cab",
              "name": "REFRESH_SECRET",
              "type": "string",
              "value": ""
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "5232ecb8-1844-4352-80cc-eff4d17856eb",
      "name": "Sticky Note39",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        992,
        112
      ],
      "parameters": {
        "color": 3,
        "height": 256,
        "content": ""
      },
      "typeVersion": 1
    }
  ],
  "connections": {
    "Merge": {
      "main": [
        [
          {
            "node": "Login Successful",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get User": {
      "main": [
        [
          {
            "node": "If User Exists",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse JWT": {
      "main": [
        [
          {
            "node": "SET ACCESS AND REFRESH SECRET2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create User": {
      "main": [
        [
          {
            "node": "Registration Successful",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Error Registration Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Verify Input": {
      "main": [
        [
          {
            "node": "Get User",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Bad Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Salt": {
      "main": [
        [
          {
            "node": "Format Password & Salt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Hash Password": {
      "main": [
        [
          {
            "node": "Format User Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Login Webhook": {
      "main": [
        [
          {
            "node": "Process login webhook",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If User Exists": {
      "main": [
        [
          {
            "node": "Extract Salt & Hash",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "User Not Found",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Verify Password": {
      "main": [
        [
          {
            "node": "Create JWT Payload",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Login Credentials Invalid Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format User Data": {
      "main": [
        [
          {
            "node": "Create User",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge JWT Tokens": {
      "main": [
        [
          {
            "node": "Format JWT Tokens",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Verify Signature": {
      "main": [
        [
          {
            "node": "Compare Refresh Token Signature",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format JWT Tokens": {
      "main": [
        [
          {
            "node": "Hash Refresh Tok
Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

How this works

This workflow provides a self-hosted JWT authentication system that handles user registration, secure login, token verification, and refresh token rotation for any web or mobile application. It stores user credentials in dataTable and uses crypto operations to generate salts, hash passwords, and sign tokens, removing the need for external authentication services. The core sequence begins when a webhook receives login or registration requests, then processes them through hashing, database lookups, and token generation.

Use this when you need full control over auth data and token logic in a single environment, but avoid it for high-traffic production sites that require dedicated identity providers. Common variations include swapping the webhook trigger for executeWorkflowTrigger calls from other workflows or extending the dataTable schema to store additional user fields.

About this workflow

A production-ready authentication workflow implementing secure user registration, login, token verification, and refresh token mechanisms. Perfect for adding authentication to any application without needing a separate auth service.

Source: https://n8n.io/workflows/9660/ — original creator credit. Request a take-down →

More General workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

General

Agendamiento. Uses n8n-nodes-evolution-api, redis, dataTable, executeWorkflowTrigger. Event-driven trigger; 60 nodes.

N8N Nodes Evolution Api, Redis, Data Table +2
General

This implementation aggregates incoming data into a Redis list from potentially concurrent workflow executions. It buffers the data for a set period before a single execution retrieves and processes t

Crypto, Redis, Execute Workflow Trigger
General

This workflow provides a reusable error handling, audit logging, and observability pattern for n8n workflows using two n8n custom Data Tables: and .

Error Trigger, Data Table, Execute Workflow Trigger
General

Dashboard. Uses dataTable. Webhook trigger; 16 nodes.

Data Table
General

If you're in need of a quick and dirty cache that doesn't need anything other than the current version of N8N, boy do I have a dodgy script for you to try!

Execute Workflow Trigger, Data Table, Stop And Error