Index: third_party/google-endpoints/google/api/auth/tokens.py |
diff --git a/third_party/google-endpoints/google/api/auth/tokens.py b/third_party/google-endpoints/google/api/auth/tokens.py |
new file mode 100644 |
index 0000000000000000000000000000000000000000..cdaa214c6aab840ff854cc6f282631c099146a1d |
--- /dev/null |
+++ b/third_party/google-endpoints/google/api/auth/tokens.py |
@@ -0,0 +1,208 @@ |
+# 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. |
+ |
+"""Decodes and verifies the signature of auth tokens.""" |
+ |
+import datetime |
+import jwkest |
+import time |
+ |
+from dogpile import cache |
+from jwkest import jws |
+from jwkest import jwt |
+ |
+from google.api.auth import suppliers |
+ |
+ |
+class Authenticator(object): # pylint: disable=too-few-public-methods |
+ """Decodes and verifies the signature of auth tokens.""" |
+ |
+ def __init__(self, issuers_to_provider_ids, jwks_supplier, cache_capacity=200): |
+ """Construct an instance of AuthTokenDecoder. |
+ |
+ Args: |
+ issuers_to_provider_ids: a dictionary mapping from issuers to provider |
+ IDs defined in the service configuration. |
+ jwks_supplier: an instance of JwksSupplier that supplies JWKS based on |
+ issuer. |
+ cache_capacity: the cache_capacity with default value of 200. |
+ """ |
+ self._issuers_to_provider_ids = issuers_to_provider_ids |
+ self._jwks_supplier = jwks_supplier |
+ |
+ arguments = {"capacity": cache_capacity} |
+ expiration_time = datetime.timedelta(minutes=5) |
+ self._cache = cache.make_region().configure("lru_cache", |
+ arguments=arguments, |
+ expiration_time=expiration_time) |
+ |
+ def authenticate(self, auth_token, auth_info, service_name): |
+ """Authenticates the current auth token. |
+ |
+ Args: |
+ auth_token: the auth token. |
+ auth_info: the auth configurations of the API method being called. |
+ service_name: the name of this service. |
+ |
+ Returns: |
+ A constructed UserInfo object representing the identity of the caller. |
+ |
+ Raises: |
+ UnauthenticatedException: When |
+ * the issuer is not allowed; |
+ * the audiences are not allowed; |
+ * the auth token has already expired. |
+ """ |
+ try: |
+ jwt_claims = self.get_jwt_claims(auth_token) |
+ except Exception as error: |
+ raise suppliers.UnauthenticatedException("Cannot decode the auth token", |
+ error) |
+ _check_jwt_claims(jwt_claims) |
+ |
+ user_info = UserInfo(jwt_claims) |
+ |
+ issuer = user_info.issuer |
+ if issuer not in self._issuers_to_provider_ids: |
+ raise suppliers.UnauthenticatedException("Unknown issuer: " + issuer) |
+ provider_id = self._issuers_to_provider_ids[issuer] |
+ |
+ if not auth_info.is_provider_allowed(provider_id): |
+ raise suppliers.UnauthenticatedException("The requested method does not " |
+ "allow provider id: " + provider_id) |
+ |
+ # Check the audiences decoded from the auth token. The auth token is |
+ # allowed when 1) an audience is equal to the service name, or 2) at least |
+ # one audience is allowed in the method configuration. |
+ audiences = user_info.audiences |
+ has_service_name = service_name in audiences |
+ |
+ allowed_audiences = auth_info.get_allowed_audiences(provider_id) |
+ intersected_audiences = set(allowed_audiences).intersection(audiences) |
+ if not has_service_name and not intersected_audiences: |
+ raise suppliers.UnauthenticatedException("Audiences not allowed") |
+ |
+ return user_info |
+ |
+ def get_jwt_claims(self, auth_token): |
+ """Decodes the auth_token into JWT claims represented as a JSON object. |
+ |
+ This method first tries to look up the cache and returns the result |
+ immediately in case of a cache hit. When cache misses, the method tries to |
+ decode the given auth token, verify its signature, and check the existence |
+ of required JWT claims. When successful, the decoded JWT claims are loaded |
+ into the cache and then returned. |
+ |
+ Args: |
+ auth_token: the auth token to be decoded. |
+ |
+ Returns: |
+ The decoded JWT claims. |
+ |
+ Raises: |
+ UnauthenticatedException: When the signature verification fails, or when |
+ required claims are missing. |
+ """ |
+ |
+ def _decode_and_verify(): |
+ jwt_claims = jwt.JWT().unpack(auth_token).payload() |
+ _verify_required_claims_exist(jwt_claims) |
+ |
+ issuer = jwt_claims["iss"] |
+ keys = self._jwks_supplier.supply(issuer) |
+ try: |
+ return jws.JWS().verify_compact(auth_token, keys) |
+ except (jwkest.BadSignature, jws.NoSuitableSigningKeys, |
+ jws.SignerAlgError) as exception: |
+ raise suppliers.UnauthenticatedException("Signature verification failed", |
+ exception) |
+ |
+ return self._cache.get_or_create(auth_token, _decode_and_verify) |
+ |
+ |
+class UserInfo(object): |
+ """An object that holds the authentication results.""" |
+ |
+ def __init__(self, jwt_claims): |
+ audiences = jwt_claims["aud"] |
+ if isinstance(audiences, basestring): |
+ audiences = [audiences] |
+ self._audiences = audiences |
+ |
+ # email is not required |
+ self._email = jwt_claims["email"] if "email" in jwt_claims else None |
+ self._subject_id = jwt_claims["sub"] |
+ self._issuer = jwt_claims["iss"] |
+ |
+ @property |
+ def audiences(self): |
+ return self._audiences |
+ |
+ @property |
+ def email(self): |
+ return self._email |
+ |
+ @property |
+ def subject_id(self): |
+ return self._subject_id |
+ |
+ @property |
+ def issuer(self): |
+ return self._issuer |
+ |
+ |
+def _check_jwt_claims(jwt_claims): |
+ """Checks whether the JWT claims should be accepted. |
+ |
+ Specifically, this method checks the "exp" claim and the "nbf" claim (if |
+ present), and raises UnauthenticatedException if 1) the current time is |
+ before the time identified by the "nbf" claim, or 2) the current time is |
+ equal to or after the time identified by the "exp" claim. |
+ |
+ Args: |
+ jwt_claims: the JWT claims whose expiratio to be checked. |
+ |
+ Raises: |
+ UnauthenticatedException: When the "exp" claim is malformed or the JWT has |
+ already expired. |
+ """ |
+ current_time = time.time() |
+ |
+ expiration = jwt_claims["exp"] |
+ if not isinstance(expiration, (int, long)): |
+ raise suppliers.UnauthenticatedException('Malformed claim: "exp" must be an integer') |
+ if current_time >= expiration: |
+ raise suppliers.UnauthenticatedException("The auth token has already expired") |
+ |
+ if "nbf" not in jwt_claims: |
+ return |
+ |
+ not_before_time = jwt_claims["nbf"] |
+ if not isinstance(not_before_time, (int, long)): |
+ raise suppliers.UnauthenticatedException('Malformed claim: "nbf" must be an integer') |
+ if current_time < not_before_time: |
+ raise suppliers.UnauthenticatedException('Current time is less than the "nbf" time') |
+ |
+def _verify_required_claims_exist(jwt_claims): |
+ """Verifies that the required claims exist. |
+ |
+ Args: |
+ jwt_claims: the JWT claims to be verified. |
+ |
+ Raises: |
+ UnauthenticatedException: if some claim doesn't exist. |
+ """ |
+ for claim_name in ["aud", "exp", "iss", "sub"]: |
+ if claim_name not in jwt_claims: |
+ raise suppliers.UnauthenticatedException('Missing "%s" claim' % claim_name) |