AutomationFlowsData & Sheets › Cc-09 Login Sso (client Care)

Cc-09 Login Sso (client Care)

CC-09 Login SSO (Client Care). Uses httpRequest, airtable. Webhook trigger; 17 nodes.

Webhook trigger★★★★☆ complexity17 nodesHTTP RequestAirtable
Data & Sheets Trigger: Webhook Nodes: 17 Complexity: ★★★★☆ Added:

This workflow follows the Airtable → HTTP Request recipe pattern — see all workflows that pair these two integrations.

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
{
  "name": "CC-09 Login SSO (Client Care)",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "cc/login-sso",
        "responseMode": "responseNode",
        "options": {
          "allowedOrigins": "*"
        }
      },
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        220,
        300
      ],
      "id": "cc09-webhook",
      "name": "Webhook"
    },
    {
      "parameters": {
        "jsCode": "// CC-09: Extract JWT Parts from Microsoft id_token\n// Input: POST body { id_token: '...' }\n// Output: JWT header info for JWKS lookup + signature verification\n\nconst body = $input.first().json.body || {};\nconst idToken = body.id_token || '';\n\nif (!idToken) {\n  return [{ json: { valid: false, error: 'Missing id_token in request body' } }];\n}\n\nconst parts = idToken.split('.');\nif (parts.length !== 3) {\n  return [{ json: { valid: false, error: 'Invalid JWT format: expected 3 parts' } }];\n}\n\ntry {\n  const headerB64 = parts[0].replace(/-/g, '+').replace(/_/g, '/');\n  const headerJson = Buffer.from(headerB64, 'base64').toString('utf8');\n  const header = JSON.parse(headerJson);\n\n  if (!header.kid) {\n    return [{ json: { valid: false, error: 'JWT header missing kid claim' } }];\n  }\n  if (header.alg !== 'RS256') {\n    return [{ json: { valid: false, error: `Unsupported JWT algorithm: ${header.alg}` } }];\n  }\n\n  return [{ json: {\n    valid: true,\n    idToken,\n    kid: header.kid,\n    alg: header.alg,\n    signedContent: parts[0] + '.' + parts[1],\n    signatureB64url: parts[2],\n    payloadB64: parts[1]\n  } }];\n} catch (e) {\n  return [{ json: { valid: false, error: 'Failed to decode JWT header: ' + e.message } }];\n}"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        440,
        300
      ],
      "id": "cc09-extract-jwt",
      "name": "Extract JWT Parts"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "cc09-if-valid",
              "leftValue": "={{ $json.valid }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true"
              }
            }
          ],
          "combinator": "and"
        }
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        660,
        300
      ],
      "id": "cc09-if-valid",
      "name": "IF Valid JWT"
    },
    {
      "parameters": {
        "url": "https://login.microsoftonline.com/8d1a9049-44e6-4a26-b9e5-d0c405e82e30/discovery/v2.0/keys",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        880,
        200
      ],
      "id": "cc09-fetch-jwks",
      "name": "Fetch Microsoft JWKS"
    },
    {
      "parameters": {
        "jsCode": "// CC-09: Verify JWT Signature + Validate Claims\n// Same RSA-SHA256 verification as WF20\n\nconst crypto = require('crypto');\n\nconst jwtData = $('Extract JWT Parts').first().json;\nconst kid = jwtData.kid;\nconst signedContent = jwtData.signedContent;\nconst signatureB64url = jwtData.signatureB64url;\nconst payloadB64 = jwtData.payloadB64;\n\nconst jwksResponse = $input.first().json;\nconst keys = jwksResponse.keys || [];\n\nconst matchingKey = keys.find(k => k.kid === kid);\nif (!matchingKey) {\n  return [{ json: { verified: false, error: `No matching key found for kid: ${kid}` } }];\n}\n\n// Build RSA public key from JWK\nconst publicKey = crypto.createPublicKey({\n  key: { kty: 'RSA', n: matchingKey.n, e: matchingKey.e },\n  format: 'jwk'\n});\n\n// Convert base64url signature to buffer\nconst sigB64 = signatureB64url.replace(/-/g, '+').replace(/_/g, '/');\nconst sigPadded = sigB64 + '='.repeat((4 - sigB64.length % 4) % 4);\nconst signatureBuffer = Buffer.from(sigPadded, 'base64');\n\n// Verify RSA-SHA256 signature\nconst verifier = crypto.createVerify('RSA-SHA256');\nverifier.update(signedContent);\nconst isValid = verifier.verify(publicKey, signatureBuffer);\n\nif (!isValid) {\n  return [{ json: { verified: false, error: 'JWT signature verification failed' } }];\n}\n\n// Decode payload\nconst payloadPadded = payloadB64.replace(/-/g, '+').replace(/_/g, '/') + '='.repeat((4 - payloadB64.length % 4) % 4);\nconst payloadJson = Buffer.from(payloadPadded, 'base64').toString('utf8');\nconst payload = JSON.parse(payloadJson);\n\n// Validate issuer\nconst expectedIssuer = 'https://login.microsoftonline.com/8d1a9049-44e6-4a26-b9e5-d0c405e82e30/v2.0';\nif (payload.iss !== expectedIssuer) {\n  return [{ json: { verified: false, error: `Invalid issuer: ${payload.iss}` } }];\n}\n\n// Validate audience (same SPA app as booking dashboard)\nconst expectedAudience = '4df869dd-ca95-49dd-8939-aa796e515df5';\nif (payload.aud !== expectedAudience) {\n  return [{ json: { verified: false, error: `Invalid audience: ${payload.aud}` } }];\n}\n\n// Validate expiration\nconst now = Math.floor(Date.now() / 1000);\nif (payload.exp && payload.exp < now) {\n  return [{ json: { verified: false, error: 'Token has expired' } }];\n}\nif (payload.nbf && payload.nbf > now + 300) {\n  return [{ json: { verified: false, error: 'Token not yet valid' } }];\n}\n\n// Extract email\nconst email = (payload.email || payload.preferred_username || payload.upn || '').toLowerCase().trim();\nif (!email) {\n  return [{ json: { verified: false, error: 'No email found in token claims' } }];\n}\n\nreturn [{ json: {\n  verified: true,\n  email,\n  name: payload.name || '',\n  oid: payload.oid || '',\n  tid: payload.tid || ''\n} }];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1100,
        200
      ],
      "id": "cc09-verify-jwt",
      "name": "Verify JWT Signature"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "cc09-if-verified",
              "leftValue": "={{ $json.verified }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true"
              }
            }
          ],
          "combinator": "and"
        }
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        1320,
        200
      ],
      "id": "cc09-if-verified",
      "name": "IF Verified"
    },
    {
      "parameters": {
        "operation": "search",
        "base": {
          "__rl": true,
          "value": "appPccm6NkaJdvqwy",
          "mode": "id"
        },
        "table": {
          "__rl": true,
          "value": "tbl2dmVrEV9sKu5o9",
          "mode": "id"
        },
        "filterByFormula": "=AND(LOWER({Email}) = '{{ $json.email }}', {Is_Active} = TRUE())",
        "options": {
          "alwaysOutputData": true
        }
      },
      "type": "n8n-nodes-base.airtable",
      "typeVersion": 2.1,
      "position": [
        1540,
        100
      ],
      "id": "cc09-find-user",
      "name": "Find CC_User by Email"
    },
    {
      "parameters": {
        "jsCode": "// CC-09: Build CRM Login Response\n// Includes user profile with CRM role, team, permissions\n// Also includes staff info for booking dashboard access + token sync flag\n\nconst ccUserItems = $('Find CC_User by Email').all();\n\nif (!ccUserItems || ccUserItems.length === 0 || !ccUserItems[0].json.id) {\n  return [{ json: {\n    found: false,\n    error: 'No active CRM account found for this email. Contact your administrator.'\n  } }];\n}\n\nconst user = ccUserItems[0].json;\nconst jwtClaims = $('Verify JWT Signature').all()[0].json;\n\n// Build user profile for frontend\nconst userProfile = {\n  id: user.id,\n  name: user.Name || jwtClaims.name || '',\n  email: user.Email || jwtClaims.email || '',\n  role: user.Role || 'READ_ONLY',\n  is_admin: (user.Role === 'ADMIN'),\n  entra_oid: user.Entra_Object_ID || jwtClaims.oid || '',\n  is_active: user.Is_Active || false\n};\n\n// Get team info if linked\nif (user.Team && user.Team.length > 0) {\n  userProfile.team_id = user.Team[0];\n}\n\n// Get manager info if linked\nif (user.Manager && user.Manager.length > 0) {\n  userProfile.manager_id = user.Manager[0];\n}\n\n// Dashboard token (pre-generated in Airtable)\nconst token = user.Dashboard_Token || '';\n\nif (!token) {\n  return [{ json: {\n    found: false,\n    error: 'CRM account has no dashboard token. Contact your administrator.'\n  } }];\n}\n\n// Check Staff table for booking dashboard access\nlet staffRecordId = '';\nlet syncToken = false;\ntry {\n  const staffItems = $('Find Staff by Email').all();\n  if (staffItems && staffItems.length > 0 && staffItems[0].json.id) {\n    const staff = staffItems[0].json;\n    userProfile.staff_id = staff.id;\n    userProfile.staff_slug = staff.Slug || '';\n    staffRecordId = staff.id;\n    // If Staff token differs from CC_Users token, flag for sync\n    if (staff.Dashboard_Token !== token) {\n      syncToken = true;\n    }\n  }\n} catch (e) {\n  // Staff lookup may not return results - that is OK\n}\n\nreturn [{ json: {\n  found: true,\n  token,\n  user: userProfile,\n  record_id: user.id,\n  staff_record_id: staffRecordId,\n  sync_token: syncToken\n} }];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1980,
        100
      ],
      "id": "cc09-build-response",
      "name": "Build CRM Response"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "cc09-if-found",
              "leftValue": "={{ $json.found }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true"
              }
            }
          ],
          "combinator": "and"
        }
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        2420,
        100
      ],
      "id": "cc09-if-found",
      "name": "IF User Found"
    },
    {
      "parameters": {
        "jsCode": "// CC-09: Prepare Login Response for Respond to Webhook node\n// Uses $input (data passed through IF User Found from Build CRM Response)\n// Avoids $() cross-node refs which break after IF splits\n\nconst resp = $input.all()[0].json;\nreturn [{ json: {\n  success: true,\n  token: resp.token || '',\n  user: resp.user || {}\n} }];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3080,
        0
      ],
      "id": "cc09-prepare-response",
      "name": "Prepare Login Response"
    },
    {
      "parameters": {
        "respondWith": "firstIncomingItem",
        "options": {
          "responseCode": 200,
          "responseHeaders": {
            "entries": [
              {
                "name": "Content-Type",
                "value": "application/json"
              }
            ]
          }
        }
      },
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        3300,
        0
      ],
      "id": "cc09-respond-200",
      "name": "Respond 200 OK"
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ success: false, error: $json.error || 'Missing or invalid id_token' }) }}",
        "options": {
          "responseCode": 400,
          "responseHeaders": {
            "entries": [
              {
                "name": "Content-Type",
                "value": "application/json"
              }
            ]
          }
        }
      },
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        880,
        420
      ],
      "id": "cc09-respond-400",
      "name": "Respond 400 Bad Request"
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ success: false, error: $json.error || 'Token verification failed' }) }}",
        "options": {
          "responseCode": 401,
          "responseHeaders": {
            "entries": [
              {
                "name": "Content-Type",
                "value": "application/json"
              }
            ]
          }
        }
      },
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        1540,
        320
      ],
      "id": "cc09-respond-401",
      "name": "Respond 401 Unauthorized"
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ success: false, error: $json.error || 'No active CRM account found' }) }}",
        "options": {
          "responseCode": 403,
          "responseHeaders": {
            "entries": [
              {
                "name": "Content-Type",
                "value": "application/json"
              }
            ]
          }
        }
      },
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        2640,
        220
      ],
      "id": "cc09-respond-403",
      "name": "Respond 403 Forbidden"
    },
    {
      "parameters": {
        "operation": "search",
        "base": {
          "__rl": true,
          "value": "appPccm6NkaJdvqwy",
          "mode": "id"
        },
        "table": {
          "__rl": true,
          "value": "tblABg23aP4YQ36aN",
          "mode": "id"
        },
        "filterByFormula": "=AND(LOWER({Email}) = '{{ $('Verify JWT Signature').first().json.email }}', {Active} = TRUE())",
        "options": {
          "alwaysOutputData": true
        }
      },
      "type": "n8n-nodes-base.airtable",
      "typeVersion": 2.1,
      "position": [
        1760,
        100
      ],
      "id": "cc09-find-staff",
      "name": "Find Staff by Email"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "cc09-if-sync",
              "leftValue": "={{ $json.sync_token }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true"
              }
            }
          ],
          "combinator": "and"
        }
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        2640,
        0
      ],
      "id": "cc09-if-sync",
      "name": "IF Needs Staff Sync"
    },
    {
      "parameters": {
        "operation": "update",
        "base": {
          "__rl": true,
          "value": "appPccm6NkaJdvqwy",
          "mode": "id"
        },
        "table": {
          "__rl": true,
          "value": "tblABg23aP4YQ36aN",
          "mode": "id"
        },
        "id": "={{ $json.staff_record_id }}",
        "fields": {
          "values": [
            {
              "fieldId": "Dashboard_Token",
              "fieldValue": "={{ $json.token }}"
            }
          ]
        },
        "options": {
          "alwaysOutputData": true
        }
      },
      "type": "n8n-nodes-base.airtable",
      "typeVersion": 2.1,
      "position": [
        2860,
        -100
      ],
      "id": "cc09-sync-token",
      "name": "Sync Staff Token"
    }
  ],
  "connections": {
    "Webhook": {
      "main": [
        [
          {
            "node": "Extract JWT Parts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract JWT Parts": {
      "main": [
        [
          {
            "node": "IF Valid JWT",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF Valid JWT": {
      "main": [
        [
          {
            "node": "Fetch Microsoft JWKS",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Respond 400 Bad Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Microsoft JWKS": {
      "main": [
        [
          {
            "node": "Verify JWT Signature",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Verify JWT Signature": {
      "main": [
        [
          {
            "node": "IF Verified",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF Verified": {
      "main": [
        [
          {
            "node": "Find CC_User by Email",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Respond 401 Unauthorized",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Find CC_User by Email": {
      "main": [
        [
          {
            "node": "Find Staff by Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build CRM Response": {
      "main": [
        [
          {
            "node": "IF User Found",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF User Found": {
      "main": [
        [
          {
            "node": "IF Needs Staff Sync",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Respond 403 Forbidden",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Login Response": {
      "main": [
        [
          {
            "node": "Respond 200 OK",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Find Staff by Email": {
      "main": [
        [
          {
            "node": "Build CRM Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF Needs Staff Sync": {
      "main": [
        [
          {
            "node": "Sync Staff Token",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Prepare Login Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sync Staff Token": {
      "main": [
        [
          {
            "node": "Prepare Login Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "tags": [
    {
      "name": "Client Care"
    }
  ]
}
Pro

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

About this workflow

CC-09 Login SSO (Client Care). Uses httpRequest, airtable. Webhook trigger; 17 nodes.

Source: https://gist.github.com/DavidLifson/7aa83892d07b640dab23db0d4b80d17e — original creator credit. Request a take-down →

More Data & Sheets workflows → · Browse all categories →

Related workflows

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

Data & Sheets

This premium n8n workflow harnesses the power of DataForSEO's API combined with Airtable's relational database capabilities to transform your keyword research process, providing deeper insights for co

HTTP Request, Airtable
Data & Sheets

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Airtable, HTTP Request, Google Drive +1
Data & Sheets

This workflow automates the entire lifecycle of a service-based client, combining four distinct business flows into a single view: Intake Leads: Receives a webhook from your form builder, validates th

Airtable, Notion, Google Calendar +3
Data & Sheets

It intelligently syncs confirmed sales orders from your Airtable base to QuickBooks, automatically creating new customers if they don't exist before generating a perfectly matched invoice. It then log

Airtable, QuickBooks, HTTP Request
Data & Sheets

Who is this for? Business who manually prep/route DocuSign envelopes and want zero-touch contract signing from form submission.

Airtable, HTTP Request