|
| 1 | +import 'dart:convert'; |
| 2 | + |
| 3 | +import 'id_token.dart'; |
| 4 | +import 'logto_storage_strategy.dart'; |
| 5 | +import '/src/interfaces/access_token.dart'; |
| 6 | + |
| 7 | +class _TokenStorageKeys { |
| 8 | + static const accessTokenKey = 'logto_access_token'; |
| 9 | + static const refreshTokenKey = 'logto_refresh_token'; |
| 10 | + static const idTokenKey = 'logto_id_token'; |
| 11 | +} |
| 12 | + |
| 13 | +class TokenStorage { |
| 14 | + IdToken? _idToken; |
| 15 | + Map<String, AccessToken>? _accessTokenMap; |
| 16 | + String? _refreshToken; |
| 17 | + |
| 18 | + late final LogtoStorageStrategy _storage; |
| 19 | + |
| 20 | + static TokenStorage? loaded; |
| 21 | + |
| 22 | + TokenStorage( |
| 23 | + LogtoStorageStrategy? storageStrategy, |
| 24 | + ) { |
| 25 | + _storage = storageStrategy ?? SecureStorageStrategy(); |
| 26 | + } |
| 27 | + |
| 28 | + static IdToken? _decodeIdToken(String? encoded) { |
| 29 | + if (encoded == null) return null; |
| 30 | + return IdToken.unverified(encoded); |
| 31 | + } |
| 32 | + |
| 33 | + static String? _encodeIdToken(IdToken? token) { |
| 34 | + return token?.toCompactSerialization(); |
| 35 | + } |
| 36 | + |
| 37 | + static String _encodeScopes(List<String>? scopes) { |
| 38 | + final List<String> scopeList = scopes ?? []; |
| 39 | + scopeList.sort(); |
| 40 | + return scopeList.join(' '); |
| 41 | + } |
| 42 | + |
| 43 | + Future<IdToken?> get idToken async { |
| 44 | + if (_idToken != null) { |
| 45 | + return _idToken!; |
| 46 | + } |
| 47 | + |
| 48 | + final idTokenFromStorage = |
| 49 | + await _storage.read(key: _TokenStorageKeys.idTokenKey); |
| 50 | + _idToken = _decodeIdToken(idTokenFromStorage); |
| 51 | + |
| 52 | + return _idToken; |
| 53 | + } |
| 54 | + |
| 55 | + Future<void> setIdToken(IdToken? idToken) async { |
| 56 | + _idToken = idToken; |
| 57 | + |
| 58 | + await _storage.write( |
| 59 | + key: _TokenStorageKeys.idTokenKey, |
| 60 | + value: _encodeIdToken(idToken), |
| 61 | + ); |
| 62 | + } |
| 63 | + |
| 64 | + static String _buildAccessTokenKey(String? resource, |
| 65 | + [List<String>? scopes]) => |
| 66 | + "${_encodeScopes(scopes)}@${resource ?? ''}"; |
| 67 | + |
| 68 | + Future<Map<String, AccessToken>?> _getAccessTokenMapFromStorage() async { |
| 69 | + final tokenMapStorage = |
| 70 | + await _storage.read(key: _TokenStorageKeys.accessTokenKey); |
| 71 | + |
| 72 | + if (tokenMapStorage != null) { |
| 73 | + try { |
| 74 | + final Map<String, dynamic> jsonMap = jsonDecode(tokenMapStorage); |
| 75 | + |
| 76 | + return Map.fromEntries(jsonMap.keys |
| 77 | + .map((key) => MapEntry(key, AccessToken.fromJson(jsonMap[key])))); |
| 78 | + } catch (e) { |
| 79 | + return null; |
| 80 | + } |
| 81 | + } |
| 82 | + |
| 83 | + return null; |
| 84 | + } |
| 85 | + |
| 86 | + Future<AccessToken?> getAccessToken( |
| 87 | + [String? resource, List<String>? scopes]) async { |
| 88 | + final key = _buildAccessTokenKey(resource, scopes); |
| 89 | + |
| 90 | + if (_accessTokenMap != null) { |
| 91 | + return _accessTokenMap![key]; |
| 92 | + } |
| 93 | + |
| 94 | + _accessTokenMap = await _getAccessTokenMapFromStorage(); |
| 95 | + |
| 96 | + return _accessTokenMap?[key]; |
| 97 | + } |
| 98 | + |
| 99 | + Future<void> _saveAccessTokenMapToStorage( |
| 100 | + Map<String, AccessToken> accessTokenMap) async { |
| 101 | + final jsonMap = Map.fromEntries(accessTokenMap.keys |
| 102 | + .map((key) => MapEntry(key, accessTokenMap[key]?.toJson()))); |
| 103 | + await _storage.write( |
| 104 | + key: _TokenStorageKeys.accessTokenKey, value: jsonEncode(jsonMap)); |
| 105 | + } |
| 106 | + |
| 107 | + Future<void> setAccessToken(String accessToken, |
| 108 | + {String? resource, List<String>? scopes, required int expiresIn}) async { |
| 109 | + final key = _buildAccessTokenKey(resource, scopes); |
| 110 | + |
| 111 | + final Map<String, AccessToken> newAccessTokenMap = |
| 112 | + Map.from(_accessTokenMap ?? {}); |
| 113 | + |
| 114 | + newAccessTokenMap.addAll({ |
| 115 | + key: AccessToken( |
| 116 | + token: accessToken, |
| 117 | + scope: _encodeScopes(scopes), |
| 118 | + |
| 119 | + /// convert the expireAt to standard utc time |
| 120 | + expiresAt: DateTime.now().add(Duration(seconds: expiresIn)).toUtc()) |
| 121 | + }); |
| 122 | + |
| 123 | + await _saveAccessTokenMapToStorage(newAccessTokenMap); |
| 124 | + |
| 125 | + _accessTokenMap = newAccessTokenMap; |
| 126 | + } |
| 127 | + |
| 128 | + Future<String?> get refreshToken async { |
| 129 | + if (_refreshToken != null) { |
| 130 | + return _refreshToken; |
| 131 | + } |
| 132 | + |
| 133 | + _refreshToken = await _storage.read(key: _TokenStorageKeys.refreshTokenKey); |
| 134 | + |
| 135 | + return _refreshToken; |
| 136 | + } |
| 137 | + |
| 138 | + Future<void> setRefreshToken(String? refreshToken) async { |
| 139 | + _refreshToken = refreshToken; |
| 140 | + |
| 141 | + await _storage.write( |
| 142 | + key: _TokenStorageKeys.refreshTokenKey, |
| 143 | + value: refreshToken, |
| 144 | + ); |
| 145 | + } |
| 146 | + |
| 147 | + /// Initial token response saving |
| 148 | + /// |
| 149 | + /// * required IdToken |
| 150 | + /// * required AccessToken |
| 151 | + /// * required expiresIn |
| 152 | + /// * optional refreshToken |
| 153 | + /// * initial AccessToken id for OpenID use only does not have resource & scope |
| 154 | + Future<void> save({ |
| 155 | + required IdToken idToken, |
| 156 | + required String accessToken, |
| 157 | + String? refreshToken, |
| 158 | + required int expiresIn, |
| 159 | + }) async { |
| 160 | + await Future.wait([ |
| 161 | + setAccessToken(accessToken, expiresIn: expiresIn), |
| 162 | + setIdToken(idToken), |
| 163 | + setRefreshToken(refreshToken), |
| 164 | + ]); |
| 165 | + } |
| 166 | + |
| 167 | + Future<void> clear() async { |
| 168 | + _accessTokenMap = null; |
| 169 | + _refreshToken = null; |
| 170 | + _idToken = null; |
| 171 | + await Future.wait<void>([ |
| 172 | + _storage.delete(key: _TokenStorageKeys.accessTokenKey), |
| 173 | + _storage.delete(key: _TokenStorageKeys.refreshTokenKey), |
| 174 | + _storage.delete(key: _TokenStorageKeys.idTokenKey), |
| 175 | + ]); |
| 176 | + } |
| 177 | +} |
0 commit comments