~gmb/launchpad/bug-644346

« back to all changes in this revision

Viewing changes to lib/canonical/launchpad/webapp/authentication.py

  • Committer: Graham Binns
  • Date: 2010-09-23 08:59:28 UTC
  • mfrom: (11575.1.37 launchpad)
  • Revision ID: graham@canonical.com-20100923085928-4x8vlt72gvzppb11
Merged devel.

Show diffs side-by-side

added added

removed removed

Lines of Context:
5
5
 
6
6
__all__ = [
7
7
    'check_oauth_signature',
8
 
    'extract_oauth_access_token',
9
 
    'get_oauth_principal',
10
8
    'get_oauth_authorization',
11
9
    'LaunchpadLoginSource',
12
10
    'LaunchpadPrincipal',
13
 
    'OAuthSignedRequest',
14
11
    'PlacelessAuthUtility',
15
12
    'SSHADigestEncryptor',
16
13
    ]
17
14
 
18
15
 
19
16
import binascii
20
 
from datetime import datetime
21
17
import hashlib
22
 
import pytz
23
18
import random
24
19
from UserDict import UserDict
25
20
 
28
23
from zope.app.security.interfaces import ILoginPassword
29
24
from zope.app.security.principalregistry import UnauthenticatedPrincipal
30
25
from zope.authentication.interfaces import IUnauthenticatedPrincipal
31
 
 
32
26
from zope.component import (
33
27
    adapts,
34
28
    getUtility,
35
29
    )
36
30
from zope.event import notify
37
 
from zope.interface import (
38
 
    alsoProvides,
39
 
    implements,
40
 
    )
 
31
from zope.interface import implements
41
32
from zope.preference.interfaces import IPreferenceGroup
42
 
from zope.security.interfaces import Unauthorized
43
33
from zope.security.proxy import removeSecurityProxy
44
34
from zope.session.interfaces import ISession
45
35
 
54
44
    ILaunchpadPrincipal,
55
45
    IPlacelessAuthUtility,
56
46
    IPlacelessLoginSource,
57
 
    OAuthPermission,
58
 
    )
59
 
from canonical.launchpad.interfaces.oauth import (
60
 
    ClockSkew,
61
 
    IOAuthConsumerSet,
62
 
    IOAuthSignedRequest,
63
 
    NonceAlreadyUsed,
64
 
    TimestampOrderingError,
65
47
    )
66
48
from lp.registry.interfaces.person import (
67
49
    IPerson,
69
51
    )
70
52
 
71
53
 
72
 
def extract_oauth_access_token(request):
73
 
    """Find the OAuth access token that signed the given request.
74
 
 
75
 
    :param request: An incoming request.
76
 
 
77
 
    :return: an IOAuthAccessToken, or None if the request is not
78
 
        signed at all.
79
 
 
80
 
    :raise Unauthorized: If the token is invalid or the request is an
81
 
        anonymously-signed request that doesn't meet our requirements.
82
 
    """
83
 
    # Fetch OAuth authorization information from the request.
84
 
    form = get_oauth_authorization(request)
85
 
 
86
 
    consumer_key = form.get('oauth_consumer_key')
87
 
    consumers = getUtility(IOAuthConsumerSet)
88
 
    consumer = consumers.getByKey(consumer_key)
89
 
    token_key = form.get('oauth_token')
90
 
    anonymous_request = (token_key == '')
91
 
 
92
 
    if consumer_key is None:
93
 
        # Either the client's OAuth implementation is broken, or
94
 
        # the user is trying to make an unauthenticated request
95
 
        # using wget or another OAuth-ignorant application.
96
 
        # Try to retrieve a consumer based on the User-Agent
97
 
        # header.
98
 
        anonymous_request = True
99
 
        consumer_key = request.getHeader('User-Agent', '')
100
 
        if consumer_key == '':
101
 
            raise Unauthorized(
102
 
                'Anonymous requests must provide a User-Agent.')
103
 
        consumer = consumers.getByKey(consumer_key)
104
 
 
105
 
    if consumer is None:
106
 
        if anonymous_request:
107
 
            # This is the first time anyone has tried to make an
108
 
            # anonymous request using this consumer name (or user
109
 
            # agent). Dynamically create the consumer.
110
 
            #
111
 
            # In the normal website this wouldn't be possible
112
 
            # because GET requests have their transactions rolled
113
 
            # back. But webservice requests always have their
114
 
            # transactions committed so that we can keep track of
115
 
            # the OAuth nonces and prevent replay attacks.
116
 
            if consumer_key == '' or consumer_key is None:
