patternpythonMinor
OAuth2 Implementation
Viewed 0 times
implementationoauth2stackoverflow
Problem
I am trying to implement an OAuth2 service. I am not planning to use it anytime soon (like will write unit tests). This is my fun project I do to practice. I have used the python_jwt library to create and validate signatures.
OAuth Request handler
```
"""
Copyright (C) 2016 Maruf Maniruzzaman
Website: https://github.com/kuasha/cosmos
Author: Maruf Maniruzzaman
License :: OSI Approved :: MIT License
This service handler follows the style found in Azure (https://msdn.microsoft.com/en-us/library/azure/dn645542.aspx)
Also for more information look at The OAuth 2.0 Authorization Framework (http://tools.ietf.org/html/rfc6749)
"""
import ast
import base64
import json
import datetime
import uuid
import tornado.web
from tornado import gen
import logging
from cosmos.rbac.object import COSMOS_USERS_OBJECT_NAME
from cosmos.auth.oauth2 import get_token
from cosmos.rbac.object import SYSTEM_USER
try:
import urlparse # py2
from urllib import urlencode
import urllib as urllib_parse
except ImportError:
import urllib.parse as urlparse # py3
from urllib.parse import urlencode
import urllib.parse as urllib_parse
from cosmos.service.requesthandler import RequestHandler
OAUTH2_REQUESTS_OBJECT_NAME = "cosmos.auth.oauth2.requests"
OAUTH2_TOKENS_OBJECT_NAME = "cosmos.auth.oauth2.tokens"
OAUTH2_CODE_STATUS_OBJECT_NAME = "cosmos.auth.oauth2.codestatus"
OAUTH2_MAX_CODE_RESPONSE_LENGTH = 200
class OAuth2RequestException(Exception):
pass
#TODO: security check for entire class
class OAuth2ServiceHandler(RequestHandler):
@gen.coroutine
def get(self, tenant_id, function):
try:
req_id = None
auth_request = None
serve_request = self.get_argument("serve_request", None)
if not (serve_request):
auth_request = self._collect_auth_request_parameters(tenant_id, function)
req_id = yield self.insert_auth_request(auth_request)
if not req_id:
OAuth Request handler
```
"""
Copyright (C) 2016 Maruf Maniruzzaman
Website: https://github.com/kuasha/cosmos
Author: Maruf Maniruzzaman
License :: OSI Approved :: MIT License
This service handler follows the style found in Azure (https://msdn.microsoft.com/en-us/library/azure/dn645542.aspx)
Also for more information look at The OAuth 2.0 Authorization Framework (http://tools.ietf.org/html/rfc6749)
"""
import ast
import base64
import json
import datetime
import uuid
import tornado.web
from tornado import gen
import logging
from cosmos.rbac.object import COSMOS_USERS_OBJECT_NAME
from cosmos.auth.oauth2 import get_token
from cosmos.rbac.object import SYSTEM_USER
try:
import urlparse # py2
from urllib import urlencode
import urllib as urllib_parse
except ImportError:
import urllib.parse as urlparse # py3
from urllib.parse import urlencode
import urllib.parse as urllib_parse
from cosmos.service.requesthandler import RequestHandler
OAUTH2_REQUESTS_OBJECT_NAME = "cosmos.auth.oauth2.requests"
OAUTH2_TOKENS_OBJECT_NAME = "cosmos.auth.oauth2.tokens"
OAUTH2_CODE_STATUS_OBJECT_NAME = "cosmos.auth.oauth2.codestatus"
OAUTH2_MAX_CODE_RESPONSE_LENGTH = 200
class OAuth2RequestException(Exception):
pass
#TODO: security check for entire class
class OAuth2ServiceHandler(RequestHandler):
@gen.coroutine
def get(self, tenant_id, function):
try:
req_id = None
auth_request = None
serve_request = self.get_argument("serve_request", None)
if not (serve_request):
auth_request = self._collect_auth_request_parameters(tenant_id, function)
req_id = yield self.insert_auth_request(auth_request)
if not req_id:
Solution
Second part first:
immediately returned. I get that it might be nice in a debugger(?)
but there's really no reason for this - just return the values. I'd
possibly create a function that wraps other functions in such an
exception handler, but that's probably overkill here.
particular reason why you'd specify that everywhere. Defaults are
really useful to remove that clutter from your code.
(i.e.
in the next point.
-
The repeated pattern with
should also be refactored to remove repetition, e.g. by using a loop
instead:
-
I'd also reorder code fragments that belong together. At the moment
the reader is jumping from
of
to last line, while
move the
operation is clearly distinguished.
That leaves the following (could probably still be improved with more
factoring of code, but hey, looks compact enough to me):
Now for the first part. Well that's a lot of code. Okay, so small
stuff:
them out.
calls can thus be written like
-
the six coroutine methods look awfully similar. I'm not too familiar
with generators or the framework, so this could be wrong:
This helper would then reused from the individual methods (though at
that point you could as well use that method directly?):
-
In
could be a bit more compact:
-
Having these giant
indentation. I'd suggest that the inner part is either compacted so
much as to fit into one screen, or moved into its own method.
from the
Initialising variables to
removing all the code moving values around between the dictionaries.
And again, sprinkling assignments out over the method like this makes
for a great treasure hunt, but doesn't help understanding the code too
much. Reassigning
it looks mostly like copy & paste from the method before. IMO that's
the biggest mistake as it requires a lot of attention when trying to
deduplicate it again. It's better to spend the effort creating a
"DRY" implementation from the start.
Actually I got confused when trying to clean up
readers might have a similar problem. I strongly suggest to
aggressively factor out common functions and creating more methods,
helper functions, whatever to help keep the line count and number of
variables to read down.
- In
verify_tokenthe return values are assigned - only to be
immediately returned. I get that it might be nice in a debugger(?)
but there's really no reason for this - just return the values. I'd
possibly create a function that wraps other functions in such an
exception handler, but that's probably overkill here.
dict.getalready defaults to returningNone- I don't see a
particular reason why you'd specify that everywhere. Defaults are
really useful to remove that clutter from your code.
- I'd also inline a lot of those variables to make the methods shorter
(i.e.
aud, exp and so on), or create a similar loop as describedin the next point.
-
The repeated pattern with
x = kwargs.get("x"); if x: ...["x"] = xshould also be refactored to remove repetition, e.g. by using a loop
instead:
for name in ["iss", "nbf", "oid", "sub", "tid", "unique_name", "upn"]:
x = kwargs.get(name)
if x:
token_payload[name] = x
-
I'd also reorder code fragments that belong together. At the moment
the reader is jumping from
service_private_pem to the initialisationof
token_payload to only go back to the first variable in the secondto last line, while
token_payload is used in the last line. I'dmove the
RSA.importKey after the first block, then handle thetoken_payload and all associated things after, that way the order ofoperation is clearly distinguished.
That leaves the following (could probably still be improved with more
factoring of code, but hey, looks compact enough to me):
def get_token(**kwargs):
service_private_pem = kwargs.get("service_private_pem")
if not service_private_pem:
raise ValueError("service_private_pem is not defined")
priv_key = RSA.importKey(service_private_pem)
token_payload = {"ver": "1.0"}
for name in ["aud", "exp", "family_name", "given_name"]:
token_payload[name] = kwargs.get(name)
for name in ["iss", "nbf", "oid", "sub", "tid", "unique_name", "upn"]:
x = kwargs.get(name)
if x:
token_payload[name] = x
return jwt.generate_jwt(token_payload, priv_key, 'RS256', exp)
Now for the first part. Well that's a lot of code. Okay, so small
stuff:
- Empty returns with no other afterwards don't do anything, so I'd leave
them out.
loggingalready supports arguments using%sand friends - the
calls can thus be written like
logging.debug("... %s", req_id) etc.-
the six coroutine methods look awfully similar. I'm not too familiar
with generators or the framework, so this could be wrong:
def _request_helper(self, *args):
obj_serv = self.settings["object_service"]
promise = obj_serv.insert(SYSTEM_USER, OAUTH2_REQUESTS_OBJECT_NAME, *args)
req_id = yield promise
return req_id
This helper would then reused from the individual methods (though at
that point you could as well use that method directly?):
@gen.coroutine
def insert_auth_request(self, params):
return self._request_helper(params)
-
In
_collect_auth_request_parameters the update of the dictionarycould be a bit more compact:
def _collect_auth_request_parameters(self, tenant_id, function):
params = {k: self.get_argument(k) for k in self.request.arguments}
params.update({
"request_protocol": self.request.protocol,
"request_host": self.request.host,
"request_uri": self.request.uri,
"tenant_id": tenant_id,
"function": function
})
return params
-
Having these giant
try/except blocks is just increasing theindentation. I'd suggest that the inner part is either compacted so
much as to fit into one screen, or moved into its own method.
- Similarly having the
returncases early can remove the indentation
from the
else branch, which makes the flow also a bit more clear.Initialising variables to
None is also mostly not that useful._do_authorizeis just convoluted and can be cleaned up a lot by
removing all the code moving values around between the dictionaries.
And again, sprinkling assignments out over the method like this makes
for a great treasure hunt, but doesn't help understanding the code too
much. Reassigning
params also tripped me up during rewriting._do_tokenis similarly just too much - perhaps no wonder considering
it looks mostly like copy & paste from the method before. IMO that's
the biggest mistake as it requires a lot of attention when trying to
deduplicate it again. It's better to spend the effort creating a
"DRY" implementation from the start.
- The exceptions are mostly similar and can be moved into methods.
Actually I got confused when trying to clean up
_do_authorize and_do_token, so I'll give up on that for now. I can imagine that otherreaders might have a similar problem. I strongly suggest to
aggressively factor out common functions and creating more methods,
helper functions, whatever to help keep the line count and number of
variables to read down.
Context
StackExchange Code Review Q#124966, answer score: 5
Revisions (0)
No revisions yet.