blob: fc9f90b903bb66421b30b9fb94235cc15afa64ff [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:io';
import 'package:appengine/appengine.dart';
import 'package:googleapis/oauth2/v2.dart';
import 'package:http/http.dart';
import 'package:meta/meta.dart';
import '../../cocoon_service.dart';
import '../foundation/providers.dart';
import '../service/logging.dart';
import 'exceptions.dart';
/// Class capable of authenticating [HttpRequest]s for PubSub messages.
///
/// This class implements an ACL on a [RequestHandler] to ensure only automated
/// systems can access the endpoints.
///
/// If the request has [HttpHeaders.authorizationHeader], the token
/// will be authenticated as a LUCI bot. This token is validated against
/// Google Auth APIs.
///
/// If there is no token, or it cannot be authenticated, [Unauthenticated] is thrown.
@immutable
class PubsubAuthenticationProvider extends AuthenticationProvider {
const PubsubAuthenticationProvider({
required super.config,
super.clientContextProvider = Providers.serviceScopeContext,
super.httpClientProvider = Providers.freshHttpClient,
});
static const String kBearerTokenPrefix = 'Bearer ';
/// 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 String? idToken = request.headers.value(HttpHeaders.authorizationHeader);
final ClientContext clientContext = clientContextProvider();
log.fine('Authenticating as pubsub message');
return authenticateIdToken(idToken, clientContext: clientContext);
}
/// Authenticate [idToken] against Google OAuth 2 API.
Future<AuthenticatedContext> authenticateIdToken(
String? idToken, {
required ClientContext clientContext,
}) async {
if (idToken == null || !idToken.startsWith(kBearerTokenPrefix)) {
throw const Unauthenticated('${HttpHeaders.authorizationHeader} is null');
}
final Client client = httpClientProvider();
final Oauth2Api oauth2api = Oauth2Api(client);
// Get token from Google oauth
final Tokeninfo info = await oauth2api.tokeninfo(
idToken: idToken.substring(kBearerTokenPrefix.length),
);
if (info.expiresIn == null || info.expiresIn! < 1) {
throw const Unauthenticated('Token is expired');
}
if (Config.allowedPubsubServiceAccounts.contains(info.email)) {
return AuthenticatedContext(clientContext: clientContext);
}
throw Unauthenticated('${info.email} is not in allowedPubsubServiceAccounts');
}
}