{
  "nodes": [
    {
      "id": "1529173e-b4ae-4352-a378-a327c39e7fed",
      "name": "success",
      "type": "n8n-nodes-base.if",
      "position": [
        448,
        -48
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "faa449f2-815c-41fa-8378-e93093ad10d7",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.success }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "140b3700-3721-4e41-b87c-8df70e995bc4",
      "name": "validation failed",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        672,
        48
      ],
      "parameters": {
        "options": {
          "responseCode": 401
        },
        "respondWith": "json",
        "responseBody": "={\n  \"success\": false,\n  \"error\": \"{{ $('validator').first().json.reason }}\"\n}"
      },
      "typeVersion": 1.4
    },
    {
      "id": "4715c17e-56d8-49f5-bcce-7f5b6f822f49",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        592,
        -304
      ],
      "parameters": {
        "color": 6,
        "width": 260,
        "height": 308,
        "content": "## Database Example\nClone this [Airtable Base](https://airtable.com/appbw5TEhn8xIxxXR/shrN8ve4dfJIXjcAm)"
      },
      "typeVersion": 1
    },
    {
      "id": "722d92c2-445c-4c0a-9e06-a58dbc075907",
      "name": "secret validation",
      "type": "n8n-nodes-base.if",
      "position": [
        1120,
        -240
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "2e4151fc-f513-4a1d-8e42-c77857de22bc",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.secret }}",
              "rightValue": "={{ $('client receiver').item.json.body.client_secret }}"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "a02e3055-eb51-4fec-a429-d8349e388fe4",
      "name": "invalid client",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        1120,
        -48
      ],
      "parameters": {
        "options": {
          "responseCode": 400
        },
        "respondWith": "json",
        "responseBody": "{\n\"success\": false,\n\"error\": \"Invalid client id\"\n}"
      },
      "typeVersion": 1.1
    },
    {
      "id": "729d6799-6f74-457c-aac5-ca651e10f170",
      "name": "invalid secret",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        1344,
        -144
      ],
      "parameters": {
        "options": {
          "responseCode": 401
        },
        "respondWith": "json",
        "responseBody": "{\n\"success\": false,\n\"error\": \"Invalid client secret\"\n}"
      },
      "typeVersion": 1.1
    },
    {
      "id": "b94ab331-23c2-473d-afdc-e591b89b38e0",
      "name": "generate token",
      "type": "n8n-nodes-base.code",
      "position": [
        1344,
        -336
      ],
      "parameters": {
        "jsCode": "function generateLongToken(length) {\n  const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';\n  let token = '';\n  for (let i = 0; i < length; i++) {\n    token += chars.charAt(Math.floor(Math.random() * chars.length));\n  }\n  return token;\n}\n\nconst token = generateLongToken(32);\n\nreturn [\n  {\n    json: {\n      token\n    }\n  }\n];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "204e04af-34a7-4824-b107-4cc3e89199e3",
      "name": "client exists",
      "type": "n8n-nodes-base.if",
      "position": [
        896,
        -144
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "6f16470a-aae4-4647-850e-332284b00a9f",
              "operator": {
                "type": "string",
                "operation": "exists",
                "singleValue": true
              },
              "leftValue": "={{ $json.id }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "e366461a-ece5-47d1-b5de-ea29aa1846ca",
      "name": "validator",
      "type": "n8n-nodes-base.code",
      "position": [
        224,
        -48
      ],
      "parameters": {
        "jsCode": "const input = $json;\n\nconst body = input.body || {};\n\n// Allowed keys\nconst allowedKeys = ['client_id', 'client_secret'];\n\n// Check required keys\nfor (const key of allowedKeys) {\n  if (!body.hasOwnProperty(key)) {\n    return [{ json: { success: false, reason: `Missing '${key}' in body` } }];\n  }\n}\n\n// Check for extra keys\nconst extraKeys = Object.keys(body).filter(k => !allowedKeys.includes(k));\nif (extraKeys.length > 0) {\n  return [{ json: { success: false, reason: `Body must contain only 'client_id' and 'client_secret', found extra key(s): ${extraKeys.join(', ')}` } }];\n}\n\n// All good\nreturn [{ json: { success: true } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "b7ea9e5f-d097-4ff7-b165-ee1560bb054d",
      "name": "client receiver",
      "type": "n8n-nodes-base.webhook",
      "position": [
        0,
        -48
      ],
      "parameters": {
        "path": "token-refresher",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 2
    },
    {
      "id": "70e09e1b-336a-4756-af23-aa4a0636e854",
      "name": "get client id",
      "type": "n8n-nodes-base.airtable",
      "position": [
        672,
        -144
      ],
      "parameters": {
        "base": {
          "__rl": true,
          "mode": "list",
          "value": "appbw5TEhn8xIxxXR",
          "cachedResultUrl": "https://airtable.com/appbw5TEhn8xIxxXR",
          "cachedResultName": "Testing Bearer YOUR_TOKEN_HERE "
        },
        "table": {
          "__rl": true,
          "mode": "list",
          "value": "tblK2jv4hLOsaKM3m",
          "cachedResultUrl": "https://airtable.com/appbw5TEhn8xIxxXR/tblK2jv4hLOsaKM3m",
          "cachedResultName": "Client IDs"
        },
        "options": {},
        "operation": "search",
        "filterByFormula": "={client} = \"{{ $('client receiver').item.json.body.client_id }}\""
      },
      "credentials": {
        "airtableTokenApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1,
      "alwaysOutputData": true
    },
    {
      "id": "d5c9d779-5805-45a0-affd-a683a17391ac",
      "name": "create token",
      "type": "n8n-nodes-base.airtable",
      "position": [
        1568,
        -336
      ],
      "parameters": {
        "base": {
          "__rl": true,
          "mode": "list",
          "value": "appbw5TEhn8xIxxXR",
          "cachedResultUrl": "https://airtable.com/appbw5TEhn8xIxxXR",
          "cachedResultName": "Testing Bearer YOUR_TOKEN_HERE "
        },
        "table": {
          "__rl": true,
          "mode": "list",
          "value": "tblnqvjl4U2t9OMQD",
          "cachedResultUrl": "https://airtable.com/appbw5TEhn8xIxxXR/tblnqvjl4U2t9OMQD",
          "cachedResultName": "Tokens"
        },
        "columns": {
          "value": {
            "Token ID": "={{ $json.token }}",
            "Client IDs": "={{ [$('client receiver').item.json.body.client_id] }}",
            "Token Type": "Bearer",
            "Creation Date": "={{ $now }}"
          },
          "schema": [
            {
              "id": "Token ID",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "Token ID",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Token Type",
              "type": "options",
              "display": true,
              "options": [
                {
                  "name": "Bearer",
                  "value": "Bearer"
                }
              ],
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "Token Type",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Expiration Date",
              "type": "dateTime",
              "display": true,
              "removed": true,
              "readOnly": false,
              "required": false,
              "displayName": "Expiration Date",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Issued To",
              "type": "array",
              "display": true,
              "removed": true,
              "readOnly": false,
              "required": false,
              "displayName": "Issued To",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Creation Date",
              "type": "dateTime",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "Creation Date",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Is Active",
              "type": "boolean",
              "display": true,
              "removed": true,
              "readOnly": false,
              "required": false,
              "displayName": "Is Active",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Days Until Expiration",
              "type": "string",
              "display": true,
              "removed": true,
              "readOnly": true,
              "required": false,
              "displayName": "Days Until Expiration",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Status",
              "type": "string",
              "display": true,
              "removed": true,
              "readOnly": true,
              "required": false,
              "displayName": "Status",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Client IDs",
              "type": "array",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "Client IDs",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {
          "typecast": true
        },
        "operation": "create"
      },
      "credentials": {
        "airtableTokenApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1,
      "alwaysOutputData": true
    },
    {
      "id": "09da5fce-3635-4125-903b-a55ca93a5b4b",
      "name": "respond ",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        1792,
        -336
      ],
      "parameters": {
        "options": {},
        "respondWith": "json",
        "responseBody": "={\n    \"access_token\": \"{{ $('generate token').item.json.token }}\",\n    \"expires_in\": 3600,\n    \"token_type\": \"{{ $json.fields['Token Type'] }}\"\n}"
      },
      "typeVersion": 1.2
    },
    {
      "id": "325443c4-31b4-472f-ab2c-e606776466ba",
      "name": "Other methods",
      "type": "n8n-nodes-base.webhook",
      "position": [
        0,
        272
      ],
      "parameters": {
        "path": "test-jobs",
        "options": {},
        "httpMethod": [
          "DELETE",
          "HEAD",
          "PATCH",
          "PUT",
          "GET"
        ],
        "responseMode": "responseNode",
        "multipleMethods": true
      },
      "typeVersion": 2
    },
    {
      "id": "8b407e94-1e92-4882-872f-f6b2e6e60673",
      "name": "405 Error",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        208,
        320
      ],
      "parameters": {
        "options": {
          "responseCode": 405
        },
        "respondWith": "json",
        "responseBody": "{\n  \"error\": \"Use POST request instead\"\n}"
      },
      "typeVersion": 1.1
    },
    {
      "id": "7bc3accc-791d-4f8a-8f14-80f2d3d4ddf9",
      "name": "When clicking \u2018Execute workflow\u2019",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        16,
        -448
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "d94ffe0e-95f7-459c-a837-a08b9ac3d87f",
      "name": "Make a request",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        240,
        -448
      ],
      "parameters": {
        "url": "https://localhost:8080/webhook/token-refresher",
        "method": "POST",
        "options": {},
        "jsonBody": "{\n    \"client_id\": \"client_a_1234567890abcdef\",\n    \"client_secret\": \"secret_a_abcdef1234567890\"\n\n}",
        "sendBody": true,
        "specifyBody": "json"
      },
      "typeVersion": 4.2
    },
    {
      "id": "b9ea6991-ba49-49f0-bffb-5cc807eedb94",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -48,
        -512
      ],
      "parameters": {
        "color": 7,
        "width": 460,
        "height": 220,
        "content": "## Test the request"
      },
      "typeVersion": 1
    },
    {
      "id": "e8814551-20d1-4114-b691-0a45cbf61086",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -48,
        208
      ],
      "parameters": {
        "color": 7,
        "width": 460,
        "height": 280,
        "content": "## HTTP Method handler"
      },
      "typeVersion": 1
    },
    {
      "id": "db9c1b3f-00ba-40fe-8e9c-f900be065940",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -688,
        -512
      ],
      "parameters": {
        "color": 4,
        "width": 580,
        "height": 1656,
        "content": "## OAuth Token Generator and Validator\n\nThis **n8n template** helps you generate, validate, and store tokens for your customers securely using:\n\n- **n8n** as your backend automation engine  \n- **Airtable** as your lightweight client and token store\n\n---\n\n### \ud83d\ude80 What It Does\n\n- Accepts `client_id` and `client_secret` via **POST webhook**.\n- Validates client credentials against Airtable.\n- Generates a **long token** on success.\n- Stores the generated token in Airtable with metadata.\n- Responds with a JSON containing the token, expiry, and type.\n- Returns clear error messages if validation fails.\n\n---\n\n### How It Works\n\n1. **Webhook node** receives `client_id` and `client_secret`.\n2. **Validator (Code node)** checks:\n   - Body contains only `client_id` and `client_secret`.\n   - Rejects missing or extra fields.\n3. **Airtable search**:\n   - Looks up the `client_id`.\n   - Rejects if not found.\n4. **Secret validation (If node)**:\n   - Compares provided `client_secret` with stored value.\n   - Rejects if incorrect.\n5. **Token generation (Code node)**:\n   - Generates a 128-character secure token.\n6. **Airtable create**:\n   - Stores token, client ID, creation date, and type.\n7. **Webhook response**:\n   - Returns JSON `{ access_token, expires_in, token_type }` on success.\n   - Returns appropriate JSON error messages on failure.\n\n---\n\n### Related Workflow\n\nYou can also use it with the published **Bearer YOUR_TOKEN_HERE Validation** workflow:\n\n\ud83d\udc49 [Validate API Requests with Bearer YOUR_TOKEN_HERE Authentication and Airtable](https://n8n.io/workflows/6184-validate-api-requests-with-bearer-token-authentication-and-airtable)\n\nto securely validate tokens you generate with this workflow across your protected endpoints.\n\n---\n\n### Why Use This\n\n- Provides **OAuth-like flows** without a complex backend.\n- Uses **n8n + Airtable** for client management and token storage.\n- Clean, modular, and ready for your SaaS or internal API automations.\n- Extendable for token expiry, refresh, and rotation handling.\n\n---\n\nEnjoy building secure token-based APIs using **n8n + Airtable**! \ud83d\ude80\n\n### Built by:\n[Nazmy](https://n8n.io/creators/islamnazmi/)\n"
      },
      "typeVersion": 1
    }
  ],
  "connections": {
    "success": {
      "main": [
        [
          {
            "node": "get client id",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "validation failed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "validator": {
      "main": [
        [
          {
            "node": "success",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "create token": {
      "main": [
        [
          {
            "node": "respond ",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Other methods": {
      "main": [
        [
          {
            "node": "405 Error",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "405 Error",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "405 Error",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "405 Error",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "405 Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "client exists": {
      "main": [
        [
          {
            "node": "secret validation",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "invalid client",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "get client id": {
      "main": [
        [
          {
            "node": "client exists",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "generate token": {
      "main": [
        [
          {
            "node": "create token",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "client receiver": {
      "main": [
        [
          {
            "node": "validator",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "secret validation": {
      "main": [
        [
          {
            "node": "generate token",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "invalid secret",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When clicking \u2018Execute workflow\u2019": {
      "main": [
        [
          {
            "node": "Make a request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}