# KID Identity Provider (KID IdP) — Integration Reference for AI This document gives a complete picture of how to integrate KID OAuth into any application. It is written for external developers and AI assistants implementing KID OAuth from scratch. **Base URL (production):** https://api.kid.koompi.org **Dashboard:** https://dash.kid.koompi.org **OIDC Discovery:** https://api.kid.koompi.org/.well-known/openid-configuration --- ## What Is KID IdP? KID IdP is a standards-compliant OAuth 2.0 / OpenID Connect (OIDC) authorization server with Web3 integration. Every user automatically receives an EVM-compatible blockchain wallet on signup. Key properties: - One KID identity per user, regardless of login method (Google, Apple, email, Telegram) - Every user has a `wallet_address` (Selendra EVM chain) automatically generated on signup - Human-readable identity number (`kid`) assigned to every user, e.g. `KID-000001` - Full OIDC compliance — works as a drop-in SSO provider for Moodle, WordPress, Grafana, etc. - SDK available: `@kid-oauth/sdk` (npm) --- ## Getting Started 1. Sign in to https://dash.kid.koompi.org 2. Create a new project → you receive: - `client_id` (development: `pk_test_...`, production: `pk_live_...`) - `client_secret` 3. Add your `redirect_uri` to the project's allowed redirect URIs 4. Select which scopes your project is allowed to request 5. Use the credentials to initiate the OAuth flow below --- ## OAuth 2.0 Authorization Code Flow KID uses the Authorization Code flow with server-side PKCE. Your app does not need to generate PKCE — KID handles it automatically. ### Step 1 — Redirect user to KID ``` GET https://api.kid.koompi.org/oauth ?client_id=pk_test_YOUR_CLIENT_ID &redirect_uri=https://yourapp.com/callback &scope=openid profile.basic profile.contact wallet.read &nonce=RANDOM_NONCE ``` Only `client_id` and `redirect_uri` are required. KID handles `response_type`, PKCE (S256), and CSRF state automatically. After the user authenticates, KID redirects to: ``` https://yourapp.com/callback?code=AUTH_CODE&state=STATE&scope=... ``` ### Step 2 — Exchange code for tokens ```http POST https://api.kid.koompi.org/oauth/token Content-Type: application/json { "grant_type": "authorization_code", "code": "AUTH_CODE", "client_id": "pk_test_YOUR_CLIENT_ID", "client_secret": "YOUR_CLIENT_SECRET", "redirect_uri": "https://yourapp.com/callback", "state": "STATE_FROM_CALLBACK" } ``` Response: ```json { "access_token": "eyJ...", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "a3f9c2e1...", "refresh_token_expires_in": 2592000, "scope": "openid profile.basic profile.contact wallet.read", "user_id": "507f1f77bcf86cd799439011", "user": { "sub": "507f1f77bcf86cd799439011", "kid": "KID-000001", "name": "John Doe", "given_name": "John", "family_name": "Doe", "preferred_username": "johndoe", "picture": "https://...", "updated_at": 1715000000, "email": "john@example.com", "email_verified": true, "phone_number": "+855...", "phone_number_verified": false, "wallet_address": "0x742d35Cc...", "public_key": "0x04a1b2c3..." }, "id_token": "eyJ..." } ``` > `id_token` is only included when `openid` scope is requested. It is RS256-signed. The `user` object follows the OpenID Connect Core 1.0 standard claims (with KID extensions `kid`, `wallet_address`, `public_key`, `telegram_id`). Field presence is filtered by granted scopes — the same shape is returned by `GET /oauth/userinfo`. ### Step 3 — Use the access token ```http GET https://api.kid.koompi.org/oauth/userinfo Authorization: Bearer ACCESS_TOKEN ``` ### Step 4 — Refresh the access token ```http POST https://api.kid.koompi.org/oauth/refresh Content-Type: application/json { "grant_type": "refresh_token", "refresh_token": "REFRESH_TOKEN", "client_id": "pk_test_YOUR_CLIENT_ID", "client_secret": "YOUR_CLIENT_SECRET" } ``` Response shape is identical to the token exchange response. **Important:** Every refresh issues a new `refresh_token` and immediately invalidates the old one. Always store and use the latest refresh token. ### Step 5 — Revoke token (Sign Out) ```http POST https://api.kid.koompi.org/oauth/revoke Content-Type: application/json { "token": "REFRESH_TOKEN", "client_id": "pk_test_YOUR_CLIENT_ID", "client_secret": "YOUR_CLIENT_SECRET" } ``` To sign the user out of **all sessions** for your project: ```json { "revoke_all": true, "client_id": "pk_test_YOUR_CLIENT_ID", "client_secret": "YOUR_CLIENT_SECRET" } ``` Always returns `200 OK` even if the token is invalid (RFC 7009). --- ## Client Authentication All token, refresh, and revoke endpoints accept credentials in two formats: **JSON body (recommended):** ```json { "client_id": "pk_test_...", "client_secret": "..." } ``` **HTTP Basic auth:** ``` Authorization: Basic base64(client_id:client_secret) ``` --- ## Scopes ### Standard OIDC scopes | Scope | Data returned | |---|---| | `openid` | Enables OIDC mode — adds `id_token` to response | | `profile` | `name`, `given_name`, `family_name`, `preferred_username`, `picture` | | `email` | `email`, `email_verified` | | `phone` | `phone_number`, `phone_number_verified` | ### KID custom scopes | Scope | Data returned | |---|---| | `profile.basic` | `kid`, `name`, `given_name`, `family_name`, `preferred_username`, `picture`, `created_at`, `updated_at` | | `profile.contact` | `email`, `phone_number` | | `profile.telegram` | Telegram provider info | | `account.status` | `is_confirmed`, account type flags | | `wallet.read` | `wallet_address`, `public_key` | | `wallet.sign` | Allows signing messages/transactions on behalf of user | | `wallet.send` | Allows broadcasting transactions on behalf of user | | `wallet.mnemonic` | Raw mnemonic — highly sensitive, requires explicit approval | **Default scopes** (when none requested): `profile.basic`, `profile.contact`, `wallet.read`. **Sensitive scopes** (`wallet.sign`, `wallet.send`, `wallet.mnemonic`) must be explicitly requested AND enabled on your project. They are never granted implicitly. --- ## Userinfo Claims Reference Returned in the `user` field of the token response and from `GET /oauth/userinfo`. All names follow the OpenID Connect Core 1.0 standard. | Claim | Scope | Description | |---|---|---| | `sub` | always | Stable unique identifier — use as your foreign key | | `kid` | always | Human-readable identity number, e.g. `KID-000001` | | `name` | `profile` / `profile.basic` | Full display name | | `given_name` | `profile` / `profile.basic` | First name | | `family_name` | `profile` / `profile.basic` | Last name | | `preferred_username` | `profile` / `profile.basic` | Username | | `picture` | `profile` / `profile.basic` | Avatar URL | | `updated_at` | `profile` / `profile.basic` | Unix seconds — last profile update | | `email` | `email` / `profile.contact` | Email address | | `email_verified` | `email` / `profile.contact` | Boolean | | `phone_number` | `phone` / `profile.contact` | Phone number (E.164) | | `phone_number_verified` | `phone` / `profile.contact` | Boolean | | `telegram_id` | `profile.telegram` | Numeric Telegram user ID | | `wallet_address` | `wallet.read` | EVM wallet address (Selendra chain) — KID extension | | `public_key` | `wallet.read` | EVM public key — KID extension | > **Note:** `kid` (identity number like "KID-000001") is a KID-specific claim and is > different from the JWT `kid` header (key ID used for signature verification). --- ## id_token (OIDC) When `openid` is in the requested scopes, the token response includes an `id_token`. It is a RS256-signed JWT with the following payload: ```json { "iss": "https://api.kid.koompi.org", "aud": "YOUR_CLIENT_ID", "sub": "USER_MONGODB_ID", "kid": "KID-000001", "name": "John Doe", "email": "john@example.com", "at_hash": "...", "nonce": "YOUR_NONCE", "iat": 1700000000, "exp": 1700003600 } ``` Verify the signature using the public key from: ``` GET https://api.kid.koompi.org/.well-known/jwks.json ``` --- ## OIDC Discovery ``` GET https://api.kid.koompi.org/.well-known/openid-configuration ``` ```json { "issuer": "https://api.kid.koompi.org", "authorization_endpoint": "https://api.kid.koompi.org/oauth", "token_endpoint": "https://api.kid.koompi.org/oauth/token", "userinfo_endpoint": "https://api.kid.koompi.org/oauth/userinfo", "jwks_uri": "https://api.kid.koompi.org/.well-known/jwks.json", "revocation_endpoint": "https://api.kid.koompi.org/oauth/revoke", "scopes_supported": ["openid", "profile", "email", "phone", "profile.basic", "profile.contact", "wallet.read", "..."], "response_types_supported": ["code"], "grant_types_supported": ["authorization_code", "refresh_token"], "id_token_signing_alg_values_supported": ["RS256"], "token_endpoint_auth_methods_supported": ["client_secret_post", "client_secret_basic"], "code_challenge_methods_supported": ["S256"] } ``` --- ## SDK Integration (`@kid-oauth/sdk`) ### Install ```bash npm install @kid-oauth/sdk # or pnpm add @kid-oauth/sdk ``` ### Next.js App Router ```ts // app/api/auth/callback/route.ts import { createTokenHandler } from '@kid-oauth/sdk/server'; const handler = createTokenHandler({ clientId: process.env.KID_CLIENT_ID!, clientSecret: process.env.KID_CLIENT_SECRET!, redirectUri: process.env.KID_REDIRECT_URI!, }); export const POST = handler; // app/api/auth/refresh/route.ts import { createRefreshHandler } from '@kid-oauth/sdk/server'; const handler = createRefreshHandler({ clientId: process.env.KID_CLIENT_ID!, clientSecret: process.env.KID_CLIENT_SECRET!, }); export const POST = handler; // app/api/auth/revoke/route.ts import { createRevokeHandler } from '@kid-oauth/sdk/server'; const handler = createRevokeHandler({ clientId: process.env.KID_CLIENT_ID!, clientSecret: process.env.KID_CLIENT_SECRET!, }); export const POST = handler; ``` ### React Provider ```tsx // app/layout.tsx import { KIDProvider } from '@kid-oauth/sdk/react'; export default function RootLayout({ children }) { return ( {children} ); } ``` ### React Hooks & Components ```tsx import { useKID, SignInButton, SignOutButton, UserButton } from '@kid-oauth/sdk/react'; function MyComponent() { const { user, isLoaded, isSignedIn } = useKID(); if (!isLoaded) return null; if (!isSignedIn) return ; return (

