The quick way to add single sign-on to a desktop app is an embedded browser window. It is also the wrong way. That window has none of the user's sessions, none of their password manager, and it asks them to type a company password into a frame your application controls. RFC 8252 is the standing answer: open the system browser, and get the result back through a URL scheme you own.
Providers increasingly refuse embedded views outright for these reasons, so this is not only a best practice question.
Register a scheme, send it as the redirect target, and handle it when the OS hands it back:
app.setAsDefaultProtocolClient('myapp') // myapp://auth/callback?code=...
app.on('open-url', (e, url) => handle(url)) // macOS
app.on('second-instance', (e, argv) => handle(argv.pop())) // Windows and Linux
MimeType=x-scheme-handler/myapp; in the .desktop file, the desktop environment has
no idea your app owns that scheme, so the browser opens the URL and nothing happens. Desktop sign-in could
not have completed under any Linux packaging, and none of it is visible in development, where the
scheme is registered by whatever you last ran.
Without a lock, the protocol URL launches a second copy of your app. That copy receives the login, and the window the user was actually looking at sits there still logged out. It reads as "sign-in did nothing", and it is one line to prevent:
if (!app.requestSingleInstanceLock()) app.quit()
This is the part worth being strict about. Redirecting with the session JWT in the query string writes a long-lived credential into browser history, into the referrer header, and into every proxy and access log along the way. It is trivial and it is permanent.
Hand back a short-lived, single-use code instead, store only its hash, and have the client POST it back to exchange for the real token:
myapp://auth/callback?code=<random> # useless on its own, expires in 2 minutes
POST /auth/exchange { code } # returns the session token in a body, not a URL
The client secret is a deployment credential, the same kind of thing as your database URL. Putting it in a settings table means any read of instance settings, by any code path that ever gets it slightly wrong, is enough to walk off with it.
Keep the feature off unless issuer, client ID and client secret are all present, and expose that state so the client knows whether to draw the button:
GET /auth/oidc/config → {"enabled": false}
Then verify on the real deployment that it reports disabled before you configure it. An auth feature that is accidentally half-on is worse than one that is off.
Two rules that prevent account takeover, and both are easy to get wrong:
(issuer, sub). An email address at a
provider can be reassigned to a new person when someone leaves. The subject identifier cannot.Also decide deliberately whether the first account can be created this way. We refused, because otherwise whoever can authenticate at the identity provider becomes the owner of an unclaimed instance.
Fetch the provider's JWKS and verify the signature, issuer, audience and expiry. This does not need a new dependency, and avoiding one is worth something when a dependency change has already taken your app down once.
The tests that matter are the ones asserting rejection:
- a tampered payload must fail
- alg: none must fail
- HS256 signed with the public key as HMAC must fail (algorithm confusion)
- wrong issuer / wrong audience / expired must fail
A test suite that only proves a valid token is accepted proves nothing about a token forger.