A Flutter package for OpenID Connect (OIDC).
Table of contents
Supported platforms
Preconditions
Authentication with OIDC requires the app to be registered with an identity provider. SBB uses Microsoft Entra ID for enterprise applications. You can manage your app registration using the self-service API or the SBB API Platform. Detailed documentation is available on this site.
Redirect URL
The redirect URL must contain scheme, host, and path components in the format scheme://host/path and be written in lowercase.
Example: myappname://myhost/redirect
MSAL redirect URL
Applications should use the Microsoft Entra ID-specific redirect URL format whenever possible:
- Android:
msauth://<PACKAGE_NAME>/<BASE64_URL_ENCODED_SIGNATURE> - iOS:
msauth.<BUNDLE_ID>://auth
See the Microsoft documentation for Android and iOS.
⚠️ This plugin does not enforce the format for backward compatibility.
Setup
Android
Minimum SDK version: 24
Open the build.gradle.kts file of your app and set the minimum SDK version to 24 or above:
...
android {
...
defaultConfig {
...
minSdk = 24
...
}
}
Add BrowserTabActivity to the AndroidManifest.xml of your app as a child of the <application> element. It handles the browser callback after authentication. The scheme, host, and path values must match the redirect URL registered with Microsoft Entra ID:
<activity
android:name="com.microsoft.identity.client.BrowserTabActivity"
android:exported="true">
<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="<scheme>"
android:host="<host>"
android:path="/<path>" />
</intent-filter>
</activity>
You also need to request the following permissions:
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
iOS
Minimum iOS deployment target: 17.0
Open the Info.plist of your iOS app to specify the custom scheme. It should contain a section similar to the following, with <scheme> replaced by the desired value.
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string><scheme></string>
</array>
</dict>
</array>
Also add LSApplicationQueriesSchemes to enable integration with the Microsoft Authenticator app if installed:
<key>LSApplicationQueriesSchemes</key>
<array>
<string>msauthv2</string>
<string>msauthv3</string>
</array>
Finally, add your desired keychain access group to the app's keychain access groups entitlement. See Create OIDC client for more details.
Usage
Add dependency
Add sbb_oidc as a dependency in your pubspec.yaml file.
sbb_oidc: ^5.0.0
Create OIDC client
Create an instance of the OIDC client.
final client = SBBOpenIDConnect.createClient(
config: OidcClientConfig(
tenantId: <tenant_id>,
clientId: <client_id>,
redirectUrl: <redirect_url>,
keychainAccessGroup: <keychain_access_group>,
),
enableLogging: <true/false>,
);
Here, replace <client_id> and <redirect_url> with the values registered with your identity provider.
<tenant_id> is the unique Microsoft tenant ID of your organisation. The SBB tenant IDs are defined in sbb_tenant.dart. We recommend using these constants. To implement multi-tenant login, you must use common as the tenant ID.
<keychain_access_group> is used to cache tokens in iOS. Apps that share the same group get silent SSO between them. Use the app's bundle identifier to keep tokens private. The value must also be declared in the app's keychain access groups entitlement. For more information, see Sharing access to keychain items among a collection of apps.
Login
To authorize and authenticate end users, call the login() method. This performs an interactive authorization request. Upon successfully completing the request, the method should return an OIDC token that contains an access token you can use to access protected APIs.
final token = await client.login(
scopes: <your_scopes>,
);
Get tokens
Access tokens are short-lived and must be refreshed as soon as they expire. Therefore, your app should not cache the token. Instead, request a token every time it is needed by calling getToken().
final token = await client.getToken(
scopes: <your_scopes>,
forceRefresh: false,
);
The OIDC client checks whether the token has expired and refreshes it automatically. You can also force a refresh by setting the forceRefresh argument to true.
Get data about the end-user
To get data about the signed-in end user, you can either use the ID token or call getUserInfo().
final userInfo = await client.getUserInfo(
scopes: <your_scopes>,
);
Using getUserInfo() is not recommended because it requires multiple HTTP requests to retrieve the data. The ID token contains the same data and requires at most one request if the token must be refreshed.
Get the SBB uid
The SBB UID (u/e number) is specified in the ID token as the sbbuid claim.
final oidcToken = ....
final idToken = JsonWebToken.decode(oidcToken.idToken);
final uid = idToken.payload['sbbuid'] as String;
Logout
Logging out deletes all OIDC tokens from the local cache. The user's session remains active on the server, so the user can sign in again without providing credentials.
await client.logout();
End session
Ending the session logs the user out of the built-in browser and deletes all cached OIDC tokens. The user must provide their credentials to log in again after ending the session.
await client.endSession();
Access multiple APIs
Azure AD has a security limitation: an access token can only be used for one API. An access token can have multiple scopes for one API, but it cannot contain scopes for other APIs. To use multiple APIs, you must request additional tokens with the scopes for the corresponding APIs. This means that the OIDC client has one access token for each API.
Suppose your app needs access to three different APIs:
- Microsoft Graph with read access to User and Calendar
- Api 1
- Api 2
The first step is to log in. As mentioned above, you can use the scopes of only one API, in this case Microsoft Graph. The scopes for this API are:
openid, profile, email, offline_access, Calendars.Read, User.Read,
final token = await client.login(
scopes: [
'openid',
'profile',
'email',
'offline_access',
'Calendars.Read',
'User.Read',
],
);
The returned token can only be used to access the Microsoft Graph API. To access the other APIs (API 1 and API 2), you must request one additional token for each API using the getToken() method.
The scopes for API 1 are:
openid, offline_access, api://aaaaaaaa-1111-2222-3333-444444444444/.default,
final tokenForApi1 = await client.getToken(
scopes: [
'openid',
'offline_access',
'api://aaaaaaaa-1111-2222-3333-444444444444/.default',
],
);
The scopes for API 2 are:
openid, offline_access, api://bbbbbbbb-1111-2222-3333-444444444444/.default,
final tokenForApi2 = await client.getToken(
scopes: [
'openid',
'offline_access',
'api://bbbbbbbb-1111-2222-3333-444444444444/.default',
],
);
Multi-Factor authentication
Some APIs require multi-factor authentication (MFA), while others do not. In the example above, the Microsoft Graph API does not require MFA, but API 1 and API 2 do. Therefore, getToken() will throw a MultiFactorAuthenticationException. In this case, you must call login() a second time and use the scopes of an API that requires MFA.
final tokenForApi1 = await client.login(
scopes: [
'openid',
'offline_access',
'api://aaaaaaaa-1111-2222-3333-444444444444/.default',
],
);
This opens a pop-up where the user can enter the second factor.
Example
See example app.