OIDC + PKCE Integration
Practical guidance for integrating clients and enforcing secure access.
OIDC + PKCE Integration
This guide shows how to integrate a public client (SPA/mobile/server-rendered web) with the Fulan Auth Service using an OAuth2/OIDC-style flow:
- OIDC Discovery + JWKS (for standard client configuration and JWT verification)
- Authorization Code + PKCE (recommended for public clients)
- Optional OTP identifier verification (policy-driven per app)
Quick checklist
Before starting:
- You have an app
client_idcreated in the Portal. - Your
redirect_uriis registered exactly (string match). - You know the Auth Service base URL (examples below assume
http://localhost:8080).
Endpoints you will use
| Purpose | Endpoint |
|---|---|
| OIDC discovery | GET /.well-known/openid-configuration |
| JWKS | GET /.well-known/jwks.json |
| Start authorization | GET /authorize |
| Exchange code for tokens | POST /token |
| Send OTP (if required) | POST /otp/send |
| Verify OTP (if required) | POST /otp/verify |
1) Discovery + JWKS
Discovery helps your client find URLs like the authorization endpoint and token endpoint.
curl -sS http://localhost:8080/.well-known/openid-configurationJWKS is used to validate JWT signatures (select the key by kid).
curl -sS http://localhost:8080/.well-known/jwks.json2) Generate PKCE parameters
You will generate:
code_verifier: high-entropy random string (kept client-side)code_challenge: base64url(SHA256(code_verifier))
Example (bash + openssl):
code_verifier="$(openssl rand -base64 32 | tr '+/' '-_' | tr -d '=' | tr -d '\n')"
code_challenge="$(printf '%s' "$code_verifier" | openssl dgst -sha256 -binary | openssl base64 -A | tr '+/' '-_' | tr -d '=')"
echo "code_verifier=$code_verifier"
echo "code_challenge=$code_challenge"3) (If required) verify identifier via OTP
Some apps require a verified identifier before tokens are issued. If the app is configured with requireVerifiedIdentifiers=true, do this step before starting /authorize.
Send OTP:
curl -sS -X POST http://localhost:8080/otp/send \
-H 'content-type: application/json' \
-d '{"client_id":"<client_id>","identifier_type":"email","identifier_value":"user@example.com"}'Verify OTP:
curl -sS -X POST http://localhost:8080/otp/verify \
-H 'content-type: application/json' \
-d '{"client_id":"<client_id>","otp_id":"<otp_id>","code":"<code>"}'4) Request an authorization code
Call /authorize with these parameters:
response_type=codeclient_idredirect_uri(must match a registered redirect URI exactly)scope(space-separated; must be allowed by the app scope allowlist)code_challengeandcode_challenge_method=S256- Recommended:
state(random string; protect against CSRF) - If
requireVerifiedIdentifiers=true, includeidentifier_typeandidentifier_value
Example:
GET http://localhost:8080/authorize?response_type=code&client_id=<client_id>&redirect_uri=<redirect_uri>&scope=openid&code_challenge=<code_challenge>&code_challenge_method=S256&state=<state>&identifier_type=email&identifier_value=user@example.comOn success, the service redirects (302) to:
<redirect_uri>?code=<auth_code>&state=<state>Verify that the returned state matches what you sent.
5) Exchange the code for tokens
Use the saved code_verifier from step 2.
curl -sS -X POST http://localhost:8080/token \
-H 'content-type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=authorization_code' \
--data-urlencode 'client_id=<client_id>' \
--data-urlencode 'code=<auth_code>' \
--data-urlencode 'redirect_uri=<redirect_uri>' \
--data-urlencode 'code_verifier=<code_verifier>'The response contains at least:
access_tokenrefresh_token
Token validation (access_token)
When validating the access token:
- Verify signature using JWKS (
kidin header selects the key). - Validate
issis exactly the expected issuer (example:http://localhost:8080). - Validate
audequals the appclient_id. - Validate
expis in the future. - Use the
scopeclaim for authorization decisions (split by spaces).
Common pitfalls
- Redirect issues:
redirect_urimust be an exact match to the registered URI (no extra slashes, different ports, or missing path). - Scope rejected: requested scopes must be in the app’s allowlist.
- PKCE mismatch: use the same
code_verifierthat generatedcode_challenge. - OTP required: if
requireVerifiedIdentifiers=true, you must verify before/authorizesucceeds.