Skip to content

feat(auth): SSO token exchange endpoint for invisible OIDC login in embedded contextsย #1133

Description

@sparkling

Use case

A surrounding application that already authenticates users with an
OIDC IdP (Auth0, Keycloak, Okta, etc.) wants to embed Artalk and have
the same sign-in cover both โ€” i.e., users who are already logged in
to the parent app post comments without a second click, without seeing
Artalk's own login UI, and without the comment widget showing a
"Login with X" button.

Concrete example: a static documentation site running an Auth0 SPA
session in the same browser tab. The Auth0 SPA SDK can hand the parent
app an access token. The parent app would like to swap that for an
Artalk session JWT and pre-populate localStorage["ArtalkUser"].token
before Artalk.init() runs.

What's there today

/api/v2/auth/{provider} opens the OIDC provider in a popup, returns
via internal/auth/callback.go::ResponseCallbackPage which calls
window.opener.postMessage({ type: 'ATK_AUTH_CALLBACK', payload: <jwt> }).
The widget's plugin-auth (ui/plugin-auth/lib/oauth-login.ts) listens
for that message and loginByToken(ctx, token)s the user in.

That popup flow is the only way to land a token in
localStorage["ArtalkUser"] short of typing a password via
/api/v2/user/access_token. The flow itself is sound โ€” but in
SSO-with-parent-app contexts the popup is jarring (the user is
already authed; opening a popup just to round-trip through a
silent-auth Auth0 redirect feels like a hiccup) and a few real
browsers block the popup without an explicit user gesture each time
(notably Safari's anti-tracking heuristics on third-party origins).

Proposed

Add POST /api/v2/sso/exchange (or similar name โ€” bike-shed welcome)
that:

  • accepts { token: <opaque-or-JWT-access-token> },
  • validates by calling {issuer}/userinfo with Authorization: Bearer <token> (OIDC standard; the IdP signs the response, so no JWKS dep on Artalk's side),
  • extracts the email claim,
  • FindCreateUsers for that name+email,
  • mints an Artalk JWT via common.LoginGetUserToken exactly the same
    way /api/v2/user/access_token and the social-login callback do,
  • returns ResponseUserLogin โ€” same shape as the existing login
    endpoints, so the frontend already knows how to consume it.

Config (defaults to off, opt-in only):

auth:
  enabled: true
  sso:
    enabled: false
    issuer: ""   # e.g. "tenant.auth0.com" or "https://tenant.auth0.com"

Gated by auth.enabled && auth.sso.enabled; 404s on misconfiguration
so it's invisible unless deliberately turned on.

Why this is small

It's purely additive โ€” one new handler file, four config struct lines,
one line in server.go, one block in conf/artalk.example.yml, one
regen of internal/config/cache.go. No new Go dependencies (uses
the existing net/http to call /userinfo rather than adding a JWKS
library). Backward compatible because the endpoint is gated off by
default.

I've implemented it on a fork

Branch: https://github.com/sparkling/Artalk/tree/upstream-pr/sso-token-exchange

Happy to open a PR against this issue if you'd take the contribution.
The fork's been running in production against an Auth0 tenant since
2026-05-19 and the static-site Comments widget posts cleanly with
zero extra UI โ€” sign-in via the parent app's Auth0 session is the
only auth surface the user sees.

Alternatives considered

  • Frontend popup, but silent: the Auth0 silent-auth flow handles
    this when the parent already has a session, but the popup itself
    still flickers and Safari blocks it. Doesn't solve the embed-UX
    problem.
  • A sidecar service that mints JWTs: works, but pushes
    app_key outside Artalk (a copy in a second service), which
    violates "secret stays where it's used." Putting the endpoint in
    Artalk keeps the key in one place.
  • Patching Artalk's frontend to load with a pre-supplied token:
    the widget already does that (reads
    localStorage["ArtalkUser"].token) โ€” the bottleneck is just
    generating the token. Hence the endpoint.

Thoughts on the shape / naming / whether you'd take a PR?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions