← All posts
Sign in with AINative: Add OAuth 2.1 to Your App in 10 Minutes
TUTORIALSMay 27, 2026· 4 min read

Sign in with AINative: Add OAuth 2.1 to Your App in 10 Minutes

By Toby Morning
# Sign in with AINative: Add OAuth 2.1 to Your App in 10 Minutes AINative supports **OAuth 2.1 Authorization Code flow with PKCE**, letting any third-party app authenticate users with their AINative account. This guide walks through the full flow: registering your app, redirecting users to sign in, exchanging the auth code for a token, and fetching the user profile. --- ## Prerequisites - An app with a server-side callback URL (e.g. `https://yourapp.com/api/auth/callback/ainative`) - Basic familiarity with OAuth 2.0 --- ## Step 1: Register Your Client Register your app once to get a `client_id` and `client_secret`. ```bash curl -X POST https://api.ainative.studio/oauth/clients/register \ -H "Content-Type: application/json" \ -d '{ "client_name": "Your App Name", "redirect_uris": ["https://yourapp.com/api/auth/callback/ainative"], "grant_types": ["authorization_code"], "scope": "openid profile email", "token_endpoint_auth_method": "client_secret_basic" }' ``` **Response:** ```json { "client_id": "550e8400-e29b-41d4-a716-446655440000", "client_secret": "your-secret-here", "client_name": "Your App Name", "redirect_uris": ["https://yourapp.com/api/auth/callback/ainative"], "grant_types": ["authorization_code"], "scope": "openid profile email" } ``` Store `client_id` and `client_secret` in environment variables. **Never expose `client_secret` in client-side code.** --- ## Step 2: Generate PKCE Values OAuth 2.1 requires PKCE. Generate these server-side before redirecting. **Python:** ```python import secrets, hashlib, base64 code_verifier = secrets.token_urlsafe(64) digest = hashlib.sha256(code_verifier.encode()).digest() code_challenge = base64.urlsafe_b64encode(digest).rstrip(b"=").decode() ``` **TypeScript / Node.js:** ```typescript import crypto from "crypto"; const codeVerifier = crypto.randomBytes(64).toString("base64url"); const codeChallenge = crypto .createHash("sha256") .update(codeVerifier) .digest("base64url"); ``` Store `code_verifier` in the user's session — you'll need it in Step 4. --- ## Step 3: Redirect the User to AINative ``` https://api.ainative.studio/oauth/authorize ?response_type=code &client_id=YOUR_CLIENT_ID &redirect_uri=https://yourapp.com/api/auth/callback/ainative &scope=openid%20profile%20email &state=RANDOM_CSRF_TOKEN &code_challenge=YOUR_CODE_CHALLENGE &code_challenge_method=S256 ``` **Python (Flask):** ```python from urllib.parse import urlencode import secrets state = secrets.token_urlsafe(16) session["oauth_state"] = state session["code_verifier"] = code_verifier params = { "response_type": "code", "client_id": CLIENT_ID, "redirect_uri": "https://yourapp.com/api/auth/callback/ainative", "scope": "openid profile email", "state": state, "code_challenge": code_challenge, "code_challenge_method": "S256", } return redirect("https://api.ainative.studio/oauth/authorize?" + urlencode(params)) ``` **TypeScript (Next.js):** ```typescript const state = crypto.randomUUID(); // Save state + codeVerifier to session/cookie const params = new URLSearchParams({ response_type: "code", client_id: process.env.AINATIVE_CLIENT_ID!, redirect_uri: "https://yourapp.com/api/auth/callback/ainative", scope: "openid profile email", state, code_challenge: codeChallenge, code_challenge_method: "S256", }); redirect(`https://api.ainative.studio/oauth/authorize?${params}`); ``` --- ## Step 4: Exchange the Code for Tokens After sign-in, AINative redirects to your `redirect_uri` with `?code=...&state=...`. Verify the state, then exchange the code: **Python:** ```python import httpx, base64 credentials = base64.b64encode(f"{CLIENT_ID}:{CLIENT_SECRET}".encode()).decode() resp = httpx.post( "https://api.ainative.studio/v1/oauth/token", headers={ "Authorization": f"Basic {credentials}", "Content-Type": "application/x-www-form-urlencoded", }, data={ "grant_type": "authorization_code", "code": request.args["code"], "redirect_uri": "https://yourapp.com/api/auth/callback/ainative", "code_verifier": session["code_verifier"], }, ) access_token = resp.json()["access_token"] ``` **TypeScript:** ```typescript const credentials = Buffer.from( `${process.env.AINATIVE_CLIENT_ID}:${process.env.AINATIVE_CLIENT_SECRET}` ).toString("base64"); const resp = await fetch("https://api.ainative.studio/v1/oauth/token", { method: "POST", headers: { Authorization: `Basic ${credentials}`, "Content-Type": "application/x-www-form-urlencoded", }, body: new URLSearchParams({ grant_type: "authorization_code", code: searchParams.get("code")!, redirect_uri: "https://yourapp.com/api/auth/callback/ainative", code_verifier: session.codeVerifier, }), }); const { access_token } = await resp.json(); ``` --- ## Step 5: Fetch the User's Profile ```bash curl https://api.ainative.studio/oauth/userinfo \ -H "Authorization: Bearer ACCESS_TOKEN" ``` **Response:** ```json { "sub": "user-uuid-here", "email": "user@example.com", "email_verified": true, "name": "Jane Developer", "given_name": "Jane", "family_name": "Developer", "plan": "pro", "account_id": "workspace-uuid", "organization_id": null } ``` Use `sub` as your stable foreign key — it never changes. **Python:** ```python profile = httpx.get( "https://api.ainative.studio/oauth/userinfo", headers={"Authorization": f"Bearer {access_token}"}, ).json() ``` **TypeScript:** ```typescript const profile = await fetch("https://api.ainative.studio/oauth/userinfo", { headers: { Authorization: `Bearer ${access_token}` }, }).then(r => r.json()); ``` --- ## Available Scopes | Scope | What it grants | |-------|----------------| | `openid` | Required for OIDC — enables `sub` claim | | `profile` | `name`, `given_name`, `family_name` | | `email` | `email`, `email_verified` | | `memory:read` | Read from the user's ZeroDB memory | | `memory:write` | Write to the user's ZeroDB memory | | `vector:search` | Query the user's vector store | | `vector:write` | Index documents into the user's vector store | | `agent:read` | List and inspect the user's agents | | `agent:write` | Create and deploy agents on the user's behalf | --- ## NextAuth.js / Auth.js Integration ```typescript // app/api/auth/[...nextauth]/route.ts import NextAuth from "next-auth"; export const { handlers, auth } = NextAuth({ providers: [ { id: "ainative", name: "AINative", type: "oauth", issuer: "https://api.ainative.studio", wellKnown: "https://api.ainative.studio/.well-known/openid-configuration", authorization: { params: { scope: "openid profile email" } }, clientId: process.env.AINATIVE_CLIENT_ID, clientSecret: process.env.AINATIVE_CLIENT_SECRET, profile(profile) { return { id: profile.sub, name: profile.name, email: profile.email, plan: profile.plan, }; }, }, ], }); ``` --- ## Endpoint Reference | Endpoint | Method | Description | |----------|--------|-------------| | `/oauth/clients/register` | POST | Register OAuth client (RFC 7591) | | `/oauth/authorize` | GET | Authorization + login page | | `/v1/oauth/token` | POST | Exchange code for access token | | `/oauth/userinfo` | GET | OIDC UserInfo claims | | `/.well-known/openid-configuration` | GET | OIDC discovery document |
AI DevelopmentGetting StartedTypeScriptPython

Check your site's AX Score

Free scan, 6 categories, under 60 seconds. See how your site ranks on the agentic web.

Run a free audit →