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.
18 An OpenID Provider. Allows Google users to log into OpenID servers using
21 Part of http://code.google.com/p/google-app-engine-samples/.
23 For more about OpenID, see:
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:
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.
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
67 # Set to True if stack traces should be shown in the browser, etc.
70 # the global openid server instance
73 def InitializeOpenId():
75 oidserver
= OpenIDServer
.Server(store
.DatastoreStore())
78 class Handler(webapp
.RequestHandler
):
79 """A base handler class with a couple OpenID-specific utilities."""
82 """Converts the URL and POST parameters to a singly-valued dictionary.
85 dict with the URL and POST body parameters
88 return dict([(arg
, req
.get(arg
)) for arg
in req
.arguments()])
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'
98 True if we remember the user, False otherwise.
100 cookies
= os
.environ
.get('HTTP_COOKIE', None)
102 morsel
= Cookie
.BaseCookie(cookies
).get('openid_remembered')
103 if morsel
and morsel
.value
== 'yes':
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.
115 An OpenIDRequest, if this user request is an OpenID request. Otherwise
119 oidrequest
= oidserver
.decodeRequest(self
.ArgsToDict())
120 logging
.debug('OpenID request: %s' % oidrequest
)
123 trace
= ''.join(traceback
.format_exception(*sys
.exc_info()))
124 self
.ReportError('Error parsing OpenID request:\n%s' % trace
)
127 def Respond(self
, oidresponse
):
128 """Send an OpenID response.
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()
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'])
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
)
159 self
.response
.out
.write('')
161 def Render(self
, template_name
, extra_values
={}):
162 """Render the given template, including the extra (optional) values.
165 template_name: string
166 The template to render.
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]
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')
188 self
.response
.out
.write(template
.render(path
, values
, debug
=_DEBUG
))
190 def ReportError(self
, message
):
191 """Shows an error HTML page.
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.
205 oidrequest: OpenIDRequest
208 'remembered', 'confirmed', or 'declined'
210 assert kind
in ['remembered', 'confirmed', 'declined']
211 user
= users
.get_current_user()
214 login
= datastore
.Entity('Login')
215 login
['relying_party'] = oidrequest
.trust_root
216 login
['time'] = datetime
.datetime
.now()
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.
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()
237 # check that the user is logging into their page, not someone else's.
238 identity
= args
['openid.identity']
239 parsed
= urlparse
.urlparse(identity
)
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
))
248 logging
.debug('User %s matched identity %s' % (user
.nickname(), identity
))
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
262 class FrontPage(Handler
):
263 """Show the default OpenID page, with the last 10 logins for this user."""
267 user
= users
.get_current_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."""
281 """Handles GET requests."""
282 user
= users
.get_current_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.
293 elif oidrequest
is None:
294 # this is a request from a browser
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
)
308 self
.Render('prompt', vars())
312 elif oidrequest
.mode
in ['associate', 'check_authentication']:
313 self
.Respond(oidserver
.handleRequest(oidrequest
))
316 self
.ReportError('Unknown mode: %s' % oidrequest
.mode
)
321 """Ask the user to confirm an OpenID login request."""
322 oidrequest
= self
.GetOpenIdRequest()
324 self
.response
.out
.write(page
)
327 class FinishLogin(Handler
):
328 """Handle a POST response to the OpenID login prompt form."""
330 if not self
.CheckUser():
334 args
= self
.ArgsToDict()
337 oidrequest
= OpenIDServer
.CheckIDRequest
.fromQuery(args
)
339 trace
= ''.join(traceback
.format_exception(*sys
.exc_info()))
340 self
.ReportError('Error decoding login request:\n%s' % trace
)
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))
363 self
.ReportError('Bad login request.')
366 # Map URLs to our RequestHandler classes above
369 ('/login', FinishLogin
),
374 application
= webapp
.WSGIApplication(_URLS
, debug
=_DEBUG
)
376 wsgiref
.handlers
.CGIHandler().run(application
)
378 if __name__
== '__main__':