Since this asks for OpenID RP only, I'm marking it as implemented now since I committed my code to 1.7. -- JohannesBerg 2007-07-10 17:37:28


Add OpenID support for moinmoin logins. Single Sign-on rules.

See: http://www.openidenabled.com/openid/libraries/python

OpenID 2.0 python libs are out (release candidate at least) - anyone upgrading the stuff? I would like to include openid into moin 1.6, if possible. -- ThomasWaldmann 2007-03-31 07:30:07

Current status:

Feel free to ask about this on irc://irc.freenode.net/openid or the OpenID development list (or perhaps MoinMoinMailingLists are appropriate, but I don't always stay up-to-date there), as discussion threads on the wiki can get rather unwieldy.

Code from KevinTurner (written for moin 1.5 and python-openid 1.0.x):

   1 # -*- Python -*-
   2 """Authenticate using OpenID.
   3 
   4 See L{ConfigMixin} for information on how to configure your
   5 installation to use this.  When this authentication method is enabled,
   6 a user can set her OpenID URL through her UserPreferences.  Once set,
   7 the user can log in to MoinMoin by entering their OpenID in the
   8 Username login field.  A password is not necessary; authorization will
   9 be done through the OpenID server.
  10 
  11 Known Issues:
  12 =============
  13 
  14  - No indication on the login form that it accepts OpenID.
  15 
  16  - Lack of feedback to the user.  There are a number of ways a user
  17    can fail to authenticate, and sometimes we allow MoinMoin to fall
  18    back to another authentication method, and sometimes we deny the
  19    request outright.  But while some cases are logged, the user gets
  20    no information about why they were not logged in.  It's impossible
  21    for them to tell if their OpenID was not found in the user database
  22    or if their server is down or if we had a problem parsing their
  23    input.  While it is in some cases advisable to hold back
  24    information so as not to leak it to attackers, feedback in most of
  25    the error cases is really only useful to a user in an honest login
  26    attempt.
  27 
  28    It's not clear how to fix this in the context of the current
  29    pluggable authentication scheme.  We should come up with revisions
  30    to the API with this in mind.
  31 
  32  - There is no validation of OpenID URL upon setting it in
  33    UserPreferences.  This leads to several potential problems.  The
  34    user won't be prompted to correct an invalid or unusable value.  No
  35    check is performed to ensure that the OpenID is unique within the
  36    user database, or that the user is authorized to use that OpenID.
  37 
  38    This leads to a per-user denial of service attack: If Alice wants
  39    to harass Bob, she can enter Bob's OpenID in her preferences.  This
  40    will not give Alice access to Bob's account; rather, it may
  41    (depending on who comes first in the user table) cause Bob to log
  42    in as Alice when he logs in with OpenID.  If Bob notices, he can
  43    then fix the preferences for that account, but it's a nuisance at
  44    best.
  45 
  46    Again, it does not seem possible to correct this as a standalone
  47    authentication module, as no hooks for the processing of the
  48    UserPreferences form are provided by MoinMoin 1.5.  The solution to
  49    this lies in either revising the MoinMoin API or folding this code
  50    into the MoinMoin core.
  51 
  52  - User creation.  Ideally, one should be able to create an account with
  53    an OpenID and never set a password in MoinMoin at all.  With
  54    C{user_autocreate} in the wiki configuration this is partially implemented,
  55    but users will quickly find that the userform code really doesn't want
  56    to let them through that screen without setting a password and an email
  57    address.
  58 
  59 @see: U{OpenID Enabled<http://www.openidenabled.com/>}
  60 
  61 @requires:
  62   U{python-openid<http://www.openidenabled.com/openid/libraries/python>} 1.0.x
  63 
  64 @requires: U{MoinMoin<http://moinmoin.wikiwikiweb.de/>}
  65 
  66 @author: Kevin Turner
  67 @contact: openid@janrain.com
  68 @organization: JanRain, Inc.
  69 @license: GPL
  70 
  71 @copyright: Copyright 2005 by JanRain, Inc.
  72 """
  73 
  74 __version__ = '0.0.2005-12-19'
  75 
  76 from openid.consumer import consumer
  77 from openid import oidutil
  78 from openid.store.filestore import FileOpenIDStore
  79 
  80 try:
  81     import cPickle as pickle
  82 except ImportError:
  83     import pickle
  84 
  85 from MoinMoin import caching, user, wikiutil
  86 
  87 def openid(request, name=None, password=None, login=None, logout=None,
  88            _consumer=None, **kw):
  89     """Authenticate by OpenID.
  90 
  91     This is an authentication plug-in for use with MoinMoin 1.5's
  92     modular authentication code.  The signature is defined by L{MoinMoin.auth}.
  93     """
  94 
  95     # Set the log function for the OpenID libraries.
  96     origlog = oidutil.log
  97     oidutil.log = request.log
  98 
  99     try:
 100         if logout:
 101             # Somebody Else's Problem
 102             return (None, True)
 103         elif login:
 104             if _consumer is None:
 105                 _consumer = _getConsumer(request)
 106 
 107             return beginAuth(request, _consumer, name)
 108         elif request.form.has_key('openid.mode'):
 109             try:
 110                 token = request.form['moidtoken'][0]
 111             except KeyError:
 112                 # XXX: Malformed reply, should let someone know that the server
 113                 # is busted.
 114                 request.log("Can't find the token for the OpenID reply.")
 115                 return (None, False)
 116             if _consumer is None:
 117                 _consumer = _getConsumer(request)
 118 
 119             theuser, cont = completeAuth(request, _consumer, token)
 120 
 121             if theuser is not None:
 122                 # If we don't set a cookie, we'll have to re-authenticate with
 123                 # every request.
 124                 
 125                 # FIXME: We have a problem interfacing with request.setCookie,
 126                 # because it sets a cookie for "the current user."  There's
 127                 # precedent for the following kludge in auth.moin_cookie and
 128                 # auth.interwiki, but that doesn't mean I think it's a good
 129                 # idea.
 130                 request.user = theuser
 131                 request.setCookie()
 132 
 133             return (theuser, cont)
 134         else:
 135             # Somebody Else's Problem
 136             return (None, True)
 137     finally:
 138         oidutil.log = origlog
 139 
 140 
 141 def beginAuth(request, theconsumer, name):
 142     """Handle user input and redirect to an OpenID server."""
 143     
 144     status, info = theconsumer.beginAuth(name)
 145 
 146     if status is not consumer.SUCCESS:
 147         # Try other auth methods.
 148         # XXX: Provides no feedback if they *did* want to log in with
 149         # OpenID and need to know if their server is down, etc.
 150         # Maybe include some heuristic here, e.g. "return an error
 151         # iff username.startswith('http')."
 152         return (None, True)
 153 
 154     auth_request = info
 155     trust_root = request.getBaseURL()
 156     if request.query_string:
 157         sep = '&'
 158     else:
 159         sep = '?'
 160     return_to = '%s%smoidtoken=%s' % (
 161         request.getQualifiedURL(request.request_uri),
 162         sep,
 163         wikiutil.url_quote(auth_request.token))
 164 
 165     redirect = theconsumer.constructRedirect(auth_request,
 166                                              return_to,
 167                                              trust_root)
 168     request.http_redirect(redirect)
 169     request.finish()
 170 
 171     # Don't bother trying more auth methods, we just hijacked the request.
 172     return (None, False)
 173 
 174 def completeAuth(request, theconsumer, token):
 175     """Handle response from the OpenID server and return an authenticated user.
 176     """
 177     args = {}
 178     for key, value in request.form.iteritems():
 179         # openid.consumer doesn't want to believe that unicode objects
 180         # come from GET requests.
 181         args[key.encode('utf-8')] = value[0].encode('utf-8')
 182 
 183     status, info = theconsumer.completeAuth(token, args)
 184     if status is not consumer.SUCCESS:
 185         # XXX: Should probably give some information to the user here.
 186         request.log("OpenID auth failed: %s, %s" % (status, info))
 187         return (None, False)
 188     if info is None:
 189         request.log("OpenID request denied or canceled.")
 190         # Request denied at OpenID server.
 191         return (None, False)
 192     identityURL = info
 193     theuser_id = userLookupByOpenID(request, identityURL)
 194     User = lambda **kw: user.User(request, auth_method="openid",
 195                                   **kw)
 196     if theuser_id is not None:
 197         theuser = User(id=theuser_id)
 198         # XXX: There is some weirdness around the User.trusted
 199         # attribute depending on which arguments you pass to the
 200         # constructor.  This may end up in us creating non-"trusted"
 201         # users, whatever that means.
 202     else:
 203         if user.getUserId(request, identityURL):
 204             # Okay, so we have someone with an authenticated OpenID,
 205             # an account *named* that OpenID, but we're not at all
 206             # sure that authorizes the user to access that account,
 207             # because the account's "openid" field wasn't set.
 208             # FIXME: Should explain to the user why they're not getting
 209             # logged in and write some log messages and stuff.
 210             request.log("OpenID auth for %r, but unsure what account"
 211                         "goes with it and can't create one." %
 212                         (identityURL,))
 213             return (None, True)
 214 
 215         # I'm not entirely sure what auth_attribs is for.  But I think
 216         # one of their effects is that if I put some things in there,
 217         # the UserPrefs form won't force me to set them, which is
 218         # behaviour I want here.  Not sure if it's abusive.
 219         theuser = User(auth_attribs=("password", "email"))
 220         theuser.openid_url = identityURL
 221         # I doubt I understand what I'm responsible for in new
 222         # user creation...  if I just make a user object here and
 223         # return it, is that sufficient?  Should I call
 224         # create_or_update?  Is there any of the new user logic
 225         # in userform that I need to invoke here?
 226         theuser.create_or_update(True)
 227         request.log("Created new user %s for OpenID %r" % (theuser,
 228                                                            identityURL))
 229     return (theuser, False)
 230 
 231 
 232 def userLookupByOpenID(request, identityURL):
 233     # This code is lifted right out of user.getUserId.  Can it be generalized?
 234     if not identityURL:
 235         return None
 236     cfg = request.cfg
 237     try:
 238         openid2id = cfg._openid2id
 239     except AttributeError:
 240         arena = 'user'
 241         key = 'openid2id'
 242         cache = caching.CacheEntry(request, arena, key)
 243         try:
 244             openid2id = pickle.loads(cache.content())
 245         except (pickle.UnpicklingError, IOError, EOFError, ValueError):
 246             openid2id = {}
 247         cfg._openid2id = openid2id
 248 
 249     id = openid2id.get(identityURL, None)
 250     if id is None:
 251         for userid in user.getUserList(request):
 252             uopenid = user.User(request, id=userid).openid_url
 253             openid2id[uopenid] = userid
 254         arena = 'user'
 255         key = 'openid2id'
 256         cache = caching.CacheEntry(request, arena, key)
 257         cache.update(pickle.dumps(openid2id, user.PICKLE_PROTOCOL))
 258         id = openid2id.get(identityURL, None)
 259     return id
 260 
 261 
 262 def _getConsumer(request):
 263     # I'm assuming that since you're using MoinMoin, you're probably
 264     # okay with a file-based association database.
 265     store = FileOpenIDStore(request.cfg.openid_assoc_dir)
 266     return consumer.OpenIDConsumer(store)
 267 
 268 
 269 from MoinMoin.multiconfig import DefaultConfig
 270 
 271 def _(text): return text
 272 
 273 class ConfigMixin:
 274     """Things you must define in your Config class to enable Open ID.
 275 
 276     One way to use this would be to define your configuration class like so::
 277 
 278         from MoinMoin.multiconfig import DefaultConfig
 279         from MoinMoin import oidauth
 280         class Config(oidauth.ConfigMixin, DefaultConfig):
 281             # ... your config values here ...
 282 
 283     The defaults for most of the things here are sane, but you likely should
 284     define L{openid_assoc_dir} with an absolute path.
 285 
 286     @ivar openid_assoc_dir: A directory (like C{data_dir}) in which the
 287         OpenID library will store its data.
 288     @ivar auth: To use OpenID, this list of authentication methods must
 289         include both the L{openid} function from the oidauth module and
 290         the default L{MoinMoin.auth.moin_cookie<moin_cookie>} function.
 291         Without the C{moin_cookie} function, you will have to re-authenticate
 292         for I{every} request.
 293     @ivar user_form_fields: For users to set their OpenID, this list must
 294         include C{openid_url}.
 295     @ivar user_form_defaults: Provide a default for the field defined in
 296         C{user_form_fields}.
 297     """
 298 
 299     openid_assoc_dir = './openid/'
 300     auth = [openid] + DefaultConfig.auth
 301     user_form_fields = DefaultConfig.user_form_fields + [
 302         ('openid_url', _('OpenID'), "text", "40",
 303          _("(Your OpenID)")),
 304         ]
 305 
 306     user_form_defaults = DefaultConfig.user_form_defaults.copy()
 307     user_form_defaults.update({
 308         'openid_url': '',
 309         })
 310 
 311 
 312 __all__ = ['openid', 'ConfigMixin']
oidauth.py test_oidauth.py

(Do we need a AuthMarket to store these things in?)

What about OpenID 2.0? How is the progress and in which version could we expect authentication via OpenID?


CategoryFeatureImplemented CategoryMoinMoinPatch

MoinMoin: FeatureRequests/OpenIDSupport (last edited 2008-02-23 19:04:42 by JohannesBerg)