Enrico Triolo
Posted on April 6, 2020
In the first part of this article I will give you an overview of the OAuth 2 standard, in particular I will introduce the two most used grants, the Client Credentials and the Authorization Code grants.
In the second part I will introduce oauth2_client, a Flutter library that considerably simplifies working with OAuth 2.
By the end of the article you will be able to implement your own client with just little lines of code.
If you are already familiar with the concepts behind OAuth2, you can directly skip to the hands on section, otherwise keep reading...
What is OAuth 2?
OAuth 2 is an open standard protocol that allows applications to access protected resources of a service on behalf of the user. OAuth 2 defines authorization flows for native apps, web apps and for mobile devices.
Many companies offer OAuth 2 endpoints: these include the usual suspects, such as Google, Facebook, LinkedIn, GitHub, as well as many other.
Generally speaking, an authorization process can be carried on as follow:
- The application sends to the authorization server an Authorization Request to access a protected resource
- The owner of the resource (usually, the user) grants access to it
- The authorization server sends back an Access Token to be used with all the subsequent requests as a sort of authorization badge
This is just an high level depiction of the whole process. The actual flow will differ depending on the authorization grant type in use.
The two most used grant types are the Client Credentials grant and the Authorization Code grant.
The Client Credentials grant
The Client Credentials grant is primarily used on machine-to-machine applications, such as daemons, services or CLI. In this case the authorization server grants access to the application itself rather than to the user.
To use this flow, an application must have been assigned a client ID and a client secret. These parameters are generated by the authorization server, and are needed to ensure that the client connecting is the one that has been authorized.
The authorization process works more or less like this:
- The application sends to the authorization server an Authorization Request along with the client id and the client secret.
- The authorization server checks the provided credentials
- The authorization server sends back to the application an Access Token, along with a Referesh Token.
- The application uses the Access Token in every subsequent request to the service.
The Authorization Code grant
The Authorization Code grant is by far the most used flow as it is employed by applications that effectively operate on some user owned resources.
To use this flow, the application must have been registered with the OAuth server, providing at least a name and a redirect URI, that will be used to inform the application that the user authorized the request. During the registration, the provider assigns the application a client ID and a client secret, that will be exchanged in the authorization process.
To access a specific resource, the application should specify one or more scopes, a list of strings that represent the "permissions" the application is requiring. The scopes are defined by the resource server and are normally included in the API documentation of the service.
This flow can be summarized like this:
- The user opens up the app
- The app opens up a browser instance pointing to the authorization endpoint on the authorization server, requiring permission to access the needed resources, and passing in the Client ID and the list of required scopes.
- The user authenticates with the authorization server and grants access to the resources
- The authorization server redirects the client to the redirect uri sending an Authorization Token
- The client app issues an Access Token Request, passing in the Authorization Token and the client secret.
- The server validates the client secret and the Authorization Token and sends back an Access Token and a Refresh Token
- The client app uses the Access Token in every subsequent request to the API service as a sort of authorization badge.
The OAuth 2 framework has been designed to be extensible, and as such it underwent some additions since its inception, for example to address potential security flaws or extend its functionalities. One of these is the PKCE extension, designed to increase the Authorization Code grant security through an exchange of a client generated code that limits the chances of the authorization code to be intercepted.
Access Token expiration / invalidation
To limit the risks of token hijacking, the Access Token often has a limited lifespan after which it cannot be used anymore. Moreover, the Access Token could be explicitly invalidated by the resource server.
When the token expires, the application must require another Access Token using another grant type, the Refresh Token grant. As the name suggests, this flow uses the Refresh Token returned by the previous authorization flows to generate a new Access Token that must be used from that moment onward.
Hands on!
By now it should be clear that to implement an OAuth 2 capable application, one must implement not only the authorization process provided by the service he is going to use, but even the mechanisms to deal with the expiration or invalidation of the Access Token. These can easily become quite wasteful activities!
oauth2_client is a Flutter library specifically created with the purpose of simplifying the process of requiring and refreshing the OAuth 2 access token. It provides pre-made classes to authenticate againts the leading providers, such as Google, Facebook, LinkedIn, GitHub, but it's particularly suited for implementing clients for custom OAuth 2 servers.
It additionally provides convenience methods for secure token storage, automatic token refreshing and to transparently perform authenticated HTTP requests.
Suppose we wanted to implement an app with the GitHub API. As per their API documentation, GitHub recommends on using the Web application flow (with the Authorization Code grant).
This means that one of the steps of the process involves opening up a browser instance and intercepting a redirection to a provided redirect uri.
If one of the targets of our app is Android, we first need to set up an Intent Filter in the AndroidManifest.xml file to define a custom uri scheme that will be intercepted by our app:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.mypackage">
...
<activity android:name="com.linusu.flutter_web_auth.CallbackActivity" >
<intent-filter android:label="flutter_web_auth">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="my.app" />
</intent-filter>
</activity>
</manifest>
The value of the android:scheme attribute can be anything you want, and must match the scheme used for the redirect uri (as we'll see later). Just make sure, if at all possible, not to use http(s), because in that case our application won't be able to intercept the server redirection, as it will be automatically handled by the system browser.
If you really want/need to use an HTTPS url as the redirect uri, you must setup it as an App Link.
First you need to specify both the android:host and android:pathPrefix attributes, as long as the android:autoVerify="true" attribute on the intent-filter tag:
<activity android:name="com.linusu.flutter_web_auth.CallbackActivity" >
<intent-filter android:label="flutter_web_auth" android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https"
android:host="www.myapp.com"
android:pathPrefix="/oauth2redirect" />
</intent-filter>
</activity>
Then you need to prove ownership of the domain host by publishing a Digital Asset Links JSON file on your website. This involves generating an App signing key and signing your app with it.
Our sample app will retrieve the list of the user's repositories. To do so, we must tell the server which "permissions" we'll need, using the scope parameter. The scope that requires access to the user's repositories (both public and private) is "repo".
Before you can interact with GitHub's OAuth 2 endpoint, you must create a new OAuth App. After this step your application will be assigned a clientId and a clientSecret.
Ok, enough of talking... Let's see some code!
import 'package:oauth2_client/github_oauth2_client.dart';
import 'package:oauth2_client/access_token_response.dart';
//Instantiate the GitHub client
OAuth2Client client = GitHubOAuth2Client(
//Corresponds to the android:scheme attribute
customUriScheme: 'my.app',
//The scheme must match the customUriScheme parameter!
redirectUri: 'my.app://oauth2redirect'
);
//Require an Access Token with the Authorization Code grant
AccessTokenResponse tknResp = await client.getTokenWithAuthCodeFlow(
clientId: 'myclientid',
clientSecret: 'myclientsecret',
scopes: ['repo']);
//From now on you can perform authenticated HTTP requests
httpClient = http.Client()
http.Response resp = await httpClient.get('https://api.github.com/user/repos',
headers: {'Authorization': 'Bearer ' + tknResp.accessToken});
//The list of the user repositories is encoded in the resp.body property...
As you see, fetching an Access Token is quite simple, and since GitHub tokens don't expire we should be all set... But what happens if the token gets invalidated by the app owner? You would need to check the validity of the token after every request and possibly fetch another token. Something like this:
...
httpClient = http.Client()
http.Response resp = await httpClient.get('https://api.github.com/user/repos',
headers: {'Authorization': 'Bearer ' + tknResp.accessToken});
//The request to the service didn't succeed...
//We must refresh the token (not supported by GitHub endpoint) or
//generate a new Access Token
if(resp.statusCode != 200) {
AccessTokenResponse tknResp = await client.getTokenWithAuthCodeFlow(
clientId: 'myclientid',
clientSecret: 'myclientsecret',
scopes: ['repo']);
//Perform another request to the server with the new access token
http.Response resp = await httpClient.get('https://api.github.com/user/repos',
headers: {'Authorization': 'Bearer ' + tknResp.accessToken});
}
Fortunately enough, the library provides an helper class that takes care of all of the burden:
//Instantiate the client...
OAuth2Client client = GitHubOAuth2Client(
redirectUri: 'my.app://oauth2redirect',
customUriScheme: 'my.app');
//Instantiate the helper passing it the client and passing in the authorization parameters
OAuth2Helper oauth2Helper = OAuth2Helper(client,
grantType: OAuth2Helper.AUTHORIZATION_CODE, //default value, can be omitted
clientId: 'myclientid',
clientSecret: 'myclientsecret',
scopes: ['repo']);
//Perform a request to the server
http.Response resp = await oauth2Helper.get('https://api.github.com/user/repos');
As long as the first request is made, the helper class performs the following operations:
- Checks if an Access Token already exists in the device storage
- If it doesn't exist, generates an Access Token using the required flow (Authorization Code when not specified) and stores it into the device secure storage.
- Sends the request to the server, adding the "Authorization" header with the access token
- If the server returns an "unauthorized" response, try to refresh the Access Token through the Refresh Token flow, if the service supports it, otherwise generate a new token. Finally, send another request with the new token.
As you see, not only the helper class takes care of the token retrieval/refresh process, it even stores the token in a secure storage on the device. This means that if your app is closed or the device is restarted, the retrieved tokens are still available and can be used without requiring a new authorization process.
That's all well and good, but what if I wanted to implement my own client?
Up until now, we saw how to use a predefined client class, but what if you needed to authenticate against a service that doesn't have a dedicated class?
Implementing your own clients is really simple, and normally requires only a couple of lines of code. For example, let's see how the GitHub client is made:
class GitHubOAuth2Client extends OAuth2Client {
GitHubOAuth2Client(
{@required String redirectUri, @required String customUriScheme})
: super(
authorizeUrl: 'https://github.com/login/oauth/authorize',
tokenUrl: 'https://github.com/login/oauth/access_token',
redirectUri: redirectUri,
customUriScheme: customUriScheme) {
//By default, GitHub responds to an Access Token request with a querystring-like
//format. We need a json formatted response, that can be required through
//the 'Accept' header...
this.accessTokenRequestHeaders = {
'Accept': 'application/json'
};
}
}
That's it! You really just need to extend the OAuth2Client class and provide the authorization and the token urls.
After that, you can use your client or even the helper as seen above, without any modifications.
Finally... The inevitable TL;DR
oauth2_client really simplifies working with OAuth 2 endpoints, as it takes care of all the peculiarities of the protocol and automates the process of refreshing the tokens upon expiration. It even transparently implements many of the best practices introduced to enforce the security and reliability of the process.
Posted on April 6, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.