/flutter-oidc

OIDC Library for Flutter apps

Primary LanguageDartMIT LicenseMIT

sbb_oidc 3.4.0

A Flutter plugin for OpenID Connect (OIDC).

Table of contents


Supported platforms

Android badge iOS badge Web badge

Preconditions

Authentication with OIDC requires the app to be registered with an Identity Provider. SBB uses Azure AD 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 a scheme, host and path component in the format scheme://host/path and and be written in lowercase.

Example: myappname://myhost/redirect

The iOS SDK has some logic to validate the redirect URL to see if it should be responsible for processing the redirect. This appears to be failing under certain circumstances. Adding a trailing slash to the redirect URL specified in your code fixes the issue.

Setup

Android

Go to the build.gradle file for your Android app to specify the custom scheme. There should be a section in it that looks similar to the following but replace <your_custom_scheme> with the desired value. Ensure that the value of <your_custom_scheme> is all in lowercase.

...
android {
    ...
    defaultConfig {
        ...
        manifestPlaceholders += [
                'appAuthRedirectScheme': '<your_custom_scheme>'
        ]
    }
}

Also set the minSdkVersion to 21 or above.

...
android {
    ...
    defaultConfig {
        ...
        minSdkVersion 21
        ...
    }
}

Android Backup

Samsung devices with Android 9.0 or newer may experience crashes related to backups because the devices restore shared preferences. Because of this, the shared preferences must be excluded from the backup. There are two options:

1. Disable backup completely.

Go to the Manifest.xml file for your Android app and add the android:allowBackup attribute to the <application> element.

...
<application
    ...
        android:allowBackup="false">
2. Keep backup enabled but exclude the shared preferences used by this plugin.

Go to the Manifest.xml file for your Android app and add the android:allowBackup and the android:fullBackupContent attributes to the <application> element.

...
<application
    ...
        android:allowBackup="true" 
        android:fullBackupContent="@xml/backup_rules">

Create or edit backup_rules.xml and exclude the shared preferences used by this plugin.

<?xml version="1.0" encoding="utf-8"?>
<full-backup-content>
    <exclude domain="sharedpref" path="FlutterSecureStorage"/>
</full-backup-content>

If your app targets Android 12 (API level 31) or higher you must specify an additional set of XML backup rules. Go to the Manifest.xml file for your Android app and add the android:dataExtractionRules attribute to the <application> element. This attribute points to an XML file that contains backup rules.

...
<application
    ...
        android:dataExtractionRules="@xml/data_extraction_rules">

Create or edit data_extraction_rules.xml and exclude the shared preferences used by this plugin.

<?xml version="1.0" encoding="utf-8"?>
<data-extraction-rules>
    <cloud-backup>
        <include domain="sharedpref" path="."/>
        <exclude domain="sharedpref" path="FlutterSecureStorage"/>
    </cloud-backup>
</data-extraction-rules>

iOS

Go to the Info.plist for your iOS app to specify the custom scheme. There should be a section in it that looks similar to the following but replace <your_custom_scheme> with the desired value.

<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleTypeRole</key>
        <string>Editor</string>
        <key>CFBundleURLSchemes</key>
        <array>
            <string><your_custom_scheme></string>
        </array>
    </dict>
</array>

Web

This plugin does not come with a build of MSAL.js. To add MSAL.js to your app add the the follwoing to your index.html file.

<head>
  ...
    <!-- MSAL.js -->
  <script type="text/javascript" 
          src="https://alcdn.msauth.net/browser/2.14.2/js/msal-browser.min.js"
          integrity="sha384-ggh+EF1aSqm+Y4yvv2n17KpurNcZTeYtUZUvhPziElsstmIEubyEB6AIVpKLuZgr" 
          crossorigin="anonymous">
  </script>
  ...
</head>

Refer to the MSAL.js documentation for detailed installation instructions. Note that you will need a CDN build of MSAL.js and not an NPM build.

Usage

Add dependency

Add sbb_oidc as a dependency in your pubspec.yaml file.

sbb_oidc:
    git:
        url: https://github.com/SchweizerischeBundesbahnen/flutter-oidc.git
        path: sbb_oidc
        ref: 3.4.0

Create OIDC client

Create an instance of the OIDC client.

final client = SBBOpenIDConnect.createClient(
  discoveryUrl: <discovery_url>,
  clientId: <client_id>,
  redirectUrl: <redirect_url>,
);

Here the <client_id> and <redirect_url> should be replaced by the values registered with your identity provider. The <discovery_url> is the URL of the discovery endpoint exposed by your identity provider. The endpoint will return a document containing information about the OAuth 2.0 endpoints among other things.

SBB discovery URLs

The SBB discovery URLs are defined in sbb_discovery_url.dart. It is recommended to use these constants.

Environment Discovery URL
DEV https://login.microsoftonline.com/93ead5cf-4825-45f3-9bc3-813cf64441af/v2.0/.well-known/openid-configuration
PROD https://login.microsoftonline.com/2cda5d11-f0ac-46b3-967d-af1b2e1bd01a/v2.0/.well-known/openid-configuration

Login

To Authorize and authenticate end-users call the login() method. This will perform an authorization request and automatically exchange the authorization code. Upon completing the request successfully, the method should return an OIDC token that contains an access token which 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 you app should not cache the token. Instead the app should request the token every time it needs it by calling getToken().

final token = await client.getToken(
  scopes: <your_scopes>,
  forceRefresh: false,
);

The OIDC client checks if the token is expired and refreshes it automatically. You can also force a refresh by settings 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 recommendet because in requires multiple HTTP requests to get 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 sbbuid claim.

final oidcToken = ....
final idToken = oidcToken.idToken;
final uid = idToken.payload['sbbuid'] as String;

Logout

Logout deletes all OIDC Tokens from the local cache. The user's session will remain active on the server and the user can be signed back in without providing credentials again.

await client.logout()

End session

❌ End session does not work properly on mobile devices.

End session is used for logging out of the built-in browser and deleting all cached OIDC tokens.

await client.endSession()

Access multiple APIs

AzureAD has a security limitation: an access token can only be used for one API. The access token can have multiple scopes for one API, but it cannot contain scope(s) of other APIs. In order to use multiple APIs, you must request additional tokens with the scope(s) of the corrensponding API. This means that the OIDC client will have one access token for each API.

Let's assume that your app neds access to three different APIs:

  1. Microsoft Graph with read acceess to User and Calendar
  2. Api 1
  3. Api 2

The first step is to login. As mentioned above you can only use the scopes of 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 other APIs (Api 1 and Api 2) you must request one additional token for each API by 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 don't. 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 will open a popup where the user can enter the second factor.

Example

See example app.