<?php
/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

class AuthTest extends BaseTest
{
  const PRIVATE_KEY_FILE = "testdata/cert.p12";
  const PUBLIC_KEY_FILE_JSON = "testdata/cacert.json";
  const PUBLIC_KEY_FILE = "testdata/cacert.pem";
  const USER_ID = "102102479283111695822";

  /** @var Google_Signer_P12  */
  private $signer;

  /** @var string */
  private $pem;

  /** @var Google_Verifier_Pem */
  private $verifier;

  public function setUp()
  {
    $this->signer = new Google_Signer_P12(
        file_get_contents(__DIR__.'/'.self::PRIVATE_KEY_FILE, true),
        "notasecret"
    );
    $this->pem = file_get_contents(__DIR__.'/'.self::PUBLIC_KEY_FILE, true);
    $this->verifier = new Google_Verifier_Pem($this->pem);
  }

  public function testDirectInject()
  {
    $privateKeyString = <<<PK
-----BEGIN RSA PRIVATE KEY-----
MIICWwIBAAKBgQC8iqFTYTrSGxddW+Tsx6cdWbQxITdM2anRbMYcohnQpQuPG46B
HO3WbUA8suC6PXqeIi4JkDrAYbI2+TN6w1FE/fh2H7WczuDVKtosBcfsoL2C5loU
mOf+4jL1xx4EL6xy8wMntZhNgimVCO9LkWCix/Qh9mpqx2zbC3OV4QsSQQIDAQAB
AoGASAosRCClifxB/DENko9iwisxV4haiemtIlEOjYg+luNJPGAKHjlAgyrxXX/3
sBGnlV53+r16RWHO54RmcCTLGwpC6zzVc6C4Or9KItdMDMnqBjmqiYDz3Na7tIPv
vwzn8k8Uto26HZF8d1bTdoinxHrv7w1OVkDQWnHmWkQRjBUCQQDpNw8F1qiJJoYr
tkkBmlObmSQRYD3mlEvRwu348e4dFb01oN2cfw/YNhh+Lt2TPHFz2GNn6VwJf1Yb
qRKBqo/jAkEAzvY91ReYrkBm50pi2nqJc1Hcxm5CVP7MMnHbn8wExKrRG2rCDY9Y
zOdsw7pP/x6mesdUy3tTrPYVbeWP6YPmiwJANx41Jbsa7/cz5KbbUE6qDe8+sACg
AJvx42x/k8OR9DvMER2o4rDBDOeUGFZ5NbAmXCu7KrbjcrcuobDu18h44wJAQ2s5
x0HxjcoS+4Ni4nMKdZOUTNu8Jf3+vOwUNGD8qKhQiBLl9g7dSZqV9sipqJzudI6c
k9Cv+GcNoggnMlWycwJAHMVgaBmNc+RVCMar/gN6i5sENjN9Itu7U1V4Qj/mG6+4
MHOXhXSKhtTe0Bqm/MssVvCmc8AraKwBMs0rkMadsA==
-----END RSA PRIVATE KEY-----
PK;
    $sign = new Google_Signer_P12($privateKeyString, null);
  }

  public function testCantOpenP12()
  {
    try {
      new Google_Signer_P12(
          file_get_contents(__DIR__.'/'.self::PRIVATE_KEY_FILE, true),
          "badpassword"
      );
      $this->fail("Should have thrown");
    } catch (Google_Auth_Exception $e) {
      $this->assertContains("mac verify failure", $e->getMessage());
    }

    try {
      new Google_Signer_P12(
          file_get_contents(__DIR__.'/'.self::PRIVATE_KEY_FILE, true) . "foo",
          "badpassword"
      );
      $this->fail("Should have thrown");
    } catch (Exception $e) {
      $this->assertContains("Unable to parse", $e->getMessage());
    }
  }

  public function testVerifySignature()
  {
    $binary_data = "\x00\x01\x02\x66\x6f\x6f";
    $signature = $this->signer->sign($binary_data);
    $this->assertTrue($this->verifier->verify($binary_data, $signature));

    $empty_string = "";
    $signature = $this->signer->sign($empty_string);
    $this->assertTrue($this->verifier->verify($empty_string, $signature));

    $text = "foobar";
    $signature = $this->signer->sign($text);
    $this->assertTrue($this->verifier->verify($text, $signature));

    $this->assertFalse($this->verifier->verify($empty_string, $signature));
  }

