Skip to content
Widget SDK

Sign-in Source Reference

The signed backend check request, Your keys claims, and the key-pair command.

On this page

Sign-in Source Reference#

The exact contracts behind the three sign-in sources: the signed check request Gydr sends to your backend, and the key and claims a Your keys source expects.

Your backend: the check request#

Gydr sends one POST with an empty body to your check URL. The URL must be https (no IP addresses, no redirects) and answer within 3 seconds.

Request headers
POST https://api.example.com/gydr/check
Gydr-Source-Id: ids_AbC123xyz
Gydr-Visitor-Token: <the visitor's token>      (absent on the activation probe)
Gydr-Challenge: <random nonce>                 (activation probe only)
Gydr-Signature: t=1790000000,n=<nonce>,v1=<hex>[,v1=<hex>]

The signature proves the call is Gydr’s. Compute it with your secret exactly like this, and compare in constant time:

The signature
v1 = hex( HMAC-SHA256( secret as UTF-8,
        "gydr-check-v1\n<t>\n<sourceId>\n<n>\n<subject>" ) )

subject = lowercase hex SHA-256 of the visitor token
          (for the activation probe: the Gydr-Challenge value)
  • Refuse a request when |now − t| is more than 300 seconds.
  • Accept any of the listed v1 values: during a secret rotation Gydr sends both versions.
  • Answer with Cache-Control: no-store. A 200 that carries an Age header or a public cache directive is refused.

What to answer#

Your answer
200 with { "userId": "42", "name"?, "email"?, "expiresIn"? }
Meaning for Gydr
Signed in. JSON of at most 4 KB with no other members; userId is a string of 1 to 255 printable characters.
Your answer
401
Meaning for Gydr
Not signed in. Cached for 60 seconds; the visitor is a guest.
Your answer
403
Meaning for Gydr
“I refuse your signature”. Reported as bad_signature with an operator alert — and never a sign-out.
Your answer
Timeout, 5xx
Meaning for Gydr
check_unavailable. Gydr keeps the visitor signed in on the same token for a short grace period.
Your answer
Anything else
Meaning for Gydr
misconfigured.

Verify and answer#

const crypto = require('node:crypto')
const SECRET = process.env.GYDR_CHECK_SECRET

const hmacHex = (key, text) =>
  crypto.createHmac('sha256', key).update(text).digest('hex')
const sha256Hex = (text) =>
  crypto.createHash('sha256').update(text).digest('hex')

function parseSignature(header) {
  const sig = { t: 0, n: '', v1: [] }
  for (const part of String(header || '').split(',')) {
    const [k, v] = part.split('=')
    if (k === 't') sig.t = Number(v)
    else if (k === 'n') sig.n = v
    else if (k === 'v1') sig.v1.push(v)
  }
  return sig.t && sig.n && sig.v1.length ? sig : null
}

function verifyGydrSignature(req, res, next) {
  const sig = parseSignature(req.get('Gydr-Signature'))
  if (!sig || Math.abs(Date.now() / 1000 - sig.t) > 300) return res.sendStatus(403)
  const subject =
    req.get('Gydr-Challenge') ?? sha256Hex(req.get('Gydr-Visitor-Token') ?? '')
  const base = ['gydr-check-v1', sig.t, req.get('Gydr-Source-Id'), sig.n, subject].join('\n')
  const expected = Buffer.from(hmacHex(SECRET, base), 'hex')
  const ok = sig.v1.some((v) => {
    const got = Buffer.from(v, 'hex')
    return got.length === expected.length && crypto.timingSafeEqual(got, expected)
  })
  return ok ? next() : res.sendStatus(403)
}

app.post('/gydr/check', verifyGydrSignature, async (req, res) => {
  res.set('Cache-Control', 'no-store')
  const challenge = req.get('Gydr-Challenge') // activation probe
  if (challenge) return res.json({ challenge: hmacHex(SECRET, challenge) })
  const user = await userForApiToken(req.get('Gydr-Visitor-Token'))
  if (!user) return res.sendStatus(401)
  res.json({ userId: String(user.id), name: user.name, email: user.email })
})

Activate, rotate, retire#

  • Activate. The Console sends a probe carrying Gydr-Challenge; your endpoint answers { "challenge": hex(HMAC(secret, challenge)) }. The snippets above already do. A source works only after a probe passes, and any edit of the check URL deactivates it until a new probe passes.
  • Rotate. Rotating issues a new secret (shown once) and Gydr signs every request with both until you press Retire the old secret, so rotation signs nobody out.
  • Egress. Gydr calls from fixed addresses, listed in Console → Settings → Visitor sign-in, if you allow-list.
Keep the secret in server configuration. Anyone holding it can make your endpoint answer as Gydr, and the Console shows it only once.

Your keys: claims and the key-pair command#

A Your keys source accepts RS256, ES256 or EdDSA JWTs. The token must carry:

Claim
iss
Required
Yes
Notes
Must equal the issuer you registered.
Claim
aud / azp / client_id
Required
Required for a JWKS URL; optional for a pasted key
Notes
One of them must equal the audience you registered.
Claim
sub
Required
Yes
Notes
The User ID, unless you name another claim. 1 to 255 printable characters; it must never change for the same person.
Claim
exp
Required
Yes
Notes
At most 24 hours after the token was issued; about 1 hour is typical.
Claim
name, email, phone_number
Required
No
Notes
Used to fill the visitor's details where empty. Phone is read from phone_number.

Limits Gydr enforces

The whole token is at most 8 KB and each claim at most 256 characters. Tokens whose header carries crit, jku, jwk, x5u or x5c are refused, and up to 60 seconds of clock difference is allowed. The issuer, JWKS URL and any URL you register must be https.

Sign your own token#

For a site with cookie sessions and no identity provider. Create a key pair once, paste the public key in the Console, and keep the private key on your server:

Create an ES256 key pair
openssl ecparam -name prime256v1 -genkey -noout -out gydr-ec.pem
openssl pkcs8 -topk8 -nocrypt -in gydr-ec.pem -out gydr-private.pem   # keep on your server
openssl ec -in gydr-ec.pem -pubout -out gydr-public.pem               # paste in the Console
Serve the token
import { SignJWT, importPKCS8 } from 'jose'
const key = await importPKCS8(process.env.GYDR_VISITOR_KEY, 'ES256')

app.get('/gydr/visitor-token', async (req, res) => {
  res.set('Cache-Control', 'no-store')
  const u = req.session.user
  if (!u) return res.json({ visitorToken: null })
  const visitorToken = await new SignJWT({ name: u.name, email: u.email })
    .setProtectedHeader({ alg: 'ES256' })
    .setSubject(String(u.id))
    .setIssuer('https://www.example.com')
    .setIssuedAt()
    .setExpirationTime('1h')
    .sign(key)
  res.json({ visitorToken })
})

Register the issuer (https://www.example.com above) and the public key; the audience is optional for a pasted key. This is the “recognise people only” setup: no plugin receives the token.

Ready to get started?

Create a free account and deploy your first chatbot in minutes.