[mq]: id_token2
diff --git a/tests/data/certs.json b/tests/data/certs.json
new file mode 100644
index 0000000..fa09416
--- /dev/null
+++ b/tests/data/certs.json
@@ -0,0 +1,3 @@
+{
+"foo": "-----BEGIN CERTIFICATE-----\r\nMIIDIzCCAgugAwIBAgIJAMfISuBQ5m+5MA0GCSqGSIb3DQEBBQUAMBUxEzARBgNV\r\nBAMTCnVuaXQtdGVzdHMwHhcNMTExMjA2MTYyNjAyWhcNMjExMjAzMTYyNjAyWjAV\r\nMRMwEQYDVQQDEwp1bml0LXRlc3RzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB\r\nCgKCAQEA4ej0p7bQ7L/r4rVGUz9RN4VQWoej1Bg1mYWIDYslvKrk1gpj7wZgkdmM\r\n7oVK2OfgrSj/FCTkInKPqaCR0gD7K80q+mLBrN3PUkDrJQZpvRZIff3/xmVU1Wer\r\nuQLFJjnFb2dqu0s/FY/2kWiJtBCakXvXEOb7zfbINuayL+MSsCGSdVYsSliS5qQp\r\ngyDap+8b5fpXZVJkq92hrcNtbkg7hCYUJczt8n9hcCTJCfUpApvaFQ18pe+zpyl4\r\n+WzkP66I28hniMQyUlA1hBiskT7qiouq0m8IOodhv2fagSZKjOTTU2xkSBc//fy3\r\nZpsL7WqgsZS7Q+0VRK8gKfqkxg5OYQIDAQABo3YwdDAdBgNVHQ4EFgQU2RQ8yO+O\r\ngN8oVW2SW7RLrfYd9jEwRQYDVR0jBD4wPIAU2RQ8yO+OgN8oVW2SW7RLrfYd9jGh\r\nGaQXMBUxEzARBgNVBAMTCnVuaXQtdGVzdHOCCQDHyErgUOZvuTAMBgNVHRMEBTAD\r\nAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQBRv+M/6+FiVu7KXNjFI5pSN17OcW5QUtPr\r\nodJMlWrJBtynn/TA1oJlYu3yV5clc/71Vr/AxuX5xGP+IXL32YDF9lTUJXG/uUGk\r\n+JETpKmQviPbRsvzYhz4pf6ZIOZMc3/GIcNq92ECbseGO+yAgyWUVKMmZM0HqXC9\r\novNslqe0M8C1sLm1zAR5z/h/litE7/8O2ietija3Q/qtl2TOXJdCA6sgjJX2WUql\r\nybrC55ct18NKf3qhpcEkGQvFU40rVYApJpi98DiZPYFdx1oBDp/f4uZ3ojpxRVFT\r\ncDwcJLfNRCPUhormsY7fDS9xSyThiHsW9mjJYdcaKQkwYZ0F11yB\r\n-----END CERTIFICATE-----\r\n"
+}
diff --git a/tests/data/create-private-keys.sh b/tests/data/create-private-keys.sh
new file mode 100644
index 0000000..b21cbdb
--- /dev/null
+++ b/tests/data/create-private-keys.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+
+openssl req -new -newkey rsa:2048 -days 3650 -nodes -x509 \
+  -keyout privatekey.pem -out publickey.pem \
+  -subj "/CN=unit-tests"
+
+openssl pkcs12 -export -out privatekey.p12 \
+  -inkey privatekey.pem -in publickey.pem \
+  -name "key" -passout pass:notasecret
diff --git a/tests/data/privatekey.p12 b/tests/data/privatekey.p12
new file mode 100644
index 0000000..c369ecb
--- /dev/null
+++ b/tests/data/privatekey.p12
Binary files differ
diff --git a/tests/data/privatekey.pem b/tests/data/privatekey.pem
new file mode 100644
index 0000000..5744354
--- /dev/null
+++ b/tests/data/privatekey.pem
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpAIBAAKCAQEA4ej0p7bQ7L/r4rVGUz9RN4VQWoej1Bg1mYWIDYslvKrk1gpj
+7wZgkdmM7oVK2OfgrSj/FCTkInKPqaCR0gD7K80q+mLBrN3PUkDrJQZpvRZIff3/
+xmVU1WeruQLFJjnFb2dqu0s/FY/2kWiJtBCakXvXEOb7zfbINuayL+MSsCGSdVYs
+SliS5qQpgyDap+8b5fpXZVJkq92hrcNtbkg7hCYUJczt8n9hcCTJCfUpApvaFQ18
+pe+zpyl4+WzkP66I28hniMQyUlA1hBiskT7qiouq0m8IOodhv2fagSZKjOTTU2xk
+SBc//fy3ZpsL7WqgsZS7Q+0VRK8gKfqkxg5OYQIDAQABAoIBAQDGGHzQxGKX+ANk
+nQi53v/c6632dJKYXVJC+PDAz4+bzU800Y+n/bOYsWf/kCp94XcG4Lgsdd0Gx+Zq
+HD9CI1IcqqBRR2AFscsmmX6YzPLTuEKBGMW8twaYy3utlFxElMwoUEsrSWRcCA1y
+nHSDzTt871c7nxCXHxuZ6Nm/XCL7Bg8uidRTSC1sQrQyKgTPhtQdYrPQ4WZ1A4J9
+IisyDYmZodSNZe5P+LTJ6M1SCgH8KH9ZGIxv3diMwzNNpk3kxJc9yCnja4mjiGE2
+YCNusSycU5IhZwVeCTlhQGcNeV/skfg64xkiJE34c2y2ttFbdwBTPixStGaF09nU
+Z422D40BAoGBAPvVyRRsC3BF+qZdaSMFwI1yiXY7vQw5+JZh01tD28NuYdRFzjcJ
+vzT2n8LFpj5ZfZFvSMLMVEFVMgQvWnN0O6xdXvGov6qlRUSGaH9u+TCPNnIldjMP
+B8+xTwFMqI7uQr54wBB+Poq7dVRP+0oHb0NYAwUBXoEuvYo3c/nDoRcZAoGBAOWl
+aLHjMv4CJbArzT8sPfic/8waSiLV9Ixs3Re5YREUTtnLq7LoymqB57UXJB3BNz/2
+eCueuW71avlWlRtE/wXASj5jx6y5mIrlV4nZbVuyYff0QlcG+fgb6pcJQuO9DxMI
+aqFGrWP3zye+LK87a6iR76dS9vRU+bHZpSVvGMKJAoGAFGt3TIKeQtJJyqeUWNSk
+klORNdcOMymYMIlqG+JatXQD1rR6ThgqOt8sgRyJqFCVT++YFMOAqXOBBLnaObZZ
+CFbh1fJ66BlSjoXff0W+SuOx5HuJJAa5+WtFHrPajwxeuRcNa8jwxUsB7n41wADu
+UqWWSRedVBg4Ijbw3nWwYDECgYB0pLew4z4bVuvdt+HgnJA9n0EuYowVdadpTEJg
+soBjNHV4msLzdNqbjrAqgz6M/n8Ztg8D2PNHMNDNJPVHjJwcR7duSTA6w2p/4k28
+bvvk/45Ta3XmzlxZcZSOct3O31Cw0i2XDVc018IY5be8qendDYM08icNo7vQYkRH
+504kQQKBgQDjx60zpz8ozvm1XAj0wVhi7GwXe+5lTxiLi9Fxq721WDxPMiHDW2XL
+YXfFVy/9/GIMvEiGYdmarK1NW+VhWl1DC5xhDg0kvMfxplt4tynoq1uTsQTY31Mx
+BeF5CT/JuNYk3bEBF0H/Q3VGO1/ggVS+YezdFbLWIRoMnLj6XCFEGg==
+-----END RSA PRIVATE KEY-----
diff --git a/tests/data/publickey.pem b/tests/data/publickey.pem
new file mode 100644
index 0000000..7af6ca3
--- /dev/null
+++ b/tests/data/publickey.pem
@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIDIzCCAgugAwIBAgIJAMfISuBQ5m+5MA0GCSqGSIb3DQEBBQUAMBUxEzARBgNV
+BAMTCnVuaXQtdGVzdHMwHhcNMTExMjA2MTYyNjAyWhcNMjExMjAzMTYyNjAyWjAV
+MRMwEQYDVQQDEwp1bml0LXRlc3RzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
+CgKCAQEA4ej0p7bQ7L/r4rVGUz9RN4VQWoej1Bg1mYWIDYslvKrk1gpj7wZgkdmM
+7oVK2OfgrSj/FCTkInKPqaCR0gD7K80q+mLBrN3PUkDrJQZpvRZIff3/xmVU1Wer
+uQLFJjnFb2dqu0s/FY/2kWiJtBCakXvXEOb7zfbINuayL+MSsCGSdVYsSliS5qQp
+gyDap+8b5fpXZVJkq92hrcNtbkg7hCYUJczt8n9hcCTJCfUpApvaFQ18pe+zpyl4
++WzkP66I28hniMQyUlA1hBiskT7qiouq0m8IOodhv2fagSZKjOTTU2xkSBc//fy3
+ZpsL7WqgsZS7Q+0VRK8gKfqkxg5OYQIDAQABo3YwdDAdBgNVHQ4EFgQU2RQ8yO+O
+gN8oVW2SW7RLrfYd9jEwRQYDVR0jBD4wPIAU2RQ8yO+OgN8oVW2SW7RLrfYd9jGh
+GaQXMBUxEzARBgNVBAMTCnVuaXQtdGVzdHOCCQDHyErgUOZvuTAMBgNVHRMEBTAD
+AQH/MA0GCSqGSIb3DQEBBQUAA4IBAQBRv+M/6+FiVu7KXNjFI5pSN17OcW5QUtPr
+odJMlWrJBtynn/TA1oJlYu3yV5clc/71Vr/AxuX5xGP+IXL32YDF9lTUJXG/uUGk
++JETpKmQviPbRsvzYhz4pf6ZIOZMc3/GIcNq92ECbseGO+yAgyWUVKMmZM0HqXC9
+ovNslqe0M8C1sLm1zAR5z/h/litE7/8O2ietija3Q/qtl2TOXJdCA6sgjJX2WUql
+ybrC55ct18NKf3qhpcEkGQvFU40rVYApJpi98DiZPYFdx1oBDp/f4uZ3ojpxRVFT
+cDwcJLfNRCPUhormsY7fDS9xSyThiHsW9mjJYdcaKQkwYZ0F11yB
+-----END CERTIFICATE-----
diff --git a/tests/test_oauth2client.py b/tests/test_oauth2client.py
index a23d1da..9acc9cf 100644
--- a/tests/test_oauth2client.py
+++ b/tests/test_oauth2client.py
@@ -22,6 +22,7 @@
 
 __author__ = 'jcgregorio@google.com (Joe Gregorio)'
 
