Integrate with Auth0 using Riverpod to manage state
Tags: flutter, riverpod, auth0
Published: 2021-12-06 03:56:53

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

- [Build a Flutter Wishlist App, Part 3: Finishing the App with User Authentication and a Secured API](https://auth0.com/blog/build-flutter-wishlist-app-with-secure-api-part-3/)
- [Adding Auth0 to a Flutter application](https://daily-dev-tips.com/posts/adding-auth0-to-a-flutter-application/)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/am9sl1ru0jppj1u677u2.jpg)

## Directory hierarchy

```bash
.
├── 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
```

## Set up in Auth0

### 1. Create your application in Auth0

Create your application named testauth0, being type of Native.


![Create application in Auth0](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nura1txuf1y3cgoq252l.jpeg)

### 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e2wg3q48bo1jkalcjjo0.jpg)

### 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lcpj0xgqtaq5ocdjerwr.jpg)

[Callback URLs](https://auth0.github.io/auth0-oidc-client-net/documentation/getting-started/callbacks.html)

## Implement the code

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

```shell
$ flutter create --org com.auth0 testauth0
```

### Package install

```yaml
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
```

Run the next command to install packages in your shell.

```bash
$ flutter pub get
```

### Build gradle (for Android)

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

### info.plist (for iOS)

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

```dart
<key>CFBundleURLTypes</key>
<array>
	<dict>
		<key>CFBundleTypeRole</key>
		<string>Editor</string>
		<key>CFBundleURLSchemes</key>
		<array>
			<string>com.auth0.testauth0</string>
		</array>
	</dict>
</array>
```

### 1. Define the const file

```dart
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';
}
```

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

```dart
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(),
        ),
      ),
    );
  }
}
```

Wrap MyApp widget by ProviderScope.

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

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

```dart
useEffect(() {
  Future.microtask(() async {
    ref.watch(auth0NotifierProvider.notifier).initAction();
  });
  return;
}, const []);
```

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.

```dart
body: Center(
  child: auth0State.isBusy
      ? const CircularProgressIndicator()
      : auth0State.isLoggedIn
          ? const ProfileView()
          : const LoginView(),
),
```

### 3-1. Controller on StateNotifier

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

```dart
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);
  }
}
```

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

```dart
Future<void> initAction() async {}
Future<void> login() async {}
Future<void> logout() async {}
```

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()` .

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

### 3-2. Freezed for the controller

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

```dart
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;
}
```

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.

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

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

```dart
part 'auth0_state.freezed.dart';
```

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

```bash
$ flutter pub run build_runner build --delete-conflicting-outputs
```

### 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()` .

```dart
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');
    }
  }
}
```

### 5-1. Login view

This is LoginView widget called from main.dart.

```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 ?? ''),
      ],
    );
  }
}
```

### 5-2. Profile view

We also define ProfileView.

```dart
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'),
        ),
      ],
    );
  }
}
```

## References

- [Build a Flutter Wishlist App, Part 3: Finishing the App with User Authentication and a Secured API](https://auth0.com/blog/build-flutter-wishlist-app-with-secure-api-part-3/)
- [Adding Auth0 to a Flutter application](https://daily-dev-tips.com/posts/adding-auth0-to-a-flutter-application/)