- Update 'webapp' library version
[gae-samples.git] / openid-provider / provider.py
blobfa2a5ce73e465fe7fe36fef4e35b5772e9e572cf
1 #!/usr/bin/python
3 # Copyright 2006 Google Inc.
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
9 # http://www.apache.org/licenses/LICENSE-2.0
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
17 """
18 An OpenID Provider. Allows Google users to log into OpenID servers using
19 their Google Account.
21 Part of http://code.google.com/p/google-app-engine-samples/.
23 For more about OpenID, see:
24 http://openid.net/
25 http://openid.net/about.bml
27 Uses JanRain's Python OpenID library, version 1.2.0, licensed under the
28 Apache Software License 2.0:
29 http://openidenabled.com/python-openid/
31 It uses version 1.2.0 (included here), not a later version, because this app
32 was originally written a long time ago when 1.2.0 was the latest version
33 available. Porting to 2.1.1 or later should be straightforward.
35 The JanRain library includes a reference OpenID consumer that can be used to
36 test this provider. After starting the dev_appserver with this app, unpack the
37 JanRain library and run these commands from its root directory:
39 setenv PYTHONPATH .
40 python ./examples/consumer.py -s localhost
42 Then go to http://localhost:8001/ in your browser, type in
43 http://localhost:8080/myname as your openid identifier, and click Verify.
44 """
46 import cgi
47 import Cookie
48 import datetime
49 import logging
50 import os
51 import pickle
52 import pprint
53 import sys
54 import traceback
55 import urlparse
56 import wsgiref.handlers
58 from google.appengine.api import datastore
59 from google.appengine.api import users
60 from google.appengine.ext import webapp
61 from google.appengine.ext.webapp import template
63 from openid.server import server as OpenIDServer
64 import store
67 # Set to True if stack traces should be shown in the browser, etc.
68 _DEBUG = False
70 # the global openid server instance
71 oidserver = None
73 def InitializeOpenId():
74 global oidserver
75 oidserver = OpenIDServer.Server(store.DatastoreStore())
78 class Handler(webapp.RequestHandler):
79 """A base handler class with a couple OpenID-specific utilities."""
81 def ArgsToDict(self):
82 """Converts the URL and POST parameters to a singly-valued dictionary.
84 Returns:
85 dict with the URL and POST body parameters
86 """
87 req = self.request
88 return dict([(arg, req.get(arg)) for arg in req.arguments()])
90 def HasCookie(self):
91 """Returns True if we "remember" the user, False otherwise.
93 Determines whether the user has used OpenID before and asked us to
94 remember them - ie, if the user agent provided an 'openid_remembered'
95 cookie.
97 Returns:
98 True if we remember the user, False otherwise.
99 """
100 cookies = os.environ.get('HTTP_COOKIE', None)
101 if cookies:
102 morsel = Cookie.BaseCookie(cookies).get('openid_remembered')
103 if morsel and morsel.value == 'yes':
104 return True
106 return False
108 def GetOpenIdRequest(self):
109 """Creates and OpenIDRequest for this request, if appropriate.
111 If this request is not an OpenID request, returns None. If an error occurs
112 while parsing the arguments, returns False and shows the error page.
114 Return:
115 An OpenIDRequest, if this user request is an OpenID request. Otherwise
116 False.
118 try:
119 oidrequest = oidserver.decodeRequest(self.ArgsToDict())
120 logging.debug('OpenID request: %s' % oidrequest)
121 return oidrequest
122 except:
123 trace = ''.join(traceback.format_exception(*sys.exc_info()))
124 self.ReportError('Error parsing OpenID request:\n%s' % trace)
125 return False
127 def Respond(self, oidresponse):
128 """Send an OpenID response.
130 Args:
131 oidresponse: OpenIDResponse
132 The response to send, usually created by OpenIDRequest.answer().
134 logging.warning('Respond: oidresponse.request.mode ' + oidresponse.request.mode)
136 if oidresponse.request.mode in ['checkid_immediate', 'checkid_setup']:
137 user = users.get_current_user()
138 if user:
139 # add nickname, using the Simple Registration Extension:
140 # http://www.openidenabled.com/openid/simple-registration-extension/
141 oidresponse.addField('sreg', 'nickname', user.nickname())
143 logging.debug('Using response: %s' % oidresponse)
144 encoded_response = oidserver.encodeResponse(oidresponse)
146 # update() would be nice, but wsgiref.headers.Headers doesn't implement it
147 for header, value in encoded_response.headers.items():
148 self.response.headers[header] = str(value)
150 if encoded_response.code in (301, 302):
151 self.redirect(self.response.headers['location'])
152 else:
153 self.response.set_status(encoded_response.code)
155 if encoded_response.body:
156 logging.debug('Sending response body: %s' % encoded_response.body)
157 self.response.out.write(encoded_response.body)
158 else:
159 self.response.out.write('')
161 def Render(self, template_name, extra_values={}):
162 """Render the given template, including the extra (optional) values.
164 Args:
165 template_name: string
166 The template to render.
168 extra_values: dict
169 Template values to provide to the template.
171 parsed = urlparse.urlparse(self.request.uri)
172 request_url_without_path = parsed[0] + '://' + parsed[1]
173 request_url_without_params = request_url_without_path + parsed[2]
175 values = {
176 'request': self.request,
177 'request_url_without_path': request_url_without_path,
178 'request_url_without_params': request_url_without_params,
179 'user': users.get_current_user(),
180 'login_url': users.create_login_url(self.request.uri),
181 'logout_url': users.create_logout_url('/'),
182 'debug': self.request.get('deb'),
184 values.update(extra_values)
185 cwd = os.path.dirname(__file__)
186 path = os.path.join(cwd, 'templates', template_name + '.html')
187 logging.debug(path)
188 self.response.out.write(template.render(path, values, debug=_DEBUG))
190 def ReportError(self, message):
191 """Shows an error HTML page.
193 Args:
194 message: string
195 A detailed error message.
197 args = pprint.pformat(self.ArgsToDict())
198 self.Render('error', vars())
199 logging.error(message)
201 def store_login(self, oidrequest, kind):
202 """Stores the details of an OpenID login in the datastore.
204 Args:
205 oidrequest: OpenIDRequest
207 kind: string
208 'remembered', 'confirmed', or 'declined'
210 assert kind in ['remembered', 'confirmed', 'declined']
211 user = users.get_current_user()
212 assert user
214 login = datastore.Entity('Login')
215 login['relying_party'] = oidrequest.trust_root
216 login['time'] = datetime.datetime.now()
217 login['kind'] = kind
218 login['user'] = user
219 datastore.Put(login)
221 def CheckUser(self):
222 """Checks that the OpenID identity being asserted is owned by this user.
224 Specifically, checks that the request URI's path is the user's nickname.
226 Returns:
227 True if the request's path is the user's nickname. Otherwise, False, and
228 prints an error page.
230 args = self.ArgsToDict()
232 user = users.get_current_user()
233 if not user:
234 # not logged in!
235 return False
237 # check that the user is logging into their page, not someone else's.
238 identity = args['openid.identity']
239 parsed = urlparse.urlparse(identity)
240 path = parsed[2]
242 if path[1:] != user.nickname():
243 expected = parsed[0] + '://' + parsed[1] + '/' + user.nickname()
244 logging.warning('Bad identity URL %s for user %s; expected %s' %
245 (identity, user.nickname(), expected))
246 return False
248 logging.debug('User %s matched identity %s' % (user.nickname(), identity))
249 return True
251 def ShowFrontPage(self):
252 """Do an internal (non-302) redirect to the front page.
254 Preserves the user agent's requested URL.
256 front_page = FrontPage()
257 front_page.request = self.request
258 front_page.response = self.response
259 front_page.get()
262 class FrontPage(Handler):
263 """Show the default OpenID page, with the last 10 logins for this user."""
264 def get(self):
265 logins = []
267 user = users.get_current_user()
268 if user:
269 query = datastore.Query('Login')
270 query['user ='] = user
271 query.Order(('time', datastore.Query.DESCENDING))
272 logins = query.Get(10)
274 self.Render('index', vars())
277 class Login(Handler):
278 """Handles OpenID requests: associate, checkid_setup, checkid_immediate."""
280 def get(self):
281 """Handles GET requests."""
282 user = users.get_current_user()
283 if user:
284 logging.debug('User: %s' % user)
286 login_url = users.create_login_url(self.request.uri)
287 logout_url = users.create_logout_url(self.request.uri)
289 oidrequest = self.GetOpenIdRequest()
290 if oidrequest is False:
291 # there was an error, and GetOpenIdRequest displayed it. bail out.
292 return
293 elif oidrequest is None:
294 # this is a request from a browser
295 self.ShowFrontPage()
296 elif oidrequest.mode in ['checkid_immediate', 'checkid_setup']:
297 if self.HasCookie() and user:
298 logging.debug('Has cookie, confirming identity to ' +
299 oidrequest.trust_root)
300 self.store_login(oidrequest, 'remembered')
301 self.Respond(oidrequest.answer(True))
302 elif oidrequest.immediate:
303 self.store_login(oidrequest, 'declined')
304 oidresponse = oidrequest.answer(False, server_url=login_url)
305 self.Respond(oidresponse)
306 else:
307 if self.CheckUser():
308 self.Render('prompt', vars())
309 else:
310 self.ShowFrontPage()
312 elif oidrequest.mode in ['associate', 'check_authentication']:
313 self.Respond(oidserver.handleRequest(oidrequest))
315 else:
316 self.ReportError('Unknown mode: %s' % oidrequest.mode)
318 post = get
320 def prompt(self):
321 """Ask the user to confirm an OpenID login request."""
322 oidrequest = self.GetOpenIdRequest()
323 if oidrequest:
324 self.response.out.write(page)
327 class FinishLogin(Handler):
328 """Handle a POST response to the OpenID login prompt form."""
329 def post(self):
330 if not self.CheckUser():
331 self.ShowFrontPage()
332 return
334 args = self.ArgsToDict()
336 try:
337 oidrequest = OpenIDServer.CheckIDRequest.fromQuery(args)
338 except:
339 trace = ''.join(traceback.format_exception(*sys.exc_info()))
340 self.ReportError('Error decoding login request:\n%s' % trace)
341 return
343 if args.has_key('yes'):
344 logging.debug('Confirming identity to %s' % oidrequest.trust_root)
345 if args.get('remember', '') == 'yes':
346 logging.info('Setting cookie to remember openid login for two weeks')
348 expires = datetime.datetime.now() + datetime.timedelta(weeks=2)
349 expires_rfc822 = expires.strftime('%a, %d %b %Y %H:%M:%S +0000')
350 self.response.headers.add_header(
351 'Set-Cookie', 'openid_remembered=yes; expires=%s' % expires_rfc822)
353 self.store_login(oidrequest, 'confirmed')
354 self.Respond(oidrequest.answer(True))
356 elif args.has_key('no'):
357 logging.debug('Login denied, sending cancel to %s' %
358 oidrequest.trust_root)
359 self.store_login(oidrequest, 'declined')
360 return self.Respond(oidrequest.answer(False))
362 else:
363 self.ReportError('Bad login request.')
366 # Map URLs to our RequestHandler classes above
367 _URLS = [
368 ('/', FrontPage),
369 ('/login', FinishLogin),
370 ('/[^/]+', Login),
373 def main(argv):
374 application = webapp.WSGIApplication(_URLS, debug=_DEBUG)
375 InitializeOpenId()
376 wsgiref.handlers.CGIHandler().run(application)
378 if __name__ == '__main__':
379 main(sys.argv)