DEV Community

Cover image for Integrate with Auth0 using Riverpod to manage state
uuta
uuta

Posted on

Integrate with Auth0 using Riverpod to manage state

Introduction

Hi all, this is my second article in Dev.to. I'm gonna try to describe how your Flutter application with Riverpod integrates to Auth0, providing authentication and authorization as a service. As you might know, some amazing articles for the Auth0 integration are here. You would also begin to make an implementation along with these.

However, I needed to know more about how should manage the state practically. This article will show you the way that handles the state with Riverpod on Flutter.

Goal

To integrate Auth0 with the Flutter app.

login on Auth0

Directory hierarchy

.
├── lib
│   ├── main.dart
│   ├── consts
│   │   ├── auth0.dart
│   ├── models
│   │   ├── controllers
│   │   │   ├── auth0
│   │   │   │   ├── auth0_controller.dart
│   │   │   │   ├── auth0_state.dart
│   │   ├── repositories
│   │   │   ├── auth0_repository.dart
│   ├── views
│   │   ├── login_view.dart
│   │   ├── profile_view.dart
Enter fullscreen mode Exit fullscreen mode

Set up in Auth0

1. Create your application in Auth0

Create your application named testauth0, being type of Native.

Create application in Auth0

2. Make sure of the Domain and Client ID

To integrate the Flutter application with Auth0, you need to know Domain and Client ID for API integration. Those information is in the Basic information upon Settings tab.

Auth0 dashboard

3. Set the call back URL

Input com.auth0.testauth0://login-callback into Allowed Callback URLs. This URL also will be reused as the constant variable in Flutter.

Configure the callback URL

Callback URLs

Implement the code

In this case, let's create an application named testauth0.

$ flutter create --org com.auth0 testauth0
Enter fullscreen mode Exit fullscreen mode

Package install

dependencies:
  flutter:
    sdk: flutter

  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^1.0.2
  riverpod: ^1.0.0
  flutter_hooks: ^0.18.0
  hooks_riverpod: ^1.0.0
  http: ^0.13.4
  freezed_annotation: ^1.0.0
  json_annotation: ^4.3.0
  flutter_appauth: ^2.1.0+1
  flutter_secure_storage: ^5.0.1

dev_dependencies:
  flutter_test:
    sdk: flutter
  freezed: ^1.0.0
  build_runner: ^2.1.5
  json_serializable: ^6.0.1
Enter fullscreen mode Exit fullscreen mode

Run the next command to install packages in your shell.

$ flutter pub get
Enter fullscreen mode Exit fullscreen mode

Build gradle (for Android)

defaultConfig {
    applicationId "com.auth0.testauth0"
    minSdkVersion 18 // up from 16
    targetSdkVersion 30
    versionCode flutterVersionCode.toInteger()
    versionName flutterVersionName
    // add this
    manifestPlaceholders = [
        'appAuthRedirectScheme': 'com.auth0.testauth0'
    ]
}
Enter fullscreen mode Exit fullscreen mode

info.plist (for iOS)

Add CFBundleURLTypes into <dict> in info.plist file.

<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleTypeRole</key>
        <string>Editor</string>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>com.auth0.testauth0</string>
        </array>
    </dict>
</array>
Enter fullscreen mode Exit fullscreen mode

1. Define the const file

class ConstsAuth0 {
  static const String AUTH0_DOMAIN = '<Domain>';
  static const String AUTH0_CLIENT_ID = '<Client ID>';
  static const String AUTH0_ISSUER = 'https://$AUTH0_DOMAIN';
  static const String AUTH0_REDIRECT_URI =
      'com.auth0.testauth0://login-callback';
}
Enter fullscreen mode Exit fullscreen mode

2. main.dart

Modify your code in main.dart as below. Don't worry about some errors after pasting the code because we'll fix those in another section.

import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import '/models/controllers/auth0/auth0_controller.dart';
import '/views/login_view.dart';
import '/views/profile_view.dart';
import 'package:flutter_hooks/flutter_hooks.dart';

void main() {
  runApp(ProviderScope(
    child: MyApp(),
  ));
}

class MyApp extends HookConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final auth0State = ref.watch(auth0NotifierProvider);
    useEffect(() {
      Future.microtask(() async {
        ref.watch(auth0NotifierProvider.notifier).initAction();
      });
      return;
    }, const []);

    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text("Auth0 state management"),
          backgroundColor: Colors.green,
        ),
        body: Center(
          child: auth0State.isBusy
              ? const CircularProgressIndicator()
              : auth0State.isLoggedIn
                  ? const ProfileView()
                  : const LoginView(),
        ),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Wrap MyApp widget by ProviderScope.

