Flutter mobile application to consume your WSO2 Cloud APIs with PKCE

Image for post
Image for post
source: https://www.360technosoft.com

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!

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.

Image for post
Image for post
source: https://www.mediaan.com/mediaan-blog

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.

flutter pub get

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.

Image for post
Image for post
  • 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.
  • 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.

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

flutter run -d all
  1. After running the application, a UI will be popped out with a button named Login to Cloud.
Image for post
Image for post
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.

Image for post
Image for post
Login page of API Cloud shown in browser

3. Then approve access to user profile information.

Image for post
Image for post
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.

Image for post
Image for post
Initial home page
Image for post
Image for post
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.

Image for post
Image for post
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.

Image for post
Image for post
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.

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

Image for post
Image for post

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.
  • redirect_uri : 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)
  • 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');
}

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

Image for post
Image for post

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:

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

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

Image for post
Image for post

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.

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.

Image for post
Image for post

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
  • 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! !

If you have any questions, drop a comment in this article or directly contact us at cloud@wso2.com.

Senior Software Engineer@WSO2 | Open-source Contributor | Basketball Enthusiast

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store