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
- 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.
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
Set up in Auth0
1. Create your application in Auth0
Create your application named testauth0, being type of Native.
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.
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.
Implement the code
In this case, let's create an application named testauth0.
$ flutter create --org com.auth0 testauth0
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
Run the next command to install packages in your shell.
$ flutter pub get
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'
]
}
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>
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';
}
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(),
),
),
);
}
}
Wrap MyApp widget by ProviderScope.
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.
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.
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.
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.
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()
.
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.
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.
@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.
part 'auth0_state.freezed.dart';
To generate auth0_state.freezed.dart, run the next code.
$ 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()
.
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.
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.
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'),
),
],
);
}
}
Top comments (1)
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
andisLoggedIn
on the same time. Could you give some advise ?Thank you!