declare local var.isAuthenticated BOOL;

# Check and authenticate an existing session token if one is supplied
# This mechanism uses a simple custom cookie format to persist a
# previously successful OAuth authentication
if (req.http.Cookie:auth) {
  log "Request contains a session cookie.  Validate it.";
  
  # Validate signature
  declare local var.decodedCookie STRING;
  declare local var.suppliedSig STRING;
  declare local var.expectedSig STRING;
  set var.decodedCookie = urldecode(req.http.Cookie:auth);
  set var.suppliedSig = subfield(var.decodedCookie, "sig", "&");
  set var.expectedSig = digest.base64(
    digest.hmac_sha1(
      table.lookup(oauth_settings, "session_secret"),
      subfield(var.decodedCookie, "email", "&") subfield(var.decodedCookie, "expires", "&")
    )
  );
  if (var.suppliedSig == var.expectedSig) {
    declare local var.expires TIME;
    set var.expires = std.integer2time(
      std.atoi(subfield(var.decodedCookie, "expires", "&"))
    );
    if (!time.is_after(now, var.expires)) {
      set var.isAuthenticated = true;
      set req.http.Google-Email = subfield(var.decodedCookie, "email", "&");
    }
  } else {
    log "Signature of session cookie is invalid.  Expected " var.expectedSig ", got " var.suppliedSig;
    error 200 "oauth_error: Invalid session cookie";
  }
}


# If the request is for the OAuth callback path, trigger validation
# of the ID token and convert it into our simple session cookie
if (req.url.path == table.lookup(oauth_settings, "redirect_uri")) {
  
  # Since we asked for an implicit grant, the initial callback will
  # include the token in a hash fragment, so we can't see it on the
  # server.  In this case, serve a synth to capture it
  if (!subfield(req.url.qs, "id_token","&")) {
    log "Received post-auth callback but no ID token present, probably because it's a hash-fragment";
    error 200 "oauth_capture_implicit_code";
  
  # If we do have an id_token query param, decode it
  } else {
    log "Post-auth callback with ID token (" req.url ").  Decoding the JWT";
    
    declare local var.oauthState STRING;
    declare local var.oauthStateTime STRING;
    declare local var.oauthStateSigExpected STRING;
    declare local var.oauthStateSigSupplied STRING;
    declare local var.jwtStringToSign STRING;
    declare local var.jwtHeader STRING;
    declare local var.jwtPayload STRING;
    declare local var.jwtSuppliedSig STRING;
    declare local var.jwtKeyID STRING;
    declare local var.google_email STRING;
    declare local var.google_email_verified STRING;
    declare local var.google_issuer STRING;
    declare local var.google_audience STRING;
    declare local var.google_issued_at STRING;
    declare local var.google_expire_time STRING;
    
    
    if (subfield(req.url.qs, "id_token","&") !~ "^(([^\.]*)\.([^\.]*))\.([^&]*)$") {
      error 200 "oauth_error: Invalid JWT format";
    }
    set var.jwtStringToSign = re.group.1;
    set var.jwtHeader = digest.base64_decode(re.group.2);
    set var.jwtPayload = digest.base64_decode(re.group.3);
    set var.jwtSuppliedSig = re.group.4;
    
    if (var.jwtHeader !~ {""alg": ?"RS256""}) {
      error 200 "oauth_error: Unsupported JWT signing algorithm";
    }
  
    if (var.jwtHeader !~ {""kid": ?"([^"]*)""}) {
      error 200 "oauth_error: Missing Google key ID";
    }
    set var.jwtKeyID = re.group.1;

    # The JWT is signed with a key pair, the public half of which
    # is available from a public URL endpoint provided by Google.
    # If we don't already have it, fetch it now.
    if (req.http.Google-Key-ID != var.jwtKeyID) {
      log "Received post-auth callback with an ID token but don't have the cert needed to verify it.  Rewrite the request to fetch the required key.";
      set req.backend = F_origin_1; # Cert store
      set req.http.parked-url = req.url;
      set req.url = table.lookup(oauth_settings, "cert_backend_path") "?kid=" var.jwtKeyID;
      return (lookup);

    # If we do have the key, and the ID token, we can validate it
    # and extract all the data from the payload
    } else {
      log "Required Google public key (" req.http.Google-Key-ID ") is available: verify the token";
      if (!digest.rsa_verify(sha256, urldecode(req.http.Google-Key), var.jwtStringToSign, var.jwtSuppliedSig)) { 
        error 200 "oauth_error: Unable to verify signature of Google JWT";
      }
      
      set var.google_email = if(var.jwtPayload ~ {".*"email":\s*"([^"]*)"}, re.group.1, "");
      set var.google_email_verified = if(var.jwtPayload ~ {".*"email_verified":\s*([^,}]*)"}, re.group.1, "");
      set var.google_issuer = if(var.jwtPayload ~ {".*"iss":\s*"([^"]*)"}, re.group.1, "");
      set var.google_audience = if(var.jwtPayload ~ {".*"aud":\s*"([^"]*)"}, re.group.1, "");

      set var.google_expire_time = if(var.jwtPayload ~ {".*"exp":\s*([^,}]*)"}, re.group.1, "");
      set var.google_issued_at = if(var.jwtPayload ~ {".*"iat":\s*"([^"]*)"}, re.group.1, "");
    
      if (var.google_expire_time !~ "^\d{10,11}$") {
        error 200 "oauth_error: Unable to parse valid Unix expiration timestamp from Google token.";
      }
    
      if (time.is_after(now, std.integer2time(std.atoi(var.google_expire_time)))) {
        error 200 "oauth_error: Google session expiration time has already elapsed.";
      }
    
      set var.oauthState = subfield(req.url.qs, "state", "&");
      if (var.oauthState !~ "^(\d+)-(\w+)$") {
        log "Querystring is " req.url.qs;
        log "State param is " var.oauthState;
        error 200 "oauth_error: Invalid or missing Fastly state token.";
      }
      set var.oauthStateTime = re.group.1;
      set var.oauthStateSigSupplied = re.group.2;
      set var.oauthStateSigExpected = digest.hash_sha256(urldecode(req.http.Cookie:oauth_orig_url) "-" var.oauthStateTime "-" table.lookup(oauth_settings, "state_secret"));
    
      if (var.oauthStateSigExpected != var.oauthStateSigSupplied) {
        error 200 "oauth_error: Unable to verify Fastly state signature.";
      }
      
      if (time.is_after(now, std.integer2time(std.atoi(var.oauthStateTime)))) {
        log "Fastly state token has expired; now (" now.sec ") is after expiry time (" var.oauthStateTime ").  Restart the auth flow.";
        # Fastly state has expired, recover by restarting auth flow
        set req.url = req.http.Cookie:oauth_orig_url;
        error 200 "oauth_authorize";
      }
    
      log "OAuth token is good. Save as a local session state.";
      set req.http.Google-Email = var.google_email;
      error 200 "oauth_save_local_session";

    }
  }
}

# If request is unauthenticated and requires authentication,
# trigger the Oauth flow
if (!var.isAuthenticated && req.url !~ "\.(ico|gif|jpg|png|js|css)$") {
  log "Unauthenticated request received.  Begin authentication process.";
  error 200 "oauth_authorize";
}

log "We've got a session cookie, so everything looks good for fetching the requested page from cache or origin";