Skip to content
This repository was archived by the owner on Jun 29, 2019. It is now read-only.

Commit 2756603

Browse files
committed
Merge pull request #16 from wndhydrnt/b-provider-exceptions
Proper handling of errors raised by store adapters
2 parents 48de210 + 200c4bb commit 2756603

17 files changed

+787
-858
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ dist/
1111
build/
1212
_build/
1313

14+
# coverage
15+
.coverage
16+
cover/
17+
1418
# Vagrant
1519
.vagrant/
1620
Berksfile.lock

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
Improvements:
44

55
- Corrected class references in doc strings (Josh Johnston)
6+
- Proper handling of errors raised by store adapters
67

78
Bugfixes:
89

oauth2/__init__.py

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -94,10 +94,13 @@ def user_has_denied_access(self, request):
9494
"""
9595

9696
import json
97-
from oauth2.error import OAuthInvalidError, OAuthUserError
97+
from oauth2.client_authenticator import ClientAuthenticator
98+
from oauth2.error import OAuthInvalidError, \
99+
ClientNotFoundError, OAuthInvalidNoRedirectError, UnsupportedGrantError
98100
from oauth2.web import Request, Response
99101
from oauth2.tokengenerator import Uuid4
100-
from oauth2.grant import Scope
102+
from oauth2.grant import Scope, AuthorizationCodeGrant, ImplicitGrant, \
103+
ClientCredentialsGrant, ResourceOwnerGrant, RefreshToken
101104

102105
VERSION = "0.6.0"
103106

@@ -121,15 +124,16 @@ def __init__(self, access_token_store, auth_code_store, client_store,
121124
:class:`oauth2.web.SiteAdapter`.
122125
:param token_generator: Object to generate unique tokens.
123126
:param response_class: Class of the response object.
124-
Default: :class:`oauth2.web.Response`.
127+
Defaults to :class:`oauth2.web.Response`.
125128
126129
"""
127130
self.grant_types = []
128131
self._input_handler = None
129132

130133
self.access_token_store = access_token_store
131134
self.auth_code_store = auth_code_store
132-
self.client_store = client_store
135+
self.client_authenticator = ClientAuthenticator(
136+
client_store=client_store)
133137
self.response_class = response_class
134138
self.site_adapter = site_adapter
135139
self.token_generator = token_generator
@@ -160,18 +164,25 @@ def dispatch(self, request, environ):
160164
grant_type.read_validate_params(request)
161165

162166
return grant_type.process(request, response, environ)
163-
except OAuthUserError as error:
167+
except OAuthInvalidNoRedirectError:
164168
response = self.response_class()
165-
return grant_type.redirect_oauth_error(error, response)
166-
except OAuthInvalidError as error:
169+
response.add_header("Content-Type", "text/plain")
170+
response.status_code = 400
171+
return response
172+
except OAuthInvalidError as err:
173+
print(err.error)
174+
print(err.explanation)
175+
response = self.response_class()
176+
return grant_type.handle_error(error=err, response=response)
177+
except UnsupportedGrantError:
167178
response = self.response_class()
168-
response.add_header("Content-type", "application/json")
179+
response.add_header("Content-Type", "application/json")
169180
response.status_code = 400
170-
json_body = {"error": error.error}
171-
if error.explanation is not None:
172-
json_body["error_description"] = error.explanation
181+
response.body = json.dumps({
182+
"error": "unsupported_response_type",
183+
"error_description": "Grant not supported"
184+
})
173185

174-
response.body = json.dumps(json_body)
175186
return response
176187

177188
def enable_unique_tokens(self):
@@ -196,6 +207,4 @@ def _determine_grant_type(self, request):
196207
if grant_handler is not None:
197208
return grant_handler
198209

199-
raise OAuthInvalidError(error="unsupported_response_type",
200-
explanation="Server does not support given "
201-
"response_type")
210+
raise UnsupportedGrantError

