Integrate from Your App
Once auth is Active, you have a conformant OpenID Connect issuer. Any OIDC client library works without modification. This page covers the integration steps: discovery, the authorization code flow, token validation, and customer SMTP for magic links.
What your app needs
You need three values from the auth configuration (GET /app-services/$APP_ID/auth):
| Value | Where to find it |
|---|---|
| Issuer URL | auth.issuer_url (e.g. https://auth-1a2b3c4d.foundrydb.com) |
| Client ID | The id field of the auth configuration (auth.id) |
| Client secret | Set by the platform at enable time; retrieve it once from GET /app-services/$APP_ID/auth (the field is auth.client_secret, write-only after initial enable; store it securely on first retrieval) |
The issuer URL is the only URL you register in your OIDC library. Everything else is discovered from the discovery document.
OIDC discovery
The discovery document is served at:
https://<issuer-host>/.well-known/openid-configuration
Fetch it to confirm the issuer is live:
curl https://auth-1a2b3c4d.foundrydb.com/.well-known/openid-configuration
The document includes authorization_endpoint, token_endpoint, jwks_uri, issuer, and the full list of supported claims, scopes, and signing algorithms (id_token_signing_alg_values_supported lists RS256). Your client library reads this automatically; you do not need to hard-code any endpoint URLs. The issuer value in the document must exactly match the iss claim you later validate on tokens, including scheme and absence of a trailing slash. If discovery is reachable but token validation fails, an issuer mismatch is the first thing to check.
Authorization code flow
App Auth implements the authorization code flow with PKCE. This is the standard flow for web apps and SPAs. The diagram below is the same flow your OIDC library drives end to end:
The steps are:
- Your app redirects the user to
authorization_endpointwithresponse_type=code, yourclient_id, aredirect_uri,scope=openid email profile, and PKCE challenge parameters (acode_challengederived from a randomcode_verifier, withcode_challenge_method=S256). - The platform serves the hosted login page. The user enters their email or clicks a social sign-in button.
- After successful authentication, the issuer redirects back to your
redirect_uriwith an authorizationcodeand thestateyou sent. - Your app exchanges the code for tokens at
token_endpoint, sending the originalcode_verifier, and receives an ID token, access token, and refresh token. - Your app validates the ID token against the JWKS and establishes a session.
PKCE is required. The issuer does not issue tokens without a valid code verifier, which means a stolen authorization code is useless to an attacker who does not also hold the verifier. Always send a cryptographically random state and reject the callback if it does not match the value you stored, since this is your defense against cross-site request forgery on the redirect. Any conformant OIDC library handles PKCE and state for you; the notes here are so you can confirm it is doing so.
Integrate with Auth.js (NextAuth)
// app/api/auth/[...nextauth]/route.ts
import NextAuth from "next-auth";
export const { handlers, signIn, signOut, auth } = NextAuth({
providers: [
{
id: "foundrydb",
name: "FoundryDB Auth",
type: "oidc",
issuer: process.env.AUTH_ISSUER_URL,
clientId: process.env.AUTH_CLIENT_ID,
clientSecret: process.env.AUTH_CLIENT_SECRET,
},
],
});
Set environment variables:
AUTH_ISSUER_URL=https://auth-1a2b3c4d.foundrydb.com
AUTH_CLIENT_ID=<auth_configuration_id>
AUTH_CLIENT_SECRET=<client_secret>
Auth.js reads the discovery document, drives the authorization code flow with PKCE, and verifies tokens against the JWKS automatically.
Integrate with oidc-client-ts (SPA)
import { UserManager, WebStorageStateStore } from "oidc-client-ts";
const userManager = new UserManager({
authority: "https://auth-1a2b3c4d.foundrydb.com",
client_id: process.env.VITE_AUTH_CLIENT_ID,
redirect_uri: `${window.location.origin}/auth/callback`,
response_type: "code",
scope: "openid email profile",
userStore: new WebStorageStateStore({ store: window.localStorage }),
});
// Redirect to login
await userManager.signinRedirect();
// Handle callback
const user = await userManager.signinRedirectCallback();
console.log(user.profile.sub); // The end-user's stable UUID
Validate tokens
The issuer signs JWTs with RS256. Your app should validate tokens using the public keys from the JWKS endpoint:
https://<issuer-host>/.well-known/jwks.json
Validate the following claims:
| Claim | Expected value |
|---|---|
iss | Your issuer URL exactly, e.g. https://auth-1a2b3c4d.foundrydb.com |
aud | Your client ID (the auth.id UUID) |
exp | Must be in the future |
sub | The end-user's stable UUID (matches _mdb_auth.users.id) |
Do not skip expiry and issuer validation. Standard JWT libraries handle these checks when you pass the issuer and audience. Validate on every request that carries a token, not just at login: the access token is your proof that the session is still valid. Verify the RS256 signature against a key from the JWKS rather than trusting the token's own header, and confirm the token's alg is RS256 so a forged token cannot downgrade you to none or to a symmetric algorithm.
Example with jose (TypeScript/Node.js):
import { createRemoteJWKSet, jwtVerify } from "jose";
const JWKS = createRemoteJWKSet(
new URL("https://auth-1a2b3c4d.foundrydb.com/.well-known/jwks.json")
);
async function verifyToken(token: string) {
const { payload } = await jwtVerify(token, JWKS, {
issuer: "https://auth-1a2b3c4d.foundrydb.com",
audience: process.env.AUTH_CLIENT_ID,
});
return payload;
}
The JWKS library caches keys and re-fetches when it encounters an unknown kid. Each token's header carries the kid of the key that signed it, and the verifier picks the matching public key from the JWKS. During key rotation, the outgoing key stays in the JWKS through its grace window, so tokens signed by either the old or new key validate without any change to your app. Use a JWKS client that fetches by kid and caches with a bounded TTL rather than pinning a single key, so rotations are picked up automatically and a compromised key can be retired without redeploying your app.
Read identity data from your database
Because the _mdb_auth schema lives in your PostgreSQL database, your application can query it directly with a read-only database user. This is useful for server-side lookups by email, listing users, or correlating sessions with application data.
The sub claim in the JWT is always the UUID from _mdb_auth.users.id. Use it to join auth identity data with your own application tables:
-- Find application data for the authenticated user
SELECT a.* FROM app_accounts a
JOIN _mdb_auth.users u ON u.id = $1::uuid
WHERE a.user_id = u.id;
Do not write to _mdb_auth from application code. The schema is owned by the mdb_auth role. Grant your app user SELECT on the schema only.
SMTP for magic links
Magic-link email delivery requires an SMTP server you control. There is no platform relay. SMTP credentials are supplied at enable time and stored in the platform secret store; they are never returned by any response.
The from_address and from_name values control the sender name and address on magic-link emails. Use a sending address that matches your domain's SPF and DKIM configuration so magic-link emails are not marked as spam.
To update SMTP credentials after enabling auth, disable auth and re-enable it with the new SMTP configuration. The _mdb_auth schema and your end-user data are preserved across disable and re-enable cycles.
Supported scopes and claims
| Scope | Claims included |
|---|---|
openid | sub, iss, aud, exp, iat |
email | email, email_verified |
profile | name (when available from the provider) |
The sub claim is always the _mdb_auth.users.id UUID. It is stable across sign-in methods: the same user signing in via magic link and via Google has the same sub.
Next steps
- Social Login: add Google and GitHub sign-in
- Enable Auth: key rotation, session revocation, and GDPR erasure
- App Hosting: deploy the application that uses this auth issuer