Skip to content

Single sign-on (JWT handoff)

Users who are already signed in to your product shouldn't have to sign in again to leave feedback. With FeedLog SSO, your backend signs a short-lived JWT with a secret that stays on your server. FeedLog uses the identity in that token to sign the user in.

This integration doesn't require directory sync, SAML metadata, or a registered callback URL.

Want a coding agent to handle the integration?

Use the SSO integration prompt. Copy it into a coding agent that has access to your project, and it will implement the product link, server-side handoff and signed-in and signed-out flows.

One secret, two integrations

The same signing secret and token format are used for both integrations.

Browser handoffWidget
EndpointGET /api/sso/jwtPOST /api/widget/auth/exchange
RequestA redirect with the token in the query stringJSON { "jwt": "…" }
ResponseA first-party session cookie, followed by a 302 redirect to return_toJSON with a bearer token, its expiry, and the user's profile
CallerThe user's browserThe widget SDK

After you configure SSO for either integration, you can use the same secret and token format for the other one.

Create a signing secret

Open Developer → Single Sign-On in the dashboard and click Create secret. Only workspace owners can access the secret list. Other members see an "Owners only" notice.

A secret consists of 64 hexadecimal characters. Add a label such as Production or Staging to identify it during rotation. Labels don't affect verification and can be edited. A workspace can hold up to five secrets. You can reveal and copy any of them again later; they are not shown only once.

The secret never leaves your server

Use the secret only to sign tokens in your backend. Anyone who has it can create a valid token for any email address and sign in as that user. Store it in a secret manager or server environment variable. Never include it in a frontend bundle or commit it to a repository.

Let signed-out visitors continue

SSO adds identity when your product already knows the user. It shouldn't make sign-in a requirement for visiting FeedLog.

Point the Feedback link in your product to a server route in your application. That route checks the current session when the link is followed:

  • If the user is signed in, sign a JWT and redirect through FeedLog's SSO URL.
  • If the user is signed out, redirect directly to the same FeedLog page without a JWT.

Don't open your product's sign-in screen for the second case. The visitor can use FeedLog anonymously and follow FeedLog's own sign-in flow if an action requires it. Also keep a failed session lookup separate from a confirmed sign-out; use your application's normal error handling instead of silently dropping the user's identity.

JWT requirements

FeedLog accepts only the HS256 algorithm. Use the UTF-8 bytes of the secret string exactly as it appears in the dashboard. Don't hex-decode or base64-decode the secret before signing. Decoding it changes the signing key and causes verification to fail.

ClaimRequiredNotes
emailYesThe identity key. Must contain @. Trimmed and lowercased before matching.
expYesExpiry, in Unix seconds, as a number. 24 hours is the hard ceiling; an hour is a good default.
nameNoDisplay name. Falls back to the email address when absent or blank.
pictureNoAvatar URL.

FeedLog doesn't read any other claims and doesn't support custom fields or kid. During verification, it tries each enabled secret until the signature matches. The token therefore doesn't need to identify which secret signed it.

The clock tolerance for exp is ±60 seconds. FeedLog rejects a token whose exp is more than 24 hours in the future; it doesn't reduce the value to the maximum.

js
import jwt from 'jsonwebtoken'

// Server-side only.
export async function feedbackRedirect(request, returnTo = '/') {
  const baseUrl = new URL(process.env.FEEDLOG_BASE_URL)
  const requestedUrl = new URL(returnTo, baseUrl)
  const safeReturnTo = requestedUrl.origin === baseUrl.origin
    ? `${requestedUrl.pathname}${requestedUrl.search}${requestedUrl.hash}`
    : '/'

  // This function must distinguish a signed-out user from a session error.
  const user = await currentUser(request)
  if (!user) {
    return Response.redirect(new URL(safeReturnTo, baseUrl), 302)
  }

  const token = jwt.sign(
    { email: user.email, name: user.name, picture: user.avatarUrl },
    process.env.FEEDLOG_SSO_SECRET,
    { algorithm: 'HS256', expiresIn: '1h' },
  )

  const handoffUrl = new URL('/api/sso/jwt', baseUrl)
  handoffUrl.searchParams.set('jwt', token)
  handoffUrl.searchParams.set('return_to', safeReturnTo)
  return Response.redirect(handoffUrl, 302)
}

Use a regular <a href> that points to this route in your application. Sign the token when the user follows the link, not when a long-lived or statically built page is rendered. This keeps the token short-lived and lets the same link choose the signed-in or anonymous branch at request time.

Browser handoff

GET /api/sso/jwt accepts two query parameters: jwt (required) and return_to (optional, defaults to /). On success, FeedLog sets a first-party session cookie on that host and responds with an HTTP 302 redirect to return_to.