+import base64
 import datetime
 import httplib2
 import unittest
@@ -50,6 +51,8 @@
 from oauth2client.client import FlowExchangeError
 from oauth2client.client import OAuth2Credentials
 from oauth2client.client import OAuth2WebServerFlow
+from oauth2client.client import VerifyJwtTokenError
+from oauth2client.client import _extract_id_token
 
 
 class OAuth2CredentialsTests(unittest.TestCase):
@@ -143,6 +146,7 @@
     resp, content = http.request('http://example.com')
     self.assertEqual(content['authorization'], 'OAuth foo')
 
+
 class TestAssertionCredentials(unittest.TestCase):
   assertion_text = "This is the assertion"
   assertion_type = "http://www.google.com/assertionType"
@@ -172,6 +176,24 @@
     self.assertEqual(content['authorization'], 'OAuth 1/3w')
 
 
+class ExtractIdTokenText(unittest.TestCase):
+  """Tests _extract_id_token()."""
+
+  def test_extract_success(self):
+    body = {'foo': 'bar'}
+    payload = base64.urlsafe_b64encode(simplejson.dumps(body)).strip('=')
+    jwt = 'stuff.' + payload + '.signature'
+
+    extracted = _extract_id_token(jwt)
+    self.assertEqual(body, extracted)
+
+  def test_extract_failure(self):
+    body = {'foo': 'bar'}
+    payload = base64.urlsafe_b64encode(simplejson.dumps(body)).strip('=')
+    jwt = 'stuff.' + payload
+
+    self.assertRaises(VerifyJwtTokenError, _extract_id_token, jwt)
+
 class OAuth2WebServerFlowTest(unittest.TestCase):
 
   def setUp(self):
