Single sign-on (JWT handoff)
Connect your product accounts to FeedLog using server-signed JWTs and a single sign-on 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 handoff | Widget | |
|---|---|---|
| Endpoint | GET /api/sso/jwt |
POST /api/widget/auth/exchange |
| Request | A redirect with the token in the query string | JSON { "jwt": "…" } |
| Response | A first-party session cookie, followed by a 302 redirect to return_to |
JSON with a bearer token, its expiry, and the user's profile |
| Caller | The user's browser | The 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.
| Claim | Required | Notes |
|---|---|---|
email |
Yes | The identity key. Must contain @. Trimmed and lowercased before matching. |
exp |
Yes | Expiry, in Unix seconds, as a number. 24 hours is the hard ceiling; an hour is a good default. |
name |
No | Display name. Falls back to the email address when absent or blank. |
picture |
No | Avatar 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:
- Create a second secret and label it.
- Deploy your backend with the new secret.
- Disable the old one after all token issuers have switched to the new secret.
- 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.
Developer integration中的更多文章
Add FeedLog to your product with a coding agentUse a ready-to-copy prompt to have a Coding Agent add your FeedLog portal to your product navigation.Install the feedback widgetInstall the widget SDK, connect guest or signed-in users, and handle authentication and integration errors.Integrate the widget with a coding agentUse a ready-to-copy prompt to have a Coding Agent integrate the widget with your existing authentication system.Integrate SSO with a coding agentUse a ready-to-copy prompt to have a Coding Agent implement and verify FeedLog single sign-on.