oauth2/client_authenticator.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
from oauth2.error import OAuthInvalidNoRedirectError, RedirectUriUnknown, \
2+
OAuthInvalidError, ClientNotFoundError
3+
4+
5+
class ClientAuthenticator(object):
6+
def __init__(self, client_store, source=None):
7+
"""
8+
Constructor.
9+
10+
:param client_store: An instance of :class:`oauth2.store.ClientStore`.
11+
:param source: A callable that returns a tuple
12+
(<client_id>, <client_secret>). Defaults to
13+
`oauth2.client_authenticator.request_body_source`.
14+
"""
15+
self.client_store = client_store
16+
self.source = source
17+
18+
if self.source is None:
19+
self.source = request_body_source
20+
21+
def by_identifier(self, request):
22+
"""
23+
Authenticates a client by its identifier.
24+
25+
:param request: An instance of :class:`oauth2.web.Request`.
26+
27+
:return: An instance of :class:`oauth2.datatype.Client`.
28+
:raises: :class OAuthInvalidNoRedirectError:
29+
"""
30+
client_id = request.get_param("client_id")
31+
32+
if client_id is None:
33+
raise OAuthInvalidNoRedirectError(error="missing_client_id")
34+
35+
try:
36+
client = self.client_store.fetch_by_client_id(client_id)
37+
except ClientNotFoundError:
38+
raise OAuthInvalidNoRedirectError(error="unknown_client")
39+
40+
redirect_uri = request.get_param("redirect_uri")
41+
if redirect_uri is not None:
42+
try:
43+
client.redirect_uri = redirect_uri
44+
except RedirectUriUnknown:
45+
raise OAuthInvalidNoRedirectError(
46+
error="invalid_redirect_uri")
47+
48+
return client
49+
50+
def by_identifier_secret(self, request):
51+
"""
52+
Authenticates a client by its identifier and secret (aka password).
53+
54+
:param request: An instance of :class:`oauth2.web.Request`.
55+
56+
:return: An instance of :class:`oauth2.datatype.Client`.
57+
"""
58+
client_id, client_secret = self.source(request=request)
59+
60+
try:
61+
client = self.client_store.fetch_by_client_id(client_id)
62+
except ClientNotFoundError:
63+
raise OAuthInvalidError(error="invalid_client",
64+
explanation="No client found")
65+
66+
grant_type = request.post_param("grant_type")
67+
if client.grant_type_supported(grant_type) is False:
68+
raise OAuthInvalidError(error="unauthorized_client",
69+
explanation="Grant type not allowed")
70+
71+
if client.secret != client_secret:
72+
raise OAuthInvalidError(error="invalid_client",
73+
explanation="Invalid client credentials")
74+
75+
return client
76+
77+
78+
def request_body_source(request):
79+
client_id = request.post_param("client_id")
80+
if client_id is None:
81+
raise OAuthInvalidError(error="invalid_request",
82+
explanation="Missing client identifier")
83+
84+
client_secret = request.post_param("client_secret")
85+
if client_secret is None:
86+
raise OAuthInvalidError(error="invalid_request",
87+
explanation="Missing client credentials")
88+
89+
return client_id, client_secret

oauth2/datatype.py

Lines changed: 68 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
"""
55

66
import time
7+
from oauth2.error import RedirectUriUnknown
8+
79

810
class AccessToken(object):
911
"""
@@ -24,7 +26,7 @@ def __init__(self, client_id, grant_type, token, data={}, expires_at=None,
2426
def expires_in(self):
2527
"""
2628
Returns the time until the token expires.
27-
29+
2830
:return: The remaining time until expiration in seconds or 0 if the
2931
token has expired.
3032
"""
@@ -37,7 +39,7 @@ def expires_in(self):
3739
def is_expired(self):
3840
"""
3941
Determines if the token has expired.
40-
42+
4143
:return: `True` if the token has expired. Otherwise `False`.
4244
"""
4345
if self.expires_at is None:
@@ -48,14 +50,6 @@ def is_expired(self):
4850

4951
return True
5052

51-
def to_json(self):
52-
json = {"access_token": self.token, "token_type": "Bearer"}
53-
54-
if self.refresh_token is not None:
55-
json["refresh_token"] = self.refresh_token
56-
json["expires_in"] = self.expires_in
57-
58-
return json
5953

6054
class AuthorizationCode(object):
6155
"""
@@ -76,17 +70,76 @@ def is_expired(self):
7670
return True
7771
return False
7872

73+
7974
class Client(object):
8075
"""
8176
Representation of a client application.
8277
"""
83-
def __init__(self, identifier, secret, redirect_uris=[]):
78+
def __init__(self, identifier, secret, authorized_grants=None,
79+
authorized_response_types=None, redirect_uris=None):
80+
"""
81+
:param identifier: The unique identifier of a client.
82+
:param secret: The secret the clients uses to authenticate.
83+
:param authorized_grants: A list of grants under which the client can
84+
request tokens.
85+
All grants are allowed if this value is set
86+
to `None` (default).
87+
:param authorized_response_types: A list of response types of which
88+
the client can request tokens.
89+
All response types are allowed if
90+
this value is set to `None`
91+
(default).
92+
:redirect_uris: A list of redirect uris this client can use.
93+
"""
94+
self.authorized_grants = authorized_grants
95+
self.authorized_response_types = authorized_response_types
8496
self.identifier = identifier
8597
self.secret = secret
86-
self.redirect_uris = redirect_uris
8798

88-
def has_redirect_uri(self, uri):
99+
if redirect_uris is None:
100+
self.redirect_uris = []
101+
else:
102+
self.redirect_uris = redirect_uris
103+
104+
self._redirect_uri = None
105+
106+
@property
107+
def redirect_uri(self):
108+
if self._redirect_uri is None:
109+
# redirect_uri is an optional param.
110+
# If not supplied, we use the first entry stored in db as default.
111+
return self.redirect_uris[0]
112+
return self._redirect_uri
113+
114+
@redirect_uri.setter
115+
def redirect_uri(self, value):
116+
if value not in self.redirect_uris:
117+
raise RedirectUriUnknown
118+
self._redirect_uri = value
119+
120+
def grant_type_supported(self, grant_type):
121+
"""
122+
Checks if the Client is authorized receive tokens for the given grant.
123+
124+
:param grant_type: The type of the grant.
125+
126+
:return: Boolean
127+
"""
128+
if self.authorized_grants is None:
129+
return True
130+
131+
return grant_type in self.authorized_grants
132+
133+
def response_type_supported(self, response_type):
89134
"""
90-
Checks if a uri is associated with the client.
135+
Checks if the client is allowed to receive tokens for the given
136+
response type.
137+
138+
:param response_type: The response type.
139+
140+
:return: Boolean
91141
"""
92-
return uri in self.redirect_uris
142+
if self.authorized_response_types is None:
143+
return True
144+
145+
return response_type in self.authorized_response_types

0 commit comments

Comments
 (0)