Switch to stable 8.0.427-dev

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 a runtime::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 from claims.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. null if not registered. Never includes the client_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 the return_to from the originating login().
  • 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::register from init()).