void main() {
  runApp(ProviderScope(
    child: MyApp(),
  ));
}
Enter fullscreen mode Exit fullscreen mode

useEffect function is effective in such cases like fetching data or calling API while building a widget. We call initAction() function, defined in auth0_controller.dart, processing login or logout.

useEffect(() {
  Future.microtask(() async {
    ref.watch(auth0NotifierProvider.notifier).initAction();
  });
  return;
}, const []);
Enter fullscreen mode Exit fullscreen mode

Let's take a look at the child argument in Center, switches widgets by depending on isBusy and isLoggedIn state. If isLoggedIn state is true, ProfileView widget is appeared. We'll also figure two widgets out later.

body: Center(
  child: auth0State.isBusy
      ? const CircularProgressIndicator()
      : auth0State.isLoggedIn
          ? const ProfileView()
          : const LoginView(),
),
Enter fullscreen mode Exit fullscreen mode

3-1. Controller on StateNotifier

To make a state management and call repository class, let's make auth0_controller.dart file.

import 'package:riverpod/riverpod.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import '/models/controllers/auth0/auth0_state.dart';
import '/models/repositories/auth0/auth0_repository.dart';
import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';

final auth0NotifierProvider =
    StateNotifierProvider<Auth0Controller, Auth0State>(
  (ref) => Auth0Controller(),
);

class Auth0Controller extends StateNotifier<Auth0State> {
  Auth0Controller() : super(const Auth0State());

  final repository = Auth0Repository();

  Future<void> initAction() async {
    state = state.copyWith(isBusy: true);
    try {
      final storedRefreshToken =
          await const FlutterSecureStorage().read(key: 'refresh_token');

      // Check stored refresh token
      if (storedRefreshToken == null) {
        state = state.copyWith(isBusy: false);
        return;
      }

      // Call init action repository
      final data = await repository.initAction(storedRefreshToken);
      state = state.copyWith(isBusy: false, isLoggedIn: true, data: data);
    } on Exception catch (e, s) {
      debugPrint('login error: $e - stack: $s');
      logout();
    }
  }

  Future<void> login() async {
    state = state.copyWith(isBusy: true);
    try {
      final data = await repository.login();
      state = state.copyWith(isBusy: false, isLoggedIn: true, data: data);
    } on Exception catch (e, s) {
      debugPrint('login error: $e - stack: $s');
      state = state.copyWith(
          isBusy: false, isLoggedIn: false, errorMessage: e.toString());
    }
  }

  Future<void> logout() async {
    state = state.copyWith(isBusy: true);
    await const FlutterSecureStorage().delete(key: 'refresh_token');
    state = state.copyWith(isBusy: false, isLoggedIn: false);
  }
}
Enter fullscreen mode Exit fullscreen mode

Three Future function are defined to manage state and call functions in the repository class.

Future<void> initAction() async {}
Future<void> login() async {}
Future<void> logout() async {}
Enter fullscreen mode Exit fullscreen mode

Auth0Controller class extends StateNotifier, possesses one state, works on Riverpod. Auth0Controller provides Auth0State() to the constructor as a variable of state. To make more concrete, let's take a look at Auth0State() .

class Auth0Controller extends StateNotifier<Auth0State> {
  Auth0Controller() : super(const Auth0State());
Enter fullscreen mode Exit fullscreen mode

3-2. Freezed for the controller

The Freezed package has an effect that changes class being immutable.

import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter/foundation.dart';

part 'auth0_state.freezed.dart';

@freezed
abstract class Auth0State with _$Auth0State {
  const factory Auth0State({
    @Default(false) bool isBusy,
    @Default(false) bool isLoggedIn,
    Map? data,
    String? errorMessage,
  }) = _Auth0State;
}
Enter fullscreen mode Exit fullscreen mode

As I mentioned before, Auth0State defines the state in Auth0Controller class. In our Flutter app, Auth0State is consisted by isBusy, isLoggedIn, data and errorMessage. These variables hinges on the definition of your app.

@freezed
abstract class Auth0State with _$Auth0State {
  const factory Auth0State({
    @Default(false) bool isBusy,
    @Default(false) bool isLoggedIn,
    Map? data,
    String? errorMessage,
  }) = _Auth0State;
}
Enter fullscreen mode Exit fullscreen mode

We also set down the below. It means that Flutter will generate auth0_state.freezed.dart file and regard it as a library.

part 'auth0_state.freezed.dart';
Enter fullscreen mode Exit fullscreen mode

To generate auth0_state.freezed.dart, run the next code.

$ flutter pub run build_runner build --delete-conflicting-outputs
Enter fullscreen mode Exit fullscreen mode

4. Repository

Here is Auth0Repository class to call Auth0 API and then return the result from it. In the course of processing, the refresh token is stored to FlutterSecureStorage() .

import 'package:flutter_appauth/flutter_appauth.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import '/consts/auth0.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';

class Auth0Repository {
  Future<Map<String, Object>> initAction(String storedRefreshToken) async {
    final response = await _tokenRequest(storedRefreshToken);

    final idToken = _parseIdToken(response?.idToken);
    final profile = await _getUserDetails(response?.accessToken);

    const FlutterSecureStorage()
        .write(key: 'refresh_token', value: response!.refreshToken);

    return {...idToken, ...profile};
  }