117
 
                raise Unauthorized("No consumer key specified.")
118
 
            consumer = consumers.new(consumer_key, '')
119
 
        else:
120
 
            # An unknown consumer can never make a non-anonymous
121
 
            # request, because access tokens are registered with a
122
 
            # specific, known consumer.
123
 
            raise Unauthorized('Unknown consumer (%s).' % consumer_key)
124
 
    if anonymous_request:
125
 
        # Skip the OAuth verification step and let the user access the
126
 
        # web service as an unauthenticated user.
127
 
        #
128
 
        # XXX leonardr 2009-12-15 bug=496964: Ideally we'd be
129
 
        # auto-creating a token for the anonymous user the first
130
 
        # time, passing it through the OAuth verification step,
131
 
        # and using it on all subsequent anonymous requests.
132
 
        return None
133
 
 
134
 
    token = consumer.getAccessToken(token_key)
135
 
    if token is None:
136
 
        raise Unauthorized('Unknown access token (%s).' % token_key)
137
 
    return token
138
 
 
139
 
 
140
 
def get_oauth_principal(request):
141
 
    """Find the principal to use for this OAuth-signed request.
142
 
 
143
 
    :param request: An incoming request.
144
 
    :return: An ILaunchpadPrincipal with the appropriate access level.
145
 
    """
146
 
    token = extract_oauth_access_token(request)
147
 
 
148
 
    if token is None:
149
 
        # The consumer is making an anonymous request. If there was a
150
 
        # problem with the access token, extract_oauth_access_token
151
 
        # would have raised Unauthorized.
152
 
        alsoProvides(request, IOAuthSignedRequest)
153
 
        auth_utility = getUtility(IPlacelessAuthUtility)
154
 
        return auth_utility.unauthenticatedPrincipal()
155
 
 
156
 
    form = get_oauth_authorization(request)
157
 
    nonce = form.get('oauth_nonce')
158
 
    timestamp = form.get('oauth_timestamp')
159
 
    try:
160
 
        token.checkNonceAndTimestamp(nonce, timestamp)
161
 
    except (NonceAlreadyUsed, TimestampOrderingError, ClockSkew), e:
162
 
        raise Unauthorized('Invalid nonce/timestamp: %s' % e)
163
 
    now = datetime.now(pytz.timezone('UTC'))
164
 
    if token.permission == OAuthPermission.UNAUTHORIZED:
165
 
        raise Unauthorized('Unauthorized token (%s).' % token.key)
166
 
    elif token.date_expires is not None and token.date_expires <= now:
167
 
        raise Unauthorized('Expired token (%s).' % token.key)
168
 
    elif not check_oauth_signature(request, token.consumer, token):
169
 
        raise Unauthorized('Invalid signature.')
170
 
    else:
171
 
        # Everything is fine, let's return the principal.
172
 
        pass
173
 
    alsoProvides(request, IOAuthSignedRequest)
174
 
    return getUtility(IPlacelessLoginSource).getPrincipal(
175
 
        token.person.account.id, access_level=token.permission,
176
 
        scope=token.context)
177
 
 
178
 
 
179
54
class PlacelessAuthUtility:
180
55
    """An authentication service which holds no state aside from its
181
56
    ZCML configuration, implemented as a utility.
200
75
                    # as the login form is never visited for BasicAuth.
201
76
                    # This we treat each request as a separate
202
77
                    # login/logout.
203
 
                    notify(
204
 
                        BasicAuthLoggedInEvent(request, login, principal))
 
78
                    notify(BasicAuthLoggedInEvent(
 
79
                        request, login, principal
 
80
                        ))
205
81
                    return principal
206
82
 
207
83
    def _authenticateUsingCookieAuth(self, request):
314
190
        plaintext = str(plaintext)
315
191
        if salt is None:
316
192
            salt = self.generate_salt()
317
 
        v = binascii.b2a_base64(
318
 
            hashlib.sha1(plaintext + salt).digest() + salt)
 
193
        v = binascii.b2a_base64(hashlib.sha1(plaintext + salt).digest() + salt)
319
194
        return v[:-1]
320
195
 
321
196
    def validate(self, plaintext, encrypted):
459
334
 
460
335
# zope.app.apidoc expects our principals to be adaptable into IAnnotations, so
461
336
# we use these dummy adapters here just to make that code not OOPS.
462
 
 
463
337
class TemporaryPrincipalAnnotations(UserDict):
464
338
    implements(IAnnotations)
465
339
    adapts(ILaunchpadPrincipal, IPreferenceGroup)