Index: third_party/google-endpoints/google/api/auth/suppliers.py |
diff --git a/third_party/google-endpoints/google/api/auth/suppliers.py b/third_party/google-endpoints/google/api/auth/suppliers.py |
new file mode 100644 |
index 0000000000000000000000000000000000000000..284f3f75d7e09e407e640b497db4ad82e3453c75 |
--- /dev/null |
+++ b/third_party/google-endpoints/google/api/auth/suppliers.py |
@@ -0,0 +1,200 @@ |
+# Copyright 2016 Google Inc. All Rights Reserved. |
+# |
+# Licensed under the Apache License, Version 2.0 (the "License"); |
+# you may not use this file except in compliance with the License. |
+# You may obtain a copy of the License at |
+# |
+# http://www.apache.org/licenses/LICENSE-2.0 |
+# |
+# Unless required by applicable law or agreed to in writing, software |
+# distributed under the License is distributed on an "AS IS" BASIS, |
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
+# See the License for the specific language governing permissions and |
+# limitations under the License. |
+ |
+"""Defines several suppliers that are used by the authenticator.""" |
+ |
+import datetime |
+from dogpile import cache |
+from jwkest import jwk |
+import requests |
+import ssl |
+ |
+ |
+_HTTP_PROTOCOL_PREFIX = "http://" |
+_HTTPS_PROTOCOL_PREFIX = "https://" |
+ |
+_OPEN_ID_CONFIG_PATH = ".well-known/openid-configuration" |
+ |
+ |
+class KeyUriSupplier(object): # pylint: disable=too-few-public-methods |
+ """A supplier that provides the `jwks_uri` for an issuer.""" |
+ |
+ def __init__(self, issuer_uri_configs): |
+ """Construct an instance of KeyUriSupplier. |
+ |
+ Args: |
+ issuer_uri_configs: a dictionary mapping from an issuer to its jwks_uri |
+ configuration. |
+ """ |
+ self._issuer_uri_configs = issuer_uri_configs |
+ |
+ def supply(self, issuer): |
+ """Supplies the `jwks_uri` for the given issuer. |
+ |
+ Args: |
+ issuer: the issuer. |
+ |
+ Returns: |
+ The `jwks_uri` that is either statically configured or retrieved via |
+ OpenId discovery. None is returned when the issuer is unknown or the |
+ OpenId discovery fails. |
+ """ |
+ issuer_uri_config = self._issuer_uri_configs.get(issuer) |
+ |
+ if not issuer_uri_config: |
+ # The issuer is unknown. |
+ return |
+ |
+ jwks_uri = issuer_uri_config.jwks_uri |
+ if jwks_uri: |
+ # When jwks_uri is set, return it directly. |
+ return jwks_uri |
+ |
+ # When jwksUri is empty, we try to retrieve it through the OpenID |
+ # discovery. |
+ open_id_valid = issuer_uri_config.open_id_valid |
+ if open_id_valid: |
+ discovered_jwks_uri = _discover_jwks_uri(issuer) |
+ self._issuer_uri_configs[issuer] = IssuerUriConfig(False, |
+ discovered_jwks_uri) |
+ return discovered_jwks_uri |
+ |
+ |
+class JwksSupplier(object): # pylint: disable=too-few-public-methods |
+ """A supplier that returns the Json Web Token Set of an issuer.""" |
+ |
+ def __init__(self, key_uri_supplier): |
+ """Constructs an instance of JwksSupplier. |
+ |
+ Args: |
+ key_uri_supplier: a KeyUriSupplier instance that returns the `jwks_uri` |
+ based on the given issuer. |
+ """ |
+ self._key_uri_supplier = key_uri_supplier |
+ self._jwks_cache = cache.make_region().configure( |
+ "dogpile.cache.memory", expiration_time=datetime.timedelta(minutes=5)) |
+ |
+ def supply(self, issuer): |
+ """Supplies the `Json Web Key Set` for the given issuer. |
+ |
+ Args: |
+ issuer: the issuer. |
+ |
+ Returns: |
+ The successfully retrieved Json Web Key Set. None is returned if the |
+ issuer is unknown or the retrieval process fails. |
+ |
+ Raises: |
+ UnauthenticatedException: When this method cannot supply JWKS for the |
+ given issuer (e.g. unknown issuer, HTTP request error). |
+ """ |
+ def _retrieve_jwks(): |
+ """Retrieve the JWKS from the given jwks_uri when cache misses.""" |
+ jwks_uri = self._key_uri_supplier.supply(issuer) |
+ |
+ if not jwks_uri: |
+ raise UnauthenticatedException("Cannot find the `jwks_uri` for issuer " |
+ "%s: either the issuer is unknown or " |
+ "the OpenID discovery failed" % issuer) |
+ |
+ try: |
+ response = requests.get(jwks_uri) |
+ json_response = response.json() |
+ except Exception as exception: |
+ message = "Cannot retrieve valid verification keys from the `jwks_uri`" |
+ raise UnauthenticatedException(message, exception) |
+ |
+ if "keys" in json_response: |
+ # De-serialize the JSON as a JWKS object. |
+ jwks_keys = jwk.KEYS() |
+ jwks_keys.load_jwks(response.text) |
+ return jwks_keys._keys |
+ else: |
+ # The JSON is a dictionary mapping from key id to X.509 certificates. |
+ # Thus we extract the public key from the X.509 certificates and |
+ # construct a JWKS object. |
+ return _extract_x509_certificates(json_response) |
+ |
+ return self._jwks_cache.get_or_create(issuer, _retrieve_jwks) |
+ |
+ |
+def _extract_x509_certificates(x509_certificates): |
+ keys = [] |
+ for kid, certificate in x509_certificates.iteritems(): |
+ try: |
+ if certificate.startswith(jwk.PREFIX): |
+ # The certificate is PEM-encoded |
+ der = ssl.PEM_cert_to_DER_cert(certificate) |
+ key = jwk.der2rsa(der) |
+ else: |
+ key = jwk.import_rsa_key(certificate) |
+ except Exception as exception: |
+ raise UnauthenticatedException("Cannot load X.509 certificate", |
+ exception) |
+ rsa_key = jwk.RSAKey().load_key(key) |
+ rsa_key.kid = kid |
+ keys.append(rsa_key) |
+ return keys |
+ |
+ |
+def _discover_jwks_uri(issuer): |
+ open_id_url = _construct_open_id_url(issuer) |
+ try: |
+ response = requests.get(open_id_url) |
+ return response.json().get("jwks_uri") |
+ except Exception as error: |
+ raise UnauthenticatedException("Cannot discover the jwks uri", error) |
+ |
+ |
+def _construct_open_id_url(issuer): |
+ url = issuer |
+ if (not url.startswith(_HTTP_PROTOCOL_PREFIX) and |
+ not url.startswith(_HTTPS_PROTOCOL_PREFIX)): |
+ url = _HTTPS_PROTOCOL_PREFIX + url |
+ if not url.endswith("/"): |
+ url += "/" |
+ url += _OPEN_ID_CONFIG_PATH |
+ return url |
+ |
+ |
+class IssuerUriConfig(object): |
+ """The jwks_uri configuration for an issuer. |
+ |
+ TODO (yangguan): this class should be removed after we figure out how to |
+ fetch the external configs. |
+ """ |
+ |
+ def __init__(self, open_id_valid, jwks_uri): |
+ """Create an instance of IsserUriConfig. |
+ |
+ Args: |
+ open_id_valid: indicates whether the corresponding issuer is valid for |
+ OpenId discovery. |
+ jwks_uri: is the saved jwks_uri. Its value can be None if the OpenId |
+ discovery process has not begun or has already failed. |
+ """ |
+ self._open_id_valid = open_id_valid |
+ self._jwks_uri = jwks_uri |
+ |
+ @property |
+ def open_id_valid(self): |
+ return self._open_id_valid |
+ |
+ @property |
+ def jwks_uri(self): |
+ return self._jwks_uri |
+ |
+ |
+class UnauthenticatedException(Exception): |
+ pass |