@@ -245,6 +267,30 @@
     credentials = self.flow.step2_exchange('some random code', http)
     self.assertEqual(credentials.token_expiry, None)
 
+  def test_exchange_id_token_fail(self):
+    http = HttpMockSequence([
+      ({'status': '200'}, """{ "access_token":"SlAV32hkKG",
+       "refresh_token":"8xLOxBtZp8",
+       "id_token": "stuff.payload"}"""),
+      ])
+
+    self.assertRaises(VerifyJwtTokenError, self.flow.step2_exchange,
+      'some random code', http)
+
+  def test_exchange_id_token_fail(self):
+    body = {'foo': 'bar'}
+    payload = base64.urlsafe_b64encode(simplejson.dumps(body)).strip('=')
+    jwt = 'stuff.' + payload + '.signature'
+
+    http = HttpMockSequence([
+      ({'status': '200'}, """{ "access_token":"SlAV32hkKG",
+       "refresh_token":"8xLOxBtZp8",
+       "id_token": "%s"}""" % jwt),
+      ])
+
+    credentials = self.flow.step2_exchange('some random code', http)
+    self.assertEquals(body, credentials.id_token)
+
 
 if __name__ == '__main__':
   unittest.main()
diff --git a/tests/test_oauth2client_appengine.py b/tests/test_oauth2client_appengine.py
index 8d8064c..ca9d940 100644
--- a/tests/test_oauth2client_appengine.py
+++ b/tests/test_oauth2client_appengine.py
@@ -32,6 +32,9 @@
 except ImportError:
     from cgi import parse_qs
 
