A Flutter SDK for seamless integration with BPS (Badan Pusat Statistik) SSO authentication system using Chrome Custom Tabs. Features ISO 27001 compliant security, customizable UI, and enhanced error handling for government and enterprise applications.
- Features
- Architecture
- Installation
- Setup
- Usage
- Security Configuration
- UI Customization
- Configuration Options
- Enhanced Configuration Screen
- OAuth2 Parameters
- Error Handling
- Troubleshooting
- Best Practices
- API Reference
- Requirements
- Contributing
- Workflows
- ✅ ISO 27001 Compliant - Enterprise-grade security for government use
- ✅ Enhanced PKCE Implementation - RFC 7636 compliant with entropy validation
- ✅ Certificate Pinning - SSL/TLS security for government endpoints
- ✅ Runtime Security Checks - Debug and root detection capabilities
- ✅ Secure Token Storage - Encrypted token storage with memory protection
- ✅ Audit Logging - Comprehensive security event logging
- ✅ Privacy Mode - Optional data obscuring for sensitive information
- ✅ Chrome Custom Tabs Customization - Full UI theming and branding control
- ✅ Government Branding Presets - Pre-configured BPS visual identity
- ✅ Security-Focused UI Options - Disable sharing, bookmarks for secure environments
- ✅ Custom Menu Items - Add help, security info, and custom actions
- ✅ Authenticated Image Loading - Secure image loading with access tokens
- ✅ Chrome Custom Tabs Authentication - Better user experience than WebView
- ✅ OAuth2/OpenID Connect - Secure authentication flow with PKCE
- ✅ Multi-realm Support - Internal and External BPS realms with different user structures
- ✅ Type-Safe Configuration - Typed enums for response types, scopes, and PKCE method
- ✅ Token Management - Automatic token refresh and validation
- ✅ Comprehensive Error Handling - Production-safe error sanitization
- ✅ Deep Link Integration - Seamless OAuth callback handling
- ✅ HTTP Interceptors - Dio interceptor support for request/response modification
- ✅ Authentication Callbacks - Comprehensive callback system for auth events
- ✅ SDK Version Exposure - Access to SDK version info for client applications
- ✅ HTTP Request Debugging - Built-in Alice integration for network inspection
- ✅ Platform-Specific Optimization - iOS-only Custom Tabs management
- ✅ Enhanced Example App - Comprehensive demo with all SDK features
Add this to your package's pubspec.yaml file:
dependencies:
bps_sso_sdk: ^1.4.0Add the required dependencies to your pubspec.yaml:
dependencies:
bps_sso_sdk: ^1.4.0
app_links: ^6.3.2 # For deep link handling
# OR if using auto_route for navigation
auto_route: ^10.1.2Add the following to your android/app/src/main/AndroidManifest.xml:
Click to expand Android configuration
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Permissions for SSO authentication -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application>
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme">
<!-- Regular launch intent -->
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<!-- Deep link intent filters for SSO callback -->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- Replace 'your-app' with your actual app name -->
<data android:scheme="id.go.bps" android:host="your-app-sso-internal" />
</intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- Replace 'your-app' with your actual app name -->
<data android:scheme="id.go.bps" android:host="your-app-sso-eksternal" />
</intent-filter>
</activity>
</application>
</manifest>Example configuration:
<!-- Replace 'your-app' with your actual app name -->
<data android:scheme="id.go.bps" android:host="your-app-sso-internal" />
<data android:scheme="id.go.bps" android:host="your-app-sso-eksternal" />Add the following to your ios/Runner/Info.plist:
Click to expand iOS configuration
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>BPS SSO</string>
<key>CFBundleURLSchemes</key>
<array>
<string>id.go.bps</string>
</array>
</dict>
</array>Choose one of the following deep link handling approaches:
Click to expand app_links implementation
import 'package:bps_sso_sdk/bps_sso_sdk.dart';
import 'package:app_links/app_links.dart';
class MyApp extends StatefulWidget {
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final _appLinks = AppLinks();
@override
void initState() {
super.initState();
_initializeSSO();
}
Future<void> _initializeSSO() async {
// Configure BPS SSO SDK
final config = BPSSsoConfig.create(
appName: 'myapp', // Creates redirect URIs like 'id.go.bps://myapp-sso-internal'
internalClientId: 'your-internal-client-id',
externalClientId: 'your-external-client-id',
responseTypes: [BPSOAuthResponseType.code],
scopes: [BPSOAuthScope.openid, BPSOAuthScope.profile, BPSOAuthScope.email],
codeChallengeMethod: BPSCodeChallengeMethod.s256,
);
// Optional: Debug incoming deep links
_appLinks.stringLinkStream.listen((String link) {
debugPrint('🔗 Received deep link: $link');
});
// Initialize the SDK
BPSSsoClient.instance.initialize(
config: config,
linkStream: _appLinks.stringLinkStream,
);
}
}Click to expand auto_route implementation
import 'package:bps_sso_sdk/bps_sso_sdk.dart';
import 'package:auto_route/auto_route.dart';
// First, set up auto_route with deep link handling
@AutoRouterConfig()
class AppRouter extends _$AppRouter {
@override
RouteInformation? matcher(String location) {
// Handle BPS SSO deep links
final uri = Uri.parse(location);
if (uri.scheme == 'id.go.bps' &&
(uri.host == 'myapp-sso-internal' || uri.host == 'myapp-sso-eksternal')) {
// This is an SSO callback, let the SDK handle it
return null; // Or handle routing to login completion page
}
return super.matcher(location);
}
@override
List<AutoRoute> get routes => [
// Your app routes
AutoRoute(page: LoginRoute.page, path: '/login', initial: true),
AutoRoute(page: HomeRoute.page, path: '/home'),
];
}
class MyApp extends StatefulWidget {
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final _appRouter = AppRouter();
@override
void initState() {
super.initState();
_initializeSSO();
}
Future<void> _initializeSSO() async {
// Configure BPS SSO SDK
final config = BPSSsoConfig.create(
appName: 'myapp',
internalClientId: 'your-internal-client-id',
externalClientId: 'your-external-client-id',
);
// Create a stream controller for deep links
final linkController = StreamController<String>.broadcast();
// Listen to auto_route deep links and forward to SDK
_appRouter.urlFor(context, '/').listen((String url) {
final uri = Uri.parse(url);
if (uri.scheme == 'id.go.bps') {
linkController.add(url);
}
});
// Initialize the SDK
BPSSsoClient.instance.initialize(
config: config,
linkStream: linkController.stream,
);
}
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: _appRouter.config(),
);
}
}class LoginPage extends StatelessWidget {
Future<void> _loginInternal(BuildContext context) async {
try {
final user = await BPSSsoClient.instance.login(
context: context,
realmType: BPSRealmType.internal,
);
print('Logged in: ${user.displayName}');
print('NIP: ${user.nip}');
print('Organization: ${user.organization}');
// Navigate to main app
} catch (e) {
if (e is AuthenticationCancelledException) {
print('User cancelled authentication');
} else if (e is NetworkException) {
print('Network error: ${e.message}');
} else {
print('Authentication failed: $e');
}
}
}
Future<void> _loginExternal(BuildContext context) async {
try {
final user = await BPSSsoClient.instance.login(
context: context,
realmType: BPSRealmType.external,
);
// Handle external user login
} catch (e) {
// Handle error
}
}
}// Check if token needs refresh
if (BPSSsoClient.instance.shouldRefreshToken(user)) {
try {
final updatedUser = await BPSSsoClient.instance.refreshToken(user);
// Save updated user
} catch (e) {
// Token refresh failed, redirect to login
}
}
// Validate token
final isValid = await BPSSsoClient.instance.validateToken(user);
if (!isValid) {
// Redirect to login
}
// Logout
await BPSSsoClient.instance.logout(user);Access SDK version and metadata for client applications:
import 'package:bps_sso_sdk/bps_sso_sdk.dart';
// Get SDK version
String version = BPSSsoSdkInfo.version; // e.g., "1.1.0"
// Get SDK name
String name = BPSSsoSdkInfo.name; // "BPS SSO SDK"
// Get full SDK info
String fullInfo = BPSSsoSdkInfo.fullInfo; // "BPS SSO SDK v1.1.0"
// Get detailed metadata
Map<String, String> details = BPSSsoSdkInfo.details;
print('Version: ${details['version']}');
print('Description: ${details['description']}');
print('Homepage: ${details['homepage']}');// Access user information
print('ID: ${user.id}');
print('Username: ${user.username}');
print('Full Name: ${user.fullName}');
print('Email: ${user.email}');
print('NIP: ${user.nip}');
print('Position: ${user.position}');
print('Organization: ${user.organization}');
print('Region: ${user.region}');
print('Province: ${user.province}');
print('Realm: ${user.realmDisplayName}');
print('Photo URL: ${user.photo}');
print('Office Address: ${user.address}');
print('Rank: ${user.rank}');
print('Old NIP: ${user.oldNip}');
print('First Name: ${user.firstName}');
print('Last Name: ${user.lastName}');
// Check user type
if (user.isInternal) {
print('Internal BPS employee');
} else if (user.isExternal) {
print('External BPS user');
}
// Check authentication status
if (user.isTokenExpired) {
print('Token has expired');
} else {
print('Token is valid until: ${user.tokenExpiry}');
}
// Check if user has photo
if (user.hasPhoto) {
print('User has profile photo');
}
// Get user initials for avatar
Text(user.initials); // e.g., "JD" for John DoeUse the factory method for standard BPS SSO setup:
final config = BPSSsoConfig.create(
appName: 'myapp', // Your app identifier
internalClientId: 'your-internal-client-id',
externalClientId: 'your-external-client-id',
);This automatically creates:
- Internal redirect URI:
id.go.bps://myapp-sso-internal - External redirect URI:
id.go.bps://myapp-sso-eksternal - Standard BPS realms:
pegawai-bps(internal) andeksternal(external)
final config = BPSSsoConfig.create(
appName: 'myapp',
internalClientId: 'your-internal-client-id',
externalClientId: 'your-external-client-id',
responseTypes: [BPSOAuthResponseType.code],
scopes: [
BPSOAuthScope.openid,
BPSOAuthScope.profile,
BPSOAuthScope.email,
BPSOAuthScope.roles,
],
codeChallengeMethod: BPSCodeChallengeMethod.s256,
);Override the default realm names for internal and external realms:
final config = BPSSsoConfig.create(
appName: 'myapp',
internalClientId: 'your-internal-client-id',
externalClientId: 'your-external-client-id',
internalRealmName: 'custom-internal-realm', // Override default 'pegawai-bps'
externalRealmName: 'custom-external-realm', // Override default 'eksternal'
);Default behavior: If internalRealmName and externalRealmName are null (default), the SDK uses the standard BPS realm names:
- Internal realm:
pegawai-bps - External realm:
eksternal
Use cases for custom realm names:
- Testing with different Keycloak realm configurations
- Integration with custom or development SSO environments
- Multi-tenant setups with organization-specific realms
The example application includes a comprehensive configuration screen that supports all SDK features:
The configuration screen provides an intuitive UI for setting up the SDK with real-time validation:
// Configuration screen features:
- General settings (Base URL, HTTP inspection)
- Internal BPS Realm configuration with custom realm names
- External BPS Realm configuration with custom realm names
- Advanced OAuth2 parameters (response types, scopes, PKCE methods)
- Real-time form validation and error handling
- Live preview of configuration changesUsers can configure custom realm names directly in the UI:
-
Internal Realm Field: Allows customization of the internal BPS realm name
- Default:
pegawai-bps - Placeholder hint: "pegawai-bps (default)"
- Real-time validation and state updates
- Default:
-
External Realm Field: Allows customization of the external BPS realm name
- Default:
eksternal - Placeholder hint: "eksternal (default)"
- Real-time validation and state updates
- Default:
The configuration screen uses HydratedBlocCubit for state persistence:
// Configuration automatically saved and restored
ConfigurationState({
baseUrl: 'https://sso.bps.go.id',
internalRealm: 'pegawai-bps', // Configurable via UI
externalRealm: 'eksternal', // Configurable via UI
// ... other configuration options
});- HTTP Inspector Integration: Built-in Alice HTTP debugging
- Multi-Select Chips: For OAuth2 scopes and response types
- Dropdown Fields: For code challenge methods
- Form Validation: Real-time validation with error messages
- Loading States: Visual feedback during SDK initialization
- Error Display: User-friendly error messages with retry options
Add Dio interceptors for request/response modification, logging, or debugging:
import 'package:alice/alice.dart';
import 'package:alice_dio/alice_dio_adapter.dart';
// Create Alice for HTTP debugging
final alice = Alice();
final aliceAdapter = AliceDioAdapter();
alice.addAdapter(aliceAdapter);
final config = BPSSsoConfig.create(
appName: 'myapp',
internalClientId: 'your-internal-client-id',
externalClientId: 'your-external-client-id',
interceptors: [
aliceAdapter, // HTTP debugging
// Add other Dio interceptors here
],
);
// Show network inspector
alice.showInspector();Handle authentication events with comprehensive callback system:
final config = BPSSsoConfig.create(
appName: 'myapp',
internalClientId: 'your-internal-client-id',
externalClientId: 'your-external-client-id',
authCallbacks: BPSSsoAuthCallback(
onLoginSuccess: (user, realmType) {
print('Login successful for ${user.fullName} in $realmType realm');
// Handle successful login
},
onLoginFailed: (error, realmType) {
print('Login failed in $realmType realm: $error');
// Handle login failure
},
onLoginCancelled: (realmType) {
print('Login cancelled for $realmType realm');
// Handle user cancellation
},
onLogoutSuccess: (user) {
print('Logout successful for ${user.fullName}');
// Handle successful logout
},
onLogoutFailed: (error, user) {
print('Logout failed for ${user.fullName}: $error');
// Handle logout failure
},
onTokenRefreshSuccess: (user) {
print('Token refreshed for ${user.fullName}');
// Handle successful token refresh
},
onTokenRefreshFailed: (error, user) {
print('Token refresh failed for ${user.fullName}: $error');
// Handle token refresh failure
},
onAuthenticationFailure: (error, user) {
print('Authentication failure for ${user?.fullName}: $error');
// Handle general authentication failures
},
),
);The SDK provides enterprise-grade security configurations for different environments:
final config = BPSSsoConfig.create(
appName: 'government-app',
internalClientId: 'gov-internal-client',
externalClientId: 'gov-external-client',
// Automatically uses ISO 27001 compliant security settings
);final config = BPSSsoConfig.create(
appName: 'dev-app',
internalClientId: 'dev-internal-client',
externalClientId: 'dev-external-client',
securityConfig: BPSSsoSecurityConfig.iso27001,
errorConfig: const BPSSsoErrorConfig(),
);- Certificate Pinning: SSL/TLS security for government endpoints
- Enhanced PKCE: RFC 7636 compliant with entropy validation
- Secure Token Storage: Encrypted token storage with memory protection
- Runtime Security Checks: Debug and root detection
- Audit Logging: Comprehensive security event logging
- Error Sanitization: Production-safe error messages
- Deep Link Validation: Security validation for OAuth callback URLs
- State Parameter Verification: CSRF protection for OAuth flows
- Token Expiry Management: Automatic token validation and refresh
Customize the Chrome Custom Tabs appearance and behavior:
final config = BPSSsoConfig.create(
appName: 'myapp',
internalClientId: 'your-internal-client-id',
externalClientId: 'your-external-client-id',
// Uses BPS default branding automatically
);final config = BPSSsoConfig.create(
appName: 'myapp',
internalClientId: 'your-internal-client-id',
externalClientId: 'your-external-client-id',
customTabsConfig: BPSSsoCustomTabConfig(
toolbarColor: Color(0xFF2196F3), // Custom color
enableUrlBarHiding: false, // Keep URL visible
showTitle: true,
enableDefaultShare: false, // Disable for security
customMenuItems: [
CustomTabMenuItem(
title: 'Help',
icon: Icons.help,
onTap: () => showHelpDialog(),
),
],
),
);final config = BPSSsoConfig.create(
appName: 'secure-app',
internalClientId: 'secure-internal-client',
externalClientId: 'secure-external-client',
customTabsConfig: BPSSsoCustomTabConfig.secure, // Disables sharing, bookmarks, downloads
);Click to expand custom configuration example
// For advanced use cases with custom realms or endpoints
const config = BPSSsoConfig(
baseUrl: 'https://your-sso-server.com', // Optional
internal: BPSRealmConfig(
clientId: 'your-internal-client-id',
redirectUri: BPSRedirectUri(scheme: 'id.go.bps', host: 'your-app-sso-internal'),
realmType: BPSRealmType.internal,
responseTypes: [BPSOAuthResponseType.code],
scopes: [BPSOAuthScope.openid, BPSOAuthScope.profile, BPSOAuthScope.email],
codeChallengeMethod: BPSCodeChallengeMethod.s256,
),
external: BPSRealmConfig(
clientId: 'your-external-client-id',
redirectUri: BPSRedirectUri(scheme: 'id.go.bps', host: 'your-app-sso-eksternal'),
realmType: BPSRealmType.external,
responseTypes: [BPSOAuthResponseType.code],
scopes: [BPSOAuthScope.openid, BPSOAuthScope.profile, BPSOAuthScope.email],
codeChallengeMethod: BPSCodeChallengeMethod.plain,
),
);The SDK supports configurable OAuth2 parameters via typed Dart enums:
Default: [BPSOAuthResponseType.code]
| Enum value | OAuth2 value | Description |
|---|---|---|
BPSOAuthResponseType.code |
code |
Authorization code flow with PKCE (recommended) |
BPSOAuthResponseType.token |
token |
Implicit flow (not recommended for mobile apps) |
BPSOAuthResponseType.idToken |
id_token |
OpenID Connect implicit flow |
Default: [BPSOAuthScope.openid, BPSOAuthScope.profile, BPSOAuthScope.email]
| Enum value | OAuth2 value |
|---|---|
BPSOAuthScope.openid |
openid |
BPSOAuthScope.profile |
profile |
BPSOAuthScope.email |
email |
BPSOAuthScope.profilePegawai |
profile-pegawai |
BPSOAuthScope.roles |
roles |
BPSOAuthScope.groups |
groups |
BPSOAuthScope.offlineAccess |
offline_access |
Default: BPSCodeChallengeMethod.s256
| Enum value | OAuth2 value | Description |
|---|---|---|
BPSCodeChallengeMethod.s256 |
S256 |
SHA256 hash method (recommended) |
BPSCodeChallengeMethod.plain |
plain |
Plain text (less secure, legacy compatibility) |
The SDK provides specific exception types:
try {
final user = await BPSSsoClient.instance.login(
context: context,
realmType: BPSRealmType.internal
);
} catch (e) {
if (e is AuthenticationCancelledException) {
// User cancelled the authentication
} else if (e is TokenExchangeException) {
// Failed to exchange authorization code for tokens
} else if (e is MissingUserDataException) {
// Required user data is missing from the response
} else if (e is NetworkException) {
// Network-related error
} else if (e is BPSSsoException) {
// Other SSO-specific error
}
}The SDK supports different user structures for internal and external realms:
- Full employee data including NIP, organization, position, rank
- Office location information (region, province, address)
- Complete profile data with photos
- Basic profile information (name, email, username)
- Simplified structure without employee-specific fields
- OAuth standard fields (given_name, family_name)
// Example configuration for a BPS application
final config = BPSSsoConfig.create(
appName: 'myapp',
internalClientId: 'internal-client-id',
externalClientId: 'external-client-id',
// Additional configuration for callbacks and UI
authCallbacks: BPSSsoAuthCallbacks(
onLoginSuccess: (user, realm) {
print('Login successful for ${user.fullName}');
},
onLoginFailed: (error, realm) {
print('Login failed: $error');
},
onTokenRefreshSuccess: (user) {
print('Token refreshed for ${user.fullName}');
},
),
);If the app gets stuck in Chrome Custom Tabs and doesn't redirect back:
# Verify intent filters are properly configured
# Make sure android:host matches your BPSSsoConfig appName# Ensure CFBundleURLSchemes includes "id.go.bps"Click to expand deep link testing commands
# Android: Test with adb
adb shell am start \
-W -a android.intent.action.VIEW \
-d "id.go.bps://your-app-sso-internal?code=test&state=test" \
your.package.name
# iOS: Test with Simulator
xcrun simctl openurl booted "id.go.bps://your-app-sso-internal?code=test&state=test"// Add this to see incoming deep links
_appLinks.stringLinkStream.listen((String link) {
debugPrint('🔗 Received deep link: $link');
});- Wrong redirect URI: Ensure your
appNameinBPSSsoConfig.create()matches the Android manifest host - Multiple listeners: Only one app should listen to the same deep link scheme
- Android autoVerify: Remove
android:autoVerify="true"if causing issues - iOS Universal Links: Disable if conflicting with custom URL schemes
AuthenticationCancelledException: User cancelled or deep link failedTokenExchangeException: Check client ID and redirect URI configurationNetworkException: Check internet connection and SSO server accessibilityMissingUserDataException: SSO server didn't return required user fields
-
Use ISO 27001 Configuration for Production
// Recommended for government/enterprise apps final config = BPSSsoConfig.create( appName: 'your-app', internalClientId: 'your-internal-client', externalClientId: 'your-external-client', securityConfig: BPSSsoSecurityConfig.iso27001, // Default );
-
Enable Security Features
- ✅ Certificate pinning for production
- ✅ Token encryption for sensitive data
- ✅ Runtime security checks
- ✅ Audit logging for compliance
-
Handle Errors Securely
try { final user = await BPSSsoClient.instance.login(context: context, realmType: realmType); } catch (e) { // Use sanitized error messages in production showError(_getSecureErrorMessage(e)); }
-
UI Security
- Use
BPSSsoCustomTabConfig.securefor high-security environments - Keep URL bar visible for transparency when needed
- Disable sharing/bookmarks for sensitive applications
- Use
Click to expand usage examples
// Default configuration - most secure
final config = BPSSsoConfig.create(
appName: 'myapp',
internalClientId: 'your-internal-client-id',
externalClientId: 'your-external-client-id',
// Defaults: responseTypes: [BPSOAuthResponseType.code],
// scopes: [BPSOAuthScope.openid, BPSOAuthScope.profile, BPSOAuthScope.email],
// codeChallengeMethod: BPSCodeChallengeMethod.s256
);// Request additional scopes for more user data
final config = BPSSsoConfig.create(
appName: 'myapp',
internalClientId: 'your-internal-client-id',
externalClientId: 'your-external-client-id',
scopes: [
BPSOAuthScope.openid,
BPSOAuthScope.profile,
BPSOAuthScope.email,
BPSOAuthScope.roles,
BPSOAuthScope.groups,
],
);// Use plain PKCE method for older systems that don't support S256
final config = BPSSsoConfig.create(
appName: 'myapp',
internalClientId: 'your-internal-client-id',
externalClientId: 'your-external-client-id',
codeChallengeMethod: BPSCodeChallengeMethod.plain,
);// Different settings for internal vs external realms
const config = BPSSsoConfig(
internal: BPSRealmConfig(
clientId: 'internal-client-id',
redirectUri: BPSRedirectUri(scheme: 'id.go.bps', host: 'myapp-sso-internal'),
realmType: BPSRealmType.internal,
scopes: [
BPSOAuthScope.openid,
BPSOAuthScope.profile,
BPSOAuthScope.email,
BPSOAuthScope.roles,
],
codeChallengeMethod: BPSCodeChallengeMethod.s256,
),
external: BPSRealmConfig(
clientId: 'external-client-id',
redirectUri: BPSRedirectUri(scheme: 'id.go.bps', host: 'myapp-sso-eksternal'),
realmType: BPSRealmType.external,
scopes: [BPSOAuthScope.openid, BPSOAuthScope.profile, BPSOAuthScope.email],
codeChallengeMethod: BPSCodeChallengeMethod.plain,
),
);-
OAuth2 Security
- Always use
responseTypes: [BPSOAuthResponseType.code]with PKCE for mobile apps - Prefer
codeChallengeMethod: BPSCodeChallengeMethod.s256over.plain - Request minimal scopes needed for your app functionality
- Always use
-
Handle Multiple Deep Link Sources
// If using both app_links and auto_route
Stream<String> combinedLinkStream = StreamGroup.merge([
_appLinks.stringLinkStream,
_autoRouteLinkStream,
]);- Graceful Timeout Handling
try {
final user = await BPSSsoClient.instance.login(
context: context,
realmType: BPSRealmType.internal,
);
} on AuthenticationCancelledException {
// Handle user cancellation gracefully
showSnackBar('Login cancelled');
}- Test on Real Devices
- Chrome Custom Tabs behavior differs between emulator and real devices
- Always test deep links on physical devices
- Test with different OAuth2 parameter combinations
| Class | Description |
|---|---|
BPSSsoClient |
Main SDK client singleton |
BPSSsoConfig |
SDK configuration |
BPSRealmConfig |
Realm-specific configuration |
BPSRedirectUri |
Typed redirect URI (scheme + host) |
BPSOAuthResponseType |
Enum for OAuth2 response types |
BPSOAuthScope |
Enum for OAuth2 scopes |
BPSCodeChallengeMethod |
Enum for PKCE code challenge method |
BPSUser |
User model with authentication data |
BPSRealmType |
Enum for realm types (internal/external) |
BPSSsoSdkInfo |
SDK version and metadata information |
| Method | Description |
|---|---|
initialize(config, linkStream) |
Initialize SDK with configuration |
login(context, realmType) |
Authenticate user |
refreshToken(user) |
Refresh access token |
validateToken(user) |
Validate current token |
logout(user) |
Logout user |
shouldRefreshToken(user) |
Check if token needs refresh |
| Exception | Description |
|---|---|
AuthenticationCancelledException |
User cancelled authentication |
TokenExchangeException |
Token exchange failed |
NetworkException |
Network error occurred |
MissingUserDataException |
Required user data missing |
BPSSsoException |
Base exception class |
- Flutter 3.0.0 or higher
- ✅ Android API level 21 or higher (for Chrome Custom Tabs)
- ✅ iOS 12.0 or higher
- ❌ Web - Not supported (Custom Tabs unavailable)
- ❌ Desktop (macOS, Windows, Linux) - Not supported
dio- HTTP client for API calls and request/response handlingflutter_custom_tabs- Chrome Custom Tabs implementation (iOS-optimized)crypto- PKCE cryptographic functions for OAuth2 securityurl_launcher- URL launching utilitiesintl- Internationalization and date formattinguuid- UUID generation for state parameters
alice- HTTP request/response debugging inspectoralice_dio- Dio adapter for Alice HTTP debuggingapp_links- Deep link handling for OAuth callbacks
- iOS: Custom Tabs management optimized for SFSafariViewController
- Android: Chrome Custom Tabs with enhanced security configurations
- Cross-platform: Unified API with platform-specific implementations
We welcome contributions to the BPS SSO SDK! Please see our Contributing Guidelines for detailed information on:
- Development setup and environment
- Git Flow workflow and branching strategy
- Coding standards and style guidelines
- Testing requirements and coverage
- Pull request process and templates
- Issue reporting guidelines
- Fork and clone the repository
- Create a feature branch from
develop - Make your changes following our coding standards
- Run tests and ensure they pass
- Submit a pull request to the
developbranch
For detailed instructions, see CONTRIBUTING.md.
This repository uses automated GitHub Actions workflows for CI/CD. See our Workflows Documentation for detailed information on:
- CI Pipeline: Automated testing, analysis, and quality checks
- Release Process: Automated versioning, tagging, and publishing
- Git Flow Integration: Branch-based workflow automation
- Manual Triggers: How to manually run workflows
- Troubleshooting: Common issues and solutions
For complete workflow documentation, see WORKFLOWS.md.
This project is licensed under the MIT License - see the LICENSE file for details.
For issues or questions:
- Bug Reports: Create an issue using our bug report template
- Feature Requests: Create an issue using our feature request template
- Questions: Start a discussion in our GitHub Discussions
- Security Issues: Email the maintainers directly (see SECURITY.md)
The SDK includes a comprehensive example application demonstrating:
- Complete OAuth2 authentication flow
- User interface with privacy mode toggle
- Authenticated image loading for user avatars
- Real-time initialization status checking
- Proper error handling and user feedback
- Navigation with deep link integration
- State management with Cubit/Bloc pattern
To run the example:
cd example
flutter pub get
flutter run- Home Screen: Shows SDK initialization status and authentication state
- Configuration Screen: Comprehensive SDK setup with custom realm names support
- Operations Screen: Login/logout functionality with real-time status
- User Info Screen: Detailed user information with privacy controls
- Privacy Mode: Toggle to obscure sensitive information
- Status Cards: Visual feedback for all operations
- Error Handling: User-friendly error messages and recovery
- HTTP Inspector: Built-in Alice integration for network debugging
- Real-time Validation: Form validation with immediate feedback




