| // 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(); |
| } |
| } |
| } |