Flutter mobile application to consume your WSO2 Cloud APIs with PKCE
By: Erandi Ganepola
Tags: Authentication, Pkce, Flutter, Wso2, Cloud

In this article, we are discussing on implementing a Flutter mobile application which securely invokes an API through WSO2 API Cloud using “Authorization code grant with proof key for code exchange (PKCE)”.

We have discussed about PKCE flow and why it has been introduced to address security threats of public clients such as mobile or single page applications in our previous article.

Still haven’t read our previous article? Have a look at “Securely Consume your WSO2 Cloud APIs from Mobile/Single-Page Applications” to catch up background and generic security mechanism of PKCE.

Let’s begin!

Choosing a cross platform mobile application framework

With our previous article we have already discussed choosing recommended security standards, choosing API Management SaaS, etc. Here let’s quickly have a look on choosing our mobile application framework.

Companies build mobile apps on both iOS and Android and thus it helps businesses to target customers across the world. But developing specific native apps increases cost and consumes time. The “write once, run anywhere” approach that comes with cross platform applications allows developers to utilize a single code on multiple platforms, which greatly reduces costs and shortens the development time , unlike native apps. Therefore world is moving towards cross platform mobile applications that can address iOS and Android both aspects.

When it comes to choosing a cross platform framework, Flutter and React Native frameworks are the leading contenders while React Native is more matured and Flutter is better in performance with more support for native components. Experts have predicted that Flutter will be the future of mobile application development. Furthermore Flutter has an ‘AppAuth’ library named ‘flutter_appauth’ which handles the ‘authorization code with PKCE’ flow. Considering those reasons and after comparing both frameworks, we decided to use Flutter to develop this sample mobile application.

However still there are pros and cons of all of these frameworks and you need to decide which one to pick according to your requirements. Please refer this article for more details on Flutter vs React Native vs Native Comparison.

Now let’s setup the development environment and start implementing a mobile application to consume your WSO2 API Cloud APIs with PKCE.

Setup your Environment

Development Environment, one of:

These IDEs integrate well with Flutter. You will need an installation of the Dart and Flutter plugins, regardless of the IDE you decide to use.

Next download and install Flutter SDK and set path in your .bashrc file and source it.

Project Setup

Clone GitHub sample project —

git clone https://github.com/erandiganepola/wso2-cloud-flutter-demo.git

In this project you will use three main dependencies. Those are http , flutter_appauth and flutter_secure_storage . Refer official documentations of http, flutter_appauth and flutter_secure_storage for more information.

, and . Refer official documentations of http, flutter_appauth and flutter_secure_storage for more information. Install dependencies by clicking Pub get in your IDE or run the following command in the project root:

flutter pub get

Prerequisites to setup in WSO2 Cloud

Note:

We have already discussed most of the prerequisites steps in our previous article. But here we have enabled “Allow authentication without client secret configuration” under the OIDC service provider config in WSO2 API Cloud to use Authorization code grant type with PKCE without client secret (You can enable it for your tenant application by requesting it via an email. Drop an email to cloud@wso2.com mentioning your tenant domain and client application name.). Therefore we are not passing client secret in our code level requests. If you have not enabled this, you need to pass “client secret” in your code level requests.

Create an account in WSO2 Cloud if you don’t have one already and login to Publisher portal.

Create an API using an existing swagger definition. You can find the relevant swagger in swagger.yaml file. When creating your API, set context as /demo and set the production, sandbox backend endpoints to https://restcountries.eu/rest/v2 . Also select Subscription Tiers in Manage tab as necessary (I have set to Unlimited in my API). Then publish your API. At the end of this demo your API will be calling REST Countries Capital City endpoint (https://restcountries.eu/#api-endpoints-capital-city) as the backend.

