blob: 27aa57d347690daf24438ddcd624f297fa4d45a9 [file] [log] [blame]
// 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_io.dart' as auth;
import 'package:googleapis_auth/src/crypto/rsa_sign.dart';
import 'package:grpc/src/shared/status.dart';
import 'package:http/http.dart' as http;
import '../client/options.dart';
const _tokenExpirationThreshold = const Duration(seconds: 30);
abstract class BaseAuthenticator {
auth.AccessToken _accessToken;
String _lastUri;
Future<void> authenticate(Map<String, String> metadata, String uri) async {
if (uri == null) {
throw new GrpcError.unauthenticated(
'Credentials require secure transport.');
}
if (_accessToken == null || _accessToken.hasExpired || uri != _lastUri) {
await obtainAccessCredentials(uri);
_lastUri = uri;
}
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(new DateTime.now().toUtc());
CallOptions get toCallOptions => new CallOptions(providers: [authenticate]);
Future<void> obtainAccessCredentials(String uri);
}
abstract class HttpBasedAuthenticator extends BaseAuthenticator {
Future<void> _call;
Future<void> obtainAccessCredentials(String uri) {
if (_call == null) {
final authClient = new http.Client();
_call = obtainCredentialsWithClient(authClient, uri).then((credentials) {
_accessToken = credentials.accessToken;
_call = null;
authClient.close();
});
}
return _call;
}
Future<auth.AccessCredentials> obtainCredentialsWithClient(
http.Client client, String uri);
}
class ComputeEngineAuthenticator extends HttpBasedAuthenticator {
Future<auth.AccessCredentials> obtainCredentialsWithClient(
http.Client client, String uri) =>
auth.obtainAccessCredentialsViaMetadataServer(client);
}
class ServiceAccountAuthenticator extends HttpBasedAuthenticator {
auth.ServiceAccountCredentials _serviceAccountCredentials;
final List<String> _scopes;
String _projectId;
ServiceAccountAuthenticator(String serviceAccountJson, this._scopes) {
final serviceAccount = jsonDecode(serviceAccountJson);
_serviceAccountCredentials =
new auth.ServiceAccountCredentials.fromJson(serviceAccount);
_projectId = serviceAccount['project_id'];
}
String get projectId => _projectId;
Future<auth.AccessCredentials> obtainCredentialsWithClient(
http.Client client, String uri) =>
auth.obtainAccessCredentialsViaServiceAccount(
_serviceAccountCredentials, _scopes, client);
}
class JwtServiceAccountAuthenticator extends BaseAuthenticator {
auth.ServiceAccountCredentials _serviceAccountCredentials;
String _projectId;
String _keyId;
JwtServiceAccountAuthenticator(String serviceAccountJson) {
final serviceAccount = jsonDecode(serviceAccountJson);
_serviceAccountCredentials =
new auth.ServiceAccountCredentials.fromJson(serviceAccount);
_projectId = serviceAccount['project_id'];
_keyId = serviceAccount['private_key_id'];
}
String get projectId => _projectId;
Future<void> 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<String> scopes}) {
// Subtracting 20 seconds from current timestamp to allow for clock skew among
// servers.
final timestamp =
(new DateTime.now().toUtc().millisecondsSinceEpoch ~/ 1000) - 20;
final expiry = timestamp + 3600;
final header = <String, String>{'alg': 'RS256', 'typ': 'JWT'};
if (keyId != null) {
header['kid'] = keyId;
}
final claims = <String, dynamic>{
'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 signer = new RS256Signer(credentials.privateRSAKey);
final signature = signer.sign(ascii.encode(data));
final jwt = '$data.${_base64url(signature)}';
return new auth.AccessToken('Bearer', jwt,
new DateTime.fromMillisecondsSinceEpoch(expiry * 1000, isUtc: true));
}
String _base64url(List<int> bytes) {
return base64Url.encode(bytes).replaceAll('=', '');
}