Mercurial > moin > 1.9
changeset 3144:7aba52041f56
add OpenID provider code
author | Johannes Berg <johannes AT sipsolutions DOT net> |
---|---|
date | Wed, 27 Feb 2008 16:12:06 +0100 |
parents | 16ae95df840a |
children | 550b35179d02 |
files | MoinMoin/Page.py MoinMoin/action/serveopenid.py MoinMoin/config/multiconfig.py |
diffstat | 3 files changed, 469 insertions(+), 0 deletions(-) [+] |
line wrap: on
line diff
--- a/MoinMoin/Page.py Wed Feb 27 15:52:30 2008 +0100 +++ b/MoinMoin/Page.py Wed Feb 27 16:12:06 2008 +0100 @@ -958,6 +958,10 @@ elif verb == "deprecated": pi['deprecated'] = True + elif verb == "openiduser": + if request.cfg.openid_server_enable_user: + pi['openid.user'] = args + elif verb == "pragma": try: key, val = args.split(' ', 1) @@ -1134,10 +1138,42 @@ title = self.split_title() + html_head = '' + openid_username = self.page_name + userid = user.getUserId(request, openid_username) + if userid is None and 'openid.user' in self.pi: + openid_username = self.pi['openid.user'] + userid = user.getUserId(request, openid_username) + + if request.cfg.openid_server_restricted_users_group: + request.dicts.addgroup(request, + request.cfg.openid_server_restricted_users_group) + if request.cfg.openid_server_enabled: + if userid is not None and not request.cfg.openid_server_restricted_users_group or \ + request.dicts.has_member(request.cfg.openid_server_restricted_users_group, openid_username): + html_head = '<link rel="openid2.provider" href="%s">' % \ + wikiutil.escape(request.getQualifiedURL(self.url(request, + querystr={'action': 'serveopenid'}, + relative=False))) + html_head += '<link rel="openid.server" href="%s">' % \ + wikiutil.escape(request.getQualifiedURL(self.url(request, + querystr={'action': 'serveopenid'}, + relative=False))) + html_head += '<meta http-equiv="x-xrds-location" content="%s">' % \ + wikiutil.escape(request.getQualifiedURL(self.url(request, + querystr={'action': 'serveopenid', 'yadis': 'ep'}, + relative=False))) + elif self.page_name == request.cfg.page_front_page: + html_head = '<meta http-equiv="x-xrds-location" content="%s">' % \ + wikiutil.escape(request.getQualifiedURL(self.url(request, + querystr={'action': 'serveopenid', 'yadis': 'idp'}, + relative=False))) + request.theme.send_title(title, page=self, print_mode=print_mode, media=media, pi_refresh=pi.get('refresh'), allow_doubleclick=1, trail=trail, + html_head=html_head, ) # new page?
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MoinMoin/action/serveopenid.py Wed Feb 27 16:12:06 2008 +0100 @@ -0,0 +1,428 @@ +# -*- coding: utf-8 -*- +""" + MoinMoin - OpenID server action + + This is the UI and provider for OpenID. + + @copyright: 2006, 2007, 2008 Johannes Berg <johannes@sipsolutions.net> + @license: GNU GPL, see COPYING for details. +""" + +from MoinMoin.util.moinoid import MoinOpenIDStore, strbase64 +from MoinMoin import wikiutil +from openid.consumer.discover import (OPENID_1_0_TYPE, + OPENID_1_1_TYPE, OPENID_2_0_TYPE, OPENID_IDP_2_0_TYPE) +from openid import sreg +from openid import server +from openid.cryptutil import randomString +from openid.server import server +from openid.message import IDENTIFIER_SELECT +from MoinMoin.widget import html +from MoinMoin.Page import Page +from MoinMoin.request import MoinMoinFinish + +def execute(pagename, request): + return MoinOpenIDServer(pagename, request).handle() + +class MoinOpenIDServer: + def __init__(self, pagename, request): + self.request = request + self._ = request.getText + self.cfg = request.cfg + + def serveYadisEP(self, endpoint_url): + request = self.request + hdrs = ['Content-type: application/xrds+xml'] + + request.emit_http_headers(hdrs) + user_url = request.getQualifiedURL(request.page.url(request, relative=False)) + self.request.write("""\ +<?xml version="1.0" encoding="UTF-8"?> +<xrds:XRDS + xmlns:xrds="xri://$xrds" + xmlns="xri://$xrd*($v*2.0)"> + <XRD> + + <Service priority="0"> + <Type>%(type10)s</Type> + <URI>%(uri)s</URI> + <LocalID>%(id)s</LocalID> + </Service> + + <Service priority="0"> + <Type>%(type11)s</Type> + <URI>%(uri)s</URI> + <LocalID>%(id)s</LocalID> + </Service> + + <!-- older version of the spec draft --> + <Service priority="0"> + <Type>http://openid.net/signon/2.0</Type> + <URI>%(uri)s</URI> + <LocalID>%(id)s</LocalID> + </Service> + + <Service priority="0"> + <Type>%(type20)s</Type> + <URI>%(uri)s</URI> + <LocalID>%(id)s</LocalID> + </Service> + + </XRD> +</xrds:XRDS> +""" % { + 'type10': OPENID_1_0_TYPE, + 'type11': OPENID_1_1_TYPE, + 'type20': OPENID_2_0_TYPE, + 'uri': endpoint_url, + 'id': user_url +}) + + def serveYadisIDP(self, endpoint_url): + request = self.request + hdrs = ['Content-type: application/xrds+xml'] + + request.emit_http_headers(hdrs) + user_url = request.getQualifiedURL(request.page.url(request, relative=False)) + self.request.write("""\ +<?xml version="1.0" encoding="UTF-8"?> +<xrds:XRDS + xmlns:xrds="xri://$xrds" + xmlns="xri://$xrd*($v*2.0)"> + <XRD> + + <Service priority="0"> + <Type>%(typeidp)s</Type> + <URI>%(uri)s</URI> + <LocalID>%(id)s</LocalID> + </Service> + + </XRD> +</xrds:XRDS> +""" % { + 'typeidp': OPENID_IDP_2_0_TYPE, + 'uri': endpoint_url, + 'id': user_url +}) + + def _verify_endpoint_identity(self, identity): + """ + Verify that the given identity matches the current endpoint. + + We always serve out /UserName?action=... for the UserName + OpenID and this is pure paranoia to make sure it is that way + on incoming data. + + Also verify that the given identity is allowed to have an OpenID. + """ + request = self.request + cfg = request.cfg + + # we can very well split on the last slash since usernames + # must not contain slashes + base, received_name = identity.rsplit('/', 1) + check_name = received_name + + if received_name == '': + pg = wikiutil.getFrontPage(request) + if pg: + received_name = pg.page_name + check_name = received_name + if 'openid.user' in pg.pi: + received_name = pg.pi['openid.user'] + + # some sanity checking + # even if someone goes to http://johannes.sipsolutions.net/ + # we'll serve out http://johannes.sipsolutions.net/JohannesBerg?action=serveopenid + # (if JohannesBerg is set as page_front_page) + # For the #OpenIDUser PI, we need to allow the page that includes the PI, + # hence use check_name here (see above for how it is assigned) + fullidentity = '/'.join([base, check_name]) + thisurl = request.getQualifiedURL(request.page.url(request, relative=False)) + if not thisurl == fullidentity: + return False + + # again, we never put an openid.server link on this page... + # why are they here? + if cfg.openid_server_restricted_users_group: + request.dicts.addgroup(request, cfg.openid_server_restricted_users_group) + if not request.dicts.has_member(cfg.openid_server_restricted_users_group, received_name): + return False + + return True + + def handleCheckIDRequest(self, identity, username, openidreq, server_url): + if self.user_trusts_url(openidreq.trust_root): + return self.approved(identity, openidreq, server_url=server_url) + + if openidreq.immediate: + return openidreq.answer(False, identity=identity, server_url=server_url) + + self.request.session['openidserver.request'] = openidreq + self.show_decide_page(identity, username, openidreq) + return None + + def _make_identity(self): + page = wikiutil.getHomePage(self.request) + if page: + server_url = self.request.getQualifiedURL( + page.url(self.request, + querystr={'action': 'serveopenid'}, + relative=False)) + identity = self.request.getQualifiedURL(page.url(self.request, relative=False)) + return identity, server_url + return None, None + + def handle(self): + _ = self._ + request = self.request + form = request.form + + username = request.page.page_name + if 'openid.user' in request.page.pi: + username = request.page.pi['openid.user'] + + + if not request.cfg.openid_server_enabled: + # since we didn't put any openid.server into + # the page to start with, this is someone trying + # to abuse us. No need to give a nice error + request.makeForbidden403() + return + + server_url = request.getQualifiedURL( + request.page.url(request, + querystr={'action':'serveopenid'}, + relative=False)) + + yadis_type = form.get('yadis', [None])[0] + if yadis_type == 'ep': + return self.serveYadisEP(server_url) + elif yadis_type == 'idp': + return self.serveYadisIDP(server_url) + + # if the identity is set it must match the server URL + # sort of arbitrary, but we have to have some restriction + identity = form.get('openid.identity', [None])[0] + if identity == IDENTIFIER_SELECT: + identity, server_url = self._make_identity() + if not identity: + return self._sorry_no_identity() + username = request.user.name + elif identity is not None: + if not self._verify_endpoint_identity(identity): + request.makeForbidden403() + request.write('verification failed') + return + + if 'openid.user' in request.page.pi: + username = request.page.pi['openid.user'] + + store = MoinOpenIDStore(request) + openidsrv = server.Server(store, op_endpoint=server_url) + + answer = None + if form.has_key('dontapprove'): + answer = self.handle_response(False, username, identity) + if answer is None: + return + elif form.has_key('approve'): + answer = self.handle_response(True, username, identity) + if answer is None: + return + else: + query = {} + for key in form.keys(): + query[key] = form[key][0] + try: + openidreq = openidsrv.decodeRequest(query) + except Exception, e: + request.makeForbidden(403, 'OpenID decode error: %r' % e) + return + + if openidreq is None: + request.makeForbidden403() + request.write('no request') + return + + if request.user.valid and username != request.user.name: + answer = openidreq.answer(False, identity=identity, server_url=server_url) + elif openidreq.mode in ["checkid_immediate", "checkid_setup"]: + answer = self.handleCheckIDRequest(identity, username, openidreq, server_url) + if answer is None: + return + else: + answer = openidsrv.handleRequest(openidreq) + webanswer = openidsrv.encodeResponse(answer) + headers = ['Status: %d OpenID status' % webanswer.code] + for hdr in webanswer.headers: + headers += [hdr+': '+webanswer.headers[hdr]] + request.emit_http_headers(headers) + request.write(webanswer.body) + raise MoinMoinFinish + + def handle_response(self, positive, username, identity): + request = self.request + form = request.form + + # check form submission nonce, use None for stored value default + # since it cannot be sent from the user + session_nonce = self.request.session.get('openidserver.nonce') + if session_nonce is not None: + del self.request.session['openidserver.nonce'] + # use empty string if nothing was sent + form_nonce = form.get('nonce', [''])[0] + if session_nonce != form_nonce: + self.request.makeForbidden403() + self.request.write('invalid nonce') + return None + + openidreq = request.session.get('openidserver.request') + if not openidreq: + request.makeForbidden403() + request.write('no response request') + return None + del request.session['openidserver.request'] + + if (not positive or + not request.user.valid or + request.user.name != username): + return openidreq.answer(False) + + + if form.get('remember', ['no'])[0] == 'yes': + if not hasattr(request.user, 'openid_trusted_roots'): + request.user.openid_trusted_roots = [] + request.user.openid_trusted_roots.append(strbase64(openidreq.trust_root)) + request.user.save() + dummyidentity, server_url = self._make_identity() + return self.approved(identity, openidreq, server_url=server_url) + + def approved(self, identity, openidreq, data=False, server_url=None): + reply = openidreq.answer(True, identity=identity, server_url=server_url) + if data: + # TODO + sreg_data = { } + sreq_req = sreg.SRegRequest.fromOpenIDRequest(openidreq.message) + sreg_resp = sreg.SRegResponse.extractResponse(openidreq, sreg_data) + sreg_resp.addToOpenIDResponse(reply.fields) + return reply + + def user_trusts_url(self, trustroot): + user = self.request.user + if hasattr(user, 'openid_trusted_roots'): + return strbase64(trustroot) in user.openid_trusted_roots + return False + + def show_decide_page(self, identity, username, openidreq): + request = self.request + _ = self._ + + if not request.user.valid or username != request.user.name: + request.makeForbidden(403, _('''You need to manually go to your OpenID provider wiki +and log in before you can use your OpenID. MoinMoin will +never allow you to enter your password here. + +Once you have logged in, simply reload this page.''', formatted=False)) + return + + request.emit_http_headers() + request.theme.send_title(_("OpenID Trust verification"), pagename=request.page.page_name) + # Start content (important for RTL support) + request.write(request.formatter.startContent("content")) + + request.write(request.formatter.paragraph(1)) + request.write(_('The site %s has asked for your identity.') % openidreq.trust_root) + request.write(request.formatter.paragraph(0)) + request.write(request.formatter.paragraph(1)) + request.write(_(''' +If you approve, the site represented by the trust root below will be +told that you control the identity URL %s. (If you are using a delegated +identity, the site will take care of reversing the +delegation on its own.)''') % openidreq.identity) + request.write(request.formatter.paragraph(0)) + + form = html.FORM(method='POST', action=request.page.url(request)) + form.append(html.INPUT(type='hidden', name='action', value='serveopenid')) + form.append(html.INPUT(type='hidden', name='openid.identity', value=openidreq.identity)) + form.append(html.INPUT(type='hidden', name='openid.return_to', value=openidreq.return_to)) + form.append(html.INPUT(type='hidden', name='openid.trust_root', value=openidreq.trust_root)) + form.append(html.INPUT(type='hidden', name='openid.mode', value=openidreq.mode)) + form.append(html.INPUT(type='hidden', name='name', value=username)) + + nonce = randomString(32, 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789') + form.append(html.INPUT(type='hidden', name='nonce', value=nonce)) + request.session['openidserver.nonce'] = nonce + + table = html.TABLE() + form.append(table) + + tr = html.TR() + table.append(tr) + tr.append(html.TD().append(html.STRONG().append(html.Text(_('Trust root'))))) + tr.append(html.TD().append(html.Text(openidreq.trust_root))) + + tr = html.TR() + table.append(tr) + tr.append(html.TD().append(html.STRONG().append(html.Text(_('Identity URL'))))) + tr.append(html.TD().append(html.Text(identity))) + + tr = html.TR() + table.append(tr) + tr.append(html.TD().append(html.STRONG().append(html.Text(_('Name'))))) + tr.append(html.TD().append(html.Text(username))) + + tr = html.TR() + table.append(tr) + tr.append(html.TD().append(html.STRONG().append(html.Text(_('Remember decision'))))) + td = html.TD() + tr.append(td) + td.append(html.INPUT(type='checkbox', name='remember', value='yes')) + td.append(html.Text(_('Remember this trust decision and don\'t ask again'))) + + tr = html.TR() + table.append(tr) + tr.append(html.TD()) + td = html.TD() + tr.append(td) + + td.append(html.INPUT(type='submit', name='approve', value=_("Approve"))) + td.append(html.INPUT(type='submit', name='dontapprove', value=_("Don't approve"))) + + request.write(unicode(form)) + + request.write(request.formatter.endContent()) + request.theme.send_footer(request.page.page_name) + request.theme.send_closing_html() + + def _sorry_no_identity(self): + request = self.request + _ = self._ + + request.emit_http_headers() + request.theme.send_title(_("OpenID not served"), pagename=request.page.page_name) + # Start content (important for RTL support) + request.write(request.formatter.startContent("content")) + + request.write(request.formatter.paragraph(1)) + request.write(_(''' +Unfortunately you have not created your homepage yet. Therefore, +we cannot serve an OpenID for you. Please create your homepage first +and then reload this page or click the button below to cancel this +verification.''')) + request.write(request.formatter.paragraph(0)) + + form = html.FORM(method='POST', action=request.page.url(request)) + form.append(html.INPUT(type='hidden', name='action', value='serveopenid')) + + nonce = randomString(32, 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789') + form.append(html.INPUT(type='hidden', name='nonce', value=nonce)) + request.session['openidserver.nonce'] = nonce + + form.append(html.INPUT(type='submit', name='dontapprove', value=_("Cancel"))) + + request.write(unicode(form)) + + request.write(request.formatter.endContent()) + request.theme.send_footer(request.page.page_name) + request.theme.send_closing_html()
--- a/MoinMoin/config/multiconfig.py Wed Feb 27 15:52:30 2008 +0100 +++ b/MoinMoin/config/multiconfig.py Wed Feb 27 16:12:06 2008 +0100 @@ -449,6 +449,11 @@ nonexist_qm = False notification_bot_uri = None + # OpenID server support + openid_server_enabled = False + openid_server_restricted_users_group = None + openid_server_enable_user = False + page_credits = [ # Feel free to add other credits, but PLEASE do NOT change or remove # the following links - you help us by keeping them "as is":