Future _tryAuthenticateJwt()

in app/lib/account/google_oauth2.dart [107:184]


  Future<AuthResult?> _tryAuthenticateJwt(String jwt) async {
    // Hit the token-info end-point documented at:
    // https://developers.google.com/identity/sign-in/web/backend-auth
    // Note: ideally, we would verify these JWTs locally, but unfortunately
    //       we don't have a solid RSA implementation available in Dart.
    final u = _tokenInfoEndPoint.replace(queryParameters: {'id_token': jwt});
    final response = await retry(
      () => _httpClient.get(u, headers: {'accept': 'application/json'}),
      maxAttempts: 2, // two attempts is enough, we don't want delays here
    );
    // Expect a 200 response
    if (response.statusCode != 200) {
      return null;
    }
    final r = json.decode(response.body) as Map<String, dynamic>;
    if (r.containsKey('error')) {
      return null; // presumably an invalid token.
    }
    // Sanity check on the algorithm
    if (r['alg'] == 'none') {
      _logger.warning('JWT rejected, alg = "none"');
      return null;
    }
    // Sanity check on the algorithm
    final typ = r['typ'];
    if (typ != 'JWT') {
      _logger.warning('JWT rejected, typ = "$typ');
      return null;
    }
    // Validate the issuer.
    final iss = r['iss'];
    if (iss != 'accounts.google.com' && iss != 'https://accounts.google.com') {
      _logger.warning('JWT rejected, iss = "$iss');
      return null;
    }
    // Validate create time
    final fiveMinFromNow = clock.now().toUtc().add(Duration(minutes: 5));
    final iat = r['iat'];
    if (iat == null || _parseTimestamp(iat).isAfter(fiveMinFromNow)) {
      _logger.warning('JWT rejected, iat = "$iat"');
      return null; // Token is created more than 5 minutes in the future
    }
    // Validate expiration time
    final fiveMinInPast = clock.now().toUtc().subtract(Duration(minutes: 5));
    final exp = r['exp'];
    if (exp == null || _parseTimestamp(exp).isBefore(fiveMinInPast)) {
      _logger.warning('JWT rejected, exp = "$exp"');
      return null; // Token is expired more than 5 minutes in the past
    }
    // Validate audience
    final aud = r['aud'];
    if (aud is! String) {
      _logger.warning('JWT rejected, aud missing');
      return null; // missing audience
    }
    if (!_trustedAudiences.contains(aud)) {
      _logger.warning('JWT rejected, aud = "$aud"');
      return null; // Not trusted audience
    }
    // Validate subject is present
    final sub = r['sub'];
    if (sub is! String) {
      _logger.warning('JWT rejected, sub missing');
      return null; // missing subject (probably missing 'openid' scope)
    }
    // Validate email is present
    final email = r['email'];
    if (email is! String) {
      _logger.warning('JWT rejected, email missing');
      return null; // missing email (probably missing 'email' scope)
    }
    final emailVerified = r['email_verified'];
    if (emailVerified != true && emailVerified != 'true') {
      _logger.warning('JWT rejected, email_verified = "$emailVerified"');
      return null; // missing email (probably missing 'email' scope)
    }
    return AuthResult(sub, email);
  }