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