base_idp 0.1.0
base_idp: ^0.1.0 copied to clipboard
Official Flutter and Dart SDK for Base IdP OAuth PKCE login and server handoff.
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.