---
title: OAuth 2.0 from scratch
---

# 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)
```