  // Creates a signed JWT similar to the one created by google authentication.
  private function makeSignedJwt($payload)
  {
    $header = array("typ" => "JWT", "alg" => "RS256");
    $segments = array();
    $segments[] = Google_Utils::urlSafeB64Encode(json_encode($header));
    $segments[] = Google_Utils::urlSafeB64Encode(json_encode($payload));
    $signing_input = implode(".", $segments);

    $signature = $this->signer->sign($signing_input);
    $segments[] = Google_Utils::urlSafeB64Encode($signature);

    return implode(".", $segments);
  }

  // Returns certificates similar to the ones used by google authentication.
  private function getSignonCerts()
  {
    return array("keyid" => $this->pem);
  }

  public function testVerifySignedJwtWithCerts()
  {
    $id_token = $this->makeSignedJwt(
        array(
          "iss" => "federated-signon@system.gserviceaccount.com",
          "aud" => "client_id",
          "sub" => self::USER_ID,
          "iat" => time(),
          "exp" => time() + 3600
        )
    );
    $certs = $this->getSignonCerts();
    $oauth2 = new Google_Auth_OAuth2($this->getClient());
    $ticket = $oauth2->verifySignedJwtWithCerts($id_token, $certs, "client_id");
    $this->assertEquals(self::USER_ID, $ticket->getUserId());
    // Check that payload and envelope got filled in.
    $attributes = $ticket->getAttributes();
    $this->assertEquals("JWT", $attributes["envelope"]["typ"]);
    $this->assertEquals("client_id", $attributes["payload"]["aud"]);
  }

  // Checks that the id token fails to verify with the expected message.
  private function checkIdTokenFailure($id_token, $msg, $issuer = null)
  {
    $certs = $this->getSignonCerts();
    $oauth2 = new Google_Auth_OAuth2($this->getClient());
    try {
      $oauth2->verifySignedJwtWithCerts($id_token, $certs, "client_id", $issuer);
      $this->fail("Should have thrown for $id_token");
    } catch (Google_Auth_Exception $e) {
      $this->assertContains($msg, $e->getMessage());
    }
  }

  public function testVerifySignedJwtWithMultipleIssuers()
  {
    $id_token = $this->makeSignedJwt(
        array(
          "iss" => "system.gserviceaccount.com",
          "aud" => "client_id",
          "sub" => self::USER_ID,
          "iat" => time(),
          "exp" => time() + 3600
        )
    );
    $certs = $this->getSignonCerts();
    $oauth2 = new Google_Auth_OAuth2($this->getClient());
    $ticket = $oauth2->verifySignedJwtWithCerts(
      $id_token,
      $certs,
      "client_id",
      array('system.gserviceaccount.com', 'https://system.gserviceaccount.com')
    );
    $this->assertEquals(self::USER_ID, $ticket->getUserId());
    // Check that payload and envelope got filled in.
    $attributes = $ticket->getAttributes();
    $this->assertEquals("JWT", $attributes["envelope"]["typ"]);
    $this->assertEquals("client_id", $attributes["payload"]["aud"]);
  }

  public function testVerifySignedJwtWithBadIssuer()
  {
    $id_token = $this->makeSignedJwt(
        array(
          "iss" => "fake.gserviceaccount.com",
          "aud" => "client_id",
          "sub" => self::USER_ID,
          "iat" => time(),
          "exp" => time() + 3600
        )
    );

    $issuers = array('system.gserviceaccount.com', 'https://system.gserviceaccount.com');
    $this->checkIdTokenFailure($id_token, 'Invalid issuer', $issuers[0]);
    $this->checkIdTokenFailure($id_token, 'Invalid issuer', $issuers);
  }

  public function testVerifySignedJwtWithBadJwt()
  {
    $this->checkIdTokenFailure("foo", "Wrong number of segments");
    $this->checkIdTokenFailure("foo.bar", "Wrong number of segments");
    $this->checkIdTokenFailure(
        "foo.bar.baz",
        "Can't parse token envelope: foo"
    );
  }

  public function testVerifySignedJwtWithBadSignature()
  {
    $id_token = $this->makeSignedJwt(
        array(
          "iss" => "federated-signon@system.gserviceaccount.com",
          "aud" => "client_id",
          "id" => self::USER_ID,
          "iat" => time(),
          "exp" => time() + 3600
        )
    );
    $id_token = $id_token . "a";
    $this->checkIdTokenFailure($id_token, "Invalid token signature");
  }

