comparison MoinMoin/action/serveopenid.py @ 3144:7aba52041f56

add OpenID provider code
author Johannes Berg <johannes AT sipsolutions DOT net>
date Wed, 27 Feb 2008 16:12:06 +0100
parents
children e1fe8dd52b83
comparison
equal deleted inserted replaced
3143:16ae95df840a 3144:7aba52041f56
1 # -*- coding: utf-8 -*-
2 """
3 MoinMoin - OpenID server action
4
5 This is the UI and provider for OpenID.
6
7 @copyright: 2006, 2007, 2008 Johannes Berg <johannes@sipsolutions.net>
8 @license: GNU GPL, see COPYING for details.
9 """
10
11 from MoinMoin.util.moinoid import MoinOpenIDStore, strbase64
12 from MoinMoin import wikiutil
13 from openid.consumer.discover import (OPENID_1_0_TYPE,
14 OPENID_1_1_TYPE, OPENID_2_0_TYPE, OPENID_IDP_2_0_TYPE)
15 from openid import sreg
16 from openid import server
17 from openid.cryptutil import randomString
18 from openid.server import server
19 from openid.message import IDENTIFIER_SELECT
20 from MoinMoin.widget import html
21 from MoinMoin.Page import Page
22 from MoinMoin.request import MoinMoinFinish
23
24 def execute(pagename, request):
25 return MoinOpenIDServer(pagename, request).handle()
26
27 class MoinOpenIDServer:
28 def __init__(self, pagename, request):
29 self.request = request
30 self._ = request.getText
31 self.cfg = request.cfg
32
33 def serveYadisEP(self, endpoint_url):
34 request = self.request
35 hdrs = ['Content-type: application/xrds+xml']
36
37 request.emit_http_headers(hdrs)
38 user_url = request.getQualifiedURL(request.page.url(request, relative=False))
39 self.request.write("""\
40 <?xml version="1.0" encoding="UTF-8"?>
41 <xrds:XRDS
42 xmlns:xrds="xri://$xrds"
43 xmlns="xri://$xrd*($v*2.0)">
44 <XRD>
45
46 <Service priority="0">
47 <Type>%(type10)s</Type>
48 <URI>%(uri)s</URI>
49 <LocalID>%(id)s</LocalID>
50 </Service>
51
52 <Service priority="0">
53 <Type>%(type11)s</Type>
54 <URI>%(uri)s</URI>
55 <LocalID>%(id)s</LocalID>
56 </Service>
57
58 <!-- older version of the spec draft -->
59 <Service priority="0">
60 <Type>http://openid.net/signon/2.0</Type>
61 <URI>%(uri)s</URI>
62 <LocalID>%(id)s</LocalID>
63 </Service>
64
65 <Service priority="0">
66 <Type>%(type20)s</Type>
67 <URI>%(uri)s</URI>
68 <LocalID>%(id)s</LocalID>
69 </Service>
70
71 </XRD>
72 </xrds:XRDS>
73 """ % {
74 'type10': OPENID_1_0_TYPE,
75 'type11': OPENID_1_1_TYPE,
76 'type20': OPENID_2_0_TYPE,
77 'uri': endpoint_url,
78 'id': user_url
79 })
80
81 def serveYadisIDP(self, endpoint_url):
82 request = self.request
83 hdrs = ['Content-type: application/xrds+xml']
84
85 request.emit_http_headers(hdrs)
86 user_url = request.getQualifiedURL(request.page.url(request, relative=False))
87 self.request.write("""\
88 <?xml version="1.0" encoding="UTF-8"?>
89 <xrds:XRDS
90 xmlns:xrds="xri://$xrds"
91 xmlns="xri://$xrd*($v*2.0)">
92 <XRD>
93
94 <Service priority="0">
95 <Type>%(typeidp)s</Type>
96 <URI>%(uri)s</URI>
97 <LocalID>%(id)s</LocalID>
98 </Service>
99
100 </XRD>
101 </xrds:XRDS>
102 """ % {
103 'typeidp': OPENID_IDP_2_0_TYPE,
104 'uri': endpoint_url,
105 'id': user_url
106 })
107
108 def _verify_endpoint_identity(self, identity):
109 """
110 Verify that the given identity matches the current endpoint.
111
112 We always serve out /UserName?action=... for the UserName
113 OpenID and this is pure paranoia to make sure it is that way
114 on incoming data.
115
116 Also verify that the given identity is allowed to have an OpenID.
117 """
118 request = self.request
119 cfg = request.cfg
120
121 # we can very well split on the last slash since usernames
122 # must not contain slashes
123 base, received_name = identity.rsplit('/', 1)
124 check_name = received_name
125
126 if received_name == '':
127 pg = wikiutil.getFrontPage(request)
128 if pg:
129 received_name = pg.page_name
130 check_name = received_name
131 if 'openid.user' in pg.pi:
132 received_name = pg.pi['openid.user']
133
134 # some sanity checking
135 # even if someone goes to http://johannes.sipsolutions.net/
136 # we'll serve out http://johannes.sipsolutions.net/JohannesBerg?action=serveopenid
137 # (if JohannesBerg is set as page_front_page)
138 # For the #OpenIDUser PI, we need to allow the page that includes the PI,
139 # hence use check_name here (see above for how it is assigned)
140 fullidentity = '/'.join([base, check_name])
141 thisurl = request.getQualifiedURL(request.page.url(request, relative=False))
142 if not thisurl == fullidentity:
143 return False
144
145 # again, we never put an openid.server link on this page...
146 # why are they here?
147 if cfg.openid_server_restricted_users_group:
148 request.dicts.addgroup(request, cfg.openid_server_restricted_users_group)
149 if not request.dicts.has_member(cfg.openid_server_restricted_users_group, received_name):
150 return False
151
152 return True
153
154 def handleCheckIDRequest(self, identity, username, openidreq, server_url):
155 if self.user_trusts_url(openidreq.trust_root):
156 return self.approved(identity, openidreq, server_url=server_url)
157
158 if openidreq.immediate:
159 return openidreq.answer(False, identity=identity, server_url=server_url)
160
161 self.request.session['openidserver.request'] = openidreq
162 self.show_decide_page(identity, username, openidreq)
163 return None
164
165 def _make_identity(self):
166 page = wikiutil.getHomePage(self.request)
167 if page:
168 server_url = self.request.getQualifiedURL(
169 page.url(self.request,
170 querystr={'action': 'serveopenid'},
171 relative=False))
172 identity = self.request.getQualifiedURL(page.url(self.request, relative=False))
173 return identity, server_url
174 return None, None
175
176 def handle(self):
177 _ = self._
178 request = self.request
179 form = request.form
180
181 username = request.page.page_name
182 if 'openid.user' in request.page.pi:
183 username = request.page.pi['openid.user']
184
185
186 if not request.cfg.openid_server_enabled:
187 # since we didn't put any openid.server into
188 # the page to start with, this is someone trying
189 # to abuse us. No need to give a nice error
190 request.makeForbidden403()
191 return
192
193 server_url = request.getQualifiedURL(
194 request.page.url(request,
195 querystr={'action':'serveopenid'},
196 relative=False))
197
198 yadis_type = form.get('yadis', [None])[0]
199 if yadis_type == 'ep':
200 return self.serveYadisEP(server_url)
201 elif yadis_type == 'idp':
202 return self.serveYadisIDP(server_url)
203
204 # if the identity is set it must match the server URL
205 # sort of arbitrary, but we have to have some restriction
206 identity = form.get('openid.identity', [None])[0]
207 if identity == IDENTIFIER_SELECT:
208 identity, server_url = self._make_identity()
209 if not identity:
210 return self._sorry_no_identity()
211 username = request.user.name
212 elif identity is not None:
213 if not self._verify_endpoint_identity(identity):
214 request.makeForbidden403()
215 request.write('verification failed')
216 return
217
218 if 'openid.user' in request.page.pi:
219 username = request.page.pi['openid.user']
220
221 store = MoinOpenIDStore(request)
222 openidsrv = server.Server(store, op_endpoint=server_url)
223
224 answer = None
225 if form.has_key('dontapprove'):
226 answer = self.handle_response(False, username, identity)
227 if answer is None:
228 return
229 elif form.has_key('approve'):
230 answer = self.handle_response(True, username, identity)
231 if answer is None:
232 return
233 else:
234 query = {}
235 for key in form.keys():
236 query[key] = form[key][0]
237 try:
238 openidreq = openidsrv.decodeRequest(query)
239 except Exception, e:
240 request.makeForbidden(403, 'OpenID decode error: %r' % e)
241 return
242
243 if openidreq is None:
244 request.makeForbidden403()
245 request.write('no request')
246 return
247
248 if request.user.valid and username != request.user.name:
249 answer = openidreq.answer(False, identity=identity, server_url=server_url)
250 elif openidreq.mode in ["checkid_immediate", "checkid_setup"]:
251 answer = self.handleCheckIDRequest(identity, username, openidreq, server_url)
252 if answer is None:
253 return
254 else:
255 answer = openidsrv.handleRequest(openidreq)
256 webanswer = openidsrv.encodeResponse(answer)
257 headers = ['Status: %d OpenID status' % webanswer.code]
258 for hdr in webanswer.headers:
259 headers += [hdr+': '+webanswer.headers[hdr]]
260 request.emit_http_headers(headers)
261 request.write(webanswer.body)
262 raise MoinMoinFinish
263
264 def handle_response(self, positive, username, identity):
265 request = self.request
266 form = request.form
267
268 # check form submission nonce, use None for stored value default
269 # since it cannot be sent from the user
270 session_nonce = self.request.session.get('openidserver.nonce')
271 if session_nonce is not None:
272 del self.request.session['openidserver.nonce']
273 # use empty string if nothing was sent
274 form_nonce = form.get('nonce', [''])[0]
275 if session_nonce != form_nonce:
276 self.request.makeForbidden403()
277 self.request.write('invalid nonce')
278 return None
279
280 openidreq = request.session.get('openidserver.request')
281 if not openidreq:
282 request.makeForbidden403()
283 request.write('no response request')
284 return None
285 del request.session['openidserver.request']
286
287 if (not positive or
288 not request.user.valid or
289 request.user.name != username):
290 return openidreq.answer(False)
291
292
293 if form.get('remember', ['no'])[0] == 'yes':
294 if not hasattr(request.user, 'openid_trusted_roots'):
295 request.user.openid_trusted_roots = []
296 request.user.openid_trusted_roots.append(strbase64(openidreq.trust_root))
297 request.user.save()
298 dummyidentity, server_url = self._make_identity()
299 return self.approved(identity, openidreq, server_url=server_url)
300
301 def approved(self, identity, openidreq, data=False, server_url=None):
302 reply = openidreq.answer(True, identity=identity, server_url=server_url)
303 if data:
304 # TODO
305 sreg_data = { }
306 sreq_req = sreg.SRegRequest.fromOpenIDRequest(openidreq.message)
307 sreg_resp = sreg.SRegResponse.extractResponse(openidreq, sreg_data)
308 sreg_resp.addToOpenIDResponse(reply.fields)
309 return reply
310
311 def user_trusts_url(self, trustroot):
312 user = self.request.user
313 if hasattr(user, 'openid_trusted_roots'):
314 return strbase64(trustroot) in user.openid_trusted_roots
315 return False
316
317 def show_decide_page(self, identity, username, openidreq):
318 request = self.request
319 _ = self._
320
321 if not request.user.valid or username != request.user.name:
322 request.makeForbidden(403, _('''You need to manually go to your OpenID provider wiki
323 and log in before you can use your OpenID. MoinMoin will
324 never allow you to enter your password here.
325
326 Once you have logged in, simply reload this page.''', formatted=False))
327 return
328
329 request.emit_http_headers()
330 request.theme.send_title(_("OpenID Trust verification"), pagename=request.page.page_name)
331 # Start content (important for RTL support)
332 request.write(request.formatter.startContent("content"))
333
334 request.write(request.formatter.paragraph(1))
335 request.write(_('The site %s has asked for your identity.') % openidreq.trust_root)
336 request.write(request.formatter.paragraph(0))
337 request.write(request.formatter.paragraph(1))
338 request.write(_('''
339 If you approve, the site represented by the trust root below will be
340 told that you control the identity URL %s. (If you are using a delegated
341 identity, the site will take care of reversing the
342 delegation on its own.)''') % openidreq.identity)
343 request.write(request.formatter.paragraph(0))
344
345 form = html.FORM(method='POST', action=request.page.url(request))
346 form.append(html.INPUT(type='hidden', name='action', value='serveopenid'))
347 form.append(html.INPUT(type='hidden', name='openid.identity', value=openidreq.identity))
348 form.append(html.INPUT(type='hidden', name='openid.return_to', value=openidreq.return_to))
349 form.append(html.INPUT(type='hidden', name='openid.trust_root', value=openidreq.trust_root))
350 form.append(html.INPUT(type='hidden', name='openid.mode', value=openidreq.mode))
351 form.append(html.INPUT(type='hidden', name='name', value=username))
352
353 nonce = randomString(32, 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789')
354 form.append(html.INPUT(type='hidden', name='nonce', value=nonce))
355 request.session['openidserver.nonce'] = nonce
356
357 table = html.TABLE()
358 form.append(table)
359
360 tr = html.TR()
361 table.append(tr)
362 tr.append(html.TD().append(html.STRONG().append(html.Text(_('Trust root')))))
363 tr.append(html.TD().append(html.Text(openidreq.trust_root)))
364
365 tr = html.TR()
366 table.append(tr)
367 tr.append(html.TD().append(html.STRONG().append(html.Text(_('Identity URL')))))
368 tr.append(html.TD().append(html.Text(identity)))
369
370 tr = html.TR()
371 table.append(tr)
372 tr.append(html.TD().append(html.STRONG().append(html.Text(_('Name')))))
373 tr.append(html.TD().append(html.Text(username)))
374
375 tr = html.TR()
376 table.append(tr)
377 tr.append(html.TD().append(html.STRONG().append(html.Text(_('Remember decision')))))
378 td = html.TD()
379 tr.append(td)
380 td.append(html.INPUT(type='checkbox', name='remember', value='yes'))
381 td.append(html.Text(_('Remember this trust decision and don\'t ask again')))
382
383 tr = html.TR()
384 table.append(tr)
385 tr.append(html.TD())
386 td = html.TD()
387 tr.append(td)
388
389 td.append(html.INPUT(type='submit', name='approve', value=_("Approve")))
390 td.append(html.INPUT(type='submit', name='dontapprove', value=_("Don't approve")))
391
392 request.write(unicode(form))
393
394 request.write(request.formatter.endContent())
395 request.theme.send_footer(request.page.page_name)
396 request.theme.send_closing_html()
397
398 def _sorry_no_identity(self):
399 request = self.request
400 _ = self._
401
402 request.emit_http_headers()
403 request.theme.send_title(_("OpenID not served"), pagename=request.page.page_name)
404 # Start content (important for RTL support)
405 request.write(request.formatter.startContent("content"))
406
407 request.write(request.formatter.paragraph(1))
408 request.write(_('''
409 Unfortunately you have not created your homepage yet. Therefore,
410 we cannot serve an OpenID for you. Please create your homepage first
411 and then reload this page or click the button below to cancel this
412 verification.'''))
413 request.write(request.formatter.paragraph(0))
414
415 form = html.FORM(method='POST', action=request.page.url(request))
416 form.append(html.INPUT(type='hidden', name='action', value='serveopenid'))
417
418 nonce = randomString(32, 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789')
419 form.append(html.INPUT(type='hidden', name='nonce', value=nonce))
420 request.session['openidserver.nonce'] = nonce
421
422 form.append(html.INPUT(type='submit', name='dontapprove', value=_("Cancel")))
423
424 request.write(unicode(form))
425
426 request.write(request.formatter.endContent())
427 request.theme.send_footer(request.page.page_name)
428 request.theme.send_closing_html()