Base IdP Flutter/Dart SDK
Official Flutter and Dart SDK for Base IdP authentication.
Base IdP owns identity. Your app owns product data. This SDK connects a Flutter or Dart app to the hosted Base IdP authorization UI, discovers the client configuration from Base IdP, runs OAuth authorization-code flow with PKCE, fetches the authenticated principal, and returns a server handoff payload that your backend can verify and merge with its own user records.
What This SDK Solves
Most apps should not rebuild login screens, password handling, magic links, or identity verification. Base IdP handles that centrally.
Your Flutter app:
- Provides its Base IdP client ID.
- Opens the hosted Base IdP auth interface.
- Receives a deep-link callback.
- Exchanges the code with PKCE.
- Gets a Base identity payload.
- Sends that payload to your backend.
Your backend:
- Verifies the Base token.
- Finds or creates the product user by stable Base ID and/or email.
- Returns product-specific data, roles, projects, billing, teams, and settings.
That keeps identity portable across product surfaces while leaving every product in control of its own database.
Install
From pub.dev:
flutter pub add base_idp
Or add it manually:
dependencies:
base_idp: ^0.1.0
For local SDK development before publishing:
dependencies:
base_idp:
path: /Users/ajmaljs/Developer/Square/products/base-idp/sdk/dart
Then run:
flutter pub get
Keys And Environments
For a Flutter mobile app, use only:
BASE_IDP_CLIENT_ID
Do not put BASE_IDP_CLIENT_SECRET in Flutter, iOS, Android, web, or desktop
client binaries. Mobile apps are public OAuth clients. They must use PKCE.
Server-side Dart can use:
BASE_IDP_CLIENT_ID
BASE_IDP_CLIENT_SECRET
The SDK also accepts the legacy BASE_IDP_SECRET value as a fallback for
server-side Dart, but new apps should use BASE_IDP_CLIENT_SECRET.
Do not duplicate these in app env files:
- Scopes
- Audience
- Redirect URI lists
- Allowed origins
- Auth methods
- Requested claims
Those belong in the Base IdP client registration. The SDK discovers them from:
POST /v1/client-config
Issuer
The SDK uses the production Base IdP issuer:
https://authlayer.squareexp.com
Run the app with the client ID:
flutter run --release \
--dart-define=BASE_IDP_CLIENT_ID=sq_live_mobile_xxxxx
Register A Mobile Client In Base IdP
Create a public mobile client in Base IdP. The client must not be confidential. If the SDK discovers that the client is confidential, mobile login fails with:
confidential_client_in_mobile
Register the exact redirect URI your app will receive:
com.example.app://auth/callback
The redirect URI must match exactly. These are different redirect URIs:
com.example.app://auth/callback
com.example.app:/auth/callback
com.example.app://callback
Flutter Login
import 'package:base_idp/base_idp.dart';
final auth = BaseIdpFlutterAuth.fromEnvironment(
redirectUri: 'com.example.app://auth/callback',
);
Future<void> signIn() async {
try {
final session = await auth.login();
print(session.principal.id);
print(session.principal.email);
print(session.principal.name);
final payload = session.toServerPayload().toJson();
await sendIdentityToBackend(payload);
} on BaseIdpException catch (error) {
// Show a clear product-specific UI message.
print('${error.code}: ${error.message}');
}
}
Future<void> sendIdentityToBackend(Map<String, Object?> payload) async {
// POST this payload to your backend login/session endpoint.
}
The BaseIdpMobileSession contains:
session.tokens.accessToken;
session.tokens.refreshToken;
session.tokens.tokenType;
session.tokens.expiresIn;
session.principal.id;
session.principal.subject;
session.principal.email;
session.principal.name;
session.principal.role;
session.principal.accountContext;
session.callback.code;
session.callback.state;
Use session.toServerPayload() when sending identity proof to a backend.
Platform Deep Links
The SDK uses app_links to receive the OAuth callback.
Android
Add an intent filter for your redirect scheme in android/app/src/main/AndroidManifest.xml:
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="com.example.app"
android:host="auth"
android:path="/callback" />
</intent-filter>
</activity>
This matches:
com.example.app://auth/callback
iOS
Add a URL type in ios/Runner/Info.plist:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>com.example.app</string>
<key>CFBundleURLSchemes</key>
<array>
<string>com.example.app</string>
</array>
</dict>
</array>
Then use a redirect URI that starts with that scheme:
com.example.app://auth/callback
Universal links and Android app links are also valid, but they must be registered exactly in Base IdP.
Manual Callback Handling
If your app already has a router that receives app links, you can parse and complete the callback yourself.
final auth = BaseIdpFlutterAuth.fromEnvironment(
redirectUri: 'com.example.app://auth/callback',
);
final callback = auth.parseCallback(incomingUri);
final session = await auth.completeCallback(
incomingUri,
codeVerifier: storedCodeVerifier,
);
Use this only if your app owns the PKCE transaction state itself. The normal
login() method handles state, PKCE, browser launch, callback waiting, token
exchange, and /v1/me for you.
Direct Dart Client
Use BaseIdpClient when you need lower-level control or server-side Dart.
import 'package:base_idp/base_idp.dart';
final client = BaseIdpClient.fromEnvironment(
redirectUri: 'com.example.app://auth/callback',
);
final config = await client.resolveConfig();
print(config.clientId);
print(config.displayName);
print(config.allowedRedirectUris);
print(config.allowedScopes);
print(config.allowedAuthMethods);
Build an authorize URL manually:
final pkce = BaseIdpPkcePair.generate();
final state = randomBaseIdpState();
final authorizeUri = await client.authorizeUri(
redirectUri: 'com.example.app://auth/callback',
state: state,
pkce: pkce,
);
Exchange a code:
final tokens = await client.exchangeCode(
code: code,
redirectUri: 'com.example.app://auth/callback',
codeVerifier: pkce.verifier,
);
final principal = await client.me(tokens.accessToken);
Refresh tokens:
final refreshed = await client.refresh(
refreshToken: tokens.refreshToken,
);
Server Handoff Pattern
Mobile should not be the final authorization boundary. Use mobile login to get identity proof, then let your backend create the product session.
Flutter:
final session = await auth.login();
await http.post(
Uri.parse('https://api.example.com/auth/base-idp/session'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode(session.toServerPayload().toJson()),
);
Backend responsibilities:
- Verify the PASETO access token with the Base IdP public key.
- Read the principal (
gid,sub,email,name,ctx,role,amr). - Find or create the product user by Base
gid. - If older data exists by email, link it to the Base
gid. - Return the product session and product-specific user schema.
Do not rely on the Flutter app to assign product permissions. The mobile app can submit identity proof; the backend decides product authorization.
Error Handling
All SDK errors use BaseIdpException.
try {
final session = await auth.login();
} on BaseIdpException catch (error) {
switch (error.code) {
case 'invalid_config':
// BASE_IDP_CLIENT_ID is missing.
break;
case 'config_discovery_failed':
// Client id is wrong, inactive, or the issuer is wrong.
break;
case 'confidential_client_in_mobile':
// Use a public mobile client, not a confidential backend client.
break;
case 'browser_launch_failed':
// The system browser could not be opened.
break;
case 'callback_timeout':
// Browser opened, but no matching app callback arrived.
break;
case 'oauth_callback_error':
// Base IdP returned error/error_description in callback.
break;
case 'token_exchange_failed':
// Code, redirect URI, client auth, or PKCE verification failed.
break;
case 'principal_fetch_failed':
// /v1/me rejected the access token.
break;
default:
// Product fallback error UI.
break;
}
}
Never log:
- Access tokens
- Refresh tokens
- Client secrets
- Authorization codes
- PKCE verifiers
- Full callback URLs
Integration Checklist
Run the Flutter app with a production Base IdP client:
flutter run \
--dart-define=BASE_IDP_CLIENT_ID=sq_live_mobile_xxxxx
Verify:
- The client ID exists in Base IdP.
- The redirect URI is registered exactly.
- The client is public for mobile usage.
- The hosted auth UI opens.
- Login returns to the app.
- The code exchange succeeds.
/v1/mereturns the expected Base identity.- The backend can merge the identity into the product user model.
Package Quality Checks
Run these before committing SDK changes:
dart format .
flutter analyze
flutter test
dart pub publish --dry-run
Expected package structure:
lib/base_idp.dart
lib/src/client.dart
lib/src/config.dart
lib/src/errors.dart
lib/src/mobile_auth.dart
lib/src/pkce.dart
lib/src/types.dart
test/base_idp_test.dart
Design Rules
- Use OAuth authorization-code flow with PKCE.
- Do not add implicit flow.
- Do not embed client secrets in Flutter apps.
- Do not hardcode scopes or audience in the mobile app unless you are doing a temporary compatibility test.
- Let Base IdP own identity policy.
- Let the product backend own product data and authorization.
- Prefer
/v1/meand backend verification over trusting locally decoded token content. - Keep token storage minimal. If refresh tokens must persist, use platform secure storage and document the risk.
- Merge users by stable Base
gid, with email as a migration/linking fallback.
Common Problems
BASE_IDP_CLIENT_ID is required
Pass the client ID at build/run time:
flutter run --dart-define=BASE_IDP_CLIENT_ID=sq_live_mobile_xxxxx
invalid_redirect_uri
The callback URI used by the app is not registered exactly in Base IdP. Register the exact value, including scheme, host, path, and port when applicable.
invalid_client
The client ID is wrong, inactive, pointed at the wrong issuer, or belongs to another environment.
confidential_client_in_mobile
You are trying to use a confidential client in a mobile app. Create a public mobile client in Base IdP.
token_exchange_failed
Check that:
- The authorization code was not reused.
- The
redirectUriin the token exchange exactly matches authorization. - The PKCE verifier matches the original challenge.
- The app is using the same issuer during start and callback.
License
See LICENSE.