firebaseAuthentication function

RequestMiddleware firebaseAuthentication(
  1. String gcpProjectName, {
  2. VerifyClaims? verifier,
  3. String verifyFailMessage = '',
  4. bool setDefaultClaimsOnContext = true,
})

Implementation

RequestMiddleware firebaseAuthentication(String gcpProjectName,
    {VerifyClaims? verifier,
    String verifyFailMessage = '',
    bool setDefaultClaimsOnContext = true}) {
  return (Request req) async {
    final authHeader = req.headers.value('Authorization');

    if (authHeader == null) {
      req.respond
          .unauthorized(msg: 'An authorization header was not provided.');
      return req;
    }

    if (!authHeader.startsWith('Bearer ')) {
      req.respond.unauthorized(msg: 'Invalid authorization header format.');
      return req;
    }
    final split = authHeader.split(' ');
    if (split.length != 2) {
      req.respond.unauthorized(
          msg:
              'Invalid authorization header format. Send as "Bearer {TOKEN}".');
      return req;
    }
    final token = split[1];
    final tokenParts = token.split('.');
    if (tokenParts.length != 3) {
      req.respond.unauthorized(
          msg: 'Invalid authorization header format. Not enough jwt segments.');
      return req;
    }

    final decodedToken = JWT.parse(token);

    if (decodedToken.algorithm != 'RS256') {
      req.respond.unauthorized(msg: 'Invalid authorization algorithm.');
      return req;
    }
    final kid = decodedToken.headers['kid'] ?? '';

    if (kid.isEmpty) {
      req.respond.unauthorized(msg: 'Invalid kid claim.');
      return req;
    }

    FirebaseCertificate? certificate;

    // Check if the cached certificate is still valid
    if (FirebaseCertificate.isValid()) {
      certificate = FirebaseCertificate.getCertificate();
    } else {
      final resp = await http.get(firebaseCertificateUrl);

      final cacheControl = resp.headers['cache-control']!;
      final cacheControlParts = cacheControl.split(', ');
      var maxAge = '';
      for (final p in cacheControlParts) {
        if (p.startsWith('max-age=')) {
          maxAge = p;
          continue;
        }
      }
      if (maxAge.isEmpty) {
        throw CertificateException(
            'The certificate was sent without a max-age header.');
      }
      final expirySeconds = int.parse(maxAge.split('=')[1]);
      final expiresAt =
          DateTime.now().toUtc().add(Duration(seconds: expirySeconds));
      final respCerts = json.decode(resp.body) as Map<String, Object>;
      final certs = <String, String?>{};
      for (final k in respCerts.keys) {
        certs[k] = respCerts[k] as String?;
      }
      certificate = FirebaseCertificate(expiresAt, certs);
    }

    try {
      final publicKey = certificate!.certificate[kid]!;
      final signer = JWTRsaSha256Signer(publicKey);
      final isValid = decodedToken.verify(signer);
      if (!isValid) {
        req.respond.unauthorized(msg: 'The jwt token was not valid.');
        return req;
      }
    } catch (e) {
      req.respond.unauthorized(msg: 'An unknown authorization error occurred.');
      return req;
    }

    if (decodedToken.expiresAt <=
        DateTime.now().millisecondsSinceEpoch / 1000) {
      req.respond.unauthorized(msg: 'Invalid exp claim.');
      return req;
    }
    if (decodedToken.issuedAt >= DateTime.now().millisecondsSinceEpoch / 1000) {
      req.respond.unauthorized(msg: 'Invalid iat claim.');
      return req;
    }
    if (decodedToken.audience != gcpProjectName) {
      req.respond.unauthorized(msg: 'Invalid aud claim.');
      return req;
    }
    if (decodedToken.issuer != _issuer(gcpProjectName)) {
      req.respond.unauthorized(msg: 'Invalid iss claim.');
      return req;
    }
    if (decodedToken.subject.isEmpty) {
      req.respond.unauthorized(msg: 'Invalid sub claim.');
      return req;
    }
    if (decodedToken.claims['auth_time'] >=
        DateTime.now().millisecondsSinceEpoch / 1000) {
      req.respond.unauthorized(msg: 'Invalid iat claim.');
      return req;
    }

    // To here the jwt token is valid
    // Now look at the claims

    // If previously verified, good to pass through
    // See if a verifier was provided
    if (verifier != null) {
      if (verifier(decodedToken.claims as Map<String, Object>) == false) {
        final failMessage = verifyFailMessage.isNotEmpty
            ? verifyFailMessage
            : 'The claims were not valid.';
        req.respond.unauthorized(msg: failMessage);
        return req;
      }
    }

    req.context.trySet(firebaseRawClaimsContextId, decodedToken.claims);

    if (setDefaultClaimsOnContext) {
      final claims = FirebaseTokenClaims.fromJson(decodedToken.claims as Map<String, Object>);
      req.context
          .trySet<FirebaseTokenClaims>(firebaseDefaultClaimsContextId, claims);
    }

    return req;
  };
}