Hello, {user.name}

Wallet: {user.wallet_address}

); } ``` ### Server-side token exchange (`KIDOAuthServer`) ```ts import { KIDOAuthServer } from '@kid-oauth/sdk/server'; const kid = new KIDOAuthServer({ clientId: process.env.KID_CLIENT_ID!, clientSecret: process.env.KID_CLIENT_SECRET!, redirectUri: process.env.KID_REDIRECT_URI!, }); // Exchange code for tokens const tokens = await kid.exchangeCode({ code, state }); // Get user info const userinfo = await kid.getUserInfo(tokens.access_token); // Refresh tokens const newTokens = await kid.refreshToken({ refresh_token: tokens.refresh_token }); // Revoke session await kid.revokeToken({ token: tokens.refresh_token }); ``` ### Token Response Shape ```ts interface TokenResponse { access_token: string; token_type: 'Bearer'; expires_in: number; // 3600 seconds refresh_token?: string; refresh_token_expires_in?: number; // 2592000 seconds (30 days) scope?: string; // space-separated granted scopes id_token?: string; // RS256 JWT, only when openid scope user_id?: string; // canonical user id (same as user.sub) user?: UserInfoResponse; // OIDC-standard claims } interface UserInfoResponse { sub: string; kid?: string | null; name?: string; given_name?: string; family_name?: string; preferred_username?: string; picture?: string; updated_at?: number; email?: string; email_verified?: boolean; phone_number?: string; phone_number_verified?: boolean; telegram_id?: number | null; wallet_address?: string; public_key?: string; } ``` --- ## Wallet API After a user authenticates with `wallet.read` or higher wallet scopes, you can interact with their Selendra EVM wallet on their behalf. All wallet endpoints require `Authorization: Bearer ACCESS_TOKEN`. ### Get wallet info ```http GET https://api.kid.koompi.org/wallet/info Authorization: Bearer ACCESS_TOKEN ``` ```json { "address": "0x742d35Cc...", "public_key": "0x04a1b2c3...", "network": "selendra", "chain_id": 1961 } ``` ### Sign a message (scope: `wallet.sign`) ```http POST https://api.kid.koompi.org/wallet/sign-message Authorization: Bearer ACCESS_TOKEN Content-Type: application/json { "message": "Verify my identity", "message_format": "text" } ``` ```json { "status": "SUCCESS", "signature": "0x8a9c7b3d...", "message_hash": "0xdef456...", "signer": "0x742d35Cc..." } ``` `message_format`: `"text"` (adds Ethereum personal sign prefix) or `"hex"` (raw bytes). ### Sign a transaction without broadcasting (scope: `wallet.sign`) ```http POST https://api.kid.koompi.org/wallet/sign-transaction Authorization: Bearer ACCESS_TOKEN Content-Type: application/json { "transaction": { "to": "0xRecipient...", "value": "1000000000000000000", "data": "0x", "gas_limit": "21000", "max_fee_per_gas": "1000000000", "max_priority_fee_per_gas": "1000000" }, "chain_id": 1961 } ``` ```json { "status": "SUCCESS", "signed_transaction": "0xf86c...", "transaction_hash": "0xabc123...", "from": "0x742d35Cc...", "nonce": 42 } ``` ### Send a transaction (scope: `wallet.send`) KID signs and broadcasts. Rate limit: 10 requests / 15 min per user. Supports automatic gas sponsorship — if user balance is below 0.01 SEL, the relayer tops up to 0.5 SEL before sending. ```http POST https://api.kid.koompi.org/wallet/send-transaction Authorization: Bearer ACCESS_TOKEN Content-Type: application/json { "transaction": { "to": "0xRecipient...", "value": "1000000000000000000" }, "idempotency_key": "order-123", "wait_for_confirmation": false, "webhook_url": "https://yourapp.com/webhook" } ``` For a registered contract call: ```json { "contract_call": { "contract": "my-token", "method": "transfer", "params": ["0xRecipient...", "10000000000000000000"] }, "idempotency_key": "order-123" } ``` ```json { "status": "SUCCESS", "transaction_hash": "0xdef789...", "from": "0x742d35Cc...", "tx_status": "pending", "gas_sponsored": true } ``` ### Check transaction status (no auth required) ```http GET https://api.kid.koompi.org/wallet/tx-status/0xdef789... ``` ```json { "tx_status": "confirmed", "block_number": 12345678, "gas_used": "21000" } ``` ### Broadcast a pre-signed transaction from your project's wallet Your project signs the transaction client-side; KID only relays it. Use your project credentials in headers (not a user Bearer token). ```http POST https://api.kid.koompi.org/wallet/broadcast x-kid-public-key: YOUR_KID_PUBLIC_KEY x-kid-secret-key: YOUR_KID_SECRET_KEY Content-Type: application/json { "signed_tx": "0xf86c...", "idempotency_key": "order-456", "webhook_url": "https://yourapp.com/webhook" } ``` --- ## User Lookup API (Project credentials) List users who have authorized your project, or look up a specific user. ```http GET https://api.kid.koompi.org/user/list x-kid-public-key: YOUR_KID_PUBLIC_KEY x-kid-secret-key: YOUR_KID_SECRET_KEY ``` Look up a user by their Telegram ID: ```http GET https://api.kid.koompi.org/user/telegram/TELEGRAM_USER_ID x-kid-public-key: YOUR_KID_PUBLIC_KEY x-kid-secret-key: YOUR_KID_SECRET_KEY ``` --- ## Social Login Providers KID supports the following login methods out of the box: - **Google** — OAuth 2.0 - **Apple** — Sign in with Apple (id_token verification) - **Telegram** — Bot auth, Telegram Mini App, or project-key-based login - **Email + password** — via Better Auth endpoints at `/api/auth/*` Your app does not need to integrate with these providers directly. KID handles all provider authentication and presents a single unified user to your app. --- ## Consent Management Users can revoke your app's access at any time via their KID account settings. Your app can also check or revoke consent programmatically. List consents granted to your project: ```http GET https://api.kid.koompi.org/consent Authorization: Bearer ACCESS_TOKEN ``` Revoke consent for your project: ```http DELETE https://api.kid.koompi.org/consent/YOUR_CLIENT_ID Authorization: Bearer ACCESS_TOKEN ``` --- ## Third-Party Platform Setup (Moodle, WordPress, Grafana, etc.) KID is a fully compliant OIDC provider. Any platform that supports OAuth2/OIDC can use KID as an SSO provider using the discovery URL for auto-configuration. **Discovery URL:** `https://api.kid.koompi.org/.well-known/openid-configuration` **Manual endpoint configuration:** | Setting | Value | |---|---| | Authorization endpoint | `https://api.kid.koompi.org/oauth` | | Token endpoint | `https://api.kid.koompi.org/oauth/token` | | Userinfo endpoint | `https://api.kid.koompi.org/oauth/userinfo` | | JWKS URI | `https://api.kid.koompi.org/.well-known/jwks.json` | | Scopes | `openid profile email` | **User field mappings:** | Platform field | KID claim | |---|---| | Username | `preferred_username` or `email` | | Email | `email` | | First name | `given_name` | | Last name | `family_name` | | Profile picture | `picture` | | Unique ID | `sub` | ### Moodle 1. Create project in KID dashboard → copy `client_id` and `client_secret` 2. Add redirect URI: `https://your-moodle.com/admin/oauth2callback.php` 3. Moodle: Site Admin → Server → OAuth 2 services → New custom service 4. Set base URL: `https://api.kid.koompi.org` and enable auto-discovery 5. Map fields as shown above and enable "Create new accounts automatically" --- ## API Endpoint Summary ``` GET /oauth Initiate OAuth flow (redirect user here) GET /oauth/authorize Issue auth code after user authenticates POST /oauth/token Exchange authorization code for tokens POST /oauth/refresh Rotate refresh token, get new access token POST /oauth/revoke Revoke refresh token (RFC 7009) GET /oauth/userinfo Get user info (Bearer token required) GET /oauth/google Initiate Google login GET /oauth/google/callback Google callback GET /oauth/apple Initiate Apple login POST /oauth/apple/callback Apple callback POST /oauth/telegram/callback Telegram bot auth GET /oauth/telegram/callback Token exchange with project key POST /oauth/telegram/app Telegram Mini App login GET /wallet/info User's wallet address & public key GET /wallet/tx-status/:hash Transaction status (public) POST /wallet/sign-message Sign a message POST /wallet/sign-transaction Sign a transaction (no broadcast) POST /wallet/send-transaction Sign & broadcast a transaction POST /wallet/broadcast Broadcast pre-signed tx from project wallet GET /user/me Get authenticated user (Bearer token; includes KID-internal fields like status/role/providers) GET /user/list List project users (project credentials) GET /user/telegram/:id Look up user by Telegram ID GET /consent List user's consents DELETE /consent/:clientId Revoke consent GET /health Service health status GET /.well-known/openid-configuration OIDC discovery document GET /.well-known/jwks.json Public key set (RS256) ``` --- ## When to Use Which User Endpoint | Endpoint | Use case | Returns | |---|---|---| | `GET /oauth/userinfo` | Standard OIDC clients (Moodle, WordPress, Grafana) and any flow scoped by OAuth permissions | OIDC claims + legacy aliases, **filtered by token scopes** | | `GET /user/me` | First-party apps that need KID-internal fields (status, role, providers, locale) | OIDC claims + legacy aliases + KID-internal fields, **all available data** | **Rule of thumb:** - Building a generic OIDC integration → use `/oauth/userinfo`. It's standards-compliant and respects your declared scopes. This is what Moodle/WordPress/Grafana hit automatically. - Building a first-party KID app that needs admin-style fields → use `/user/me`. It returns extra fields like `status`, `role`, `is_confirmed`, `providers`, `locale`, `timezone`, `last_login_at` that are not part of OIDC userinfo. Both endpoints return the same OIDC-standard claims for shared fields, so swapping between them does not require remapping. --- ## Common Integration Mistakes - **Do not generate your own PKCE.** KID handles PKCE server-side. Just pass `state` from the callback into the token request. - **Do not forward `state` as a parameter name in the token body thinking it is the auth state.** In the token request, `state` is used by KID to look up the server-side PKCE record. It is the same `state` value you received in the callback query string. - **Always store the new refresh token after every refresh call.** The old one is immediately invalidated — replaying it will revoke all your sessions for that user. - **Do not request `wallet.sign` or `wallet.send` unless your project explicitly needs them.** These scopes require your project to have them enabled in the dashboard. - **`sub` is the stable user identifier** — use it as your foreign key, not `email` or `preferred_username` which can change. - **`kid` in userinfo (e.g. `KID-000001`) is the human identity number**, not the JWT `kid` header used for key rotation. They are unrelated. - **Development vs production keys are separate.** `pk_test_` keys work in development mode; `pk_live_` keys require production approval. Do not mix them. - **`id_token` is only included when you request the `openid` scope.** Without it, you get `access_token` + `refresh_token` only. - **The token response has a single `user` field — not separate `user` and `userinfo`.** All OIDC-standard claims are flat under `user`. Older docs may reference a separate `userinfo` object; it does not exist. --- ## Environment & Credentials | Credential type | Prefix | Use case | |---|---|---| | Development client ID | `pk_test_...` | Local development, testing | | Production client ID | `pk_live_...` | Live users (requires dashboard approval) | | Project public key | `kid_pk_...` | Server-to-server calls (user list, broadcast) | | Project secret key | `kid_sk_...` | Server-to-server calls (keep secret) | Never expose `client_secret`, `kid_secret_key`, or any `sk_` key to the browser or mobile client. These must only be used server-side.