Add XSRF protection to oauth2decorator callback.
Also update all samples to use XSRF callback protection.

Reviewed in https://codereview.appspot.com/6473053/.
diff --git a/oauth2client/appengine.py b/oauth2client/appengine.py
index aebbd32..e9cb17e 100644
--- a/oauth2client/appengine.py
+++ b/oauth2client/appengine.py
@@ -22,18 +22,20 @@
 import base64
 import httplib2
 import logging
+import os
 import pickle
 import time
 
-import clientsecrets
-
 from google.appengine.api import app_identity
+from google.appengine.api import memcache
 from google.appengine.api import users
 from google.appengine.ext import db
 from google.appengine.ext import webapp
 from google.appengine.ext.webapp.util import login_required
 from google.appengine.ext.webapp.util import run_wsgi_app
+from oauth2client import clientsecrets
 from oauth2client import util
+from oauth2client import xsrfutil
 from oauth2client.anyjson import simplejson
 from oauth2client.client import AccessTokenRefreshError
 from oauth2client.client import AssertionCredentials
@@ -46,19 +48,60 @@
 
 OAUTH2CLIENT_NAMESPACE = 'oauth2client#ns'
 
+XSRF_MEMCACHE_ID = 'xsrf_secret_key'
+
 
 class InvalidClientSecretsError(Exception):
   """The client_secrets.json file is malformed or missing required fields."""
-  pass
+
+
+class InvalidXsrfTokenError(Exception):
+  """The XSRF token is invalid or expired."""
+
+
+class SiteXsrfSecretKey(db.Model):
+  """Storage for the sites XSRF secret key.
+
+  There will only be one instance stored of this model, the one used for the
+  site.  """
+  secret = db.StringProperty()
+
+
+def _generate_new_xsrf_secret_key():
+  """Returns a random XSRF secret key.
+  """
+  return os.urandom(16).encode("hex")
+
+
+def xsrf_secret_key():
+  """Return the secret key for use for XSRF protection.
+
+  If the Site entity does not have a secret key, this method will also create
+  one and persist it.
+
+  Returns:
+    The secret key.
+  """
+  secret = memcache.get(XSRF_MEMCACHE_ID, namespace=OAUTH2CLIENT_NAMESPACE)
+  if not secret:
+    # Load the one and only instance of SiteXsrfSecretKey.
+    model = SiteXsrfSecretKey.get_or_insert(key_name='site')
+    if not model.secret:
+      model.secret = _generate_new_xsrf_secret_key()
+      model.put()
+    secret = model.secret
+    memcache.add(XSRF_MEMCACHE_ID, secret, namespace=OAUTH2CLIENT_NAMESPACE)
+
+  return str(secret)
 
 
 class AppAssertionCredentials(AssertionCredentials):
   """Credentials object for App Engine Assertion Grants
 
   This object will allow an App Engine application to identify itself to Google
-  and other OAuth 2.0 servers that can verify assertions. It can be used for
-  the purpose of accessing data stored under an account assigned to the App
-  Engine application itself.
+  and other OAuth 2.0 servers that can verify assertions. It can be used for the
+  purpose of accessing data stored under an account assigned to the App Engine
+  application itself.
 
   This credential does not require a flow to instantiate because it represents
   a two legged flow, and therefore has all of the required information to
@@ -263,6 +306,48 @@
   credentials = CredentialsProperty()
 
 
+def _build_state_value(request_handler, user):
+  """Composes the value for the 'state' parameter.
+
+  Packs the current request URI and an XSRF token into an opaque string that
+  can be passed to the authentication server via the 'state' parameter.
+
+  Args:
+    request_handler: webapp.RequestHandler, The request.
+    user: google.appengine.api.users.User, The current user.
+
+  Returns:
+    The state value as a string.
+  """
+  uri = request_handler.request.url
+  token = xsrfutil.generate_token(xsrf_secret_key(), user.user_id(),
+                                  action_id=str(uri))
+  return  uri + ':' + token
+
+
+def _parse_state_value(state, user):
+  """Parse the value of the 'state' parameter.
+
+  Parses the value and validates the XSRF token in the state parameter.
+
+  Args:
+    state: string, The value of the state parameter.
+    user: google.appengine.api.users.User, The current user.
+
+  Raises:
+    InvalidXsrfTokenError: if the XSRF token is invalid.
+
+  Returns:
+    The redirect URI.
+  """
+  uri, token = state.rsplit(':', 1)
+  if not xsrfutil.validate_token(xsrf_secret_key(), token, user.user_id(),
+                                 action_id=uri):
+    raise InvalidXsrfTokenError()
+
+  return uri
+
+
 class OAuth2Decorator(object):
   """Utility for making OAuth 2.0 easier.
 
