Skip to content

Commit

Permalink
Add oauth metadata discovery as per RFC8414
Browse files Browse the repository at this point in the history
  • Loading branch information
djw8605 committed Jul 31, 2018
1 parent 1fbf4cd commit 14f3fca
Show file tree
Hide file tree
Showing 3 changed files with 69 additions and 22 deletions.
61 changes: 41 additions & 20 deletions src/scitokens/utils/keycache.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
import cryptography.hazmat.backends as backends
import cryptography.hazmat.primitives.asymmetric.ec as ec
import cryptography.hazmat.primitives.asymmetric.rsa as rsa
from scitokens.utils.errors import MissingKeyException, NonHTTPSIssuer, UnableToCreateCache, UnsupportedKeyException
from scitokens.utils.errors import MissingKeyException, NonHTTPSIssuer, UnableToCreateCache, UnsupportedKeyException, MissingIssuerException
from scitokens.utils import long_from_bytes
import scitokens.utils.config as config

Expand Down Expand Up @@ -233,26 +233,47 @@ def _get_issuer_publickey(issuer, key_id=None, insecure=False):
headers={'User-Agent' : 'SciTokens/{}'.format(PKG_VERSION)}

# Go to the issuer's website, and download the OAuth well known bits
# https://tools.ietf.org/html/draft-ietf-oauth-discovery-07
well_known_uri = ".well-known/openid-configuration"
if not issuer.endswith("/"):
issuer = issuer + "/"
parsed_url = urlparse.urlparse(issuer)
updated_url = urlparse.urljoin(parsed_url.path, well_known_uri)
parsed_url_list = list(parsed_url)
parsed_url_list[2] = updated_url
meta_uri = urlparse.urlunparse(parsed_url_list)

# Make sure the protocol is https
if not insecure:
parsed_url = urlparse.urlparse(meta_uri)
if parsed_url.scheme != "https":
raise NonHTTPSIssuer("Issuer is not over HTTPS. RFC requires it to be over HTTPS")
response = request.urlopen(request.Request(meta_uri, headers=headers))
data = json.loads(response.read().decode('utf-8'))
# https://tools.ietf.org/html/rfc8414
# We first try the RFC's metadata location, then try the openid defined
well_known_uri = [".well-known/oauth-authorization-server", ".well-known/openid-configuration"]
last_exception = MissingIssuerException("Unable to retrieve public key from issuer")
jwks_uri = None
for uri in well_known_uri:
if not issuer.endswith("/"):
issuer = issuer + "/"
parsed_url = urlparse.urlparse(issuer)
updated_url = urlparse.urljoin(parsed_url.path, uri)
parsed_url_list = list(parsed_url)
parsed_url_list[2] = updated_url
meta_uri = urlparse.urlunparse(parsed_url_list)

# Make sure the protocol is https
if not insecure:
parsed_url = urlparse.urlparse(meta_uri)
if parsed_url.scheme != "https":
raise NonHTTPSIssuer("Issuer is not over HTTPS. RFC requires it to be over HTTPS")

# Get the keys URL from the openid-configuration
jwks_uri = data['jwks_uri']
try:
response = request.urlopen(request.Request(meta_uri, headers=headers))
except (request.HTTPError, request.URLError) as error:
logging.info("Unable to open URL: %s: %s", meta_uri, str(error))
last_exception = error
# Continue to the next URI, if there is one
continue

try:
data = json.loads(response.read().decode('utf-8'))
# Get the keys URL from the openid-configuration
jwks_uri = data['jwks_uri']
except ValueError as error:
logging.error("Unable to load JSON from URL: %s", meta_uri)
last_exception = error
continue

# Tried both URIs above, didn't get a valid response from either,
# so throw an exception
if jwks_uri is None:
raise last_exception

# Now, get the keys
if not insecure:
Expand Down
8 changes: 6 additions & 2 deletions tests/create_webserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,12 @@ def do_GET(self): # pylint: disable=invalid-name
if not user_agent.startswith("SciTokens"):
self.send_response(404)
return
self._set_headers()
to_write = ""
if self.path == "/.well-known/openid-configuration":
self._set_headers()
to_write = json.dumps({"jwks_uri": "http://localhost:{}/oauth2/certs".format(HTTPD.server_address[1])})
elif self.path == "/oauth2/certs":

self._set_headers()
# Dummy Key
dummy_key = {
'kid': 'dummykey',
Expand Down Expand Up @@ -83,6 +83,10 @@ def do_GET(self): # pylint: disable=invalid-name
to_write = json.dumps({'keys': [dummy_key, key_info, ec_key_info]})
else:
to_write = json.dumps({'keys': [dummy_key, key_info]})
# If the path isn't recognized, return 404
else:
self.send_response(404)
return

self.wfile.write(to_write.encode())

Expand Down
22 changes: 22 additions & 0 deletions tests/test_with_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
import sys
import unittest

try:
import urllib.request as request
except ImportError:
import urllib2 as request

# Allow unittests to be run from within the project base.
if os.path.exists("src"):
sys.path.append("src")
Expand Down Expand Up @@ -100,6 +105,23 @@ def test_ec_deserialization(self):
scitoken = scitokens.SciToken.deserialize(serialized_token, insecure=True)


def test_failures(self):
"""
Test HTTP failure routes
"""
server_address = start_server(self.public_numbers.n, self.public_numbers.e, self.test_id)
print(server_address)

# Give it a bad issuer
issuer = "http://localhost:{}/asdf".format(server_address[1])
token = scitokens.SciToken(key=self.private_key, key_id=self.test_id)
token.update_claims({"test": "true"})
serialized_token = token.serialize(issuer=issuer)

with self.assertRaises(request.HTTPError):
scitokens.SciToken.deserialize(serialized_token, insecure=True)



if __name__ == '__main__':
unittest.main()

0 comments on commit 14f3fca

Please sign in to comment.