Skip to content

OAuth 2.0 from scratch

If you can't (or don't want to) use a Little X Little SDK, integrate directly against the OIDC endpoints. The flow below works in any language.

0. Discover endpoints

Always read the discovery document at runtime — endpoint paths may change.

bash curl https://id.littlexlittle.org/.well-known/openid-configuration

Returns:

json { "issuer": "https://id.littlexlittle.org", "authorization_endpoint": "https://id.littlexlittle.org/oidc/authorize", "token_endpoint": "https://id.littlexlittle.org/oidc/token", "userinfo_endpoint": "https://id.littlexlittle.org/oidc/userinfo", "jwks_uri": "https://id.littlexlittle.org/.well-known/jwks.json", "revocation_endpoint": "https://id.littlexlittle.org/oidc/revoke", "introspection_endpoint": "https://id.littlexlittle.org/oidc/introspect", "end_session_endpoint": "https://id.littlexlittle.org/oidc/logout", "response_types_supported": ["code"], "grant_types_supported": ["authorization_code", "refresh_token"], "code_challenge_methods_supported": ["S256"], "id_token_signing_alg_values_supported": ["RS256"], "scopes_supported": ["openid","profile","email","offline_access","lxl.access"] }

1. Build the authorize URL

Generate PKCE values:

python import base64, hashlib, secrets verifier = base64.urlsafe_b64encode(secrets.token_bytes(32)).rstrip(b'=').decode() challenge = base64.urlsafe_b64encode(hashlib.sha256(verifier.encode()).digest()).rstrip(b'=').decode()

Redirect the user to:

https://id.littlexlittle.org/oidc/authorize ?response_type=code &client_id=YOUR_CLIENT_ID &redirect_uri=https%3A%2F%2Fyoursite.org%2Fcallback &scope=openid+profile+email+lxl.access &state=RANDOM_CSRF &nonce=RANDOM_NONCE &code_challenge=CHALLENGE &code_challenge_method=S256

2. Exchange code for tokens

After the user consents, they're redirected to redirect_uri?code=...&state=.... Verify state, then:

bash curl -X POST https://id.littlexlittle.org/oidc/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=authorization_code" \ -d "code=$CODE" \ -d "redirect_uri=https://yoursite.org/callback" \ -d "client_id=YOUR_CLIENT_ID" \ -d "code_verifier=$VERIFIER"

Confidential clients add -d client_secret=... (or use Authorization: Basic with base64(client_id:client_secret)).

Response:

json { "access_token": "...", "refresh_token": "...", "id_token": "eyJ...", "token_type": "Bearer", "expires_in": 3600, "scope": "openid profile email lxl.access" }

3. Verify the id_token

python import jwt, requests jwks = requests.get('https://id.littlexlittle.org/.well-known/jwks.json').json() header = jwt.get_unverified_header(id_token) key = next(k for k in jwks['keys'] if k['kid'] == header['kid']) claims = jwt.decode( id_token, jwt.algorithms.RSAAlgorithm.from_jwk(key), algorithms=['RS256'], audience='YOUR_CLIENT_ID', issuer='https://id.littlexlittle.org', ) assert claims['nonce'] == saved_nonce

4. Get the user profile

bash curl https://id.littlexlittle.org/oidc/userinfo \ -H "Authorization: Bearer $ACCESS_TOKEN"

5. Refresh

bash curl -X POST https://id.littlexlittle.org/oidc/token \ -d "grant_type=refresh_token" \ -d "refresh_token=$REFRESH" \ -d "client_id=YOUR_CLIENT_ID"

The response includes a new refresh_token — store it and discard the old one. Re-using an old refresh token revokes the entire chain.

Verifying in Node.js

```js import { jwtVerify, createRemoteJWKSet } from 'jose';

const JWKS = createRemoteJWKSet(new URL('https://id.littlexlittle.org/.well-known/jwks.json')); const { payload } = await jwtVerify(id_token, JWKS, { issuer: 'https://id.littlexlittle.org', audience: 'YOUR_CLIENT_ID', }); ```

Verifying in Python

```python from jose import jwt from jose.utils import base64url_decode import requests

jwks = requests.get('https://id.littlexlittle.org/.well-known/jwks.json').json() claims = jwt.decode( id_token, jwks, algorithms=['RS256'], audience='YOUR_CLIENT_ID', issuer='https://id.littlexlittle.org', ) ```

Verifying in Go

go provider, _ := oidc.NewProvider(ctx, "https://id.littlexlittle.org") verifier := provider.Verifier(&oidc.Config{ClientID: "YOUR_CLIENT_ID"}) idToken, _ := verifier.Verify(ctx, rawIDToken) var claims map[string]any idToken.Claims(&claims)