  Future<TokenResponse?> _tokenRequest(String? storedRefreshToken) async {
    return await FlutterAppAuth().token(TokenRequest(
      ConstsAuth0.AUTH0_CLIENT_ID,
      ConstsAuth0.AUTH0_REDIRECT_URI,
      issuer: ConstsAuth0.AUTH0_ISSUER,
      refreshToken: storedRefreshToken,
    ));
  }

  Future<Map<String, Object>> login() async {
    final AuthorizationTokenResponse? result = await _authorizeExchange();

    final Map<String, Object> idToken = _parseIdToken(result?.idToken);
    final Map<String, Object> profile =
        await _getUserDetails(result?.accessToken);

    await const FlutterSecureStorage()
        .write(key: 'refresh_token', value: result!.refreshToken);
    return {...idToken, ...profile};
  }

  Future<AuthorizationTokenResponse?> _authorizeExchange() async {
    return await FlutterAppAuth().authorizeAndExchangeCode(
      AuthorizationTokenRequest(
          ConstsAuth0.AUTH0_CLIENT_ID, ConstsAuth0.AUTH0_REDIRECT_URI,
          issuer: ConstsAuth0.AUTH0_ISSUER,
          scopes: <String>['openid', 'profile', 'offline_access', 'email'],
          promptValues: ['login']),
    );
  }

  Map<String, Object> _parseIdToken(String? idToken) {
    final List<String> parts = idToken!.split('.');
    assert(parts.length == 3);

    return Map<String, Object>.from(jsonDecode(
        utf8.decode(base64Url.decode(base64Url.normalize(parts[1])))));
  }

  Future<Map<String, Object>> _getUserDetails(String? accessToken) async {
    final http.Response response = await http.get(
      Uri.parse(ConstsAuth0.AUTH0_ISSUER + '/userinfo'),
      headers: <String, String>{'Authorization': 'Bearer $accessToken'},
    );

    if (response.statusCode == 200) {
      return Map<String, Object>.from(jsonDecode(response.body));
    } else {
      throw Exception('Failed to get user details');
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

5-1. Login view

This is LoginView widget called from main.dart.

import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import '/models/controllers/auth0/auth0_controller.dart';

class LoginView extends HookConsumerWidget {
  const LoginView({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final auth0State = ref.watch(auth0NotifierProvider);

    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        ElevatedButton(
          onPressed: () async {
            ref.read(auth0NotifierProvider.notifier).login();
          },
          child: const Text('Login'),
        ),
        Text(auth0State.errorMessage ?? ''),
      ],
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

5-2. Profile view

We also define ProfileView.

import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import '/models/controllers/auth0/auth0_controller.dart';

class ProfileView extends HookConsumerWidget {
  const ProfileView({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final auth0State = ref.watch(auth0NotifierProvider);

    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        Container(
          width: 150,
          height: 150,
          decoration: BoxDecoration(
            border: Border.all(color: Colors.blue, width: 4),
            shape: BoxShape.circle,
            image: DecorationImage(
              fit: BoxFit.fill,
              image: NetworkImage(auth0State.data!['picture'] ?? ''),
            ),
          ),
        ),
        const SizedBox(height: 24),
        Text('Name: ' + auth0State.data!['name']),
        const SizedBox(height: 48),
        ElevatedButton(
          onPressed: () async {
            ref.read(auth0NotifierProvider.notifier).logout();
          },
          child: const Text('Logout'),
        ),
      ],
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

References

Top comments (1)

Collapse
 
hoangnguyen679 profile image
HoangNguyen679

Thank you for your post.
It helped me a lot.
I am now integrating with go_router for route between screen.
But I am being stuck at redirect rule. I can not handle both isBusy and isLoggedIn on the same time. Could you give some advise ?
Thank you!