openid
@library("openid", "0.0.0");
OpenID Connect (OIDC) single sign-on for GreyCat. Multi-provider, secure-by-default.
Supports two integration shapes:
- Redirect flow (server-driven). The browser hits the shipped
index.html(the provider picker), is sent to the provider, and returns to that same page — which detects the?code=&state=query and finishes the login. GreyCat runs Authorization Code + PKCE itself; verifies state, nonce, signature,iss,aud,exp; resolves the ID token to aruntime::Identity; impersonates that Identity on the response (cookie set automatically). - Token exchange. A client that already holds an ID token calls
Openid::token_login(provider, jwt)to trade it for a GreyCat session.
Quick start
In your project:
@library("openid");
fn main() {
Openid::register("google", OidcProvider {
issuer: "https://accounts.google.com",
client_id: System::getEnv("GOOGLE_CLIENT_ID"),
client_secret: System::getEnv("GOOGLE_CLIENT_SECRET"),
scopes: ["openid", "email", "profile"],
redirect_uri: "https://app.example.com/",
// auto_create_identity: true, // create missing Identities on first login
});
// optional — without this, OpenidHelpers::default_match runs
// (claims.preferred_username → claims.email → reject; or auto-create
// when the provider has auto_create_identity = true)
// Openid::set_hook(my_resolver);
}
Point redirect_uri at your site root. GreyCat serves an embedded
index.html there which handles password login, the OpenID provider
picker, and the post-redirect callback all in one page. Drop your own
webroot/index.html next to project.gcl to override it; the embedded
fallback is used when none is found.
Custom resolver
If default_match doesn’t fit (auto-provisioning, role assignment, domain
allow-list, etc.), register a hook:
fn my_resolver(provider_id: String, r: OidcResult): Identity? {
var email = r.claims.email;
if (email == null || !email.endsWith("@example.com")) {
return null; // reject
}
return OpenidHelpers::ensure(email, "user");
}
fn main() {
Openid::register(...);
Openid::set_hook(my_resolver);
}
The hook signature is fn(provider_id: String, r: OidcResult): Identity?:
return the Identity to log in as, or null to reject the login.
Helpers
type OpenidHelpers exposes opt-in utilities:
default_match(provider_id, r): Identity?— the default resolver.ensure(name, role): Identity?— find-or-create.stable_name(provider_id, claims): String—"google:1234567890"-style collision-proof name fromclaims.sub.role_from_groups(groups, mapping, default_role): String— map provider groups to a GreyCat role.
Logout
await runtime.Identity.logout(); // clears the cookie
const url = await openid.Openid.end_session_url(...); // RP-initiated logout URL
if (url) location.replace(url);
end_session_url(provider_id, id_token_hint, post_logout_redirect_uri?)
returns null when the provider has no end_session_endpoint.
Endpoints
All @expose @permission("public"):
Openid::providers(): Array<String>— list registered provider ids.Openid::public_config(id): OidcPublicConfig?— secret-free provider config (issuer,client_id,redirect_uri,scopes) for browser clients driving the token-exchange flow.nullif not registered. Never includes theclient_secret.Openid::login(provider_id, return_to?): String— start the redirect flow, returns the authorization URL.Openid::callback(code, state): String— finish the redirect flow, returns thereturn_tofrom the originatinglogin().Openid::token_login(provider_id, jwt)— verify an ID token and impersonate.Openid::end_session_url(provider_id, id_token_hint, post_logout_redirect_uri?): String?
Browser SDK (token-exchange flow)
openid/webroot/openid.ts is a zero-dependency, fully-typed ES-module helper
for the token-exchange flow: it runs Authorization Code + PKCE in the browser
against the provider, validates state + nonce locally, then calls
Openid::token_login to obtain the GreyCat session cookie. The OAuth client
must be a public client (no secret), and the provider’s authorization /
token / JWKS endpoints must allow CORS for your frontend origin. Bundle it into
webroot/ like any TypeScript module (it needs the DOM lib types).
import "@greycat/web";
// Hydrate issuer/client_id/redirect_uri/scopes from the server (single source
// of truth) via Openid::public_config:
const oidc = await gc.sdk.OpenidClient.fromProvider("keycloak");
const result = await oidc.handleRedirect(); // finishes a redirect if present
if (!result) {
signInButton.onclick = () => oidc.login(); // otherwise start one
}
// later:
await oidc.logout(); // clears cookie + provider logout
OpenidClient.listProviders() returns the registered ids for a login picker;
new OpenidClient({ provider, issuer, clientId, ... }) configures everything
client-side without the public_config lookup.
Notes
- Pending-login state (state / nonce / pkce_verifier / return_to) lives in a native in-memory TTL map (~5 minutes). Single-process deployment assumed.
- Discovery + JWKS are cached for an hour, with auto-refresh on key miss (handles key rotation).
- Supported signing algorithms:
RS256/384/512,ES256/384/512,PS256/384/512. - Provider configuration is in-memory only — re-register on every server start
(call
Openid::registerfrominit()).