// Copyright (c) 2018, the gRPC project authors. Please see the AUTHORS file // for details. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import 'dart:async'; import 'dart:convert'; import 'package:googleapis_auth/auth.dart' as auth; import 'package:http/http.dart' as http; import '../client/call.dart'; import 'rsa.dart'; const _tokenExpirationThreshold = Duration(seconds: 30); abstract class BaseAuthenticator { auth.AccessToken? _accessToken; late String _lastUri; var _lastUriSet = false; Future authenticate(Map metadata, String uri) async { if (_accessToken == null || _accessToken!.hasExpired || !_lastUriSet || uri != _lastUri) { await obtainAccessCredentials(uri); _lastUri = uri; _lastUriSet = true; } final auth = '${_accessToken!.type} ${_accessToken!.data}'; metadata['authorization'] = auth; if (_tokenExpiresSoon) { // Token is about to expire. Extend it prematurely. obtainAccessCredentials(_lastUri).catchError((_) {}); } } bool get _tokenExpiresSoon => _accessToken!.expiry .subtract(_tokenExpirationThreshold) .isBefore(DateTime.now().toUtc()); CallOptions get toCallOptions => CallOptions(providers: [authenticate]); Future obtainAccessCredentials(String uri); } abstract class HttpBasedAuthenticator extends BaseAuthenticator { Future? _call; @override Future obtainAccessCredentials(String uri) { if (_call == null) { final authClient = http.Client(); _call = obtainCredentialsWithClient(authClient, uri).then((credentials) { _accessToken = credentials.accessToken; _call = null; authClient.close(); }); } return _call!; } Future obtainCredentialsWithClient( http.Client client, String uri); } class JwtServiceAccountAuthenticator extends BaseAuthenticator { auth.ServiceAccountCredentials _serviceAccountCredentials; String? _projectId; String? _keyId; JwtServiceAccountAuthenticator.fromJson( Map serviceAccountJson) : _serviceAccountCredentials = auth.ServiceAccountCredentials.fromJson(serviceAccountJson), _projectId = serviceAccountJson['project_id'], _keyId = serviceAccountJson['private_key_id']; factory JwtServiceAccountAuthenticator(String serviceAccountJsonString) => JwtServiceAccountAuthenticator.fromJson( jsonDecode(serviceAccountJsonString)); String? get projectId => _projectId; @override Future obtainAccessCredentials(String uri) async { _accessToken = _jwtTokenFor(_serviceAccountCredentials, _keyId, uri); } } // TODO(jakobr): Expose in googleapis_auth. auth.AccessToken _jwtTokenFor( auth.ServiceAccountCredentials credentials, String? keyId, String uri, {String? user, List? scopes}) { // Subtracting 20 seconds from current timestamp to allow for clock skew among // servers. final timestamp = (DateTime.now().toUtc().millisecondsSinceEpoch ~/ 1000) - 20; final expiry = timestamp + 3600; final header = {'alg': 'RS256', 'typ': 'JWT'}; if (keyId != null) { header['kid'] = keyId; } final claims = { 'iss': credentials.email, 'aud': uri, 'exp': expiry, 'iat': timestamp, 'sub': user ?? credentials.email }; if (scopes != null) { claims['scope'] = scopes.join(' '); } final headerBase64 = _base64url(ascii.encode(jsonEncode(header))); final claimsBase64 = _base64url(utf8.encode(jsonEncode(claims))); final data = '$headerBase64.$claimsBase64'; final key = credentials.privateRSAKey; // We convert to our internal version of RSAPrivateKey. See rsa.dart for more // explanation. final signer = RS256Signer(RSAPrivateKey( key.n, key.e, key.d, key.p, key.q, key.dmp1, key.dmq1, key.coeff)); final signature = signer.sign(ascii.encode(data)); final jwt = '$data.${_base64url(signature)}'; return auth.AccessToken('Bearer', jwt, DateTime.fromMillisecondsSinceEpoch(expiry * 1000, isUtc: true)); } String _base64url(List bytes) { return base64Url.encode(bytes).replaceAll('=', ''); }