OAuth2 with Flutter Web 12 Mar, 2021

OAuth2 with Flutter Web

Sometimes 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.

comments powered by Disqus