From python-checkins at python.org Sun Nov 6 02:58:01 2011 From: python-checkins at python.org (richard) Date: Sun, 6 Nov 2011 02:58:01 +0100 (CET) Subject: [Pypi-checkins] r983 - trunk/pypi/templates Message-ID: <3Sbg616kv4zMft@mail.python.org> Author: richard Date: Sun Nov 6 02:58:01 2011 New Revision: 983 Modified: trunk/pypi/templates/register.pt Log: notes about rego email reception Modified: trunk/pypi/templates/register.pt ============================================================================== --- trunk/pypi/templates/register.pt (original) +++ trunk/pypi/templates/register.pt Sun Nov 6 02:58:01 2011 @@ -68,8 +68,10 @@ -

A confirmation email will be sent to the address you nominate above.

-

To complete the registration process, visit the link indicated in the +

A confirmation email will be sent to the address you nominate above.

+

Please ensure you will be able to receive email from cheeseshop at python.org + (check any "sender confirmation" systems you might be using.)

+

To complete the registration process, you must visit the link indicated in the email.

From python-checkins at python.org Sat Nov 12 18:44:55 2011 From: python-checkins at python.org (martin.von.loewis) Date: Sat, 12 Nov 2011 18:44:55 +0100 (CET) Subject: [Pypi-checkins] r984 - trunk/pypi Message-ID: <3Sglqq5bNVzP7d@mail.python.org> Author: martin.von.loewis Date: Sat Nov 12 18:44:55 2011 New Revision: 984 Modified: trunk/pypi/mirrors.txt Log: Add g. Modified: trunk/pypi/mirrors.txt ============================================================================== --- trunk/pypi/mirrors.txt (original) +++ trunk/pypi/mirrors.txt Sat Nov 12 18:44:55 2011 @@ -3,3 +3,4 @@ d: pypi.websushi.org, jezdez+pypi at enn.io e: 141.89.226.2, martin at v.loewis.de f: services02.fe.rzob.gocept.net, support at gocept.com +g: 129.21.171.98, rc-help at rit.edu From python-checkins at python.org Sat Nov 19 16:03:07 2011 From: python-checkins at python.org (martin.von.loewis) Date: Sat, 19 Nov 2011 16:03:07 +0100 (CET) Subject: [Pypi-checkins] r985 - in trunk/pypi: . tools Message-ID: <3Slzvv5CpQzM0b@mail.python.org> Author: martin.von.loewis Date: Sat Nov 19 16:03:07 2011 New Revision: 985 Added: trunk/pypi/tools/sql-migrate-20111119.sql Modified: trunk/pypi/openid2rp.py trunk/pypi/pkgbase_schema.sql trunk/pypi/store.py trunk/pypi/webui.py Log: Redo openid login to support direct verification. Modified: trunk/pypi/openid2rp.py ============================================================================== --- trunk/pypi/openid2rp.py (original) +++ trunk/pypi/openid2rp.py Sat Nov 19 16:03:07 2011 @@ -6,7 +6,7 @@ # This library implements OpenID Authentication 2.0, # in the role of a relying party -import urlparse, urllib, httplib, time, cgi, htmllib, formatter +import urlparse, urllib, httplib, time, cgi, HTMLParser import cStringIO, base64, hmac, hashlib, datetime, re, random import itertools, cPickle, sys @@ -26,9 +26,51 @@ if sys.version_info < (3,): def b(s): return s + # Convert byte to integer + b2i = ord + def bytes_from_ints(L): + return ''.join([chr(i) for i in L]) else: def b(s): - return s.encode('ascii') + return s.encode('latin-1') + def b2i(char): + # 3.x: bytes are already sequences of integers + return char + bytes_from_ints = bytes + +class NotAuthenticated(Exception): + CONNECTION_REFUSED = 1 + DIRECT_VERIFICATION_FAILED = 2 + CANCELLED = 3 + UNSUPPORTED_VERSION = 4 + UNEXPECTED_MODE = 5 + CLAIMED_ID_MISSING = 6 + DISCOVERY_FAILED = 7 + INCONSISTENT_IDS = 8 + REPLAY_ATTACK = 9 + MISSING_NONCE = 10 + msgs = { + CONNECTION_REFUSED : 'OP refuses connection with status %d', + DIRECT_VERIFICATION_FAILED : 'OP doesn\'t assert that the signature of the verification request is valid', + CANCELLED : 'OP did not authenticate user (cancelled)', + UNSUPPORTED_VERSION : 'Unsupported OpenID version', + UNEXPECTED_MODE : 'Unexpected mode %s', + CLAIMED_ID_MISSING : 'Cannot determine claimed ID', + DISCOVERY_FAILED : 'Claimed ID %s cannot be rediscovered', + INCONSISTENT_IDS : 'Discovered and asserted endpoints differ', + REPLAY_ATTACK : 'Replay attack detected', + MISSING_NONCE : 'Nonce missing in OpenID 2 response', + } + + def __init__(self, errno, *args): + msg = self.msgs[errno] + if args: + msg %= args + self.errno = errno + Exception.__init__(self, msg, errno, *args) + + def __str__(self): + return self.args[0] def normalize_uri(uri): """Normalize an uri according to OpenID section 7.2. Return a pair @@ -138,24 +180,24 @@ res[k] = v return res -class OpenIDParser(htmllib.HTMLParser): +class OpenIDParser(HTMLParser.HTMLParser): def __init__(self): - htmllib.HTMLParser.__init__(self, formatter.NullFormatter()) + HTMLParser.HTMLParser.__init__(self) self.links = {} self.xrds_location=None - def do_link(self, attrs): - attrs = dict(attrs) - try: - self.links[attrs['rel']] = attrs['href'] - except KeyError: - pass - - def do_meta(self, attrs): - attrs = dict(attrs) - # Yadis 6.2.5 option 1: meta tag - if attrs.get('http-equiv','').lower() == 'x-xrds-location': - self.xrds_location = attrs['content'] + def handle_starttag(self, tag, attrs): + if tag == 'link': + attrs = dict(attrs) + try: + self.links[attrs['rel']] = attrs['href'] + except KeyError: + pass + elif tag == 'meta': + attrs = dict(attrs) + # Yadis 6.2.5 option 1: meta tag + if attrs.get('http-equiv','').lower() == 'x-xrds-location': + self.xrds_location = attrs['content'] def _extract_services(doc): for svc in doc.findall(".//{xri://$xrd*($v*2.0)}Service"): @@ -241,7 +283,7 @@ if content_type in ('text/html', 'application/xhtml+xml'): parser = OpenIDParser() - parser.feed(data) + parser.feed(data.decode('latin-1')) parser.close() # Yadis 6.2.5 option 1: meta tag if parser.xrds_location: @@ -331,13 +373,13 @@ res = cPickle.dumps(l, 2) # Pickle result: proto 2, long1 (integer < 256 bytes) # number of bytes, little-endian integer, stop - assert res[:3] == '\x80\x02\x8a' + assert res[:3] == b('\x80\x02\x8a') # btwoc ought to produce the shortest representation in two's # complement. Fortunately, pickle already does that. - return res[3+ord(res[3]):3:-1] + return res[3+b2i(res[3]):3:-1] -def unbtwoc(b): - return cPickle.loads('\x80\x02\x8a'+chr(len(b))+b[::-1]+'.') +def unbtwoc(B): + return cPickle.loads(b('\x80\x02\x8a')+bytes_from_ints([len(B)])+B[::-1]+(b'.')) # Appendix B; DH default prime dh_prime = """ @@ -351,8 +393,8 @@ def string_xor(s1, s2): res = [] for c1, c2 in itertools.izip(s1, s2): - res.append(chr(ord(c1) ^ ord(c2))) - return ''.join(res) + res.append(b2i(c1) ^ b2i(c2)) + return bytes_from_ints(res) def associate(services, url): '''Create an association (OpenID section 8) between RP and OP. @@ -378,25 +420,25 @@ if data['openid.session_type'] == "no-encryption": data['openid.session_type'] = '' del data['openid.ns'] - res = urllib.urlopen(url, urllib.urlencode(data)) + res = urllib.urlopen(url, b(urllib.urlencode(data))) if res.getcode() != 200: raise ValueError, "OpenID provider refuses connection with status %d" % res.getcode() data = parse_response(res.read()) if 'error' in data: raise ValueError, "associate failed: "+data['error'] if url.startswith('http:'): - enc_mac_key = data.get('enc_mac_key') + enc_mac_key = b(data.get('enc_mac_key')) if not enc_mac_key: raise ValueError, "Provider protocol error: not using DH-SHA1" - enc_mac_key = base64.b64decode(data['enc_mac_key']) - dh_server_public = unbtwoc(base64.b64decode(data['dh_server_public'])) + enc_mac_key = base64.b64decode(enc_mac_key) + dh_server_public = unbtwoc(base64.b64decode(b(data['dh_server_public']))) # shared secret: sha1(2^(server_priv*priv) mod prime) xor enc_mac_key shared_secret = btwoc(pow(dh_server_public, priv, dh_prime)) shared_secret = hashlib.sha1(shared_secret).digest() if len(shared_secret) != len(enc_mac_key): raise ValueError, "incorrect DH key size" # Fake mac_key result - data['mac_key'] = base64.b64encode(string_xor(enc_mac_key, shared_secret)) + data['mac_key'] = b(base64.b64encode(string_xor(enc_mac_key, shared_secret))) return data class _AX: @@ -471,10 +513,15 @@ if sreg_opt: data['openid.sreg.optional'] = sreg11['openid.sreg11.optional'] =','.join(sreg_opt) if is_compat_1x(services): + # OpenID 1.1 does not communicate claimed_ids. Put them into the return URL + return_to += '&' if '?' in return_to else '?' + return_to += '&openid1=' + urllib.quote(claimed) + data['openid.return_to'] = return_to del data['openid.ns'] del data['openid.claimed_id'] del data['openid.realm'] - data['openid.trust_root'] = return_to + trust_root = urlparse.urlparse(return_to)[:3] + (None,None,None) + data['openid.trust_root'] = urlparse.urlunparse(trust_root) ax_req, ax_opt = ax if "http://openid.net/srv/ax/1.0" in services and (ax_req or ax_opt): data.update({ @@ -497,8 +544,20 @@ else: return url+"?"+urllib.urlencode(data) -class NotAuthenticated(Exception): - pass +# 11.4.2 Verifying Directly with the OpenID Provider +def verify_signature_directly(op_endpoint, response): + '''Request that the OP verify the signature via Direct Verification''' + + request = [('openid.mode', 'check_authentication')] + # Exact copies of all fields from the authentication response, except for + # "openid.mode" + request.extend((k, v) for k, (v,) in response.items() if 'openid.mode' != k) + res = urllib.urlopen(op_endpoint, urllib.urlencode(request)) + if 200 != res.getcode(): + raise NotAuthenticated(NotAuthenticated.CONNECTION_REFUSED, res.getcode()) + response = parse_response(res.read()) + if 'true' != response['is_valid']: + raise NotAuthenticated(NotAuthenticated.DIRECT_VERIFICATION_FAILED) def _prepare_response(response): if isinstance(response, str): @@ -529,7 +588,7 @@ if session['assoc_handle'] != response['openid.assoc_handle'][0]: raise ValueError('incorrect session') if response['openid.mode'][0] == 'cancel': - raise NotAuthenticated('provider did not authenticate user (cancelled)') + raise NotAuthenticated(NotAuthenticated.CANCELLED) if response['openid.mode'][0] != 'id_res': raise ValueError('invalid openid.mode') if 'openid.identity' not in response: @@ -547,7 +606,7 @@ query.append(value) query = b('').join(query) - mac_key = base64.decodestring(b(session['mac_key'])) + mac_key = base64.decodestring(session['mac_key']) transmitted_sig = base64.decodestring(b(response['openid.sig'][0])) computed_sig = hmac.new(mac_key, query, hashlib.sha1).digest() @@ -569,6 +628,69 @@ return signed +def verify(response, discovery_cache, find_association, nonce_seen): + response = _prepare_response(response) + if 'openid.ns' in response: + ns = response['openid.ns'][0] + if ns != 'http://specs.openid.net/auth/2.0': + raise NotAuthenticated(NotAuthenticate.UNSUPPORTED_VERSION) + else: + ns = None + mode = response['openid.mode'][0] + if mode == 'cancel': + raise NotAuthenticated(NotAuthenticated.CANCELLED) + if mode != 'id_res': + raise NotAuthenticated(NotAuthenticated.UNEXPECTED_MODE, mode) + # Establish claimed ID + if 'openid.claimed_id' in response: + claimed_id = response['openid.claimed_id'][0] + # 11.2. Drop Fragment from claimed_id + fragment = claimed_id.find('#') + if fragment != -1: + claimed_id = claimed_id[:fragment] + elif 'openid1' in response: + claimed_id = response['openid1'][0] + else: + raise NotAuthenticated(NotAuthenticated.CLAIMED_ID_MISSING) + discovered = discovery_cache(claimed_id) + if not discovered: + discovered = discover(claimed_id) + if not discovered: + raise NotAuthenticated(NotAuthenticated.DISCOVERY_FAILED, claimed_id) + services, op_endpoint, op_local = discovered + # For a provider-allocated claimed_id, there will be no op_local ID, + # and there is no point checking it. + if op_local and op_local != response['openid.identity'][0]: + raise NotAuthenticated('Discovered and asserted local identifiers differ') + # For OpenID 1.1, op_endpoint may not be included in the response + if ('openid.op_endpoint' in response and + op_endpoint != response['openid.op_endpoint'][0]): + raise NotAuthenticated(NotAuthenticated.INCONSISTENT_IDS) + # XXX verify protocol version, verify claimed_id wrt. original request, + # verify return_to URL + + # verify the signature + assoc_handle = response['openid.assoc_handle'][0] + session = find_association(assoc_handle) + if session: + signed = authenticate(session, response) + else: + verify_signature_directly(op_endpoint, response) + signed = response['openid.signed'][0].split(',') + + # Check the nonce. OpenID 1.1 doesn't have them + if 'openid.response_nonce' in response: + nonce = response['openid.response_nonce'][0] + timestamp = parse_nonce(nonce) + if (datetime.datetime.utcnow() - timestamp).total_seconds() > 10: + # allow for at most 10s transmission time and time shift + raise NotAuthenticated(NotAuthenticated.REPLAY_ATTACK) + if nonce_seen(nonce): + raise NotAuthenticated(NotAuthenticated.REPLAY_ATTACK) + elif ns: + raise NotAuthenticated(NotAuthenticated.MISSING_NONCE) + return signed, claimed_id + def parse_nonce(nonce): '''Extract a datetime.datetime stamp from the nonce''' stamp = nonce.split('Z', 1)[0] Modified: trunk/pypi/pkgbase_schema.sql ============================================================================== --- trunk/pypi/pkgbase_schema.sql (original) +++ trunk/pypi/pkgbase_schema.sql Sat Nov 19 16:03:07 2011 @@ -16,6 +16,14 @@ name TEXT REFERENCES users ON DELETE CASCADE ); +CREATE TABLE openid_discovered ( + created TIMESTAMP, + url TEXT PRIMARY KEY, + services BYTEA, + op_endpoint TEXT, + op_local TEXT +); + CREATE TABLE openid_sessions ( id SERIAL PRIMARY KEY, provider TEXT, Modified: trunk/pypi/store.py ============================================================================== --- trunk/pypi/store.py (original) +++ trunk/pypi/store.py Sat Nov 19 16:03:07 2011 @@ -3,6 +3,7 @@ import sys, os, re, time, hashlib, random, types, math, stat, errno import logging, cStringIO, string, datetime, calendar, binascii, urllib2, cgi from collections import defaultdict +import cPickle as pickle try: import psycopg2 except ImportError: @@ -199,6 +200,12 @@ safe_params.append(param) return cursor.execute(sql, safe_params) +def binary(cursor, bytes): + if isinstance(cursor, sqlite3_cursor): + # XXX is this correct? + return bytes + return psycopg2.Binary(bytes) + class StorageError(Exception): pass @@ -1826,50 +1833,69 @@ # OpenID + def store_discovered(self, url, services, op_endpoint, op_local): + cursor = self.get_cursor() + sql = '''delete from openid_discovered where url = %s''' + safe_execute(cursor, sql, (url,)) + services = binary(cursor, pickle.dumps(services, pickle.HIGHEST_PROTOCOL)) + sql = '''insert into openid_discovered(created, url, services, op_endpoint, op_local) + values(%s, %s, %s, %s, %s)''' + now = datetime.datetime.now() + safe_execute(cursor, sql, (now, url, services, op_endpoint, op_local)) + + def discovered(self, url): + cursor = self.get_cursor() + sql = '''select services, op_endpoint, op_local from openid_discovered where url=%s''' + safe_execute(cursor, sql, (url,)) + result = cursor.fetchall() + if result: + services, endpoint, local = result[0] + services = pickle.loads(str(services)) + return services, endpoint, local + else: + return None + def get_provider_session(self, provider): cursor = self.get_cursor() + # discover service URL, possibly from cache + res = self.discovered(provider[2]) + if not res: + res = openid2rp.discover(provider[2]) + assert res + self.store_discovered(provider[2], *res) + stypes, url, op_local = res # Check for existing session - sql = '''select id,url, assoc_handle from openid_sessions - where provider=%s and expires>current_timestamp''' - safe_execute(cursor, sql, (provider[0],)) + sql = '''select assoc_handle from openid_sessions + where url=%s and expires>current_timestamp''' + safe_execute(cursor, sql, (url,)) sessions = cursor.fetchall() if sessions: - id, url, assoc_handle = sessions[0] - safe_execute(cursor, 'select stype from openid_stypes where id=%s', - (id,)) - stypes = [t[0] for t in cursor.fetchall()] + assoc_handle = sessions[0][0] return stypes, url, assoc_handle # start from scratch: - # discover service URL - stypes, url, op_local = openid2rp.discover(provider[2]) # associate session now = datetime.datetime.now() session = openid2rp.associate(stypes, url) # store it sql = '''insert into openid_sessions - (provider, url, assoc_handle, expires, mac_key) - values (%s, %s, %s, %s, %s)''' - safe_execute(cursor, sql, (provider[0], url, + (url, assoc_handle, expires, mac_key) + values (%s, %s, %s, %s)''' + safe_execute(cursor, sql, (url, session['assoc_handle'], now+datetime.timedelta(0,int(session['expires_in'])), session['mac_key'])) - safe_execute(cursor, 'select %s' % self.last_id('openid_sessions')) - session_id = cursor.fetchone()[0] - for t in stypes: - safe_execute(cursor, '''insert into openid_stypes(id, stype) - values(%s, %s)''', (session_id, t)) return stypes, url, session['assoc_handle'] - def get_session_for_endpoint(self, claimed, stypes, endpoint): + def get_session_for_endpoint(self, endpoint, stypes): '''Return the assoc_handle for the a claimed ID/endpoint pair; create a new session if necessary. Discovery is supposed to be done by the caller.''' cursor = self.get_cursor() # Check for existing session sql = '''select assoc_handle from openid_sessions - where provider=%s and url=%s and expires>current_timestamp''' - safe_execute(cursor, sql, (claimed, endpoint,)) + where url=%s and expires>current_timestamp''' + safe_execute(cursor, sql, (endpoint,)) sessions = cursor.fetchall() if sessions: return sessions[0][0] @@ -1879,34 +1905,26 @@ session = openid2rp.associate(stypes, endpoint) # store it sql = '''insert into openid_sessions - (provider, url, assoc_handle, expires, mac_key) + (url, assoc_handle, expires, mac_key) values (%s, %s, %s, %s, %s)''' - safe_execute(cursor, sql, (claimed, endpoint, + safe_execute(cursor, sql, (endpoint, session['assoc_handle'], now+datetime.timedelta(0,int(session['expires_in'])), session['mac_key'])) safe_execute(cursor, 'select %s' % self.last_id('openid_sessions')) session_id = cursor.fetchone()[0] - # store stypes as well, so we can remember whether claimed is an OP ID or a user ID - for t in stypes: - safe_execute(cursor, '''insert into openid_stypes(id, stype) - values(%s, %s)''', (session_id, t)) return session['assoc_handle'] - def get_session_by_handle(self, assoc_handle): + def find_association(self, assoc_handle): cursor = self.get_cursor() - sql = 'select id, provider, url, mac_key from openid_sessions where assoc_handle=%s' + sql ='select mac_key from openid_sessions where assoc_handle=%s' safe_execute(cursor, sql, (assoc_handle,)) sessions = cursor.fetchall() if sessions: - id, provider, url, mac_key = sessions[0] - safe_execute(cursor, 'select stype from openid_stypes where id=%s', - (id,)) - stypes = [t[0] for t in cursor.fetchall()] - return provider, url, stypes, {'assoc_handle':assoc_handle, 'mac_key':mac_key} + return {'assoc_handle':assoc_handle, 'mac_key':sessions[0][0]} return None - def duplicate_nonce(self, nonce): + def duplicate_nonce(self, nonce, checkonly = False): '''Return true if we might have seen this nonce before.''' stamp = openid2rp.parse_nonce(nonce) utc = calendar.timegm(stamp.utctimetuple()) @@ -1919,10 +1937,14 @@ (nonce,)) if cursor.fetchone(): return True - safe_execute(cursor, '''insert into openid_nonces(created, nonce) - values(%s,%s)''', (stamp, nonce)) + if not checkonly: + safe_execute(cursor, '''insert into openid_nonces(created, nonce) + values(%s,%s)''', (stamp, nonce)) return False + def check_nonce(self, nonce): + return self.duplicate_nonce(nonce, checkonly=True) + def associate_openid(self, username, openid): cursor = self.get_cursor() safe_execute(cursor, 'insert into openids(id, name) values(%s,%s)', Added: trunk/pypi/tools/sql-migrate-20111119.sql ============================================================================== --- (empty file) +++ trunk/pypi/tools/sql-migrate-20111119.sql Sat Nov 19 16:03:07 2011 @@ -0,0 +1,11 @@ +CREATE TABLE openid_discovered ( + created TIMESTAMP, + url TEXT PRIMARY KEY, + services BYTEA, + op_endpoint TEXT, + op_local TEXT +); +alter table openid_sessions drop provider; +drop table openid_stypes; + + Modified: trunk/pypi/webui.py ============================================================================== --- trunk/pypi/webui.py (original) +++ trunk/pypi/webui.py Sat Nov 19 16:03:07 2011 @@ -947,10 +947,11 @@ if not res: return self.fail('Discovery failed. If you think this is in error, please submit a bug report.') stypes, op_endpoint, op_local = res + self.store.store_discovered(claimed_id, stypes, op_endpoint, op_local) if not op_local: op_local = claimed_id try: - assoc_handle = self.store.get_session_for_endpoint(claimed_id, stypes, op_endpoint) + assoc_handle = self.store.get_session_for_endpoint(op_endpoint, stypes) except ValueError, e: return self.fail('Cannot establish OpenID session: ' + str(e)) return_to = self.config.url+'?:action=openid_return' @@ -2505,6 +2506,7 @@ otk=info['otk'], user=user) return elif self.username is None: + nonce = None for param in 'name email'.split(): if not info.has_key(param): raise FormError, '%s is required'%param @@ -2517,33 +2519,16 @@ qs = {} for key, value in self.form.items(): qs[key] = [value.encode('utf-8')] - session = self.store.get_session_by_handle(self.form['openid.assoc_handle']) - if not session: - raise FormError, "Invalid session" - provider, url, stypes, session = session try: - signed = openid2rp.authenticate(session, qs) + signed, claimed_id = openid2rp.verify(qs, self.store.discovered, + self.store.find_association, + self.store.check_nonce) except Exception, e: return self.fail('OpenID response has been tampered with:'+repr(e)) - if not openid2rp.is_op_endpoint(stypes): - claimed_id = provider - elif 'claimed_id' in signed: - claimed_id = qs['openid.claimed_id'][0] - # Need to perform discovery to verify claimed ID is really managed by provider - discovered = openid2rp.discover(claimed_id) - if not discovered or discovered[1] != url: - return self.fail('Provider %s cannot make assertions about ID %s' % (url, claimed_id)) - else: - return self.fail('Claimed ID got lost. Please report this as a bug.') - if self.store.get_user_by_openid(claimed_id): - return self.fail('OpenID already associated with a different account') if 'response_nonce' in signed: - nonce = qs['openid.response_nonce'][0] - else: - # OpenID 1.1 - nonce = None + nonce = qs['response_nonce'][0] else: - claimed_id = nonce = None + claimed_id = None if not info.has_key('confirm') or info['password'] <> info['confirm']: self.fail("password and confirm don't match", heading='Users') return @@ -2813,37 +2798,19 @@ return self.fail('OpenID login failed: '+qs['openid.error'][0]) if mode != 'id_res': return self.fail('OpenID login failed') - session = self.store.get_session_by_handle(qs['openid.assoc_handle'][0]) - if not session: - return self.fail('invalid session') - provider, url, stypes, session = session try: - signed = openid2rp.authenticate(session, qs) + signed, claimed_id = openid2rp.verify(qs, self.store.discovered, + self.store.find_association, + self.store.check_nonce) except Exception, e: return self.fail('Login failed:'+repr(e)) - # the claimed ID in the response can't be trusted for signon requests, - # as the user may have changed it when getting redirected. - # For a signon login, the database has stored the claimed id in the - # provider field of the session table. - # XXX as the assoc_handle may not be signed, the return_to url should - # contain a nonce for 1.1 providers - if not openid2rp.is_op_endpoint(stypes): - claimed_id = provider - elif 'claimed_id' in signed: - claimed_id = qs['openid.claimed_id'][0] - # Need to perform discovery to verify claimed ID is really managed by provider - discovered = openid2rp.discover(claimed_id) - if not discovered or discovered[1] != url: - return self.fail('Provider %s cannot make assertions about ID %s' % (url, claimed_id)) - else: - return self.fail('Claimed ID got lost. Please report this as a bug.') + if 'response_nonce' in signed: nonce = qs['openid.response_nonce'][0] else: # OpenID 1.1 nonce = None - if 'openid.ns' in qs and qs['openid.ns'][0] == 'http://specs.openid.net/auth/2.0': - return self.fail('OpenID 2.0 provider failed to protect against replay attacks') + user = self.store.get_user_by_openid(claimed_id) # Three cases: logged-in user claimed some ID, # new login, or registration From python-checkins at python.org Sat Nov 19 16:13:22 2011 From: python-checkins at python.org (martin.von.loewis) Date: Sat, 19 Nov 2011 16:13:22 +0100 (CET) Subject: [Pypi-checkins] r986 - trunk/pypi Message-ID: <3Sm07k18MwzPVZ@mail.python.org> Author: martin.von.loewis Date: Sat Nov 19 16:13:21 2011 New Revision: 986 Modified: trunk/pypi/openid2rp.py Log: Work around lack of .total_seconds() in 2.6. Modified: trunk/pypi/openid2rp.py ============================================================================== --- trunk/pypi/openid2rp.py (original) +++ trunk/pypi/openid2rp.py Sat Nov 19 16:13:21 2011 @@ -628,6 +628,10 @@ return signed +# td.total_seconds only works in 2.7 +def _total_seconds(td): + return td.days*24*3600 + td.seconds + def verify(response, discovery_cache, find_association, nonce_seen): response = _prepare_response(response) if 'openid.ns' in response: @@ -682,7 +686,7 @@ if 'openid.response_nonce' in response: nonce = response['openid.response_nonce'][0] timestamp = parse_nonce(nonce) - if (datetime.datetime.utcnow() - timestamp).total_seconds() > 10: + if _total_seconds(datetime.datetime.utcnow() - timestamp) > 10: # allow for at most 10s transmission time and time shift raise NotAuthenticated(NotAuthenticated.REPLAY_ATTACK) if nonce_seen(nonce): From python-checkins at python.org Sat Nov 19 16:15:47 2011 From: python-checkins at python.org (martin.von.loewis) Date: Sat, 19 Nov 2011 16:15:47 +0100 (CET) Subject: [Pypi-checkins] r987 - trunk/pypi Message-ID: <3Sm0BW0fXpzPVZ@mail.python.org> Author: martin.von.loewis Date: Sat Nov 19 16:15:46 2011 New Revision: 987 Modified: trunk/pypi/store.py Log: Fix query parameters. Modified: trunk/pypi/store.py ============================================================================== --- trunk/pypi/store.py (original) +++ trunk/pypi/store.py Sat Nov 19 16:15:46 2011 @@ -1906,7 +1906,7 @@ # store it sql = '''insert into openid_sessions (url, assoc_handle, expires, mac_key) - values (%s, %s, %s, %s, %s)''' + values (%s, %s, %s, %s)''' safe_execute(cursor, sql, (endpoint, session['assoc_handle'], now+datetime.timedelta(0,int(session['expires_in'])), From python-checkins at python.org Sat Nov 19 16:17:34 2011 From: python-checkins at python.org (martin.von.loewis) Date: Sat, 19 Nov 2011 16:17:34 +0100 (CET) Subject: [Pypi-checkins] r988 - trunk/pypi Message-ID: <3Sm0DZ4CftzPd5@mail.python.org> Author: martin.von.loewis Date: Sat Nov 19 16:17:34 2011 New Revision: 988 Modified: trunk/pypi/webui.py Log: Drop extra parameter when claiming openid. Modified: trunk/pypi/webui.py ============================================================================== --- trunk/pypi/webui.py (original) +++ trunk/pypi/webui.py Sat Nov 19 16:17:34 2011 @@ -2765,7 +2765,7 @@ if not op_local: op_local = claimed_id try: - assoc_handle = self.store.get_session_for_endpoint(claimed_id, stypes, op_endpoint) + assoc_handle = self.store.get_session_for_endpoint(op_endpoint, stypes) except ValueError, e: return self.fail('Cannot establish OpenID session: ' + str(e)) return_to = self.config.url+'?:action=openid_return' From python-checkins at python.org Sat Nov 19 17:10:42 2011 From: python-checkins at python.org (martin.von.loewis) Date: Sat, 19 Nov 2011 17:10:42 +0100 (CET) Subject: [Pypi-checkins] r989 - trunk/pypi Message-ID: <3Sm1Pt3JPXzPTb@mail.python.org> Author: martin.von.loewis Date: Sat Nov 19 17:10:42 2011 New Revision: 989 Modified: trunk/pypi/webui.py Log: Fix typo. Modified: trunk/pypi/webui.py ============================================================================== --- trunk/pypi/webui.py (original) +++ trunk/pypi/webui.py Sat Nov 19 17:10:42 2011 @@ -2526,7 +2526,7 @@ except Exception, e: return self.fail('OpenID response has been tampered with:'+repr(e)) if 'response_nonce' in signed: - nonce = qs['response_nonce'][0] + nonce = qs['openid.response_nonce'][0] else: claimed_id = None if not info.has_key('confirm') or info['password'] <> info['confirm']: From python-checkins at python.org Sun Nov 20 21:16:39 2011 From: python-checkins at python.org (martin.von.loewis) Date: Sun, 20 Nov 2011 21:16:39 +0100 (CET) Subject: [Pypi-checkins] r990 - trunk/pypi Message-ID: <3SmkqC1gGdzPnc@mail.python.org> Author: martin.von.loewis Date: Sun Nov 20 21:16:39 2011 New Revision: 990 Modified: trunk/pypi/pkgbase_schema.sql Log: Add missing changes. Modified: trunk/pypi/pkgbase_schema.sql ============================================================================== --- trunk/pypi/pkgbase_schema.sql (original) +++ trunk/pypi/pkgbase_schema.sql Sun Nov 20 21:16:39 2011 @@ -26,20 +26,12 @@ CREATE TABLE openid_sessions ( id SERIAL PRIMARY KEY, - provider TEXT, url TEXT, assoc_handle TEXT, expires TIMESTAMP, mac_key TEXT ); -CREATE TABLE openid_stypes ( - id INTEGER REFERENCES openid_sessions ON DELETE CASCADE, - stype TEXT -); -CREATE INDEX openid_stypes_id ON openid_stypes(id); - - CREATE TABLE openid_nonces ( created TIMESTAMP, nonce TEXT From python-checkins at python.org Sun Nov 20 22:00:05 2011 From: python-checkins at python.org (martin.von.loewis) Date: Sun, 20 Nov 2011 22:00:05 +0100 (CET) Subject: [Pypi-checkins] r991 - trunk/pypi Message-ID: <3SmlnK49bCzPnf@mail.python.org> Author: martin.von.loewis Date: Sun Nov 20 22:00:05 2011 New Revision: 991 Modified: trunk/pypi/webui.py Log: Support identifier_select in openid_is_authorized. Modified: trunk/pypi/webui.py ============================================================================== --- trunk/pypi/webui.py (original) +++ trunk/pypi/webui.py Sun Nov 20 22:00:05 2011 @@ -2970,7 +2970,8 @@ return if orequest.mode in ['checkid_immediate', 'checkid_setup']: if self.openid_is_authorized(orequest): - return self.openid_response(orequest.answer(True)) + answer = orequest.answer(True, identity=self.openid_user_url()) + return self.openid_response(answer) elif orequest.immediate: return self.openid_response(orequest.answer(False)) else: @@ -3058,7 +3059,7 @@ if not self.authenticated: return False if identity == 'http://specs.openid.net/auth/2.0/identifier_select': - return False + identity = self.openid_user_url() id_prefix = self.config.scheme_host + "/id/" if not identity.startswith(id_prefix): return False @@ -3069,8 +3070,7 @@ else: return False # identity is not owned by user so decline the request - answer = orequest.answer(False) - self.openid_response(answer) + return False def openid_user_url(self): if self.authenticated: From python-checkins at python.org Sun Nov 20 22:20:20 2011 From: python-checkins at python.org (martin.von.loewis) Date: Sun, 20 Nov 2011 22:20:20 +0100 (CET) Subject: [Pypi-checkins] r992 - in trunk/pypi: . tools Message-ID: <3SmmDh41JdzPpv@mail.python.org> Author: martin.von.loewis Date: Sun Nov 20 22:20:20 2011 New Revision: 992 Added: trunk/pypi/tools/sql-migrate-20111120.sql (contents, props changed) Modified: trunk/pypi/pkgbase_schema.sql trunk/pypi/store.py trunk/pypi/webui.py Log: Use SQL store for associations. Modified: trunk/pypi/pkgbase_schema.sql ============================================================================== --- trunk/pypi/pkgbase_schema.sql (original) +++ trunk/pypi/pkgbase_schema.sql Sun Nov 20 22:20:20 2011 @@ -293,4 +293,25 @@ CONSTRAINT openid_whitelist__pkey PRIMARY KEY (name, trust_root) ); +-- tables for the python-openid library, using default table names +CREATE TABLE oid_nonces +( + server_url VARCHAR(2047) NOT NULL, + timestamp INTEGER NOT NULL, + salt CHAR(40) NOT NULL, + PRIMARY KEY (server_url, timestamp, salt) +); + +CREATE TABLE oid_associations +( + server_url VARCHAR(2047) NOT NULL, + handle VARCHAR(255) NOT NULL, + secret BYTEA NOT NULL, + issued INTEGER NOT NULL, + lifetime INTEGER NOT NULL, + assoc_type VARCHAR(64) NOT NULL, + PRIMARY KEY (server_url, handle), + CONSTRAINT secret_length_constraint CHECK (LENGTH(secret) <= 128) +); + commit; Modified: trunk/pypi/store.py ============================================================================== --- trunk/pypi/store.py (original) +++ trunk/pypi/store.py Sun Nov 20 22:20:20 2011 @@ -20,6 +20,7 @@ # csrf modules import hmac from base64 import b64encode +import openid.store.sqlstore def enumerate(sequence): return [(i, sequence[i]) for i in range(len(sequence))] @@ -2019,6 +2020,11 @@ cursor = self._cursor = self._conn.cursor() + def oid_store(self): + if self.config.database_driver == 'sqlite3': + return openid.store.sqlstore.SQLiteStore(self._conn) + return openid.store.sqlstore.PostgreSQLStore(self._conn) + def force_close(self): '''Force closure of the current persistent connection. ''' Added: trunk/pypi/tools/sql-migrate-20111120.sql ============================================================================== --- (empty file) +++ trunk/pypi/tools/sql-migrate-20111120.sql Sun Nov 20 22:20:20 2011 @@ -0,0 +1,23 @@ +begin; +CREATE TABLE oid_nonces +( + server_url VARCHAR(2047) NOT NULL, + timestamp INTEGER NOT NULL, + salt CHAR(40) NOT NULL, + PRIMARY KEY (server_url, timestamp, salt) +); + +CREATE TABLE oid_associations +( + server_url VARCHAR(2047) NOT NULL, + handle VARCHAR(255) NOT NULL, + secret BYTEA NOT NULL, + issued INTEGER NOT NULL, + lifetime INTEGER NOT NULL, + assoc_type VARCHAR(64) NOT NULL, + PRIMARY KEY (server_url, handle), + CONSTRAINT secret_length_constraint CHECK (LENGTH(secret) <= 128) +); + +GRANT ALL ON oid_nonces, oid_associations TO pypi; +commit; Modified: trunk/pypi/webui.py ============================================================================== --- trunk/pypi/webui.py (original) +++ trunk/pypi/webui.py Sun Nov 20 22:20:20 2011 @@ -29,7 +29,6 @@ # OpenId provider imports OPENID_FILESTORE = '/tmp/openid-filestore' -from openid.store.filestore import FileOpenIDStore from openid.server import server as OpenIDServer # local imports @@ -218,8 +217,6 @@ self.loggedin = False # was a valid cookie sent? self.usercookie = None self.failed = None # error message if initialization already produced a failure - op_endpoint = "%s?:action=openid_endpoint" % (self.config.url,) - self.oid_server = OpenIDServer.Server(FileOpenIDStore(OPENID_FILESTORE), op_endpoint=op_endpoint) # XMLRPC request or not? if self.env.get('CONTENT_TYPE') != 'text/xml': @@ -269,6 +266,8 @@ try: try: self.store.get_cursor() # make sure we can connect + op_endpoint = "%s?:action=openid_endpoint" % (self.config.url,) + self.oid_server = OpenIDServer.Server(self.store.oid_store(), op_endpoint=op_endpoint) self.inner_run() except NotFound, err: self.fail('Not Found (%s)' % err, code=404) From python-checkins at python.org Thu Nov 24 20:24:11 2011 From: python-checkins at python.org (martin.von.loewis) Date: Thu, 24 Nov 2011 20:24:11 +0100 (CET) Subject: [Pypi-checkins] r993 - trunk/pypi Message-ID: <3Sq9Sq3w1hzNSq@mail.python.org> Author: martin.von.loewis Date: Thu Nov 24 20:24:11 2011 New Revision: 993 Modified: trunk/pypi/webui.py Log: Catch bogus PATH_INFO. Modified: trunk/pypi/webui.py ============================================================================== --- trunk/pypi/webui.py (original) +++ trunk/pypi/webui.py Thu Nov 24 20:24:11 2011 @@ -533,6 +533,7 @@ items = path.decode('utf-8').split('/')[1:] except UnicodeError: raise NotFound(path + " is not UTF-8 encoded") + action = None if path == '/': self.form['name'] = '' action = 'index' @@ -544,6 +545,8 @@ action = 'display' if len(items) == 3 and items[2]: action = self.form[':action'] = items[2] + if not action: + raise NotFound else: action = 'home' From python-checkins at python.org Wed Nov 30 10:28:16 2011 From: python-checkins at python.org (martin.von.loewis) Date: Wed, 30 Nov 2011 10:28:16 +0100 (CET) Subject: [Pypi-checkins] r994 - trunk/pypi/tools Message-ID: <3StbyS6gVzzMBs@mail.python.org> Author: martin.von.loewis Date: Wed Nov 30 10:28:10 2011 New Revision: 994 Added: trunk/pypi/tools/sql-migrate-20111130.sql (contents, props changed) Log: Support renaming users. Added: trunk/pypi/tools/sql-migrate-20111130.sql ============================================================================== --- (empty file) +++ trunk/pypi/tools/sql-migrate-20111130.sql Wed Nov 30 10:28:10 2011 @@ -0,0 +1,4 @@ +begin; +alter table journals drop constraint "$1"; +alter table journals add foreign key (submitted_by) references users (name) on update cascade; +end; From python-checkins at python.org Wed Nov 30 10:30:39 2011 From: python-checkins at python.org (martin.von.loewis) Date: Wed, 30 Nov 2011 10:30:39 +0100 (CET) Subject: [Pypi-checkins] r995 - trunk/pypi/tools Message-ID: <3Stc1C2WLVzMBs@mail.python.org> Author: martin.von.loewis Date: Wed Nov 30 10:30:39 2011 New Revision: 995 Modified: trunk/pypi/tools/sql-migrate-20111130.sql Log: Support renaming users for roles. Modified: trunk/pypi/tools/sql-migrate-20111130.sql ============================================================================== --- trunk/pypi/tools/sql-migrate-20111130.sql (original) +++ trunk/pypi/tools/sql-migrate-20111130.sql Wed Nov 30 10:30:39 2011 @@ -2,3 +2,7 @@ alter table journals drop constraint "$1"; alter table journals add foreign key (submitted_by) references users (name) on update cascade; end; +begin; +alter table roles drop constraint "$1"; +alter table roles add foreign key (user_name) references users (name) on update cascade; +end; From python-checkins at python.org Wed Nov 30 10:32:36 2011 From: python-checkins at python.org (martin.von.loewis) Date: Wed, 30 Nov 2011 10:32:36 +0100 (CET) Subject: [Pypi-checkins] r996 - trunk/pypi/tools Message-ID: <3Stc3S3ZZjzMGh@mail.python.org> Author: martin.von.loewis Date: Wed Nov 30 10:32:36 2011 New Revision: 996 Modified: trunk/pypi/tools/sql-migrate-20111130.sql Log: Support renaming users in cookies Modified: trunk/pypi/tools/sql-migrate-20111130.sql ============================================================================== --- trunk/pypi/tools/sql-migrate-20111130.sql (original) +++ trunk/pypi/tools/sql-migrate-20111130.sql Wed Nov 30 10:32:36 2011 @@ -6,3 +6,7 @@ alter table roles drop constraint "$1"; alter table roles add foreign key (user_name) references users (name) on update cascade; end; +begin; +alter table cookies drop constraint cookies_name_fkey; +alter table cookies add foreign key (name) references users (name) on update cascade; +end; From python-checkins at python.org Wed Nov 30 10:42:20 2011 From: python-checkins at python.org (martin.von.loewis) Date: Wed, 30 Nov 2011 10:42:20 +0100 (CET) Subject: [Pypi-checkins] r997 - trunk/pypi/tools Message-ID: <3StcGh5lMGzLx0@mail.python.org> Author: martin.von.loewis Date: Wed Nov 30 10:42:20 2011 New Revision: 997 Modified: trunk/pypi/tools/sql-migrate-20111130.sql Log: Change more user name foreign keys to be updatable. Modified: trunk/pypi/tools/sql-migrate-20111130.sql ============================================================================== --- trunk/pypi/tools/sql-migrate-20111130.sql (original) +++ trunk/pypi/tools/sql-migrate-20111130.sql Wed Nov 30 10:42:20 2011 @@ -8,5 +8,17 @@ end; begin; alter table cookies drop constraint cookies_name_fkey; -alter table cookies add foreign key (name) references users (name) on update cascade; +alter table cookies add foreign key (name) references users (name) on update cascade on delete cascade; +end; +begin; +alter table openids drop constraint openids_name_fkey; +alter table openids add foreign key (name) references users (name) on update cascade on delete cascade; +end; +begin; +alter table sshkeys drop constraint sshkeys_name_fkey; +alter table sshkeys add foreign key (name) references users (name) on update cascade on delete cascade; +end; +begin; +alter table csrf_tokens drop constraint csrf_tokens_name_fkey; +alter table csrf_tokens add foreign key (name) references users (name) on update cascade on delete cascade; end; From python-checkins at python.org Wed Nov 30 10:48:55 2011 From: python-checkins at python.org (martin.von.loewis) Date: Wed, 30 Nov 2011 10:48:55 +0100 (CET) Subject: [Pypi-checkins] r998 - trunk/pypi Message-ID: <3StcQH2D0RzMqY@mail.python.org> Author: martin.von.loewis Date: Wed Nov 30 10:48:55 2011 New Revision: 998 Modified: trunk/pypi/pkgbase_schema.sql Log: Reflect additions of cascaded updates. Modified: trunk/pypi/pkgbase_schema.sql ============================================================================== --- trunk/pypi/pkgbase_schema.sql (original) +++ trunk/pypi/pkgbase_schema.sql Wed Nov 30 10:48:55 2011 @@ -13,7 +13,7 @@ CREATE TABLE openids ( id TEXT PRIMARY KEY, - name TEXT REFERENCES users ON DELETE CASCADE + name TEXT REFERENCES users ON UPDATE CASCADE ON DELETE CASCADE ); CREATE TABLE openid_discovered ( @@ -41,20 +41,21 @@ CREATE TABLE cookies ( cookie text PRIMARY KEY, - name text references users, + name text references users ON UPDATE CASCADE ON DELETE CASCADE, last_seen timestamp ); CREATE INDEX cookies_last_seen ON cookies(last_seen); CREATE TABLE sshkeys( id SERIAL PRIMARY KEY, - name TEXT REFERENCES users ON DELETE CASCADE, + name TEXT REFERENCES users ON UPDATE CASCADE ON DELETE CASCADE, key TEXT ); CREATE INDEX sshkeys_name ON sshkeys(name); -- Table structure for table: rego_otk CREATE TABLE rego_otk ( + -- not cascading: rego_otk will have to expire to allow user name changes name TEXT REFERENCES users, otk TEXT UNIQUE, date TIMESTAMP ); @@ -67,7 +68,8 @@ version TEXT, action TEXT, submitted_date TIMESTAMP, - submitted_by TEXT REFERENCES users, + -- no cascaded delete: need to check whether journal has useful information + submitted_by TEXT REFERENCES users ON UPDATE CASCADED, submitted_from TEXT ); CREATE INDEX journals_name_idx ON journals(name); @@ -219,7 +221,8 @@ -- Note: roles are Maintainer, Admin, Owner CREATE TABLE roles ( role_name TEXT, - user_name TEXT REFERENCES users, + -- no cascaded delete: user needs to drop all roles explicitly + user_name TEXT REFERENCES users ON UPDATE CASCADE, package_name TEXT REFERENCES packages ON UPDATE CASCADE ); CREATE INDEX roles_pack_name_idx ON roles(package_name); @@ -279,7 +282,7 @@ ); CREATE TABLE csrf_tokens ( - name text REFERENCES users(name) ON DELETE CASCADE, + name text REFERENCES users(name) ON UPDATE CASCADE ON DELETE CASCADE, token text, end_date timestamp without time zone, PRIMARY KEY(name)