Cookbook: Flutter Integration
Practical guidance for integrating clients and enforcing secure access.
Cookbook: Integrate Fulan Auth in a Flutter App
This cookbook shows a step-by-step integration for a Flutter mobile app using Authorization Code + PKCE.
What you will build
- A "Sign in" button that opens the system browser to
/authorize - A deep link callback handler that receives
code+state - A token exchange call to
/token - A simple in-app session (store tokens securely)
Prerequisites
- A registered app in the Portal with:
client_id- a redirect URI registered (exact match)
- scope allowlist (example:
openid) - Auth Service base URL (examples assume
http://localhost:8080)
Step 1: Choose a redirect URI strategy
On mobile you typically use one of these:
- Custom scheme redirect (example:
com.example.app://auth/callback) - Universal Links (iOS) / App Links (Android) using
https://...
Custom scheme is the simplest to start with. Register your chosen redirect URI in the Portal.
Step 2: Add deep link handling to your app
Configure your app to receive the redirect URI.
Examples:
- Android: add an intent-filter for your custom scheme/host
- iOS: add URL types for your custom scheme
Once configured, your app should receive a callback like:
com.example.app://auth/callback?code=<auth_code>&state=<state>Step 3: Generate PKCE parameters
You need:
code_verifier: random string stored on device for the login attemptcode_challenge: base64url(SHA256(code_verifier))
You can implement PKCE yourself or use a small helper package. The important requirement is that code_verifier used at /token must match the one used to create the code_challenge.
Store code_verifier and state in memory until the callback returns.
Step 4: Build the /authorize URL and open it in the browser
Create the authorize URL with:
response_type=codeclient_idredirect_uriscopecode_challengecode_challenge_method=S256state
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>Open it using the platform browser (recommended), not an embedded WebView.
Step 5: Handle the callback and validate state
When the app receives the callback URL:
- Extract
codeandstate - Validate
statematches what you generated (CSRF protection) - If missing or mismatched, treat it as a failed login attempt
Step 6: Exchange the code for tokens
Call the token endpoint:
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>'In Flutter, send the same form values using your HTTP client.
Step 7: Store tokens securely
Recommended storage:
- iOS Keychain
- Android Keystore
Avoid storing tokens in plain shared preferences.
At minimum store:
access_tokenrefresh_token(if your app uses refresh)
Step 8: Call your backend / APIs with the access token
For API calls:
- Add
Authorization: Bearer <access_token> - Enforce authorization using scopes in the token’s
scopeclaim
Step 9: Refresh and logout
If you use refresh tokens:
- Refresh when access token is near expiration
- Store refresh token securely
Logout:
- Clear stored tokens
- Clear any pending login state (PKCE verifier, state)
Troubleshooting
- Redirect not returning to app: confirm deep link configuration and the exact registered redirect URI.
- Token exchange fails: ensure
code_verifiermatches the originalcode_challenge. - State mismatch: ensure you persist
stateuntil the callback returns. - Scope rejected: request only scopes that are in the app allowlist.