OAuth 2 in Flutter Web Using AWS Cognito - by Muhammad Shahrukh - AWS in Plain English
OAuth 2 in Flutter Web Using AWS Cognito - by Muhammad Shahrukh - AWS in Plain English
OAuth 2 in Flutter Web Using AWS Cognito - by Muhammad Shahrukh - AWS in Plain English
This is your last free member-only story this month. Upgrade for unlimited access.
In this article, we will go over how we can implement OAuth in a Flutter Web project using AWS Cognito as the Identity Provider (the
steps shown here would be similar if you are using any other Identity Provider like OKTA, etc). However, if you are merely looking for
Flutter side implementation, and want to use your own Identity provider you can ignore Step 1.
Implementing OAuth in a web project can be quite tricky, and can be even more so with Flutter, which still lacks a lot of the web-based
features that you would get out of the box with other frameworks like React or Angular. But you can still get a pretty neat solution just
using native Dart & Flutter components.
Setup an App Client for your Flutter Web Project in your User Pool (I will call it Flutter Web Example) and take note of your
Amazon Cognito Domain (which is the URL of your AWS Cognito OAuth 2.0 authorization server), Client ID and Client Secret.
Enable Code Flow and implicit flow for obtaining Authorisation Code to exchange for User Tokens. Also enable other OAuth
Scopes like phone, email, openid, aws.cognito.signin.user.admin, profile. We will need them for setting up OAuth Flow on Flutter
side. For detailed explanation on all these parameters, check out official AWS Docs.
You will also need to add the SignIn and SignOut callback URLs in the app client for your dev environment. These must be exact
and stay the same in your dev environment to save you the headache during development (though you can specify multiple
callback URLs in the app client). You can configure flutter web to run on a specific port using the VSCode launch.json file.
1 {
2 "version": "0.2.0",
3 "configurations": [
4 {
5 "name": "flutter-example-debug",
6 "request": "launch",
7 "type": "dart",
8 "program": "lib/main.dart",
9 "args": ["--web-port", "8081"]
10 },
11 {
12 " " "fl tt l l "
12 "name": "flutter-example-release",
13 "request": "launch",
Open in app
14 "type": "dart",
15 "program": "lib/main.dart",
16 "args": ["--web-port", "8081", "--release"]
17 }
18 ]
19 }
flutter_example_launch.json
hosted with ❤ by GitHub view raw
I am setting my Url Strategy as Path URL Strategy in the main function of my project so that my URL paths are written without any
hashes. See the flutter docs for more info about URL strategies.
1 void main() {
2 setUrlStrategy(PathUrlStrategy());
3 runApp(FlutterWebOAuthExample());
4 }
5
6 class FlutterWebOAuthExample extends StatelessWidget {
7 @override
8 Widget build(BuildContext context) {
9 return MaterialApp(
10 title: 'Flutter Web Example',
11 theme: ThemeData(
12 primarySwatch: Colors.blue,
13 textTheme: Theme.of(context).textTheme.apply(fontFamily: 'Open Sans'),
14 ),
15 onGenerateRoute: (settings) => generateExampleRoutes(settings),
16 initialRoute: LoginRoute,
17 );
18 }
19 }
flutter_web_oauth_main.dart
hosted with ❤ by GitHub view raw
Here we created a simple Flutter App and provided configurations like Themes and Routes. Note that the initial route is set to
LoginRoute and we are providing a set of Navigation Routes for the app to use in the onGenerateRoute function. We will see this
shortly.
You will also need to set an extension on String to be able to extract query parameters from the page URL. I learnt this from
FilledStacks, who is a master developer and very generous with sharing his knowledge. He has one of the best content available on
Flutter development, and for free!! Check him out!
string_url_extension.dart
hosted with ❤ by GitHub view raw
We will use this extension to extract different parameters from our web app’s URL in the Router. Below is the Route configuration that
we passed in at the route of our App in onGenerateRoute function:
1 generateExampleRoutes(RouteSettings settings) {
2 var routingData = settings.name.getRoutingData;
3 switch (routingData.route) {
4 case LoginRoute:
5 return LoginView();
6 case HomeRoute:
7 return HomeView();
8 default:
9 if (routingData["code"] != null) {
10 String authCode = routingData["code"];
11 return HomeView(authCode: authCode);
12 }
13 return NoRouteDefinedView();
14 }
15 }
flutter_web_oauth_router.dart
hosted with ❤ by GitHub view raw
The app enters this function every time it navigates to a different view or if its URL changes due to any redirects during Auth flow.
Here we are making use of our String extension defined above to check if there is an authorisation “code” parameter in the URL of our
app, if so we extract it and pass it to our HomeView that will call it to fetch the user tokens from our auth server later on.
But first, let’s go over how we can obtain the authorisation code in the URL in the first place.
Open in app
1. Our Flutter web app, once started, will navigate to the LoginView as that is the initial route.
2. Our LoginView will have a “Login” button that will call a login() function to initiate the Auth Flow. I have defined this login()
function in my Api service class as I am using an MVVM pattern using Stacked Architecture (Also by FilledStacks), but you are
free to use any design pattern you feel comfortable with.
flutter_web_oauth_example_api_login.dart
hosted with ❤ by GitHub view raw
The login() function first checks if the user has any auth credentials like access token or refresh token stored inside our browser local
storage using the authenticate() function.
If not, it will navigate to the URL of our Auth Server (this is AWS Cognito Domain that we obtained from Step 1) with the query
parameters configured for Authorisation Code Grant Flow through the browser.
There are a number of parameters defined in this URL and I would advise you to read the official AWS Cognito Docs for a thorough
explanation, but I will explain the most important ones briefly here:
client_id: This is the Client ID of your registered App Client in your Cognito User Pool (We obtained this in Step 1).
response_type: This is the response you would like from the Auth Server: It can be either “code” for Authorisation Code Grant
Flow or “token” for Implicit Flow which returns user tokens like Id Token and Access Token (Note: A refresh token is never
returned in this flow). We are using “code” as we will exchange this for user tokens later on with a direct HTTP call and because
we want the refresh token to automatically refresh our access token when it expires.
scope: This can be a combination of any system-reserved scopes or custom scopes associated with a client. Note: An ID token is
only returned if openid scope is requested. The access token can be only used against Amazon Cognito User Pools if
aws.cognito.signin.user.admin scope is requested. The phone , email , and profile scopes can only be requested if openid scope
Open in app
is also requested. These scopes dictate the claims that go inside the ID token.
redirect_uri: This is the URL you want to be redirected to after successful authentication with the auth provider. This is
commonly the base URL of your app such as https://myapp.com or http://localhost:8080 during dev.
Once we log in to Cognito Auth Server using the URL above, we will be redirected back to our app via the redirect_uri specified in the
URL above with the authorisation code as a query parameter in the redirected Url. We will extract this through the router as
explained before:
1 generateExampleRoutes(RouteSettings settings) {
2 var routingData = settings.name.getRoutingData;
3 switch (routingData.route) {
4 case LoginRoute:
5 return LoginView();
6 case HomeRoute:
7 return HomeView();
8 default:
9 if (routingData["code"] != null) {
10 String authCode = routingData["code"];
11 return HomeView(authCode: authCode);
12 }
13 return NoRouteDefinedView();
14 }
15 }
flutter_web_oauth_router.dart
hosted with ❤ by GitHub view raw
Once we have the authorisation code, we can make a request to fetch user pool tokens via an HTTP client from our API service class:
1 ....
2 _tokenClient.options.baseUrl = _baseAuthUrl;
3 _tokenClient.options.headers = {
4 "Content-Type": "application/x-www-form-urlencoded",
5 "Authorization": "Basic " +
6 base64Encode(utf8.encode('$_authClientId:$_authClientSecret'))
7 };
8 ....
9
10 Future getTokens(String authCode) async {
11
12 String tokenPath = '/oauth2/token';
13
14 Map<String, String> queryParams = {
15 "grant_type": "authorization_code",
16 "client_id": _authClientId,
17 "code": authCode,
18 "redirect_uri": _redirectUri.toString(),
19 };
20 var response;
21 try {
22 response = await _tokenClient.post(tokenPath, queryParameters: queryParams);
23 } catch (e) {
24 return "Error retrieving user tokens: ${e.toString()}";
25 }
26
27 if (response.statusCode == 200) {
28 _accessToken = response.data['access_token'];
29 _refreshToken = response.data['refresh_token'];
30 _storage.addAll(
31 {"access_token": _accessToken, "refresh_token": _refreshToken});
32 await getCurrentUsersProfile(); Open in app
33 } else {
34 return "Error retrieving user tokens: ${response.statusMessage}";
35 }
36 }
37
38
flutter_web_exampel_api_tokens.dart
hosted with ❤ by GitHub view raw
Here we defined the base URL (the URL of our Cognito auth servers) for our HTTP _tokenClient and its headers which specify
Content-Type and Authorisation. Since our app client on Cognito has a Client Secret, we encode that Secret along with our Client
ID using base64 encoding in the Authorisation Header.
I have set up this base URL and headers inside my HTTP client interceptors (I am using the Dio HTTP client), so that I don’t have to
type this information each time when I make a new request. See the full API code below for its implementation.
We then use that _tokenClient to fetch the tokens and store them in our browser local storage for later use.
Note: the refresh_token returned by the auth server is valid for the duration of it’s expiry date, and you can continue to use it (as
many times as you can) to refresh your access_token until the refresh_token expires; after which you will have to re-authenticate your
user by signing into your AWS user pool again.
Note: the _redirectUri used for getting authorisation code during login and fetching user tokens must be the same.
Also Note: You should not be using the local storage of your browser for storing sensitive information like Access and Refresh Tokens
in a production environment.
I have linked my entire API service class here for your reading. Notice how we have set up HTTP client interceptors (which intercept
our HTTP request before it is sent). This is especially useful when it comes to automatically refreshing our access token once it expires.
Open in app
Here we are using the GetIt package to create a single instance of our Api service to access anywhere using locator, and passing
the reference of the service to the generateExampleRoutes() function.
Open in app
We call the isLoggedIn() method on the Api class to check if the application has authentication credentials (access token, refresh
token) for the current user to determine which view to show.
Final Words
This is my first technical blog post. So, if I have made any mistake with terminology or concepts, feel free to leave constructive
feedback; and if you found any of this helpful, please drop a like, etc.