@@ -361,14 +446,14 @@
       self._create_flow(request_handler)
 
       # Store the request URI in 'state' so we can use it later
-      self.flow.params['state'] = request_handler.request.url
+      self.flow.params['state'] = _build_state_value(request_handler, user)
       self.credentials = StorageByKeyName(
           CredentialsModel, user.user_id(), 'credentials').get()
 
       if not self.has_credentials():
         return request_handler.redirect(self.authorize_url())
       try:
-        method(request_handler, *args, **kwargs)
+        return method(request_handler, *args, **kwargs)
       except AccessTokenRefreshError:
         return request_handler.redirect(self.authorize_url())
 
@@ -422,10 +507,10 @@
 
       self._create_flow(request_handler)
 
-      self.flow.params['state'] = request_handler.request.url
+      self.flow.params['state'] = _build_state_value(request_handler, user)
       self.credentials = StorageByKeyName(
           CredentialsModel, user.user_id(), 'credentials').get()
-      method(request_handler, *args, **kwargs)
+      return method(request_handler, *args, **kwargs)
     return setup_oauth
 
   def has_credentials(self):
@@ -500,7 +585,9 @@
           credentials = decorator.flow.step2_exchange(self.request.params)
           StorageByKeyName(
               CredentialsModel, user.user_id(), 'credentials').put(credentials)
-          self.redirect(str(self.request.get('state')))
+          redirect_uri = _parse_state_value(str(self.request.get('state')),
+                                            user)
+          self.redirect(redirect_uri)
 
     return OAuth2Handler
 
@@ -550,26 +637,24 @@
       scope: string or list of strings, scope(s) of the credentials being
         requested.
       message: string, A friendly string to display to the user if the
