UAuth Docs
Integration Guide
raw .md
← Back to app
EN
TH
Loading…
# UAuth — OIDC Integration Guide > This guide is for **developers / AI agents** building a relying party (your app) that connects to UAuth. > UAuth is a standard **OpenID Connect (OIDC) provider** — use any OIDC client library, > point it at the discovery URL, and run a normal authorization_code flow. --- ## 1. Overview UAuth is a central identity provider that wraps social login (Google/Facebook/LINE/Discord). Your app talks to **UAuth only** over OIDC — it never needs to know about the social providers behind it. ``` your app ──(OIDC authorization_code + PKCE)──► UAuth ──► Google/Facebook/LINE/Discord ▲ │ └──────────── id_token + access_token ───────┘ ``` - **Supported flow:** `authorization_code` + **PKCE (S256)** + `refresh_token` - **id_token:** a JWT signed with **RS256** (verify it with the public key from JWKS) - **access_token:** opaque (call `/userinfo` to read claims) --- ## 2. Endpoints Base / **issuer**: `https://uauth.u2.skin` | Purpose | URL | |---|---| | **Discovery** (start here) | `https://uauth.u2.skin/.well-known/openid-configuration` | | JWKS (public key) | `https://uauth.u2.skin/.well-known/jwks.json` | | Authorization | `https://uauth.u2.skin/authorize` | | Token | `https://uauth.u2.skin/token` | | UserInfo | `https://uauth.u2.skin/userinfo` | > **Recommended:** use an OIDC library that reads discovery automatically (e.g. `openid-client`, NextAuth/Auth.js, > `coreos/go-oidc`) and set `issuer = https://uauth.u2.skin` — the library will find the endpoints for you. The real discovery document: ```json { "issuer": "https://uauth.u2.skin", "response_types_supported": ["code"], "grant_types_supported": ["authorization_code", "refresh_token"], "scopes_supported": ["openid", "profile", "email", "offline_access"], "id_token_signing_alg_values_supported": ["RS256"], "code_challenge_methods_supported": ["S256"], "token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post", "none"] } ``` --- ## 3. Register a client (in the UAuth admin) > **Before you can do this, you need a developer account.** Open `https://uauth.u2.skin/admin/register`, > sign up with an allowed email, then log in to the admin. Go to `https://uauth.u2.skin/admin/clients` → **create a new client**. There are two kinds: ### a) Public client (SPA / mobile / frontend-only) — recommended with PKCE - Tick **public client** when creating → you get a `client_id` (no secret) - **PKCE is required** - `token_endpoint_auth_method = none` ### b) Confidential client (has a backend server that can keep a secret) - Leave public unticked → you get a `client_id` + `client_secret` (**shown once — save it now**) - Send the secret at `/token` using `client_secret_post` or `client_secret_basic` ### Redirect URI (important — security) On the client detail page → add your app's **redirect URI**, e.g. `http://localhost:3000/api/auth/callback` - Validated by **exact match** (scheme/host/port/path must match exactly) — **no wildcards** - **dev:** allows `http://localhost:<port>` and `https://` - **prod:** requires `https://` only, no localhost --- ## 4. Authorization Code + PKCE flow (step by step) ### Step 0 — create PKCE + state (in your app) ``` code_verifier = random 43–128 chars (base64url) code_challenge = BASE64URL(SHA256(code_verifier)) state = random ≥ 8 chars ⚠️ must be ≥ 8 long (otherwise UAuth rejects it: "invalid_state") nonce = random (recommended) ``` Store `code_verifier`, `state`, and `nonce` in your app's session/cookie. ### Step 1 — redirect the user to `/authorize` ``` GET https://uauth.u2.skin/authorize ?response_type=code &client_id=<CLIENT_ID> &redirect_uri=<REDIRECT_URI> # must exactly match the registered one &scope=openid%20profile%20email%20offline_access &state=<STATE> &nonce=<NONCE> &code_challenge=<CODE_CHALLENGE> &code_challenge_method=S256 ``` UAuth has the user log in (social) and consent, then **303 redirects** back to: ``` <REDIRECT_URI>?code=<AUTH_CODE>&scope=...&state=<STATE> ``` > Check that the returned `state` == the state you sent (CSRF protection). ### Step 2 — exchange the code at `/token` ``` POST https://uauth.u2.skin/token Content-Type: application/x-www-form-urlencoded grant_type=authorization_code &code=<AUTH_CODE> &redirect_uri=<REDIRECT_URI> # must match the one from /authorize &client_id=<CLIENT_ID> &code_verifier=<CODE_VERIFIER> # confidential client: also send client_secret=<SECRET> (or use HTTP Basic) ``` Response: ```json { "access_token": "ory_at_...", "token_type": "bearer", "expires_in": 3600, "id_token": "eyJ...", // JWT (RS256) "refresh_token": "ory_rt_...", // only when you request the offline_access scope "scope": "openid profile email offline_access" } ``` ### Step 3 — use / verify the tokens - **id_token** = the user's identity (see §5) — verify its signature with JWKS - **access_token** = an opaque token for calling `/userinfo` ``` GET https://uauth.u2.skin/userinfo Authorization: Bearer <ACCESS_TOKEN> ``` Response (only the claims allowed by the granted scopes): ```json { "sub": "<user uuid>", "email": "user@example.com", "email_verified": true, "name": "User Name", "picture": "https://.../avatar.png" } ``` --- ## 5. id_token (claims + verification) An example decoded payload: ```json { "iss": "https://uauth.u2.skin", "sub": "00000000-0000-0000-0000-000000000001", // = users.id (stable per user) "aud": ["<CLIENT_ID>"], "exp": 1780128076, "iat": 1780124476, "auth_time": 1780124476, "nonce": "<NONCE>", "email": "user@example.com", "email_verified": true, "name": "User Name", "picture": "https://.../avatar.png" } ``` **How to verify (required):** 1. RS256 signature with the public key from `jwks_uri` — pick the key by the `kid` in the JWT header 2. `iss` == `https://uauth.u2.skin` 3. `aud` contains your `client_id` 4. `exp` is not expired 5. `nonce` == the one you sent > **`sub` is the user's primary identifier** (= `users.id` in UAuth) — use it to link the user in your app. > Don't use email as the key: some providers (LINE/Facebook) may not return one, so `email` can be `null`/missing. --- ## 6. Refresh token Request the `offline_access` scope and you'll also receive a `refresh_token`. Renew with: ``` POST /token grant_type=refresh_token &refresh_token=<REFRESH_TOKEN> &client_id=<CLIENT_ID> # confidential: + client_secret ``` > The refresh token **rotates** every time — the old one stops working, so store the new one you get back. --- ## 7. Scopes | scope | what you get | |---|---| | `openid` | **required** — enables OIDC, returns an id_token | | `profile` | `name`, `picture` | | `email` | `email`, `email_verified` | | `offline_access` | a `refresh_token` | --- ## 8. Examples ### Next.js (Auth.js / NextAuth v5) — custom OIDC provider ```ts // auth.ts import NextAuth from "next-auth" export const { handlers, auth } = NextAuth({ providers: [{ id: "uauth", name: "UAuth", type: "oidc", issuer: "https://uauth.u2.skin", clientId: process.env.UAUTH_CLIENT_ID, clientSecret: process.env.UAUTH_CLIENT_SECRET, // public client: leave empty + use PKCE authorization: { params: { scope: "openid profile email offline_access" } }, checks: ["pkce", "state", "nonce"], }], }) ``` Redirect URI to add in the UAuth admin: `http://localhost:3000/api/auth/callback/uauth` ### Go (coreos/go-oidc + x/oauth2) ```go provider, _ := oidc.NewProvider(ctx, "https://uauth.u2.skin") verifier := provider.Verifier(&oidc.Config{ClientID: clientID}) oauth2Cfg := &oauth2.Config{ ClientID: clientID, ClientSecret: clientSecret, Endpoint: provider.Endpoint(), RedirectURL: "http://localhost:3000/callback", Scopes: []string{oidc.ScopeOpenID, "profile", "email", "offline_access"}, } // use oauth2.GenerateVerifier() / S256ChallengeOption() for PKCE ``` ### curl (test token/userinfo once you have a code) See `scripts/oidc_flow.sh` in the UAuth repo. --- ## 9. Gotchas (real ones — read before you build) - ⚠️ **`state` must be ≥ 8 characters**, otherwise `/authorize` returns `invalid_state` (UAuth enforces a minimum entropy) - ⚠️ **`redirect_uri` must match exactly** what's registered (including scheme/host/port/path) — no wildcards - ⚠️ **PKCE is required for public clients** — and it must be `S256` (`plain` is not accepted) - ⚠️ **dev** allows `http://localhost:<port>` redirects · **prod** requires `https://` only - `/authorize` redirects with **303 See Other** (not 302) - An `authorization_code` works **once** — reusing it revokes every token in that grant - `email` may be **null/missing** (LINE/Facebook) → don't rely on email as a key, use `sub` - The access_token is **opaque** (not a JWT) → read claims only via `/userinfo` --- ## 10. Error reference (OAuth2 standard) | error | cause | |---|---| | `invalid_state` | state is shorter than 8 chars | | `invalid_redirect_uri` | redirect_uri doesn't match what's registered / violates policy (remote http / wildcard / localhost in prod) | | `invalid_client` | wrong client_id/secret | | `invalid_grant` | code expired/already used, or the PKCE verifier doesn't match | | `access_denied` | user declined consent, or the account is banned | | `invalid_scope` | requested a scope the client isn't allowed to use | --- ## 11. Checklist for a new app - [ ] Create a client at `/admin/clients` (choose public/confidential) - [ ] Add your app's redirect URI on the client detail page - [ ] Set your app's env: `issuer=https://uauth.u2.skin`, `client_id`, (`client_secret` if confidential) - [ ] Request scopes `openid profile email` (+ `offline_access` if you need refresh) - [ ] Do PKCE + state (≥8) + nonce - [ ] Verify the id_token (sig/iss/aud/exp/nonce) and link the user by `sub`