OLD | NEW |
(Empty) | |
| 1 # Copyright 2016 Google Inc. All Rights Reserved. |
| 2 # |
| 3 # Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 # you may not use this file except in compliance with the License. |
| 5 # You may obtain a copy of the License at |
| 6 # |
| 7 # http://www.apache.org/licenses/LICENSE-2.0 |
| 8 # |
| 9 # Unless required by applicable law or agreed to in writing, software |
| 10 # distributed under the License is distributed on an "AS IS" BASIS, |
| 11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 # See the License for the specific language governing permissions and |
| 13 # limitations under the License. |
| 14 |
| 15 """Defines several suppliers that are used by the authenticator.""" |
| 16 |
| 17 import datetime |
| 18 from dogpile import cache |
| 19 from jwkest import jwk |
| 20 import requests |
| 21 import ssl |
| 22 |
| 23 |
| 24 _HTTP_PROTOCOL_PREFIX = "http://" |
| 25 _HTTPS_PROTOCOL_PREFIX = "https://" |
| 26 |
| 27 _OPEN_ID_CONFIG_PATH = ".well-known/openid-configuration" |
| 28 |
| 29 |
| 30 class KeyUriSupplier(object): # pylint: disable=too-few-public-methods |
| 31 """A supplier that provides the `jwks_uri` for an issuer.""" |
| 32 |
| 33 def __init__(self, issuer_uri_configs): |
| 34 """Construct an instance of KeyUriSupplier. |
| 35 |
| 36 Args: |
| 37 issuer_uri_configs: a dictionary mapping from an issuer to its jwks_uri |
| 38 configuration. |
| 39 """ |
| 40 self._issuer_uri_configs = issuer_uri_configs |
| 41 |
| 42 def supply(self, issuer): |
| 43 """Supplies the `jwks_uri` for the given issuer. |
| 44 |
| 45 Args: |
| 46 issuer: the issuer. |
| 47 |
| 48 Returns: |
| 49 The `jwks_uri` that is either statically configured or retrieved via |
| 50 OpenId discovery. None is returned when the issuer is unknown or the |
| 51 OpenId discovery fails. |
| 52 """ |
| 53 issuer_uri_config = self._issuer_uri_configs.get(issuer) |
| 54 |
| 55 if not issuer_uri_config: |
| 56 # The issuer is unknown. |
| 57 return |
| 58 |
| 59 jwks_uri = issuer_uri_config.jwks_uri |
| 60 if jwks_uri: |
| 61 # When jwks_uri is set, return it directly. |
| 62 return jwks_uri |
| 63 |
| 64 # When jwksUri is empty, we try to retrieve it through the OpenID |
| 65 # discovery. |
| 66 open_id_valid = issuer_uri_config.open_id_valid |
| 67 if open_id_valid: |
| 68 discovered_jwks_uri = _discover_jwks_uri(issuer) |
| 69 self._issuer_uri_configs[issuer] = IssuerUriConfig(False, |
| 70 discovered_jwks_uri) |
| 71 return discovered_jwks_uri |
| 72 |
| 73 |
| 74 class JwksSupplier(object): # pylint: disable=too-few-public-methods |
| 75 """A supplier that returns the Json Web Token Set of an issuer.""" |
| 76 |
| 77 def __init__(self, key_uri_supplier): |
| 78 """Constructs an instance of JwksSupplier. |
| 79 |
| 80 Args: |
| 81 key_uri_supplier: a KeyUriSupplier instance that returns the `jwks_uri` |
| 82 based on the given issuer. |
| 83 """ |
| 84 self._key_uri_supplier = key_uri_supplier |
| 85 self._jwks_cache = cache.make_region().configure( |
| 86 "dogpile.cache.memory", expiration_time=datetime.timedelta(minutes=5)) |
| 87 |
| 88 def supply(self, issuer): |
| 89 """Supplies the `Json Web Key Set` for the given issuer. |
| 90 |
| 91 Args: |
| 92 issuer: the issuer. |
| 93 |
| 94 Returns: |
| 95 The successfully retrieved Json Web Key Set. None is returned if the |
| 96 issuer is unknown or the retrieval process fails. |
| 97 |
| 98 Raises: |
| 99 UnauthenticatedException: When this method cannot supply JWKS for the |
| 100 given issuer (e.g. unknown issuer, HTTP request error). |
| 101 """ |
| 102 def _retrieve_jwks(): |
| 103 """Retrieve the JWKS from the given jwks_uri when cache misses.""" |
| 104 jwks_uri = self._key_uri_supplier.supply(issuer) |
| 105 |
| 106 if not jwks_uri: |
| 107 raise UnauthenticatedException("Cannot find the `jwks_uri` for issuer " |
| 108 "%s: either the issuer is unknown or " |
| 109 "the OpenID discovery failed" % issuer) |
| 110 |
| 111 try: |
| 112 response = requests.get(jwks_uri) |
| 113 json_response = response.json() |
| 114 except Exception as exception: |
| 115 message = "Cannot retrieve valid verification keys from the `jwks_uri`" |
| 116 raise UnauthenticatedException(message, exception) |
| 117 |
| 118 if "keys" in json_response: |
| 119 # De-serialize the JSON as a JWKS object. |
| 120 jwks_keys = jwk.KEYS() |
| 121 jwks_keys.load_jwks(response.text) |
| 122 return jwks_keys._keys |
| 123 else: |
| 124 # The JSON is a dictionary mapping from key id to X.509 certificates. |
| 125 # Thus we extract the public key from the X.509 certificates and |
| 126 # construct a JWKS object. |
| 127 return _extract_x509_certificates(json_response) |
| 128 |
| 129 return self._jwks_cache.get_or_create(issuer, _retrieve_jwks) |
| 130 |
| 131 |
| 132 def _extract_x509_certificates(x509_certificates): |
| 133 keys = [] |
| 134 for kid, certificate in x509_certificates.iteritems(): |
| 135 try: |
| 136 if certificate.startswith(jwk.PREFIX): |
| 137 # The certificate is PEM-encoded |
| 138 der = ssl.PEM_cert_to_DER_cert(certificate) |
| 139 key = jwk.der2rsa(der) |
| 140 else: |
| 141 key = jwk.import_rsa_key(certificate) |
| 142 except Exception as exception: |
| 143 raise UnauthenticatedException("Cannot load X.509 certificate", |
| 144 exception) |
| 145 rsa_key = jwk.RSAKey().load_key(key) |
| 146 rsa_key.kid = kid |
| 147 keys.append(rsa_key) |
| 148 return keys |
| 149 |
| 150 |
| 151 def _discover_jwks_uri(issuer): |
| 152 open_id_url = _construct_open_id_url(issuer) |
| 153 try: |
| 154 response = requests.get(open_id_url) |
| 155 return response.json().get("jwks_uri") |
| 156 except Exception as error: |
| 157 raise UnauthenticatedException("Cannot discover the jwks uri", error) |
| 158 |
| 159 |
| 160 def _construct_open_id_url(issuer): |
| 161 url = issuer |
| 162 if (not url.startswith(_HTTP_PROTOCOL_PREFIX) and |
| 163 not url.startswith(_HTTPS_PROTOCOL_PREFIX)): |
| 164 url = _HTTPS_PROTOCOL_PREFIX + url |
| 165 if not url.endswith("/"): |
| 166 url += "/" |
| 167 url += _OPEN_ID_CONFIG_PATH |
| 168 return url |
| 169 |
| 170 |
| 171 class IssuerUriConfig(object): |
| 172 """The jwks_uri configuration for an issuer. |
| 173 |
| 174 TODO (yangguan): this class should be removed after we figure out how to |
| 175 fetch the external configs. |
| 176 """ |
| 177 |
| 178 def __init__(self, open_id_valid, jwks_uri): |
| 179 """Create an instance of IsserUriConfig. |
| 180 |
| 181 Args: |
| 182 open_id_valid: indicates whether the corresponding issuer is valid for |
| 183 OpenId discovery. |
| 184 jwks_uri: is the saved jwks_uri. Its value can be None if the OpenId |
| 185 discovery process has not begun or has already failed. |
| 186 """ |
| 187 self._open_id_valid = open_id_valid |
| 188 self._jwks_uri = jwks_uri |
| 189 |
| 190 @property |
| 191 def open_id_valid(self): |
| 192 return self._open_id_valid |
| 193 |
| 194 @property |
| 195 def jwks_uri(self): |
| 196 return self._jwks_uri |
| 197 |
| 198 |
| 199 class UnauthenticatedException(Exception): |
| 200 pass |
OLD | NEW |