-        clientsecrets file is missing or invalid. The message may contain HTML and
-        will be presented on the web interface for any method that uses the
+        clientsecrets file is missing or invalid. The message may contain HTML
+        and will be presented on the web interface for any method that uses the
         decorator.
       cache: An optional cache service client that implements get() and set()
         methods. See clientsecrets.loadfile() for details.
     """
-    try:
-      client_type, client_info = clientsecrets.loadfile(filename, cache=cache)
-      if client_type not in [clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]:
-        raise InvalidClientSecretsError('OAuth2Decorator doesn\'t support this OAuth 2.0 flow.')
-      super(OAuth2DecoratorFromClientSecrets,
-            self).__init__(
-                client_info['client_id'],
-                client_info['client_secret'],
-                scope,
-                auth_uri=client_info['auth_uri'],
-                token_uri=client_info['token_uri'],
-                message=message)
-    except clientsecrets.InvalidClientSecretsError:
-      self._in_error = True
+    client_type, client_info = clientsecrets.loadfile(filename, cache=cache)
+    if client_type not in [
+        clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]:
+      raise InvalidClientSecretsError(
+          'OAuth2Decorator doesn\'t support this OAuth 2.0 flow.')
+    super(OAuth2DecoratorFromClientSecrets, self).__init__(
+              client_info['client_id'],
+              client_info['client_secret'],
+              scope,
+              auth_uri=client_info['auth_uri'],
+              token_uri=client_info['token_uri'],
+              message=message)
     if message is not None:
       self._message = message
     else:
diff --git a/oauth2client/client.py b/oauth2client/client.py
index 18a6ce1..851b639 100644
--- a/oauth2client/client.py
+++ b/oauth2client/client.py
@@ -1141,7 +1141,7 @@
       if 'id_token' in d:
         d['id_token'] = _extract_id_token(d['id_token'])
 
-      logger.info('Successfully retrieved access token: %s' % content)
+      logger.info('Successfully retrieved access token')
       return OAuth2Credentials(access_token, self.client_id,
                                self.client_secret, refresh_token, token_expiry,
                                self.token_uri, self.user_agent,
diff --git a/oauth2client/xsrfutil.py b/oauth2client/xsrfutil.py
new file mode 100644
index 0000000..7d5fdbe
--- /dev/null
+++ b/oauth2client/xsrfutil.py
@@ -0,0 +1,106 @@
+#!/usr/bin/python2.5
+#
+# Copyright 2010 the Melange authors.
+#
+# 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.
+
+"""Helper methods for creating & verifying XSRF tokens."""
+
+__authors__ = [
+  '"Doug Coker" <dcoker@google.com>',
+  '"Joe Gregorio" <jcgregorio@google.com>',
+]
+
+
+import base64
+import hmac
+import os  # for urandom
+import time
+
+from oauth2client import util
+
+
+# Delimiter character
+DELIMITER = ':'
+
+# 1 hour in seconds
+DEFAULT_TIMEOUT_SECS = 1*60*60
+
+@util.positional(2)
+def generate_token(key, user_id, action_id="", when=None):
+  """Generates a URL-safe token for the given user, action, time tuple.
+
+  Args:
+    key: secret key to use.
+    user_id: the user ID of the authenticated user.
+    action_id: a string identifier of the action they requested
+      authorization for.
+    when: the time in seconds since the epoch at which the user was
+      authorized for this action. If not set the current time is used.
+
+  Returns:
+    A string XSRF protection token.
+  """
+  when = when or int(time.time())
+  digester = hmac.new(key)
+  digester.update(str(user_id))
+  digester.update(DELIMITER)
+  digester.update(action_id)
+  digester.update(DELIMITER)
+  digester.update(str(when))
+  digest = digester.digest()
+
+  token = base64.urlsafe_b64encode('%s%s%d' % (digest,
+                                               DELIMITER,
+                                               when))
+  return token
+
+
+@util.positional(3)
+def validate_token(key, token, user_id, action_id="", current_time=None):
+  """Validates that the given token authorizes the user for the action.
+
+  Tokens are invalid if the time of issue is too old or if the token
+  does not match what generateToken outputs (i.e. the token was forged).
+
+  Args:
+    key: secret key to use.
+    token: a string of the token generated by generateToken.
+    user_id: the user ID of the authenticated user.
+    action_id: a string identifier of the action they requested
+      authorization for.
+
+  Returns:
+    A boolean - True if the user is authorized for the action, False
+    otherwise.
+  """
+  if not token:
+    return False
+  try:
+    decoded = base64.urlsafe_b64decode(str(token))
+    token_time = long(decoded.split(DELIMITER)[-1])
+  except (TypeError, ValueError):
+    return False
+  if current_time is None:
+    current_time = time.time()
+  # If the token is too old it's not valid.
+  if current_time - token_time > DEFAULT_TIMEOUT_SECS:
+    return False
+
+  # The given token should match the generated one with the same time.
+  expected_token = generate_token(key, user_id, action_id=action_id,
+                                  when=token_time)
+  if token != expected_token:
+    return False
+
+  return True
diff --git a/runtests.sh b/runtests.sh
index a725faf..9b4ba05 100755
--- a/runtests.sh
+++ b/runtests.sh
@@ -21,3 +21,4 @@
 $1 runtests.py $FLAGS tests/test_oauth2client_appengine.py
 $1 runtests.py $FLAGS tests/test_oauth2client_keyring.py
 $1 runtests.py $FLAGS tests/test_oauth2client_gce.py
+$1 runtests.py $FLAGS tests/test_oauth2client_xsrfutil.py
diff --git a/samples/appengine/grant.html b/samples/appengine/grant.html
index 0087325..aeb678b 100644
--- a/samples/appengine/grant.html
+++ b/samples/appengine/grant.html
@@ -8,7 +8,7 @@
       application</a>.</p>
     {% else %}
     <p><a href="{{ url }}">Grant</a> this application permission to read your
-    Buzz information and it will let you know how many followers you have.</p>
+    Google+ information and it will let you know how many followers you have.</p>
     {% endif %}
     <p>You can always <a
       href="https://www.google.com/accounts/b/0/IssuedAuthSubTokens">revoke</a>
diff --git a/samples/dailymotion/README b/samples/dailymotion/README
deleted file mode 100644
index 7139bde..0000000
--- a/samples/dailymotion/README
+++ /dev/null
@@ -1,3 +0,0 @@
-Demonstrates using oauth2client against the DailyMotion API.
-
-keywords: oauth2 appengine
diff --git a/samples/dailymotion/app.yaml b/samples/dailymotion/app.yaml
deleted file mode 100644
index a712bd2..0000000
--- a/samples/dailymotion/app.yaml
+++ /dev/null
@@ -1,9 +0,0 @@
-application: dailymotoauth2test
-version: 1
-runtime: python
-api_version: 1
-
-handlers:
-- url: .*
-  script: main.py
-
diff --git a/samples/dailymotion/index.yaml b/samples/dailymotion/index.yaml
deleted file mode 100644
index a3b9e05..0000000
--- a/samples/dailymotion/index.yaml
+++ /dev/null
@@ -1,11 +0,0 @@
-indexes:
-
-# AUTOGENERATED
-
-# This index.yaml is automatically updated whenever the dev_appserver
-# detects that a new type of query is run.  If you want to manage the
-# index.yaml file manually, remove the above marker line (the line
-# saying "# AUTOGENERATED").  If you want to manage some indexes
-# manually, move them above the marker line.  The index.yaml file is
-# automatically uploaded to the admin console when you next deploy
-# your application using appcfg.py.
diff --git a/samples/dailymotion/main.py b/samples/dailymotion/main.py
deleted file mode 100644
index 7f7256f..0000000
--- a/samples/dailymotion/main.py
+++ /dev/null
@@ -1,100 +0,0 @@
-#!/usr/bin/env python
-#
-# Copyright 2007 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.
-#
-
-__author__ = 'jcgregorio@google.com (Joe Gregorio)'
-
-
-import httplib2
-import logging
-import os
-import pickle
-
-from oauth2client.appengine import CredentialsProperty
-from oauth2client.appengine import StorageByKeyName
-from oauth2client.client import OAuth2WebServerFlow
-from google.appengine.api import users
-from google.appengine.ext import db
-from google.appengine.ext import webapp
-from google.appengine.ext.webapp import template
-from google.appengine.ext.webapp import util
-from google.appengine.ext.webapp.util import login_required
-
-
-FLOW = OAuth2WebServerFlow(
-    client_id='2ad565600216d25d9cde',
-    client_secret='03b56df2949a520be6049ff98b89813f17b467dc',
-    scope='read',
-    redirect_uri='https://dailymotoauth2test.appspot.com/auth_return',
-    user_agent='oauth2client-sample/1.0',
-    auth_uri='https://api.dailymotion.com/oauth/authorize',
-    token_uri='https://api.dailymotion.com/oauth/token'
-    )
-
-
-class Credentials(db.Model):
-  credentials = CredentialsProperty()
-
-
-class MainHandler(webapp.RequestHandler):
-
-  @login_required
-  def get(self):
-    user = users.get_current_user()
-    credentials = StorageByKeyName(
-        Credentials, user.user_id(), 'credentials').get()
-
-    if credentials is None or credentials.invalid == True:
-      authorize_url = FLOW.step1_get_authorize_url()
-      self.redirect(authorize_url)
-    else:
-      http = httplib2.Http()
-      http = credentials.authorize(http)
-
-      resp, content = http.request('https://api.dailymotion.com/me')
-
-      path = os.path.join(os.path.dirname(__file__), 'welcome.html')
-      logout = users.create_logout_url('/')
-      variables = {
-          'content': content,
-          'logout': logout
-          }
-      self.response.out.write(template.render(path, variables))
-
-
-class OAuthHandler(webapp.RequestHandler):
-
-  @login_required
-  def get(self):
-    user = users.get_current_user()
-    credentials = FLOW.step2_exchange(self.request.params)
-    StorageByKeyName(
-        Credentials, user.user_id(), 'credentials').put(credentials)
-    self.redirect("/")
-
-
-def main():
-  application = webapp.WSGIApplication(
-      [
-      ('/', MainHandler),
-      ('/auth_return', OAuthHandler)
-      ],
-      debug=True)
-  util.run_wsgi_app(application)
-
-
-if __name__ == '__main__':
-  main()
diff --git a/samples/dailymotion/welcome.html b/samples/dailymotion/welcome.html
deleted file mode 100644
index f86902f..0000000
--- a/samples/dailymotion/welcome.html
+++ /dev/null
@@ -1,14 +0,0 @@
-<html>
-  <head>
-    <title>Daily Motion Sample</title>
-    <style type=text/css>
-      td  { vertical-align: top; padding: 0.5em }
-      img { border:0 }
-    </style>
-  </head>
-  <body>
-    <p><a href="{{ logout }}">Logout</a></p>
-    <h2>Response body:</h2>
-    <pre>{{ content }} </pre>
-  </body>
-</html>
diff --git a/samples/django_sample/client_secrets.json b/samples/django_sample/client_secrets.json
new file mode 100644
index 0000000..a232f37
--- /dev/null
+++ b/samples/django_sample/client_secrets.json
@@ -0,0 +1,9 @@
+{
+  "web": {
+    "client_id": "[[INSERT CLIENT ID HERE]]",
+    "client_secret": "[[INSERT CLIENT SECRET HERE]]",
+    "redirect_uris": [],
+    "auth_uri": "https://accounts.google.com/o/oauth2/auth",
+    "token_uri": "https://accounts.google.com/o/oauth2/token"
+  }
+}
diff --git a/samples/django_sample/plus/models.py b/samples/django_sample/plus/models.py
index fd4bf74..890b278 100644
--- a/samples/django_sample/plus/models.py
+++ b/samples/django_sample/plus/models.py
@@ -18,8 +18,4 @@
     pass
 
 
-class FlowAdmin(admin.ModelAdmin):
-    pass
-
-
 admin.site.register(CredentialsModel, CredentialsAdmin)
diff --git a/samples/django_sample/plus/views.py b/samples/django_sample/plus/views.py
index f5d5d18..f273b8d 100644
--- a/samples/django_sample/plus/views.py
+++ b/samples/django_sample/plus/views.py
@@ -2,27 +2,29 @@
 import logging
 import httplib2
 
-from django.http import HttpResponse
-from django.core.urlresolvers import reverse
-from django.contrib.auth.decorators import login_required
-
-from oauth2client.django_orm import Storage
-from oauth2client.client import OAuth2WebServerFlow
-from django_sample.plus.models import CredentialsModel
 from apiclient.discovery import build
-
+from django.contrib.auth.decorators import login_required
+from django.core.urlresolvers import reverse
+from django.http import HttpResponse
+from django.http import HttpResponseBadRequest
 from django.http import HttpResponseRedirect
 from django.shortcuts import render_to_response
+from django_sample.plus.models import CredentialsModel
+from django_sample import settings
+from oauth2client import xsrfutil
+from oauth2client.client import flow_from_clientsecrets
+from oauth2client.django_orm import Storage
 
-STEP2_URI = 'http://localhost:8000/oauth2callback'
+# CLIENT_SECRETS, name of a file containing the OAuth 2.0 information for this
+# application, including client_id and client_secret, which are found
+# on the API Access tab on the Google APIs
+# Console <http://code.google.com/apis/console>
+CLIENT_SECRETS = os.path.join(os.path.dirname(__file__), '..', 'client_secrets.json')
 
-FLOW = OAuth2WebServerFlow(
-    client_id='[[Insert Client ID here.]]',
-    client_secret='[[Insert Client Secret here.]]',
+FLOW = flow_from_clientsecrets(
+    CLIENT_SECRETS,
     scope='https://www.googleapis.com/auth/plus.me',
-    redirect_uri=STEP2_URI,
-    user_agent='plus-django-sample/1.0',
-    )
+    redirect_uri='http://localhost:8000/oauth2callback')
 
 
 @login_required
@@ -30,6 +32,8 @@
   storage = Storage(CredentialsModel, 'id', request.user, 'credential')
   credential = storage.get()
   if credential is None or credential.invalid == True:
+    FLOW.params['state'] = xsrfutil.generate_token(settings.SECRET_KEY,
+                                                   request.user)
     authorize_url = FLOW.step1_get_authorize_url()
     return HttpResponseRedirect(authorize_url)
   else:
@@ -48,6 +52,9 @@
 
 @login_required
 def auth_return(request):
+  if not xsrfutil.validate_token(settings.SECRET_KEY, request.REQUEST['state'],
+                                 request.user):
+    return  HttpResponseBadRequest()
   credential = FLOW.step2_exchange(request.REQUEST)
   storage = Storage(CredentialsModel, 'id', request.user, 'credential')
   storage.put(credential)
diff --git a/tests/test_oauth2client_appengine.py b/tests/test_oauth2client_appengine.py
index 6774abd..1c2d17a 100644
--- a/tests/test_oauth2client_appengine.py
+++ b/tests/test_oauth2client_appengine.py
@@ -25,6 +25,7 @@
 import base64
 import datetime
 import httplib2
+import mox
 import os
 import time
 import unittest
@@ -49,8 +50,10 @@
 from google.appengine.ext import db
 from google.appengine.ext import testbed
 from google.appengine.runtime import apiproxy_errors
+from oauth2client import appengine
 from oauth2client.anyjson import simplejson
 from oauth2client.clientsecrets import _loadfile
+from oauth2client.clientsecrets import InvalidClientSecretsError
 from oauth2client.appengine import AppAssertionCredentials
 from oauth2client.appengine import CredentialsModel
 from oauth2client.appengine import FlowProperty
@@ -276,15 +279,18 @@
     self.assertEqual(None, credentials)
     self.assertEqual(None, memcache.get('foo'))
 
+
 class MockRequest(object):
   url = 'https://example.org'
 
   def relative_url(self, rel):
     return self.url + rel
 
+
 class MockRequestHandler(object):
   request = MockRequest()
 
+
 class DecoratorTests(unittest.TestCase):
 
   def setUp(self):
@@ -343,18 +349,28 @@
     self.assertEqual('http://localhost/oauth2callback', q['redirect_uri'][0])
     self.assertEqual('foo_client_id', q['client_id'][0])
     self.assertEqual('foo_scope bar_scope', q['scope'][0])
-    self.assertEqual('http://localhost/foo_path', q['state'][0])
+    self.assertEqual('http://localhost/foo_path',
+                     q['state'][0].rsplit(':', 1)[0])
     self.assertEqual('code', q['response_type'][0])
     self.assertEqual(False, self.decorator.has_credentials())
 
+    m = mox.Mox()
+    m.StubOutWithMock(appengine, "_parse_state_value")
+    appengine._parse_state_value('foo_path:xsrfkey123',
+                       mox.IgnoreArg()).AndReturn('foo_path')
+    m.ReplayAll()
+
     # Now simulate the callback to /oauth2callback.
     response = self.app.get('/oauth2callback', {
         'code': 'foo_access_code',
-        'state': 'foo_path',
+        'state': 'foo_path:xsrfkey123',
         })
     self.assertEqual('http://localhost/foo_path', response.headers['Location'])
     self.assertEqual(None, self.decorator.credentials)
 
+    m.UnsetStubs()
+    m.VerifyAll()
+
     # Now requesting the decorated path should work.
     response = self.app.get('/foo_path')
     self.assertEqual('200 OK', response.status)
@@ -380,10 +396,16 @@
     response = self.app.get('/foo_path')
     self.assertTrue(response.status.startswith('302'))
 
+    m = mox.Mox()
+    m.StubOutWithMock(appengine, "_parse_state_value")
+    appengine._parse_state_value('foo_path:xsrfkey123',
+                       mox.IgnoreArg()).AndReturn('foo_path')
+    m.ReplayAll()
+
     # Now simulate the callback to /oauth2callback.
     response = self.app.get('/oauth2callback', {
         'code': 'foo_access_code',
-        'state': 'foo_path',
+        'state': 'foo_path:xsrfkey123',
         })
     self.assertEqual('http://localhost/foo_path', response.headers['Location'])
     self.assertEqual(None, self.decorator.credentials)
@@ -398,6 +420,9 @@
     response = self.app.get('/foo_path')
     self.assertTrue(response.status.startswith('302'))
 
+    m.UnsetStubs()
+    m.VerifyAll()
+
   def test_aware(self):
     # An initial request to an oauth_aware decorated path should not redirect.
     response = self.app.get('/bar_path/2012/01')
@@ -409,18 +434,28 @@
     self.assertEqual('http://localhost/oauth2callback', q['redirect_uri'][0])
     self.assertEqual('foo_client_id', q['client_id'][0])
     self.assertEqual('foo_scope bar_scope', q['scope'][0])
-    self.assertEqual('http://localhost/bar_path/2012/01', q['state'][0])
+    self.assertEqual('http://localhost/bar_path/2012/01',
+                     q['state'][0].rsplit(':', 1)[0])
     self.assertEqual('code', q['response_type'][0])
 
+    m = mox.Mox()
+    m.StubOutWithMock(appengine, "_parse_state_value")
+    appengine._parse_state_value('bar_path:xsrfkey456',
+                       mox.IgnoreArg()).AndReturn('bar_path')
+    m.ReplayAll()
+
     # Now simulate the callback to /oauth2callback.
     url = self.decorator.authorize_url()
     response = self.app.get('/oauth2callback', {
         'code': 'foo_access_code',
-        'state': 'bar_path',
+        'state': 'bar_path:xsrfkey456',
         })
     self.assertEqual('http://localhost/bar_path', response.headers['Location'])
     self.assertEqual(False, self.decorator.has_credentials())
 
+    m.UnsetStubs()
+    m.VerifyAll()
+
     # Now requesting the decorated path will have credentials.
     response = self.app.get('/bar_path/2012/01')
     self.assertEqual('200 OK', response.status)
@@ -431,7 +466,6 @@
     self.assertEqual('foo_access_token',
                      self.decorator.credentials.access_token)
 
-
   def test_error_in_step2(self):
     # An initial request to an oauth_aware decorated path should not redirect.
     response = self.app.get('/bar_path/2012/01')
@@ -509,33 +543,77 @@
 
   def test_decorator_from_unfilled_client_secrets_required(self):
     MESSAGE = 'File is missing'
-    decorator = oauth2decorator_from_clientsecrets(
-        datafile('unfilled_client_secrets.json'),
-        scope=['foo_scope', 'bar_scope'], message=MESSAGE)
-    self._finish_setup(decorator, user_mock=UserNotLoggedInMock)
-    self.assertTrue(decorator._in_error)
-    self.assertEqual(MESSAGE, decorator._message)
-
-    # An initial request to an oauth_required decorated path should be an
-    # error message.
-    response = self.app.get('/foo_path')
-    self.assertTrue(response.status.startswith('200'))
-    self.assertTrue(MESSAGE in str(response))
+    try:
+      decorator = oauth2decorator_from_clientsecrets(
+          datafile('unfilled_client_secrets.json'),
+          scope=['foo_scope', 'bar_scope'], message=MESSAGE)
+    except InvalidClientSecretsError:
+      pass
 
   def test_decorator_from_unfilled_client_secrets_aware(self):
     MESSAGE = 'File is missing'
-    decorator = oauth2decorator_from_clientsecrets(
-        datafile('unfilled_client_secrets.json'),
-        scope=['foo_scope', 'bar_scope'], message=MESSAGE)
-    self._finish_setup(decorator, user_mock=UserNotLoggedInMock)
-    self.assertTrue(decorator._in_error)
-    self.assertEqual(MESSAGE, decorator._message)
+    try:
+      decorator = oauth2decorator_from_clientsecrets(
+          datafile('unfilled_client_secrets.json'),
+          scope=['foo_scope', 'bar_scope'], message=MESSAGE)
+    except InvalidClientSecretsError:
+      pass
 
-    # An initial request to an oauth_aware decorated path should be an
-    # error message.
-    response = self.app.get('/bar_path/2012/03')
-    self.assertTrue(response.status.startswith('200'))
-    self.assertTrue(MESSAGE in str(response))
+
+class DecoratorXsrfSecretTests(unittest.TestCase):
+  """Test xsrf_secret_key."""
+
+  def setUp(self):
+    self.testbed = testbed.Testbed()
+    self.testbed.activate()
+    self.testbed.init_datastore_v3_stub()
+    self.testbed.init_memcache_stub()
+
+  def tearDown(self):
+    self.testbed.deactivate()
+
+  def test_build_and_parse_state(self):
+    secret = appengine.xsrf_secret_key()
+
+    # Secret shouldn't change from call to call.
+    secret2 = appengine.xsrf_secret_key()
+    self.assertEqual(secret, secret2)
+
+    # Secret shouldn't change if memcache goes away.
+    memcache.delete(appengine.XSRF_MEMCACHE_ID,
+                             namespace=appengine.OAUTH2CLIENT_NAMESPACE)
+    secret3 = appengine.xsrf_secret_key()
+    self.assertEqual(secret2, secret3)
+
+    # Secret should change if both memcache and the model goes away.
+    memcache.delete(appengine.XSRF_MEMCACHE_ID,
+                             namespace=appengine.OAUTH2CLIENT_NAMESPACE)
+    model = appengine.SiteXsrfSecretKey.get_or_insert('site')
+    model.delete()
+
+    secret4 = appengine.xsrf_secret_key()
+    self.assertNotEqual(secret3, secret4)
+
+
+class DecoratorXsrfProtectionTests(unittest.TestCase):
+  """Test _build_state_value and _parse_state_value."""
+
+  def setUp(self):
+    self.testbed = testbed.Testbed()
+    self.testbed.activate()
+    self.testbed.init_datastore_v3_stub()
+    self.testbed.init_memcache_stub()
+
+  def tearDown(self):
+    self.testbed.deactivate()
+
+  def test_build_and_parse_state(self):
+    state = appengine._build_state_value(MockRequestHandler(), UserMock())
+    self.assertEqual(
+        'https://example.org',
+        appengine._parse_state_value(state, UserMock()))
+    self.assertRaises(appengine.InvalidXsrfTokenError,
+                      appengine._parse_state_value, state[1:], UserMock())
 
 
 if __name__ == '__main__':
diff --git a/tests/test_oauth2client_xsrfutil.py b/tests/test_oauth2client_xsrfutil.py
new file mode 100644
index 0000000..a86a15b
--- /dev/null
+++ b/tests/test_oauth2client_xsrfutil.py
@@ -0,0 +1,111 @@
+# Copyright 2012 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.
+"""Tests for oauth2client.xsrfutil.
+
+Unit tests for oauth2client.xsrfutil.
+"""
+
+__author__ = 'jcgregorio@google.com (Joe Gregorio)'
+
+import unittest
+
+from oauth2client import xsrfutil
+
+# Jan 17 2008, 5:40PM
+TEST_KEY = 'test key'
+TEST_TIME = 1200609642081230
+TEST_USER_ID_1 = 123832983
+TEST_USER_ID_2 = 938297432
+TEST_ACTION_ID_1 = 'some_action'
+TEST_ACTION_ID_2 = 'some_other_action'
+TEST_EXTRA_INFO_1 = 'extra_info_1'
+TEST_EXTRA_INFO_2 = 'more_extra_info'
+
+
+class XsrfUtilTests(unittest.TestCase):
+  """Test xsrfutil functions."""
+
+  def testGenerateAndValidateToken(self):
+    """Test generating and validating a token."""
+    token = xsrfutil.generate_token(TEST_KEY,
+                                   TEST_USER_ID_1,
+                                   action_id=TEST_ACTION_ID_1,
+                                   when=TEST_TIME)
+
+    # Check that the token is considered valid when it should be.
+    self.assertTrue(xsrfutil.validate_token(TEST_KEY,
+                                           token,
+                                           TEST_USER_ID_1,
+                                           action_id=TEST_ACTION_ID_1,
+                                           current_time=TEST_TIME))
+
+    # Should still be valid 15 minutes later.
+    later15mins = TEST_TIME + 15*60
+    self.assertTrue(xsrfutil.validate_token(TEST_KEY,
+                                           token,
+                                           TEST_USER_ID_1,
+                                           action_id=TEST_ACTION_ID_1,
+                                           current_time=later15mins))
+
+    # But not if beyond the timeout.
+    later2hours = TEST_TIME + 2*60*60
+    self.assertFalse(xsrfutil.validate_token(TEST_KEY,
+                                            token,
+                                            TEST_USER_ID_1,
+                                            action_id=TEST_ACTION_ID_1,
+                                            current_time=later2hours))
+
+    # Or if the key is different.
+    self.assertFalse(xsrfutil.validate_token('another key',
+                                            token,
+                                            TEST_USER_ID_1,
+                                            action_id=TEST_ACTION_ID_1,
+                                            current_time=later15mins))
+
+    # Or the user ID....
+    self.assertFalse(xsrfutil.validate_token(TEST_KEY,
+                                            token,
+                                            TEST_USER_ID_2,
+                                            action_id=TEST_ACTION_ID_1,
+                                            current_time=later15mins))
+
+    # Or the action ID...
+    self.assertFalse(xsrfutil.validate_token(TEST_KEY,
+                                            token,
+                                            TEST_USER_ID_1,
+                                            action_id=TEST_ACTION_ID_2,
+                                            current_time=later15mins))
+
+    # Invalid when truncated
+    self.assertFalse(xsrfutil.validate_token(TEST_KEY,
+                                            token[:-1],
+                                            TEST_USER_ID_1,
+                                            action_id=TEST_ACTION_ID_1,
+                                            current_time=later15mins))
+
+    # Invalid with extra garbage
+    self.assertFalse(xsrfutil.validate_token(TEST_KEY,
+                                            token + 'x',
+                                            TEST_USER_ID_1,
+                                            action_id=TEST_ACTION_ID_1,
+                                            current_time=later15mins))
+
+    # Invalid with token of None
+    self.assertFalse(xsrfutil.validate_token(TEST_KEY,
+                                             None,
+                                             TEST_USER_ID_1,
+                                             action_id=TEST_ACTION_ID_1))
+
+if __name__ == '__main__':
+  unittest.main()