<tl;dr>This story contains several posts, this post is an introduction to the requirements, to the problem and to the players. This post doesn’t contain any Dart code for the Flutter frontend or php for Sanctum/Socialite backend. </tl;dr>
The requirements I’m trying to meet
- Frontend application that is initially loaded from an another domain than where the backend application is running can access the backend with bearer token auth.
- Frontend application can be written by 3rd party and doesn’t require registration to the backend.
- There will be a Flutter web application that will consume backend REST-API that uses social authentication with OAuth 2.0.
- The backend REST-API is almost stateless.
The chosen backend tools for this project
- Laravel Sanctum, a simple bearer token based authorization for Laravel.
- Laravel Socialite, a simple helper to use OAuth 1/2 to authenticate user against identity providers with lots of additions.
- Laravel, anything but a simple framework to build php applications.
- Postgresql database server, at least version 13. The solution doesn’t actually require any advanced features of Postgresql and could in theory be implemented even with Sqllite. But I’m customized in doing things with real databases and probably won’t even notice when I’m doing something that can’t be done my way in your preferred database thus requirement of Postgresql and version 13.
I have only rewritten and fixed one small Laravel app but I have more than 20 years of experience in php coding. The way I’m using Laravel will be awkward and clumsy but it will work.
The chosen frontend tools for this project
This is my very first Flutter application and first ”declarative” UI. I have also zero Dart programming experience, so this will be a bumpy ride. On the other hand I have more than 30 years of experience in programming with over 20 different programming languages. I’m doing this as I think I have here something worth of sharing. This assumption is based on the google searches I did trying to find a recipe to do what I’m doing here. And no, I’m not coder/developer by profession and ”full stack developer” doesn’t describe me. If asked, I would say I’m a sourceror far beyond the acolyte level who mostly practices full trick deck development. I am also the scum from the devops.
Target authentication flow
When a single page application loads from the launching server it is supposed to continue operation by itself. This means that when login requires a redirection back to the app by means of the http redirect this redirect should be caught be the app. Also doing social login with auth provider and our backend requires relinquishing the control of user interface first to our backend and then to the auth provider backend. In practice this means that we need a web browser inside the app. In our case this browser is the Fluent webview.
The authentication flow (picture 1.) begins inside the app when the app needs bearer token to our API and doesn’t yet have one.
- The app launches it’s browser
- and points it towards our backend login url with it’s own redirect link.
- Backend forms a redirect request to the auth provider’s authorization end point and
- the browser starts the authentication dance with the authorization provider which
- either ends with
- failure, as user denies the use of the provider id or fails to authenticate or
- success, both user and authentication provider are granting the access.
- If the result was
- failure then we redirect back to the app and deliver the sad news. The flow continues from step 9.
- success, then the backend requests access token from the authorization provider.
- The backend either
- gets the token it requested and generates it’s own bearer token or
- is denied and doesn’t generate it’s own bearer token.
- Backend returns (redirects) back to the app.
- The app listens requests inside it’s browser and lets everything but it’s own redirect pass. It should also start timer on step 1 to timeout in case the flow hangs.
- Either the auth flow ended with redirect and the app can read the result from the request parameters or it timed out and results a failure.
In practice the above flow implements the corresponding OAuth 2.0 flow called Authorization Code Flow. The resulting OAuth 2.0 access token and refresh token are both bearer tokens. Whoever has them, can use them as long as they are valid in behalf of the user for the purposes defined when the authorization was requested. This means that the process of authorization could also happen:
- completely inside the Flutter app, the resulting access token would be given as the argument to the backend (see Flutter OAuth2) or
- started inside the Flutter app, the first request towards the provider backend would happen inside the app and the redirect would point to the backend which would exchange the grant token to access and refresh tokens.
If the authorization code flow was done completely inside the Flutter app or even partly inside the Flutter app then the Flutter app would be required to register itself with the social login provider(s) which the backend supports. The app would also be the one who has the ”contract” to use the identity with the user and the identity provider, not the backend. If the process was shared then in the worst case scenario both the backend and the frontend would share both the state/session and the registration secret and app id with the identity providers. This is generally a bad idea when the goal is to allow 3rd party apps to use your backend.
As we are not actually interested in accessing anything but the user identity from the authorization provider. We could drop the access token and refresh token after the initial fetch of the user details. This isn’t completely bad idea – unless the user gets hacked on the provider or deletes her identity on the provider in hopes of denying access to all resources simultaneously. If we never again check the validity of the initial access token then deleting the provider identity would not cascade to our backend. Also if user’s name or email change – then we will miss the change of the details. Then there are providers whose tokens never expire by default – like GitHub. We could mitigate both by setting our access token to expire frequently without ability to refresh it. When our token expires then the holder of the expired token would be required to login again through the identity provider.
The identity provider is not required to support refresh tokens, so refreshing isn’t always an option but we should do that when it is offered and provider’s token expiration time is less than our expiration time.
Token bearer flow
The goal here is to offer the most convenient way for the app to use the API – which means less opening browser windows and surprise ”access denied” or even worse ”redirects to auth providers”. To simplify state management in the app we should provide a simple GET to the backend which would validate the token and give expiration time for our bearer token. This simplified flow is described in the picture 2.
- ”Is token valid”-get request to the backend (token in the authorization header).
- Backend checks if the token is valid or not.
- If token
- was invalid then we return immediately
- is valid then the we proceed to the next step.
- Either we try to refresh the identity provider token or access their user end point to see if the accesstoken from them is still valid
- If the accesstoken
- was refreshed / is valid we return the current expiration time in minutes
- failed, we delete/invalidate our own token and return failure.
In the next step I will write and prepare the backend. This is the topic of the next post.