return_to must stay on the same host. It can be a relative path such as /b/feature-requests or an absolute URL on the FeedLog host. FeedLog replaces any other value, including a protocol-relative URL such as //example.com, with /. It doesn't return an error for an invalid return_to. If users are sent to the home board instead of the requested page, check this parameter first.

FeedLog verifies the token against the secrets for the workspace resolved from the request host. Use the host for the workspace that the user should access.

Widget exchange

The widget SDK handles the exchange. Implement auth.getToken() and return the same signed JWT. The SDK posts it to /api/widget/auth/exchange, caches the returned bearer token by email address, and exchanges the JWT again after the bearer token expires. Your integration doesn't call this endpoint directly. See Install the feedback widget.

Because each exchange creates a session, the endpoint is limited to 30 requests per minute per IP. The SDK normally stays below this limit by caching bearer tokens. If the integration reaches the limit, check whether it is requesting and exchanging a newly signed JWT on every call.

Identity matching and permissions

FeedLog matches users globally by email address. The first token containing a new email address creates an end-user account without showing another login screen.

FeedLog reuses the existing account on later sign-ins. It reads name and picture only when it creates the account and doesn't refresh them on later sign-ins. Changes to these fields in your product therefore don't update the FeedLog profile.

If an email address changes in your product, FeedLog treats the new address as a new account. Posts, votes, and comments remain associated with the old address.

An SSO session is limited to end-user access. It can create feedback, vote, and comment. It can't open the dashboard, set or change a password, change the email address, edit the profile, or manage the workspace; those actions return 403. The session is also bound to the host that issued it.

Rotating a secret

Verification accepts any enabled secret, so you can rotate secrets without rejecting tokens signed by the old secret during deployment:

  1. Create a second secret and label it.
  2. Deploy your backend with the new secret.
  3. Disable the old one after all token issuers have switched to the new secret.
  4. Delete it a few days later.

Disabling a secret is reversible. If you re-enable it, unexpired tokens signed with it pass verification again. Deleting a secret is irreversible, and tokens signed with it stop passing verification immediately.

Signing tokens in local development

The production integration requires a backend that holds the secret. During local development, you can sign a token manually and add it to the URL:

bash
SECRET=paste-a-secret-here node -e "const jwt=require('jsonwebtoken');\
console.log(jwt.sign({email:'[email protected]',name:'Dev User'},\
process.env.SECRET,{algorithm:'HS256',expiresIn:'1h'}))"

Use the result in http://localhost:3000/api/sso/jwt?jwt=<token>. While developing the widget integration, you can instead return the same string from a hard-coded auth.getToken().

Development only

Sign tokens in a terminal or on a development server. Never sign them in browser code, because that would include the secret in the frontend bundle. A development secret can still create a valid token for any email address in its workspace. Use an isolated test workspace, or create a separate secret and disable it when you finish testing.

Common errors

Signed-out visitors are sent to your product's sign-in page. The application route behind the Feedback link is requiring authentication. Let the route read an optional session: when it confirms that no user is signed in, redirect directly to the requested FeedLog page without creating a JWT.

The user lands on the board signed out, with no error shown. Browser-handoff failures aren't displayed as raw errors. /api/sso/jwt redirects to a "We couldn't sign you in" page, which continues to the board after three seconds. Check the FeedLog server log for a line starting with [sso] login failed: to find the cause. The widget exchange endpoint returns its error reason as JSON.

Invalid or expired SSO token (400). The signature didn't match any enabled secret, or exp has passed. Both conditions return the same message. Check for a secret from the wrong environment, a disabled or deleted secret, a secret that was hex-decoded before signing, or a server clock offset greater than 60 seconds.

SSO token must carry an exp claim (400). exp is missing or isn't a number. Some libraries add this claim only when you pass an expiry option.

SSO token exp is too far in the future (400). exp exceeds the 24-hour maximum plus the 60-second tolerance. FeedLog rejects the token instead of reducing its expiry. For a link that must remain valid longer, such as a link in an email, point it to a redirect in your application and sign the token when the user follows that link.

SSO token must carry a valid email claim (400). The email claim is missing, isn't a string, or has no @ in it.

SSO is not configured for this organization (404). The workspace has no enabled secret. During rotation, verify that the new secret has been deployed before disabling the old one.

Organization not found (404). The request host doesn't resolve to a workspace. Check the domain used in the handoff or exchange request.

Widget is not enabled for this organization (403). The widget is disabled in workspace settings. This error isn't caused by the SSO configuration.

Too many token exchanges, try again shortly (429). More than 30 exchanges from one IP in a minute.

Widget token exchange failed with status 400. The SDK reports only the status code. Open the failed request in the browser network panel to read the message in the response body.

A user's name or avatar is out of date. FeedLog reads profile fields only when it creates the account. Later SSO sign-ins don't update them.

Open-source feedback management. Self-host it or let us run it.