{
  "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 Token for Storage",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get User By Email": {
      "main": [
        [
          {
            "node": "If Email Is Available",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sign Access Token": {
      "main": [
        [
          {
            "node": "Merge JWT Tokens",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code in JavaScript": {
      "main": [
        [
          {
            "node": "Update User Refresh Token",
            "type": "main",
            "index": 0
          },
          {
            "node": "Store Refresh Token",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Compare Signatures": {
      "main": [
        [
          {
            "node": "Access Token Valid",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Access Token Invalid",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create JWT Payload": {
      "main": [
        [
          {
            "node": "Sign Access Token",
            "type": "main",
            "index": 0
          },
          {
            "node": "Sign Refresh Token",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    },
    "Sign Refresh Token": {
      "main": [
        [
          {
            "node": "Merge JWT Tokens",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Extract Salt & Hash": {
      "main": [
        [
          {
            "node": "Hash Input Password",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Hash Input Password": {
      "main": [
        [
          {
            "node": "Verify Password",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Refresh Token": {
      "main": [
        [
          {
            "node": "Verify Signature",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Store Refresh Token": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Verify Access Token": {
      "main": [
        [
          {
            "node": "Process Verify Token Webhook",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get User By Username": {
      "main": [
        [
          {
            "node": "If Username Is Available",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Refresh Access Token": {
      "main": [
        [
          {
            "node": "Process Refresh Token",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Registration Webhook": {
      "main": [
        [
          {
            "node": "Parse Register Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If Email Is Available": {
      "main": [
        [
          {
            "node": "Generate Salt",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Email Taken Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Process Refresh Token": {
      "main": [
        [
          {
            "node": "SET ACCESS AND REFRESH SECRET1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Process login webhook": {
      "main": [
        [
          {
            "node": "SET ACCESS AND REFRESH SECRET",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sign New Access Token": {
      "main": [
        [
          {
            "node": "Format Access Token JWT",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Verify HMAC Signature": {
      "main": [
        [
          {
            "node": "Compare Signatures",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Password & Salt": {
      "main": [
        [
          {
            "node": "Hash Password",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Register Request": {
      "main": [
        [
          {
            "node": "Validate Registration Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Access Token JWT": {
      "main": [
        [
          {
            "node": "Return New Access Token",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If Refresh Token Exists": {
      "main": [
        [
          {
            "node": "If Refresh Token Is Valid",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If Username Is Available": {
      "main": [
        [
          {
            "node": "Get User By Email",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Username Taken Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If Refresh Token Is Valid": {
      "main": [
        [
          {
            "node": "Create Access Token Payload",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Session Expired",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update User Refresh Token": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Access Token Payload": {
      "main": [
        [
          {
            "node": "Sign New Access Token",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Process Verify Token Webhook": {
      "main": [
        [
          {
            "node": "Parse JWT",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SET ACCESS AND REFRESH SECRET": {
      "main": [
        [
          {
            "node": "Verify Input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate Registration Request": {
      "main": [
        [
          {
            "node": "Get User By Username",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Registration Request Invalid Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Hash Refresh Token for Storage": {
      "main": [
        [
          {
            "node": "Code in JavaScript",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SET ACCESS AND REFRESH SECRET1": {
      "main": [
        [
          {
            "node": "Parse Refresh Token",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SET ACCESS AND REFRESH SECRET2": {
      "main": [
        [
          {
            "node": "Verify HMAC Signature",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Compare Refresh Token Signature": {
      "main": [
        [
          {
            "node": "Hash Refresh Token For DB Lookup",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Hash Refresh Token For DB Lookup": {
      "main": [
        [
          {
            "node": "If Refresh Token Exists",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When Executed by Another Workflow": {
      "main": [
        [
          {
            "node": "Parse JWT",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}