development
12. Feb 2021
OAuth2 with Flutter Web
Development
12. Feb 2021

Autor
Robin JankeSometimes it can be very difficult to handle OAuth with Flutter (Web).
I have done it the following way but can’t find any official documentation about this:
First of all ddd the route to the main MaterialApp:
class MyApp extends StatelessWidget {
AuthService _authService = getIt.get<AuthService>();
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Demo App',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
onGenerateRoute: (RouteSettings routeSettings) {
if (routeSettings.name.contains("callback")) {
String code = Uri.base.toString().substring(Uri.base.toString().indexOf('code=') + 5);
this._authService.doAuthOnWeb({'code': code});
}
return MaterialPageRoute(builder: (BuildContext context) {
return TestScreen();
});
},
);
}
}
Next create the AuthService:
import 'dart:convert';
import 'package:flutter_web_auth/flutter_web_auth.dart';
import 'package:http/http.dart' as http;
import 'package:rxdart/rxdart.dart';
import 'package:oauth2/oauth2.dart' as oauth2;
import 'package:shared_preferences/shared_preferences.dart';
import 'package:universal_html/html.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
class AuthService {
String AUTH0_DOMAIN = 'xxx.eu.auth0.com';
String AUTH0_CLIENT_ID = 'XXXXXXXXXXXXXXXXXXXXXX';
String get AUTH0_REDIRECT_URI {
if (kIsWeb) {
if (isInDebugMode) {
return 'http://localhost:8080/callback.html';
} else {
return 'https://xxx.com/callback.html';
}
} else {
return 'de.xx.xyz://login-callback';
}
}
String AUTH0_ISSUER = 'https://xxx.eu.auth0.com';
String CLIENT_SECRET = 'XXXXXXXXXXXXXXXXXXXXXXXXXXX';
String authorizationEndpoint = "https://xxx.eu.auth0.com/authorize";
String tokenEndpoint = "https://xxx.eu.auth0.com/oauth/token";
String userInfoEndpoint = "https://xxx.eu.auth0.com/userinfo";
getCredentialsFile() {
return this._sharedPreferences.getString('auth_credentials');
}
setCredentialsFile(String value) {
this._sharedPreferences.setString('auth_credentials', value);
}
clearCredentialsFile() {
this._sharedPreferences.setString('auth_credentials', null);
}
final BehaviorSubject<bool> isBusy = new BehaviorSubject();
final BehaviorSubject<bool> isLoggedIn = new BehaviorSubject();
final BehaviorSubject<String> jwtToken = new BehaviorSubject();
SharedPreferences _sharedPreferences = getIt.get<SharedPreferences>();
Map<String, dynamic> parseIdToken(String idToken) {
final parts = idToken.split(r'.');
assert(parts.length == 3);
return jsonDecode(utf8.decode(base64Url.decode(base64Url.normalize(parts[1]))));
}
Future<void> initAction() async {
if (this.getCredentialsFile() == null) {
return;
}
this.isBusy.add(true);
try {
var credentials = oauth2.Credentials.fromJson(this.getCredentialsFile());
var client = oauth2.Client(credentials, identifier: this.AUTH0_CLIENT_ID, secret: this.CLIENT_SECRET);
await this.setLoginData(client);
} catch (e, s) {
print('error on refresh token: $e - stack: $s');
logoutAction();
}
}
Future<void> loginAction() async {
this.isBusy.add(true);
try {
if (this.getCredentialsFile() != null) {
var credentials = oauth2.Credentials.fromJson(this.getCredentialsFile());
return oauth2.Client(credentials, identifier: this.AUTH0_CLIENT_ID, secret: this.CLIENT_SECRET);
}
var grant = oauth2.AuthorizationCodeGrant(
this.AUTH0_CLIENT_ID, Uri.parse(this.authorizationEndpoint), Uri.parse(this.tokenEndpoint),
secret: this.CLIENT_SECRET, redirectEndpoint: Uri.parse(this.AUTH0_REDIRECT_URI));
this._sharedPreferences.setString('grant', jsonEncode(grant.toJson()));
Iterable<String> scopes = ['openid', 'profile', 'offline_access'];
var authorizationUrl = grant.getAuthorizationUrl(Uri.parse(this.AUTH0_REDIRECT_URI), scopes: scopes);
if (kIsWeb) {
window.location.assign(authorizationUrl.toString());
} else {
final String result = await FlutterWebAuth.authenticate(url: authorizationUrl.toString(), callbackUrlScheme: "de.xxx.xyz");
this.doAuthOnMobile(result);
}
} catch (e, s) {
print('login error: $e - stack: $s');
this.isBusy.add(false);
this.isLoggedIn.add(false);
}
}
setLoginData(oauth2.Client client) async {
final idTokenAsObject = parseIdToken(client.credentials.idToken);
this.jwtToken.add(client.credentials.idToken);
this.setCredentialsFile(client.credentials.toJson());
this.isBusy.add(false);
this.isLoggedIn.add(true);
this.name.add(idTokenAsObject['name']);
}
doAuthOnMobile(String result) async {
final String code = Uri.parse(result).queryParameters['code'];
final grant = oauth2.AuthorizationCodeGrant.fromJson(
jsonDecode(this._sharedPreferences.getString('grant')),
);
final oauth2.Client client = await grant.handleAuthorizationResponse({'code': code});
await this.setLoginData(client);
}
doAuthOnWeb(var callback) async {
final grant = oauth2.AuthorizationCodeGrant.fromJson(
jsonDecode(this._sharedPreferences.getString('grant')),
);
final oauth2.Client client = await grant.handleAuthorizationResponse(callback);
window.location.assign("#/");
await this.setLoginData(client);
}
Future<String> getAccessTokenIfLoggedIn() async {
if (this.isLoggedIn.hasValue) {
if (this.isLoggedIn.value == true) {
if (this.jwtToken.hasValue) {
return this.jwtToken.value;
} else {
return null;
}
} else {
return null;
}
} else {
return null;
}
}
void logoutAction() async {
this.clearCredentialsFile();
this.name.add(null);
this.isLoggedIn.add(false);
this.isBusy.add(false);
}
}
Next create a filled called callback.html in your web folder:
<html>
<body>
</body>
<script>
function findGetParameter(parameterName) {
var result = null,
tmp = [];
location.search
.substr(1)
.split("&")
.forEach(function (item) {
tmp = item.split("=");
if (tmp[0] === parameterName) result = decodeURIComponent(tmp[1]);
});
return result;
}
let code = findGetParameter('code');
// Get Hostname
var url = window.location.href
var arr = url.split("/");
var currentUrl = arr[0] + "//" + arr[2]
// Build new URL
let newUrl = currentUrl + "/#/callback?code=" + code;
// Send to new URL
window.location.href = newUrl;
</script>
</html>
Now you got OAuth2 working on Flutter Web and Flutter Mobile.
If you wanna got an Authorization Header value to call your backend you can use this._authService.getAccessTokenIfLoggedIn.