plugins/auth0/skills/auth0-flutter-native/SKILL.md
Use when adding Auth0 authentication to a Flutter mobile application (iOS/Android) — integrates the auth0_flutter SDK (native platform) for Web Auth login/logout via the system browser, with secure credential storage and biometric protection through the CredentialsManager.
npx skillsauth add auth0/agent-skills auth0-flutter-nativeInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
auth0_flutter is the official Auth0 SDK for Flutter applications. On mobile (iOS/Android), it performs authentication through the device's system browser (Web Auth / Universal Login) and stores tokens securely in the platform keychain/keystore via the built-in CredentialsManager, with optional biometric protection.
Agent instruction: Before providing SDK setup instructions, fetch the latest release version by running one of:
gh api repos/auth0/auth0-flutter/releases/latest --jq '.tag_name'flutter pub info auth0_flutter 2>/dev/null | head -5Or check pub.dev:
curl -s https://pub.dev/api/packages/auth0_flutter | python3 -c "import sys,json;print(json.load(sys.stdin)['latest']['version'])"Use the returned version in all dependency lines instead of any hardcoded version below. Current known version:
2.1.0.
Auth0Web) wrapping Auth0 SPA JSAgent instruction: Follow these steps in order. If you encounter an error at any step, attempt to fix it up to 5 times before calling
AskUserQuestionto ask the user for guidance. Always search existing code first — if there are existing login/logout handlers, hook into them rather than creating new ones.
Agent instruction: Check the project directory for
pubspec.yaml. If present, add the dependency. If not found, this is not a Flutter project — ask the user.Run in the project root:
flutter pub add auth0_flutterVerify the dependency was added to
pubspec.yaml:dependencies: auth0_flutter: ^2.1.0
Note: The Auth0 Domain and Client ID are public configuration (not secrets) — a native app uses PKCE with no client secret. Pass them directly to
Auth0(domain, clientId); there is no need to store them in environment variables or hide them.Agent instruction:
- If Auth0 credentials (domain AND client ID) are already in the user's prompt: Use those values directly in the
Auth0(...)constructor and proceed to Step 3.- If no credentials are provided: Ask the user which setup they prefer using
AskUserQuestion: "How would you like to set up the Auth0 application — automatic (I run the Auth0 CLI to create it) or manual (you create it in the Auth0 Dashboard and give me the Domain + Client ID)?"
- Automatic: Follow the Auth0 CLI steps in the Setup Guide to create the Native application.
- Manual: Ask the user for their Auth0 Domain and Client ID and use them directly.
Follow Setup Guide — Auth0 Configuration for the pre-flight checks and the
auth0 apps createcommand.
Agent instruction: Edit
android/app/build.gradle(orbuild.gradle.kts) and addmanifestPlaceholdersinsideandroid { defaultConfig { ... } }. These supply the callback URL the SDK'sRedirectActivityintent filter registers — without them the app will not build correctly for Auth0.
For android/app/build.gradle (Groovy):
android {
defaultConfig {
manifestPlaceholders = [auth0Domain: "YOUR_AUTH0_DOMAIN", auth0Scheme: "https"]
}
}
For android/app/build.gradle.kts (Kotlin DSL):
android {
defaultConfig {
manifestPlaceholders["auth0Domain"] = "YOUR_AUTH0_DOMAIN"
manifestPlaceholders["auth0Scheme"] = "https"
}
}
Agent instruction: Use
auth0Scheme: "https"to use Android App Links (recommended). If the app targets a custom scheme instead, set it to a lowercase scheme string and pass the same scheme towebAuthentication(scheme: ...)in Dart. See Setup Guide for details.
Agent instruction: For the default HTTPS (Universal Link) flow on iOS 17.4+, no
Info.plistchange is required, but the Associated Domains capability must be added in Xcode (webcredentials:YOUR_AUTH0_DOMAIN). For older iOS or a custom URL scheme, add aCFBundleURLTypesentry toios/Runner/Info.plist:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>None</string>
<key>CFBundleURLName</key>
<string>auth0</string>
<key>CFBundleURLSchemes</key>
<array>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
</array>
</dict>
</array>
Agent instruction: Register the platform-specific callback and logout URLs using the Auth0 CLI. Determine the Android package name (from
android/app/build.gradleapplicationId) and the iOS bundle identifier (from Xcode /PRODUCT_BUNDLE_IDENTIFIER), then run the command below, replacing the placeholders (CLIENT_ID,YOUR_DOMAIN,ANDROID_PACKAGE_NAME,IOS_BUNDLE_ID) with the project's values:auth0 apps update CLIENT_ID \ --callbacks "https://YOUR_DOMAIN/android/ANDROID_PACKAGE_NAME/callback,https://YOUR_DOMAIN/ios/IOS_BUNDLE_ID/callback" \ --logout-urls "https://YOUR_DOMAIN/android/ANDROID_PACKAGE_NAME/callback,https://YOUR_DOMAIN/ios/IOS_BUNDLE_ID/callback" \ --no-input
The callback URL formats are:
https://YOUR_DOMAIN/android/YOUR_PACKAGE_NAME/callbackhttps://YOUR_DOMAIN/ios/YOUR_BUNDLE_ID/callbackAgent instruction: Search the project for the main app entry point (
main.dart). Determine the state management approach:
- Look for
provider,riverpod,bloc,GetX, ormobximports- If none found, use basic
StatefulWidgetwithsetStateThen follow only the matching path below. If ambiguous, ask via
AskUserQuestion: "Which state management approach does your Flutter app use — Provider, Riverpod, Bloc, or basic setState?"
Agent instruction: Create an
AuthServiceclass, then wire it into the app's root widget. Search for theMaterialApporCupertinoAppwidget and update accordingly. On startup, restore the session from theCredentialsManagercache.
// lib/auth_service.dart
import 'package:auth0_flutter/auth0_flutter.dart';
class AuthService {
late final Auth0 _auth0;
Credentials? _credentials;
AuthService({required String domain, required String clientId}) {
_auth0 = Auth0(domain, clientId);
}
bool get isAuthenticated => _credentials != null;
UserProfile? get user => _credentials?.user;
/// Restore a stored session on app startup, if one exists.
Future<void> init() async {
final hasValid = await _auth0.credentialsManager.hasValidCredentials();
if (hasValid) {
_credentials = await _auth0.credentialsManager.credentials();
}
}
/// Launch Web Auth via the system browser. Tokens are stored automatically.
Future<void> login() async {
_credentials = await _auth0
.webAuthentication()
.login(scopes: {'openid', 'profile', 'email', 'offline_access'});
}
/// Clear the session in the browser and wipe stored credentials.
Future<void> logout() async {
await _auth0.webAuthentication().logout();
await _auth0.credentialsManager.clearCredentials();
_credentials = null;
}
}
// lib/main.dart
import 'package:flutter/material.dart';
import 'package:auth0_flutter/auth0_flutter.dart'; // for WebAuthenticationException
import 'auth_service.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final _authService = AuthService(
domain: 'YOUR_AUTH0_DOMAIN',
clientId: 'YOUR_AUTH0_CLIENT_ID',
);
bool _isLoading = true;
@override
void initState() {
super.initState();
_initAuth();
}
Future<void> _initAuth() async {
await _authService.init();
setState(() => _isLoading = false);
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: _isLoading
? const Scaffold(body: Center(child: CircularProgressIndicator()))
: _authService.isAuthenticated
? HomeScreen(authService: _authService, onChanged: _refresh)
: LoginScreen(authService: _authService, onChanged: _refresh),
);
}
void _refresh() => setState(() {});
}
class LoginScreen extends StatelessWidget {
final AuthService authService;
final VoidCallback onChanged;
const LoginScreen({super.key, required this.authService, required this.onChanged});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ElevatedButton(
onPressed: () async {
// Capture the messenger before the await to avoid using
// BuildContext across an async gap.
final messenger = ScaffoldMessenger.of(context);
try {
await authService.login();
onChanged();
} on WebAuthenticationException catch (e) {
messenger.showSnackBar(
SnackBar(content: Text('Login failed: ${e.message}')),
);
}
},
child: const Text('Log In'),
),
),
);
}
}
class HomeScreen extends StatelessWidget {
final AuthService authService;
final VoidCallback onChanged;
const HomeScreen({super.key, required this.authService, required this.onChanged});
@override
Widget build(BuildContext context) {
final user = authService.user;
return Scaffold(
appBar: AppBar(
title: const Text('Home'),
actions: [
IconButton(
onPressed: () async {
await authService.logout();
onChanged();
},
icon: const Icon(Icons.logout),
),
],
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (user?.pictureUrl != null)
CircleAvatar(
backgroundImage: NetworkImage(user!.pictureUrl.toString()),
radius: 40,
),
const SizedBox(height: 16),
Text('Welcome, ${user?.name ?? 'User'}!'),
Text(user?.email ?? ''),
],
),
),
);
}
}
Agent instruction: If the project uses
provider, createAuthServiceas aChangeNotifierand inject it viaChangeNotifierProviderat the app root.
// lib/auth_service.dart
import 'package:flutter/foundation.dart';
import 'package:auth0_flutter/auth0_flutter.dart';
class AuthService extends ChangeNotifier {
late final Auth0 _auth0;
Credentials? _credentials;
bool _isLoading = true;
AuthService({required String domain, required String clientId}) {
_auth0 = Auth0(domain, clientId);
}
bool get isAuthenticated => _credentials != null;
bool get isLoading => _isLoading;
UserProfile? get user => _credentials?.user;
Future<void> init() async {
if (await _auth0.credentialsManager.hasValidCredentials()) {
_credentials = await _auth0.credentialsManager.credentials();
}
_isLoading = false;
notifyListeners();
}
Future<void> login() async {
_credentials = await _auth0
.webAuthentication()
.login(scopes: {'openid', 'profile', 'email', 'offline_access'});
notifyListeners();
}
Future<void> logout() async {
await _auth0.webAuthentication().logout();
await _auth0.credentialsManager.clearCredentials();
_credentials = null;
notifyListeners();
}
}
// lib/main.dart — wrap with ChangeNotifierProvider
import 'package:provider/provider.dart';
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => AuthService(
domain: 'YOUR_AUTH0_DOMAIN',
clientId: 'YOUR_AUTH0_CLIENT_ID',
)..init(),
child: const MyApp(),
),
);
}
For complete patterns with Riverpod, Bloc, biometrics, and advanced scenarios, see Integration Patterns.
Agent instruction: Run a build to verify the integration compiles without errors:
flutter build apk --debug # Android flutter build ios --no-codesign # iOS (on macOS)Then run the app on a device or emulator to test:
flutter runIf the build fails, review error messages and fix up to 5 times before asking the user.
Physical device testing: Biometric protection (Face ID / Touch ID / fingerprint) cannot be exercised on a simulator/emulator — the iOS Simulator and Android emulator have limited or no biometric hardware. Test biometrics and the full Universal Login redirect on a real physical device before release.
manifestPlaceholders, iOS Info.plist / Associated Domains, callback URL registration| Mistake | Fix |
|---------|-----|
| Auth0 app type not set to Native | Create the application with auth0 apps create --type native (or select "Native" in the Auth0 Dashboard) |
| Missing manifestPlaceholders on Android | Add manifestPlaceholders = [auth0Domain: "...", auth0Scheme: "https"] to android/app/build.gradle defaultConfig — the build fails without it |
| Using Auth0Web on mobile | Mobile uses the Auth0 class with webAuthentication(), not Auth0Web (that's the web-only API) |
| Importing auth0_flutter_web.dart on mobile | Only import package:auth0_flutter/auth0_flutter.dart — the _web import is for Flutter web |
| Callback URL mismatch | Register https://YOUR_DOMAIN/android/PACKAGE_NAME/callback and https://YOUR_DOMAIN/ios/BUNDLE_ID/callback in Allowed Callback URLs |
| Scheme mismatch between Gradle and Dart | If auth0Scheme is a custom scheme, pass the same value to webAuthentication(scheme: 'myscheme') |
| Custom scheme with uppercase letters on Android | Android custom schemes must be all lowercase |
| Biometrics prompt never appears on Android | MainActivity must extend FlutterFragmentActivity (not FlutterActivity) for the biometric prompt to work |
| Not storing credentials after login | webAuthentication().login() stores credentials automatically; do NOT also re-store unless renewing manually via api.renewCredentials |
| Not restoring session on startup | Call credentialsManager.hasValidCredentials() + credentials() in initState() to restore the session |
| Missing offline_access scope | Add 'offline_access' to scopes so the CredentialsManager can silently renew expired access tokens with a refresh token |
| Catching generic Exception | Catch WebAuthenticationException (login/logout) and CredentialsManagerException (credential errors) and inspect isUserCancelledException, isNoCredentialsFound, isTokenRenewFailed, etc. |
| API | Purpose |
|-----|---------|
| Auth0(domain, clientId) | Create the SDK client |
| auth0.webAuthentication().login(...) | Launch Universal Login in the system browser |
| auth0.webAuthentication().logout() | Clear the browser session |
| auth0.webAuthentication(scheme: '...') | Use a custom URL scheme |
| auth0.credentialsManager.credentials() | Get stored credentials (auto-renews if expired) |
| auth0.credentialsManager.hasValidCredentials() | Check for a valid stored session |
| auth0.credentialsManager.clearCredentials() | Wipe stored credentials |
| LocalAuthentication(title: ...) | Enable biometric protection of stored credentials |
development
Use when adding login, logout, and user profile to a Laravel web application using session-based authentication - integrates auth0/login (laravel-auth0) for guard-based auth with auto-registered routes.
tools
Use when securing Laravel API endpoints with JWT Bearer token validation, scope/permission checks, or stateless auth - integrates auth0/login (laravel-auth0) with the AuthorizationGuard for REST APIs receiving access tokens from SPAs, mobile apps, or other clients. Triggers on: Laravel API auth, auth0.authorizer, AuthorizationGuard, Laravel JWT, stateless Bearer.
development
Use when adding Auth0 authentication to a Flutter web application — integrates the auth0_flutter SDK (web platform) for browser-based authentication using redirect login, popup login, and credential caching.
tools
Use when adding Auth0 authentication to Windows Forms (WinForms) desktop applications - integrates Auth0.OidcClient.WinForms NuGet package for native login, logout, token refresh, and user profile. Trigger on WinForms authentication, add login to WinForms, Auth0 WinForms, .NET Windows Forms auth, Windows desktop auth