Get started
~15 minWorks with a new Flutter application or one you already have. Requires a running ThunderID instance; the steps use https://localhost:8090, the default for a local install. The scheme com.example.appauth is a placeholder: use your own, in lowercase, everywhere it appears. To start from working code instead, the ready-made sample app under Sample app below runs every step of this guide.
Create the application in ThunderID
A Flutter application is a public client: it has no client secret, and ThunderID requires PKCE for it. The authorization code and refresh token grants are already enabled.
Open the Console at https://localhost:8090/console and navigate to Applications
Click Add Application and select Flutter
Enter a name, select the sign-in options, and select a theme
Under Sign-In Approach, select Redirect to ThunderID Gate
Under Deep Link / Universal Link, enter com.example.appauth://callback
Click Create, then copy the Client ID
Select the redirect approach, not Bring Your Own UI. flutter_appauth signs users in through the system browser, so it needs the ThunderID-hosted sign-in pages.
Allow sign-out and user claims
Two settings on the new application. The post-logout redirect URI must match the one the application sends exactly. The attributes decide which claims reach the ID token: with none selected, only sub is returned, even when the profile and email scopes are granted.
On the Advanced tab, add com.example.appauth://logout under Post-Logout Redirect URIs and save
On the Token tab, open User, then ID Token, and add the attributes you need (for example email and name)
Add the same attributes under User Info Endpoint if you call the userinfo endpoint
Add flutter_appauth
Create a project first with flutter create, or skip that and add the plugin to the project you already have. The sample stores the refresh token with flutter_secure_storage; use any secure store you prefer.
flutter create --org com.example my_appcd my_appflutter pub add flutter_appauth flutter_secure_storage
Register the redirect scheme on Android
AppAuth's redirect activity receives the browser's callback through this scheme. It covers both the sign-in and the post-logout redirect, since they share it.
android {defaultConfig {manifestPlaceholders.putAll(mapOf("appAuthRedirectScheme" to "com.example.appauth"))}}
Keep the scheme in lowercase; the plugin's authors report failed redirects otherwise. On a Groovy build.gradle, use manifestPlaceholders += ['appAuthRedirectScheme': 'com.example.appauth'].
Let the redirect return to the application on Android
Recent flutter create templates set android:taskAffinity="" on MainActivity. That puts AppAuth's redirect handling in a separate task, so the browser returns to the application but sign-in never completes, and Logcat shows "No stored state - unable to handle response". Remove the attribute.
<activityandroid:name=".MainActivity"android:exported="true"android:launchMode="singleTop"android:theme="@style/LaunchTheme"...><!-- No android:taskAffinity="" here. --></activity>
Register the redirect scheme on iOS
Add the same scheme as a URL type of the Runner target.
<key>CFBundleURLTypes</key><array><dict><key>CFBundleTypeRole</key><string>Editor</string><key>CFBundleURLSchemes</key><array><string>com.example.appauth</string></array></dict></array>
Point the application at your ThunderID issuer
AppAuth reads every endpoint from the discovery document, so the issuer, the client ID, and the two redirect URIs are all the configuration there is. A refresh token is issued without the offline_access scope, so the scopes stay at the OpenID Connect basics.
class AuthConfig {static const issuer = 'https://localhost:8090';static const clientId = '<your-client-id>';static const discoveryUrl = '$issuer/.well-known/openid-configuration';static const redirectUrl = 'com.example.appauth://callback';static const postLogoutRedirectUrl = 'com.example.appauth://logout';static const scopes = ['openid', 'profile', 'email'];}
Sign in
One call opens the ThunderID sign-in page in the system browser (ASWebAuthenticationSession on iOS, a Custom Tab on Android), then exchanges the authorization code for tokens. AppAuth generates the PKCE verifier and S256 challenge itself; ThunderID supports only S256 and rejects requests without a challenge.
import 'package:flutter_appauth/flutter_appauth.dart';import 'package:flutter_secure_storage/flutter_secure_storage.dart';import 'auth_config.dart';const appAuth = FlutterAppAuth();const storage = FlutterSecureStorage();Future<TokenResponse?> signIn() async {try {final response = await appAuth.authorizeAndExchangeCode(AuthorizationTokenRequest(AuthConfig.clientId,AuthConfig.redirectUrl,discoveryUrl: AuthConfig.discoveryUrl,scopes: AuthConfig.scopes,),);await storage.write(key: 'refresh_token', value: response.refreshToken);return response;} on FlutterAppAuthUserCancelledException {return null; // The user closed the browser.}}
The response carries the access token, its expiry, the ID token, and the refresh token. AppAuth validates the ID token's issuer, audience, and nonce before returning it.
Refresh the tokens
Exchange the refresh token for new tokens before the access token expires. ThunderID rotates refresh tokens: each refresh revokes the one it used, and reusing a revoked token revokes the whole chain. Always store the token from the latest response.
Future<TokenResponse> refresh() async {final response = await appAuth.token(TokenRequest(AuthConfig.clientId,AuthConfig.redirectUrl,discoveryUrl: AuthConfig.discoveryUrl,refreshToken: await storage.read(key: 'refresh_token'),scopes: AuthConfig.scopes,),);await storage.write(key: 'refresh_token', value: response.refreshToken);return response;}
Rotation does not extend the session: a new refresh token keeps the original expiry, 24 hours by default. When a refresh fails, sign the user in again.
Sign out
Ending the session in the browser signs the user out of ThunderID as well as the application, so the next sign-in asks for credentials again. Passing the ID token as a hint lets ThunderID skip the confirmation prompt.
Future<void> signOut(String idToken) async {try {await appAuth.endSession(EndSessionRequest(idTokenHint: idToken,postLogoutRedirectUrl: AuthConfig.postLogoutRedirectUrl,discoveryUrl: AuthConfig.discoveryUrl,),);} finally {await storage.delete(key: 'refresh_token');}}
Trust the local certificate
A local ThunderID serves a self-signed certificate from <THUNDERID_HOME>/config/certs/server.cert. Both the system browser and AppAuth's own HTTP client reject it until the device trusts it, and flutter_appauth's allowInsecureConnections option does not help, since it only permits plain http. Run these once per emulator or simulator.
# Forward the emulator's localhost:8090 to your machine.adb reverse tcp:8090 tcp:8090# Copy the certificate, then install it on the emulator: Settings > Security & privacy ># More security & privacy > Encryption & credentials > Install a certificate > CA certificate,# and select thunderid-dev.crt from Downloads. Older Android versions list# Encryption & credentials directly under Settings > Security.adb push <THUNDERID_HOME>/config/certs/server.cert /sdcard/Download/thunderid-dev.crt
Development only. Android apps ignore user-installed certificates unless the debug configuration above opts in, and keeping it in the debug source set leaves release builds untouched. In production, serve ThunderID with a certificate from a public certificate authority and skip this step.
Sample app
A ready-made Flutter application that runs the code above, from the ThunderID samples repository at samples/integrations/flutter-app-auth. Register it in the Console, pass its client ID, and run it.
Endpoint mapping
Where each flutter_appauth call lands on ThunderID. AppAuth resolves every endpoint from the discovery document, so none of these paths appear in your code.
Requirements
Beyond this guide
Everything past this configuration is ordinary flutter_appauth and AppAuth, documented by their authors, plus the ThunderID application settings this guide touches.
Where to get help
Two projects, two places to ask.
Something wrong in this guide?
The guide and the ThunderID configuration it documents are ours, along with the sample application that goes with it.
Something wrong in flutter_appauth?
The plugin itself is maintained by its own authors. Bugs and feature requests belong on their tracker, not ours.