  public function testVerifySignedJwtWithNoIssueTime()
  {
    $id_token = $this->makeSignedJwt(
        array(
          "iss" => "federated-signon@system.gserviceaccount.com",
          "aud" => "client_id",
          "id" => self::USER_ID,
          "exp" => time() + 3600
        )
    );
    $this->checkIdTokenFailure($id_token, "No issue time");
  }

  public function testVerifySignedJwtWithNoExpirationTime()
  {
    $id_token = $this->makeSignedJwt(
        array(
          "iss" => "federated-signon@system.gserviceaccount.com",
          "aud" => "client_id",
          "id" => self::USER_ID,
          "iat" => time()
        )
    );
    $this->checkIdTokenFailure($id_token, "No expiration time");
  }

  public function testVerifySignedJwtWithTooEarly()
  {
    $id_token = $this->makeSignedJwt(
        array(
          "iss" => "federated-signon@system.gserviceaccount.com",
          "aud" => "client_id",
          "id" => self::USER_ID,
          "iat" => time() + 1800,
          "exp" => time() + 3600
        )
    );
    $this->checkIdTokenFailure($id_token, "Token used too early");
  }

  public function testVerifySignedJwtWithTooLate()
  {
    $id_token = $this->makeSignedJwt(
        array(
            "iss" => "federated-signon@system.gserviceaccount.com",
            "aud" => "client_id",
            "id" => self::USER_ID,
            "iat" => time() - 3600,
            "exp" => time() - 1800
        )
    );
    $this->checkIdTokenFailure($id_token, "Token used too late");
  }

  public function testVerifySignedJwtWithLifetimeTooLong()
  {
    $id_token = $this->makeSignedJwt(
        array(
          "iss" => "federated-signon@system.gserviceaccount.com",
          "aud" => "client_id",
          "id" => self::USER_ID,
          "iat" => time(),
          "exp" => time() + 3600 * 25
        )
    );
    $this->checkIdTokenFailure($id_token, "Expiration time too far in future");
  }

  public function testVerifySignedJwtWithBadAudience()
  {
    $id_token = $this->makeSignedJwt(
        array(
          "iss" => "federated-signon@system.gserviceaccount.com",
          "aud" => "wrong_client_id",
          "id" => self::USER_ID,
          "iat" => time(),
          "exp" => time() + 3600
        )
    );
    $this->checkIdTokenFailure($id_token, "Wrong recipient");
  }

  public function testNoAuth()
  {
    /** @var $noAuth Google_Auth_Simple */
    $noAuth = new Google_Auth_Simple($this->getClient());
    $oldAuth = $this->getClient()->getAuth();
    $this->getClient()->setAuth($noAuth);
    $this->getClient()->setDeveloperKey(null);
    $req = new Google_Http_Request("http://example.com");

    $resp = $noAuth->sign($req);
    $this->assertEquals("http://example.com", $resp->getUrl());
    $this->getClient()->setAuth($oldAuth);
  }

  public function testAssertionCredentials()
  {
    $assertion = new Google_Auth_AssertionCredentials(
        'name',
        'scope',
        file_get_contents(__DIR__.'/'.self::PRIVATE_KEY_FILE, true)
    );

    $token = explode(".", $assertion->generateAssertion());
    $this->assertEquals('{"typ":"JWT","alg":"RS256"}', base64_decode($token[0]));

    $jwt = json_decode(base64_decode($token[1]), true);
    $this->assertEquals('https://accounts.google.com/o/oauth2/token', $jwt['aud']);
    $this->assertEquals('scope', $jwt['scope']);
    $this->assertEquals('name', $jwt['iss']);

    $key = $assertion->getCacheKey();
    $this->assertTrue($key != false);
    $assertion = new Google_Auth_AssertionCredentials(
        'name2',
        'scope',
        file_get_contents(__DIR__.'/'.self::PRIVATE_KEY_FILE, true)
    );
    $this->assertNotEquals($key, $assertion->getCacheKey());
  }

  public function testVerifySignedJWT()
  {
    $assertion = new Google_Auth_AssertionCredentials(
        'issuer',
        'scope',
        file_get_contents(__DIR__.'/'.self::PRIVATE_KEY_FILE, true)
    );
    $client = $this->getClient();

    $this->assertInstanceOf(
        'Google_Auth_LoginTicket',
        $client->verifySignedJwt(
            $assertion->generateAssertion(),
            __DIR__ . DIRECTORY_SEPARATOR .  self::PUBLIC_KEY_FILE_JSON,
            'https://accounts.google.com/o/oauth2/token',
            'issuer'
        )
    );
  }
}