and set the production, sandbox backend endpoints to . Also select in tab as necessary (I have set to in my API). Then publish your API. At the end of this demo your API will be calling REST Countries Capital City endpoint (https://restcountries.eu/#api-endpoints-capital-city) as the backend. Visit WSO2 API Store and create an application. Enable (put a tick for the Code grant box) code grant with the callback URI. We have set callback URI to org.wso2.cloud.flutterdemo://login-callback in this sample. Then generate keys. Client ID/Consumer key, client secret/Consumer secret will be useful in our next steps.

Subscribe to previously published API from the newly created application.

You need to enable “Allow authentication without client secret configuration” under the OIDC service provider config in WSO2 API Cloud to use Authorization code grant type with PKCE without client secret. For that please send an email to cloud@wso2.com to configure it for your application. Instead, you can pass the “client_secret” in requests as well .

without client secret. For that please send an email to to configure it for your application. . Set relevant values as your AUTH_CLIENT_ID and TENANT_DOMAIN in cloned project's lib/utils/constants.dart file. Sample is given below:

/// Global Constants // WSO2 API Cloud URL domain

const String AUTH_DOMAIN = 'gateway.api.cloud.wso2.com'; // Your client ID obtained by creating an application in WSO2 Store

const String AUTH_CLIENT_ID = 'fO0rk7lzuWZKRofN13zxxxxxxx'; // Call back URL specified in your application

const String AUTH_REDIRECT_URI = 'org.wso2.cloud.flutterdemo://login-callback'; // Auth token issuer domain

const String AUTH_ISSUER = 'https://$AUTH_DOMAIN'; // Your tenant domain

const String TENANT_DOMAIN = 'erandiorg';

Run the Application

To run the application you have two options. Either to run in your mobile application or to run in a simulator.

Option 01 : Enable developer options and USB Debugging in mobile phone’s settings and run the application after connecting your mobile phone to your development environment.

: Enable developer options and USB Debugging in mobile phone’s settings and run the application after connecting your mobile phone to your development environment. Option 02 : Launch either the iOS simulator or Android emulators, then run the application.

You can run the application from your IDE or using following command:

flutter run -d all

Login, Invoke API, Change Settings and Logout

After running the application, a UI will be popped out with a button named Login to Cloud .

Login page of mobile app

2. Once user clicks Login to Cloud button, s/he will be navigated to WSO2 authorization login page. There user needs to enter username as youruser@email.com@<tenantdomain> and give password.

Login page of API Cloud shown in browser

3. Then approve access to user profile information.

Page to allow access to requested scopes

4. When it’s successful, user will be navigated to Home page. In the Home page you can enter a capital of a country and click search icon in the right side of the search box. You will see results in the UI.

Initial home page

Home page with requested results for search term ‘Colombo’

5. If you need to try out the sample for different tenant domains and different client applications rather than the one you configured in the constant.dart file, you can change those configurations by clicking settings icon in the top bar. Then a dialog box will be popped up with the existing values for client ID and tenant domain. You can edit them and click Update to save.

Page to update ‘tenant domain’ and ‘client ID’

6. If user needs to sign out, click power icon in the top bar right side corner. Then confirmation box will be popped up. When you click Yes it will log out user from the application.

Logout page

Implementation

To understand the implementation, first let’s identify the flow of this scenario. We can divide the complete flow in to following sections.

Login and token generation

API invocation with a valid access token

API invocation with an invalid access token (refreshing access token)

Change settings (tenant domain and client ID)

Logout

Now let’s go through above sub topics one by one in detail. You can get the relevant source code from this GitHub link.

Login and token generation

In the following diagram we have shown how login and token generation flows work with Authorization code grant with PKCE .

When user opens the mobile application, user is navigated to the Login page. For navigations, it is recommended to use Flutter routes when it comes to Flutter mobile apps. We have used navigations with named routes in this implementation.

When user opens the application, from code level it starts the app with the /login named initial route. Therefore app starts with the Login widget. Inside that Login class it initializes the app and returns app layout with Login to Cloud button from Widget build(BuildContext context) .

Once user clicks Login to Cloud button, loginAction() gets called. Inside that function, login() function initializes user login and get the access token. Inside login() function appAuth.authorize() function pops up web browser with WSO2 Cloud's Key manager /authorize URL. In this call AppAuth internally generates code challenge and sends code_challenge and code_challenger_algorithm (SHA256) with the request since we use authorization code grant with PKCE flow. Apart from that, client-id , redirect URI , scope , grant_type , etc are sent as query parameters in the opened URL.

Followings explain the parameters we are using in these code level authentication requests:

client_id : Consumer ID you obtained in ‘Prerequisites to setup in WSO2 Cloud’ section by creating an application in WSO2 API Store.

: Consumer ID you obtained in ‘Prerequisites to setup in WSO2 Cloud’ section by creating an application in WSO2 API Store. redirect_uri : Call back URI we configured when creating the application/configuring constants file in ‘Prerequisites to setup in WSO2 Cloud’ section.

: Call back URI we configured when creating the application/configuring constants file in ‘Prerequisites to setup in WSO2 Cloud’ section. scope : OAuth2 scopes required (which permissions should be delegated to the client)

: OAuth2 scopes required (which permissions should be delegated to the client) state : A random string used for CSRF protection. Also this gives your app a chance to persist data between the user being directed to the authorization server and back again, such as using the state parameter as a session key.

Following code snippet shows the implementation of the auth request:

final AuthorizationServiceConfiguration serviceConfiguration =

AuthorizationServiceConfiguration('https://$domain/authorize',

'https://$domain/token?tenantDomain=$TENANT_DOMAIN'); final AuthorizationResponse authorizationResponse =

await flutterAppAuth.authorize(

AuthorizationRequest(clientId, redirectUri,

issuer: 'https://$authDomain',

scopes: <String>['openid', 'profile', 'offline_access'],

serviceConfiguration: serviceConfiguration),

);

Then WSO2 API Cloud will redirect the web browser to login page. Next user gets UIs to enter login details and give consent.

Once login details are entered, WSO2 key manager validates them and redirects back to the pre-configured redirect URI with auth code if validation successful. This redirectURI should match with the Callback URI we configured in the API store's application.

Next from code level it invokes /token endpoint to get tokens with the received auth code and code verifier which is generated by flutter_appauth underneath (we can get the AppAuth generated codeVerifier as authorizationResponse.codeVerifier and pass it to the /token request) since it's required in PKCE flow for security validation. /token request is made by the following code snippet:

final TokenResponse tokenResponse = await flutterAppAuth.token(TokenRequest(

clientId, redirectUri,

serviceConfiguration: serviceConfiguration,

authorizationCode: authorizationResponse.authorizationCode,

codeVerifier: authorizationResponse.codeVerifier));

Note:

If you have not enabled “Allow authentication without client secret configuration” under the OIDC service provider config in WSO2 API Cloud as mentioned in “Prerequisites to setup in WSO2 Cloud”, you need to pass client secret in the code level request as follows:

final TokenResponse tokenResponse = await flutterAppAuth.token(TokenRequest(

clientId, redirectUri,

clientSecret: <your_client_secret>,

serviceConfiguration: serviceConfiguration,

authorizationCode: authorizationResponse.authorizationCode,

codeVerifier: authorizationResponse.codeVerifier));

With a successful response we receive refresh_token, access_token and id_token. Flutter has a library called flutter_secure_storage to securely persist data locally. Therefore once we receive tokens, we add them to secure storage (we can read those values from secure storage when it’s needed). Following code snippet show how to set and get values from secure storage:

/// Function to set access token to secure storage

Future<String> setAccessToken(String accessToken) async {

await secureStorage.write(key: 'access_token', value: accessToken);

} /// Function to get access token from secure storage

Future<String> getAccessToken() async {

return secureStorage.read(key: 'access_token');

}

API invocation with a valid access token

Following diagram shows how to invoke an already published API using a valid access token (token we retrieved from previous step).

Once login and token generation flow is successful, user is navigated to the Home widget where user can enter a capital of a country and search country details.

As an example we enter ‘colombo’ and clicks the search icon in the right side of the search box. Then from code level invokeApiAction() function gets called. Inside that we call fetchCountries() function by passing tenant domain, input value (capital) and access token to send a GET request to the pre-configured API context URL. fetchCountries() functionality is shown in the code snippet below:

Future<http.Response> fetchCountries(

String tenantDomain, String capital, String accessToken) async { // Full API context path (apart from URL param attached)

String API_CONTEXT_PATH =

'https://$AUTH_DOMAIN/t/$tenantDomain/demo/v1.0/capital/';

// Sends a get request to configured API context URL with access //token

final String url = '$API_CONTEXT_PATH$capital';

http.Response response = await http.get(

url,

headers: <String, String>{

'Authorization': 'Bearer $accessToken',

HttpHeaders.contentTypeHeader: ContentType.json.mimeType

},

);

return response;

}

In the happy path when we are sending this GET request to API Cloud gateway with a valid access token, WSO2 Cloud’s key manager validates the access token and then gateway calls the backend and invoke backend API. If it gets successful, gateway sends us the JSON response with status code 200. Successful responses after mapping to ‘Country’ objects are visible in the UIs as follows:

UI with multiple matching results for the search term ‘c’

API invocation with an invalid access token (refreshing access token)

Following diagram shows an API invocation with an invalid access token:

Access tokens are getting expired after a defined period of time (by default it’s 3600s). At that kind of a situation, when user searches a capital of a country, from application level it sends the GET request to API Cloud gateway with an invalid access token. At this point, as shown in the diagram, token validation gets failed and response comes with a error message and 401 status code.

When application receives 401 response, from code level it calls to refreshAccessToken() function to refresh access token using refresh token grant. Then again sends the GET request with the newly received valid access token to API Cloud gateway as shown in the below snippet:

if (response.statusCode == 401) {

final String accessToken = await refreshAccessToken(

clientId: await getClientID(),

redirectUri: AUTH_REDIRECT_URI,

issuer: AUTH_ISSUER,

domain: AUTH_DOMAIN); // Call fetchCountries() with new access token

response = await fetchCountries(tenantDomain, capital, accessToken);

}

Note:

If you have not enabled “Allow authentication without client secret configuration” under the OIDC service provider config in WSO2 API Cloud as mentioned in “Prerequisites to setup in WSO2 Cloud”, you need to pass client secret in the code level request as follows:

final TokenResponse response = await flutterAppAuth.token(TokenRequest(

clientId, redirectUri,

clientSecret: <your_clinet_secret>,

issuer: issuer,

refreshToken: storedRefreshToken,

serviceConfiguration: serviceConfiguration));

With that user gets the expected successful results in the home page. Checking the status code for 200 and 401 runs underneath from code level so that refreshing access token and resending the GET request if we receive 401 from first request are not visible to end user.

As a practice we recommend only to refresh token if it’s expired/invalid. Unless regenerating a token for every call is very costly.

Change settings (tenant domain and client ID)

As we discussed in the Prerequisites to setup in WSO2 Cloud section, user needs to update AUTH_CLIENT_ID and TENANT_DOMAIN in the lib/utils/constants.dart file. Therefore those configs will be used as default values for this application. However if user needs to try the same application for different tenants, s/he can change AUTH_CLIENT_ID and TENANT_DOMAIN from mobile application UI later. We have discussed how to do it in the Login, Invoke API, Change Settings and Logout section.

Once user changes tenant domain and client ID from application dialog box, those values will be saved in the secure storage. Therefore from code level it checks whether these values are available when invoking the API, if not available it will use the default values configured in the constants.dart file. This functionality is handled in the SettingsButton class from code level.

Logout

If user wants to logout from the application, s/he needs to click power icon in the tab bar right side corner. Then it will pop up a confirmation dialog. If user clicks Yes , then from code level it calls logoutAction(). Then it will delete the refresh token from secure storage and will navigate user to Login widget as follows:

Future<void> logoutAction() async {

await clearRefreshToken(); setState(() {

isBusy = false;

}); await Navigator.pushNamedAndRemoveUntil(

context, '/login', (route) => false);

}

All sub sections we have discussed under ‘Implementation’ section cover the overall implementation of this sample application.

What’s Next

This sample mobile application can be improved further to cater your business use cases. Here we have few more suggestions that you can start with:

Keep multiple main files (ex: main_dev.dart, main_prod.dart) and use flutter build target to build for different development environments or release types. You can find more details on how to do it with Flutter in following documentations:

— Medium article — Flavors in dart code

— Creating flavors for Flutter — Official documentation

to build for different development environments or release types. You can find more details on how to do it with Flutter in following documentations: — Medium article — Flavors in dart code — Creating flavors for Flutter — Official documentation Handle exceptions, error codes and error messages in a more informative way.

Implement exponential backoff strategy on failed requests. Refer this example algorithm for more details on exponential backoff strategy.

Users can theme WSO2 authorization login pages matching to their organization’s logo and theme. Drop an email to cloud@wso2.com if you are interested in trying out this with WSO2 Cloud. Find more details on theming login pages in product documentation from this link.

That’s all about implementing PKCE with WSO2 API Cloud for a Flutter based mobile application.

Hope you enjoyed the article! !