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:
JohannesBerg has done some work with session storage in Moin's current development branch that will make it easier to use recent versions of openid.consumer. (Are anonymous sessions working yet? Which bits of the documentation should people look at to use this?) (2007-04)
- A CS student at GWU is considering making this a school project. (2007-04)
KevinTurner can help answer OpenID and library usage questions.
JohannesBerg has it mostly working: JohannesBerg/OpenID support/client
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']
(Do we need a AuthMarket to store these things in?)
- Yeah, feel free to establish one.
What about OpenID 2.0? How is the progress and in which version could we expect authentication via OpenID?
The code in MoinMoin 1.7 supports OpenID 2. -- JohannesBerg 2008-02-23 19:04:41