Line data Source code
1 : part of apptive_grid_network; 2 : 3 : /// Class for handling authentication related methods for ApptiveGrid 4 : class ApptiveGridAuthenticator { 5 : /// Create a new [ApptiveGridAuthenticator] for [apptiveGridClient] 6 3 : ApptiveGridAuthenticator({ 7 : this.options = const ApptiveGridOptions(), 8 : this.httpClient, 9 : }) { 10 : if (!kIsWeb) { 11 6 : _authCallbackSubscription = uni_links.uriLinkStream 12 3 : .where( 13 1 : (event) => 14 : event != null && 15 2 : event.scheme == 16 4 : options.authenticationOptions.redirectScheme?.toLowerCase(), 17 : ) 18 5 : .listen((event) => _handleAuthRedirect(event!)); 19 : } 20 : 21 9 : if (options.authenticationOptions.persistCredentials) { 22 1 : _authenticationStorage = const FlutterSecureStorageCredentialStorage(); 23 : } 24 : } 25 : 26 : /// Creates an [ApptiveGridAuthenticator] with a specific [AuthenticationStorage] 27 1 : @visibleForTesting 28 : ApptiveGridAuthenticator.withAuthenticationStorage({ 29 : this.options = const ApptiveGridOptions(), 30 : this.httpClient, 31 : required AuthenticationStorage? storage, 32 : }) : _authenticationStorage = storage, 33 : _authCallbackSubscription = null; 34 : 35 : /// [ApptiveGridOptions] used for getting the correct [ApptiveGridEnvironment.authRealm] 36 : /// and checking if authentication should automatically be handled 37 : ApptiveGridOptions options; 38 : 39 2 : Uri get _uri => Uri.parse( 40 4 : 'https://iam.zweidenker.de/auth/realms/${options.environment.authRealm}', 41 : ); 42 : 43 : /// Http Client that should be used for Auth Requests 44 : final http.Client? httpClient; 45 : 46 : Client? _authClient; 47 : 48 : TokenResponse? _token; 49 : Credential? _credential; 50 : 51 : AuthenticationStorage? _authenticationStorage; 52 : 53 : /// Override the token for testing purposes 54 2 : @visibleForTesting 55 2 : void setToken(TokenResponse? token) => _token = token; 56 : 57 : /// Override the Credential for testing purposes 58 2 : @visibleForTesting 59 : void setCredential(Credential? credential) { 60 3 : _authenticationStorage?.saveCredential( 61 2 : credential != null ? jsonEncode(credential.toJson()) : null, 62 : ); 63 2 : _credential = credential; 64 : } 65 : 66 : /// Override the [Client] for testing purposes 67 1 : @visibleForTesting 68 1 : void setAuthClient(Client client) => _authClient = client; 69 : 70 : /// Override the [Authenticator] for testing purposes 71 : @visibleForTesting 72 : Authenticator? testAuthenticator; 73 : 74 : late final StreamSubscription<Uri?>? _authCallbackSubscription; 75 : 76 1 : Future<Client> get _client async { 77 1 : Future<Client> createClient() async { 78 4 : final issuer = await Issuer.discover(_uri, httpClient: httpClient); 79 2 : return Client(issuer, 'app', httpClient: httpClient, clientSecret: ''); 80 : } 81 : 82 2 : return _authClient ??= await createClient(); 83 : } 84 : 85 : /// Used to test implementation of get _client 86 1 : @visibleForTesting 87 1 : Future<Client> get authClient => _client; 88 : 89 : /// Open the Authentication Webpage 90 : /// 91 : /// Returns [Credential] from the authentication call 92 1 : Future<Credential?> authenticate() async { 93 2 : final client = await _client; 94 : 95 1 : final authenticator = testAuthenticator ?? 96 1 : Authenticator( 97 : client, 98 1 : scopes: [], 99 1 : urlLauncher: _launchUrl, 100 3 : redirectUri: options.authenticationOptions.redirectScheme != null 101 1 : ? Uri( 102 3 : scheme: options.authenticationOptions.redirectScheme, 103 5 : host: Uri.parse(options.environment.url).host, 104 : ) 105 : : null, 106 : ); 107 3 : setCredential(await authenticator.authorize()); 108 : 109 4 : setToken(await _credential?.getTokenResponse()); 110 : 111 : try { 112 2 : await closeWebView(); 113 1 : } on MissingPluginException { 114 1 : debugPrint('closeWebView is not available on this platform'); 115 1 : } on UnimplementedError { 116 1 : debugPrint('closeWebView is not available on this platform'); 117 : } 118 : 119 1 : return _credential; 120 : } 121 : 122 1 : Future<void> _handleAuthRedirect(Uri uri) async { 123 2 : final client = await _client; 124 1 : client.createCredential( 125 1 : refreshToken: _token?.refreshToken, 126 : ); 127 1 : final authenticator = testAuthenticator ?? 128 1 : Authenticator( 129 : client, // coverage:ignore-line 130 3 : redirectUri: options.authenticationOptions.redirectScheme != null 131 1 : ? Uri( 132 3 : scheme: options.authenticationOptions.redirectScheme, 133 5 : host: Uri.parse(options.environment.url).host, 134 : ) 135 : : null, 136 1 : urlLauncher: _launchUrl, 137 : ); 138 : 139 3 : await authenticator.processResult(uri.queryParameters); 140 : } 141 : 142 : /// Dispose any resources in the Authenticator 143 3 : void dispose() { 144 6 : _authCallbackSubscription?.cancel(); 145 : } 146 : 147 : /// Checks the authentication status and performs actions depending on the status 148 : /// 149 : /// If there is a [ApptiveGridAuthenticationOptions.apiKey] is set in [options] this will return without any Action 150 : /// 151 : /// If the User is not authenticated and [ApptiveGridAuthenticationOptions.autoAuthenticate] is true this will call [authenticate] 152 : /// 153 : /// If the token is expired it will refresh the token using the refresh token 154 2 : Future<void> checkAuthentication() async { 155 2 : if (_token == null) { 156 4 : await Future.value( 157 3 : _authenticationStorage?.credential, 158 4 : ).then((credentialString) async { 159 2 : final jsonCredential = jsonDecode(credentialString ?? 'null'); 160 : if (jsonCredential != null) { 161 1 : final credential = Credential.fromJson( 162 : jsonCredential, 163 1 : httpClient: httpClient, 164 : ); 165 1 : setCredential(credential); 166 : try { 167 2 : final token = await credential.getTokenResponse(true); 168 1 : setToken(token); 169 : return; 170 1 : } on OpenIdException catch (_) { 171 1 : setCredential(null); 172 1 : debugPrint('Could not refresh saved token'); 173 : } 174 : } 175 6 : if (options.authenticationOptions.apiKey != null) { 176 : // User has ApiKey provided 177 : return; 178 6 : } else if (options.authenticationOptions.autoAuthenticate) { 179 2 : await authenticate(); 180 : } 181 : }); 182 6 : } else if ((_token?.expiresAt?.difference(DateTime.now()).inSeconds ?? 0) < 183 : 70) { 184 4 : setToken(await _credential?.getTokenResponse(true)); 185 : } 186 : } 187 : 188 : /// Performs a call to Logout the User 189 : /// 190 : /// even if the Call Fails the token and credential will be cleared 191 2 : Future<http.Response?> logout() async { 192 3 : final logoutUrl = _credential?.generateLogoutUrl(); 193 : http.Response? response; 194 : if (logoutUrl != null) { 195 3 : response = await (httpClient ?? http.Client()).get( 196 : logoutUrl, 197 1 : headers: { 198 1 : HttpHeaders.authorizationHeader: header!, 199 : }, 200 : ); 201 : } 202 2 : setToken(null); 203 2 : setCredential(null); 204 2 : _authClient = null; 205 : 206 : return response; 207 : } 208 : 209 : /// If there is a authenticated User this will return the authentication header 210 : /// 211 : /// User Authentication is prioritized over ApiKey Authentication 212 2 : String? get header { 213 2 : if (_token != null) { 214 1 : final token = _token!; 215 3 : return '${token.tokenType} ${token.accessToken}'; 216 : } 217 6 : if (options.authenticationOptions.apiKey != null) { 218 3 : final apiKey = options.authenticationOptions.apiKey!; 219 6 : return 'Basic ${base64Encode(utf8.encode('${apiKey.authKey}:${apiKey.password}'))}'; 220 : } 221 : } 222 : 223 1 : Future<void> _launchUrl(String url) async { 224 2 : if (await canLaunch(url)) { 225 : try { 226 2 : await launch(url); 227 0 : } on PlatformException { 228 : // Could not launch Url 229 : } 230 : } 231 : } 232 : 233 : /// Checks if the User is Authenticated 234 1 : bool get isAuthenticated => 235 4 : options.authenticationOptions.apiKey != null || _token != null; 236 : } 237 : 238 : /// Interface to provide common functionality for authorization operations 239 : abstract class IAuthenticator { 240 : /// Authorizes the User against the Auth Server 241 : Future<Credential?> authorize(); 242 : }