+import dev_appserver
+dev_appserver.fix_sys_path()
+
 from apiclient.anyjson import simplejson
 from apiclient.http import HttpMockSequence
 from google.appengine.api import apiproxy_stub
diff --git a/tests/test_oauth2client_django_orm.py b/tests/test_oauth2client_django_orm.py
index 67ec314..dbd2e20 100644
--- a/tests/test_oauth2client_django_orm.py
+++ b/tests/test_oauth2client_django_orm.py
@@ -73,3 +73,7 @@
   def test_field_pickled(self):
     prep_value = self.field.get_db_prep_value(self.flow, connection=None)
     self.assertEqual(prep_value, self.pickle)
+
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/tests/test_oauth2client_jwt.py b/tests/test_oauth2client_jwt.py
new file mode 100644
index 0000000..01a7331
--- /dev/null
+++ b/tests/test_oauth2client_jwt.py
@@ -0,0 +1,211 @@
+#!/usr/bin/python2.4
+#
+# Copyright 2010 Google Inc.
+#
+# Licensed 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.
+
+
+"""Oauth2client tests
+
+Unit tests for oauth2client.
+"""
+
+__author__ = 'jcgregorio@google.com (Joe Gregorio)'
+
+import httplib2
+import os
+import sys
+import time
+import unittest
+import urlparse
+
+try:
+    from urlparse import parse_qs
+except ImportError:
+    from cgi import parse_qs
+
+try:  # pragma: no cover
+  import simplejson
+except ImportError:  # pragma: no cover
+  try:
+    # Try to import from django, should work on App Engine
+    from django.utils import simplejson
+  except ImportError:
+    # Should work for Python2.6 and higher.
+    import json as simplejson
+
+from apiclient.http import HttpMockSequence
+from oauth2client import crypt
+from oauth2client.client import SignedJwtAssertionCredentials
+from oauth2client.client import verify_id_token
+from oauth2client.client import VerifyJwtTokenError
+
+
+def datafile(filename):
+  f = open(os.path.join(os.path.dirname(__file__), 'data', filename), 'r')
+  data = f.read()
+  f.close()
+  return data
+
+
+class CryptTests(unittest.TestCase):
+
+  def test_sign_and_verify(self):
+    private_key = datafile('privatekey.p12')
+    public_key = datafile('publickey.pem')
+
+    signer = crypt.Signer.from_string(private_key)
+    signature = signer.sign('foo')
+
+    verifier = crypt.Verifier.from_string(public_key, True)
+
+    self.assertTrue(verifier.verify('foo', signature))
+
+    self.assertFalse(verifier.verify('bar', signature))
+    self.assertFalse(verifier.verify('foo', 'bad signagure'))
+
+  def _check_jwt_failure(self, jwt, expected_error):
+      try:
+          public_key = datafile('publickey.pem')
+          certs = {'foo': public_key}
+          audience = 'https://www.googleapis.com/auth/id?client_id=' + \
+              'external_public_key@testing.gserviceaccount.com'
+          contents = crypt.verify_signed_jwt_with_certs(jwt, certs, audience)
+          self.fail('Should have thrown for %s' % jwt)
+      except:
+          e = sys.exc_info()[1]
+          msg = e.args[0]
+          self.assertTrue(expected_error in msg)
+
+  def _create_signed_jwt(self):
+    private_key = datafile('privatekey.p12')
+    signer = crypt.Signer.from_string(private_key)
+    audience = 'some_audience_address@testing.gserviceaccount.com'
+    now = long(time.time())
+
+    return crypt.make_signed_jwt(
+        signer,
+        {
+        'aud': audience,
+        'iat': now,
+        'exp': now + 300,
+        'user': 'billy bob',
+        'metadata': {'meta': 'data'},
+        })
+
+  def test_verify_id_token(self):
+    jwt = self._create_signed_jwt()
+    public_key = datafile('publickey.pem')
+    certs = {'foo': public_key }
+    audience = 'some_audience_address@testing.gserviceaccount.com'
+    contents = crypt.verify_signed_jwt_with_certs(jwt, certs, audience)
+    self.assertEquals('billy bob', contents['user'])
+    self.assertEquals('data', contents['metadata']['meta'])
+
+  def test_verify_id_token_with_certs_uri(self):
+    jwt = self._create_signed_jwt()
+
+    http = HttpMockSequence([
+      ({'status': '200'}, datafile('certs.json')),
+      ])
+
+    contents = verify_id_token(jwt,
+        'some_audience_address@testing.gserviceaccount.com', http)
+    self.assertEquals('billy bob', contents['user'])
+    self.assertEquals('data', contents['metadata']['meta'])
+
+
+  def test_verify_id_token_with_certs_uri_fails(self):
+    jwt = self._create_signed_jwt()
+
+    http = HttpMockSequence([
+      ({'status': '404'}, datafile('certs.json')),
+      ])
+
+    self.assertRaises(VerifyJwtTokenError, verify_id_token, jwt,
+        'some_audience_address@testing.gserviceaccount.com', http)
+
+  def test_verify_id_token_bad_tokens(self):
+    private_key = datafile('privatekey.p12')
+
+    # Wrong number of segments
+    self._check_jwt_failure('foo', 'Wrong number of segments')
+
+    # Not json
+    self._check_jwt_failure('foo.bar.baz',
+        'Can\'t parse token')
+
+    # Bad signature
+    jwt = 'foo.%s.baz' % crypt._urlsafe_b64encode('{"a":"b"}')
+    self._check_jwt_failure(jwt, 'Invalid token signature')
+
+    # No expiration
+    signer = crypt.Signer.from_string(private_key)
+    audience = 'https:#www.googleapis.com/auth/id?client_id=' + \
+        'external_public_key@testing.gserviceaccount.com'
+    jwt = crypt.make_signed_jwt(signer, {
+          'aud': 'audience',
+          'iat': time.time(),
+          }
+        )
+    self._check_jwt_failure(jwt, 'No exp field in token')
+
+    # No issued at
+    jwt = crypt.make_signed_jwt(signer, {
+          'aud': 'audience',
+          'exp': time.time() + 400,
+        }
+      )
+    self._check_jwt_failure(jwt, 'No iat field in token')
+
+    # Too early
+    jwt = crypt.make_signed_jwt(signer, {
+        'aud': 'audience',
+        'iat': time.time() + 301,
+        'exp': time.time() + 400,
+    })
+    self._check_jwt_failure(jwt, 'Token used too early')
+
+    # Too late
+    jwt = crypt.make_signed_jwt(signer, {
+        'aud': 'audience',
+        'iat': time.time() - 500,
+        'exp': time.time() - 301,
+    })
+    self._check_jwt_failure(jwt, 'Token used too late')
+
+    # Wrong target
+    jwt = crypt.make_signed_jwt(signer, {
+        'aud': 'somebody else',
+        'iat': time.time(),
+        'exp': time.time() + 300,
+    })
+    self._check_jwt_failure(jwt, 'Wrong recipient')
+
+  def test_signed_jwt_assertion_credentials(self):
+    private_key = datafile('privatekey.p12')
+    credentials = SignedJwtAssertionCredentials(
+        'some_account@example.com',
+        private_key,
+        scope='read+write',
+        prn='joe@example.org')
+    http = HttpMockSequence([
+      ({'status': '200'}, '{"access_token":"1/3w","expires_in":3600}'),
+      ({'status': '200'}, 'echo_request_headers'),
+      ])
+    http = credentials.authorize(http)
+    resp, content = http.request('http://example.org')
+    self.assertEquals(content['authorization'], 'OAuth 1/3w')
+
+if __name__ == '__main__':
+  unittest.main()