Sign in with Comick
Add profile-only OpenID Connect login to backend, browser, and native apps.
Use OpenID Connect (OIDC) when your app needs Sign in with Comick. Comick issues a signed ID token identifying the user. Any standards-compatible OIDC client can integrate; your app does not need Better Auth.
Library access uses a separate OAuth flow. OIDC credentials cannot call the Library API, and library credentials cannot be exchanged at the OIDC issuer. A native app can use OIDC for login and separately request library access when needed.
Registration and discovery
Register an app with a verified Comick account. Enable Enable Sign in with Comick in the app settings. Existing client IDs, confidential-client secrets, and exact registered callbacks work with both flows. Enabling login does not add permissions to existing user grants.
| Purpose | Production URL |
|---|---|
| Issuer | https://comick.dev/api/auth/oidc |
| Discovery | https://comick.dev/api/auth/oidc/.well-known/openid-configuration |
| JWKS | https://comick.dev/api/auth/oidc/jwks |
| Authorization | https://comick.dev/api/auth/oidc/oauth2/authorize |
| Token exchange and refresh | https://comick.dev/api/auth/oidc/oauth2/token |
| UserInfo | https://comick.dev/api/auth/oidc/oauth2/userinfo |
| Revocation | https://comick.dev/api/auth/oidc/oauth2/revoke |
Use the issuer and endpoints from discovery. They run on the website, including in local development; do not add /api2 or use the catalog API host. For local testing, replace the production website origin with your Comick installation's website origin. Deployments may keep OIDC disabled while these guides remain public.
Profile-only permissions
| Scope | Permission |
|---|---|
openid | Required. Identify the account by its stable public UUID in sub. |
profile | Optional. Share available public name, preferred_username, and picture. Missing values are omitted. |
offline_access | Optional. Issue a rotating refresh token for ongoing approved access. |
Users can decline profile sharing and ongoing access on consent. Check the returned scope; do not assume optional scopes were granted. Email is unavailable. Requests for email, library scopes, unknown scopes, the claims parameter, or a resource parameter are rejected. Only authorization code with S256 PKCE is supported; implicit, password, machine grants, and dynamic registration are unavailable.
Start authorization
Create a fresh random state, nonce, and PKCE verifier for each attempt. Bind them to an expiring, one-time application session or native attempt. Keep the verifier and nonce out of logs. For a backend, store the attempt server-side and use a secure, HttpOnly, SameSite cookie to identify it. For a browser/native client, retain it only for the pending attempt.
The verifier uses 43–128 unreserved URI characters. A cryptographically random 32-byte base64url value works; the S256 challenge is base64url SHA-256 of the verifier, without padding.
import { createHash, randomBytes } from "node:crypto"
const verifier = randomBytes(32).toString("base64url")
const state = randomBytes(32).toString("base64url")
const nonce = randomBytes(32).toString("base64url")
const query = new URLSearchParams({
client_id: "YOUR_CLIENT_ID",
redirect_uri: "https://your-app.example/oidc/comick/callback",
response_type: "code",
scope: "openid profile offline_access",
state,
nonce,
code_challenge: createHash("sha256").update(verifier).digest("base64url"),
code_challenge_method: "S256",
})
const authorizationURL = `https://comick.dev/api/auth/oidc/oauth2/authorize?${query}`
// Save this attempt before redirecting the browser to authorizationURL.The registered callback must match exactly. Open authorization in the user's browser. Native apps use the operating system's authentication browser, rather than an embedded WebView. Native clients are public and must not embed a secret. Register an HTTPS app callback, loopback callback, or a reverse-domain scheme such as dev.example.reader:/oidc/comick.
Comick handles login and consent. Verify returned state against the saved attempt before accepting either code or an error such as access_denied. Validate the returned authorization-response iss against the issuer when present. Reject missing or mismatched state, unsolicited callbacks, expired attempts, and duplicate parameters.
Exchange the code
Send a form POST to the OIDC token endpoint. Include the original callback and verifier. Codes are short-lived and single-use; restart authorization if exchange fails or a response is lost.
curl 'https://comick.dev/api/auth/oidc/oauth2/token' \
--user 'YOUR_CLIENT_ID:YOUR_CLIENT_SECRET' \
--data-urlencode 'grant_type=authorization_code' \
--data-urlencode 'code=CODE_FROM_VALIDATED_CALLBACK' \
--data-urlencode 'redirect_uri=https://your-app.example/oidc/comick/callback' \
--data-urlencode 'code_verifier=ORIGINAL_VERIFIER'Confidential clients authenticate using HTTP Basic (client_secret_basic); keep their secret on the backend. Public browser/native clients omit Basic authentication and send client_id in the form. PKCE is required for both types. Do not send resource, claims, or a body client_secret.
{
"access_token": "OPAQUE_OIDC_ACCESS_TOKEN",
"token_type": "Bearer",
"expires_in": 900,
"scope": "openid profile offline_access",
"id_token": "SIGNED_RS256_ID_TOKEN",
"refresh_token": "OPAQUE_OIDC_REFRESH_TOKEN"
}The ID token lasts five minutes. Access tokens last 15 minutes, and refresh tokens last 30 days, subject to earlier revocation, session expiry, and account/app eligibility. Access and refresh tokens are opaque: do not decode them as JWTs. The ID token authenticates the user to your app; it is not a bearer token for Comick APIs.
Validate the ID token
Use a maintained OIDC library to validate the authorization response and ID token. Configure the exact issuer, your client ID as the expected audience, RS256 only, and the saved nonce and PKCE attempt. Validate the signature through discovered JWKS, iss, aud, exp, iat, and nonce; check azp when multiple audiences are present. Validate at_hash when your library supports it. Decoding a JWT is not validation.
For example, the signature and core claims can be checked with jose; an OIDC client should also perform the full response checks:
import { createRemoteJWKSet, jwtVerify } from "jose"
import { timingSafeEqual } from "node:crypto"
const issuer = "https://comick.dev/api/auth/oidc"
const keys = createRemoteJWKSet(new URL(`${issuer}/jwks`))
const { payload } = await jwtVerify(tokens.id_token, keys, {
issuer,
audience: "YOUR_CLIENT_ID",
algorithms: ["RS256"],
requiredClaims: ["sub", "iat", "exp", "nonce"],
maxTokenAge: "5m",
})
const actual = Buffer.from(String(payload.nonce || ""))
const expected = Buffer.from(savedAttempt.nonce)
if (actual.length !== expected.length || !timingSafeEqual(actual, expected)) {
throw new Error("Invalid login nonce")
}
// Consume the saved attempt and establish your own application session.
// Use (issuer, payload.sub) as the account key, never username or avatar.sub is the stable public Comick account UUID. Public names can change. ID tokens contain protocol claims and only the consented public profile fields. Email, roles, identity traits, and internal session IDs are unavailable. Signing keys rotate every 30 days with a one-day verification grace period; cache JWKS and refresh once when an unfamiliar kid appears.
UserInfo
Send the OIDC access token in the Authorization header. Both GET and an empty POST are supported; query/body tokens are rejected.
curl 'https://comick.dev/api/auth/oidc/oauth2/userinfo' \
--header 'Authorization: Bearer OPAQUE_OIDC_ACCESS_TOKEN'Without profile, the response is only { "sub": "PUBLIC_ACCOUNT_UUID" }. With profile consent, available name, preferred_username, and picture are also returned. Confirm UserInfo sub equals the validated ID-token sub before using its profile. Email and library data are never returned.
Registered public HTTP(S) callback origins can make cross-origin token, UserInfo, and revocation requests. Use credentials: "omit", with no Comick session cookie. Native reverse-domain callbacks do not add a browser CORS origin. Discovery and JWKS are publicly readable.
Refresh, revocation, and logout
Refresh is available only with offline_access consent. POST grant_type=refresh_token and the current refresh_token to the same token endpoint, using the same client authentication. Public clients also send client_id. Omit resource. Replace the saved refresh token atomically after every successful refresh; serialize refresh requests and never reuse the previous token. Replay can invalidate the token family. Never increase scopes on refresh.
Validate any new ID token against the same issuer/audience/signature/lifetime rules. If a refresh ID token includes a nonce, it must match the original authorization nonce. Keep refresh credentials in backend storage or OS secure storage. Secret rotation in the dashboard immediately invalidates the old secret for new exchanges and refreshes.
To revoke a credential, form POST token=OPAQUE_OIDC_TOKEN to the revocation endpoint, authenticating the app as at the token endpoint. Public clients send client_id. Users can revoke the full login and library connection in Connected apps. Disconnect blocks future issuance, refresh, and UserInfo, including pending codes. Expired authorizing sessions and ineligible users, developers, or apps also block access.
Issued ID tokens remain valid until expiry. Third-party applications control their own login sessions. Disconnecting or signing out of Comick does not automatically sign the user out of another app. RP-initiated logout and back-channel logout are outside this release. End your own app session when your user signs out, and require new authorization after a disconnected grant.