blob: 57d1324e55805ab4664bc4e15fae96ab2268d4f9 [file] [log] [blame]
// Copyright 2019 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:appengine/appengine.dart';
import 'package:cocoon_server/logging.dart';
import 'package:meta/meta.dart';
import '../../cocoon_service.dart';
import '../foundation/providers.dart';
import '../foundation/typedefs.dart';
import '../model/google/token_info.dart';
import 'exceptions.dart';
/// Class capable of authenticating [HttpRequest]s for infra endpoints.
///
/// This class implements an ACL on a [RequestHandler] to ensure only automated
/// systems can access the endpoints.
///
/// If the request has the `Service-Account-Token` HTTP header, the token
/// will be authenticated as a LUCI bot. This token is validated against
/// Google Auth APIs.
///
/// If none of the above authentication methods yield an authenticated
/// request, then the request is unauthenticated, and any call to
/// [authenticate] will throw an [Unauthenticated] exception.
@immutable
class SwarmingAuthenticationProvider implements AuthenticationProvider {
const SwarmingAuthenticationProvider({
required this.config,
this.clientContextProvider = Providers.serviceScopeContext,
this.httpClientProvider = Providers.freshHttpClient,
});
/// The Cocoon config, guaranteed to be non-null.
final Config config;
/// Provides the App Engine client context as part of the
/// [AuthenticatedContext].
///
/// This is guaranteed to be non-null.
final ClientContextProvider clientContextProvider;
/// Provides the HTTP client that will be used (if necessary) to verify OAuth
/// ID tokens (JWT tokens).
///
/// This is guaranteed to be non-null.
final HttpClientProvider httpClientProvider;
/// Name of the header that LUCI requests will put their service account token.
static const String kSwarmingTokenHeader = 'Service-Account-Token';
/// Authenticates the specified [request] and returns the associated
/// [AuthenticatedContext].
///
/// See the class documentation on [AuthenticationProvider] for a discussion
/// of the different types of authentication that are accepted.
///
/// This will throw an [Unauthenticated] exception if the request is
/// unauthenticated.
@override
Future<AuthenticatedContext> authenticate(HttpRequest request) async {
final swarmingToken = request.headers.value(kSwarmingTokenHeader);
final clientContext = clientContextProvider();
if (swarmingToken != null) {
log.debug('Authenticating as swarming task');
return authenticateAccessToken(
swarmingToken,
clientContext: clientContext,
);
}
throw const Unauthenticated('Request rejected due to not from LUCI');
}
/// Authenticate [accessToken] against Google OAuth 2 API.
///
/// Access tokens are the legacy authentication strategy for Google OAuth, where ID tokens
/// are the new technique to use. LUCI auth only generates access tokens, and must be
/// validated against a different endpoint. We only authenticate access tokens
/// if they belong to a LUCI prod service account.
///
/// If LUCI auth adds id tokens, we can switch to that and remove this.
Future<AuthenticatedContext> authenticateAccessToken(
String accessToken, {
required ClientContext clientContext,
}) async {
// Authenticate as a signed-in Google account via OAuth id token.
final client = httpClientProvider();
try {
log.debug('Sending token request to Google OAuth');
final verifyTokenResponse = await client.get(
Uri.https('oauth2.googleapis.com', '/tokeninfo', <String, String>{
'access_token': accessToken,
}),
);
if (verifyTokenResponse.statusCode != HttpStatus.ok) {
/// Google Auth API returns a message in the response body explaining why
/// the request failed. Such as "Invalid Token".
final body = verifyTokenResponse.body;
log.warnJson(
'Token verification failed: ${verifyTokenResponse.statusCode}; $body',
);
throw const Unauthenticated('Invalid access token');
}
TokenInfo token;
try {
token = TokenInfo.fromJson(
json.decode(verifyTokenResponse.body) as Map<String, dynamic>,
);
} on FormatException {
log.warn('Failed to decode token JSON: ${verifyTokenResponse.body}');
throw InternalServerError(
'Invalid JSON: "${verifyTokenResponse.body}"',
);
}
// Update is from Flutter LUCI builds
if (token.email == Config.luciProdAccount) {
return AuthenticatedContext(
clientContext: clientContext,
email: token.email!,
);
}
if (token.email == Config.frobAccount) {
log.debug('Authenticating as FRoB request');
return AuthenticatedContext(
clientContext: clientContext,
email: token.email!,
);
}
log.debug(verifyTokenResponse.body);
log.warn('${token.email} is not allowed');
throw Unauthenticated('${token.email} is not allowed');
} finally {
client.close();
}
}
}