i have a rest api server with two endpoint for JWT authentication (/auth/token/ and /auth/token/refresh). and i want to create an app that use that rest server. what i did until now is: created a userManager class .when the user login i save the username, password , access token , refresh token and expiration time for both tokens (i'm usin secure storage package) . whenever i send a request (if the request require authentication) i verify if the access token is valid .if its not valid i refresh it and send the request. the access token have 5 min exp time and refresh token have 24 hours (the default for django).
my code :
import 'dart:convert';
import 'package:ads_site_app/utils.dart';
import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:http/http.dart';
import 'data.dart';
class UserManager {
static final _storage = FlutterSecureStorage();
static const _keyUsername = 'username';
static const _keyPassword = 'password';
static const _keyEmail = 'email';
static const _keyAccessToken = 'accesstoken';
static const _keyRefreshToken = 'refreshtoken';
static const _keyAccessExpTime = 'accesstokenExptime';
static const _keyRefreshExpTime = 'refreshtokenExptime';
static String _loginError = "";
static String get loginError => _loginError;
static set loginError(String loginError) {
_loginError = loginError;
}
static Future setUsername(String username) async =>
await _storage.write(key: _keyUsername, value: username);
static Future<String?> getUsername() async =>
await _storage.read(key: _keyUsername);
static Future _setPassword(String password) async =>
await _storage.write(key: _keyPassword, value: password);
static Future<String?> _getPassword() async {
final value = await _storage.read(key: _keyPassword);
return value;
}
static Future _setAccessToken(String token) async {
final value = token;
await _storage.write(key: _keyAccessToken, value: value);
}
static Future<String?> getAccessToken() async {
final value = await _storage.read(key: _keyAccessToken);
return value;
}
static Future _setRefreshToken(String token) async {
final value = token;
await _storage.write(key: _keyRefreshToken, value: value);
}
static Future<String?> getRefreshToken() async {
final value = await _storage.read(key: _keyRefreshToken);
return value;
}
static Future _setAccessTokenExpTime(DateTime time) async {
final value = time;
await _storage.write(key: _keyAccessExpTime, value: value.toString());
}
static Future<DateTime?> getAccessTokenExpTime() async {
final value = await _storage.read(key: _keyAccessExpTime);
return value == null ? null : DateTime.parse(value);
}
static Future _setRefreshTokenExpTime(DateTime time) async {
final value = time;
await _storage.write(key: _keyRefreshExpTime, value: value.toString());
}
static Future<DateTime?> getRefreshTokenExpTime() async {
final value = await _storage.read(key: _keyRefreshExpTime);
return value == null ? null : DateTime.parse(value);
}
//////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////
static Future<void> login(
String url, String path, String username, String password) async {
Response response;
try {
response = await post(
Uri.http(
url,
path,
),
body: {'username': username, 'password': password});
if (response.statusCode >= 200 && response.statusCode < 300) {
var time = DateTime.now();
var data = jsonDecode(response.body) as Map;
var refreshParsedToken = parseJwt(data['refresh']);
var accessParsedToken = parseJwt(data['access']);
var refreshExpTime = DateTime.fromMillisecondsSinceEpoch(
refreshParsedToken['exp'] * 1000)
.subtract(Duration(minutes: 30));
var accessExpTime =
DateTime.fromMillisecondsSinceEpoch(accessParsedToken['exp'] * 1000)
.subtract(Duration(minutes: 2));
_setRefreshToken(data['refresh']);
_setAccessToken(data['access']);
_setAccessTokenExpTime(accessExpTime);
_setRefreshTokenExpTime(refreshExpTime);
setUsername(username);
_setPassword(password);
} else {
throw InvalidCredentials('invalid username or password');
}
} catch (e) {
print(e);
throw e;
}
}
static Future<bool> refreshToken(String url, String path) async {
Response response;
String? token = await getRefreshToken();
try {
response = await post(
Uri.http(
url,
path,
),
body: {'refresh': ' ${token}'});
if (response.statusCode >= 200 && response.statusCode < 300) {
var data = jsonDecode(response.body) as Map;
await _setAccessToken(data['access']);
return true;
} else {
return false;
}
} catch (e) {
print('user manager 145');
return false;
}
}
static Future<dynamic> getRequest(
String url, String path, bool require_Auth) async {
Response response;
if (require_Auth) {
if (await isLogedIn()) {
String? token = await getAccessToken();
try {
response = await get(
Uri.http(
url,
path,
),
headers: {'Authorization': 'Bearer ${token}'});
return response;
} catch (e) {
print('connection lost usermanager 112');
print(e);
return null;
}
} else {
var username = await getUsername();
var password = await _getPassword();
if (username != null && password != null) {
try {
await login(url, '/api/auth/token/', username, password);
String? token = await getAccessToken();
try {
response = await get(
Uri.http(
url,
path,
),
headers: {'Authorization': 'Bearer ${token}'});
return response;
} catch (e) {
print('connection lost usermanager 112');
print(e);
return null;
}
} catch (e) {
print('login failed user manager 140');
return null;
}
}
}
} else {
try {
response = await get(
Uri.http(
url,
path,
),
);
return response;
} catch (e) {
print('connection lost usermanager 209');
print(e);
return null;
}
}
}
static Future<bool> isLogedIn() async {
var accessExpTime = await getAccessTokenExpTime();
var refreshExpTime = await getRefreshTokenExpTime();
if (accessExpTime == null ||
refreshExpTime == null ||
(accessExpTime.isBefore(DateTime.now()) &&
refreshExpTime.isBefore(DateTime.now()))) {
return false;
} else if (accessExpTime.isAfter(DateTime.now())) {
return true;
} else if (refreshExpTime.isAfter(DateTime.now())) {
return await refreshToken(url, '/api/auth/token/refresh');
}
return false;
}
static void register(String username, String email, String password1,
String password2) async {}
static void logout(BuildContext context, String redirectPath) async {
_storage.delete(key: _keyAccessExpTime);
_storage.delete(key: _keyAccessToken);
_storage.delete(key: _keyRefreshExpTime);
_storage.delete(key: _keyRefreshToken);
Navigator.pushReplacementNamed(context, redirectPath);
}
}
parseJWT method is here : How to get the claims from a JWT in my Flutter Application
my questions are:
- how to let the user stay connected always (is the only way is storing the credentials and sending theme when the refresh token expire? if so is it secure ? and how to revoke the session in this case )
- if i'm sending a request and found that the refresh token expired what should i do (relogin with the saved cridentials? loging out the user?).
- is my implimentaion good? if not what to change in it.