PageRenderTime 78ms CodeModel.GetById 1ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/endpoints-1.0/endpoints/users_id_token.py

https://github.com/theosp/google_appengine
Python | 634 lines | 558 code | 33 blank | 43 comment | 22 complexity | 2d01afac37d783cf8a8b2342e8fa513e MD5 | raw file
  1. #!/usr/bin/env python
  2. #
  3. # Copyright 2007 Google Inc.
  4. #
  5. # Licensed under the Apache License, Version 2.0 (the "License");
  6. # you may not use this file except in compliance with the License.
  7. # You may obtain a copy of the License at
  8. #
  9. # http://www.apache.org/licenses/LICENSE-2.0
  10. #
  11. # Unless required by applicable law or agreed to in writing, software
  12. # distributed under the License is distributed on an "AS IS" BASIS,
  13. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. # See the License for the specific language governing permissions and
  15. # limitations under the License.
  16. #
  17. """Utility library for reading user information from an id_token.
  18. This is an experimental library that can temporarily be used to extract
  19. a user from an id_token. The functionality provided by this library
  20. will be provided elsewhere in the future.
  21. """
  22. import base64
  23. try:
  24. import json
  25. except ImportError:
  26. import simplejson as json
  27. import logging
  28. import os
  29. import re
  30. import time
  31. import urllib
  32. try:
  33. from google.appengine.api import memcache
  34. from google.appengine.api import oauth
  35. from google.appengine.api import urlfetch
  36. from google.appengine.api import users
  37. except ImportError:
  38. from google.appengine.api import memcache
  39. from google.appengine.api import oauth
  40. from google.appengine.api import urlfetch
  41. from google.appengine.api import users
  42. try:
  43. from Crypto.Hash import SHA256
  44. from Crypto.PublicKey import RSA
  45. _CRYPTO_LOADED = True
  46. except ImportError:
  47. _CRYPTO_LOADED = False
  48. __all__ = ['get_current_user',
  49. 'InvalidGetUserCall',
  50. 'SKIP_CLIENT_ID_CHECK']
  51. SKIP_CLIENT_ID_CHECK = ['*']
  52. _CLOCK_SKEW_SECS = 300
  53. _MAX_TOKEN_LIFETIME_SECS = 86400
  54. _DEFAULT_CERT_URI = ('https://www.googleapis.com/service_accounts/v1/metadata/'
  55. 'raw/federated-signon@system.gserviceaccount.com')
  56. _ENV_USE_OAUTH_SCOPE = 'ENDPOINTS_USE_OAUTH_SCOPE'
  57. _ENV_AUTH_EMAIL = 'ENDPOINTS_AUTH_EMAIL'
  58. _ENV_AUTH_DOMAIN = 'ENDPOINTS_AUTH_DOMAIN'
  59. _EMAIL_SCOPE = 'https://www.googleapis.com/auth/userinfo.email'
  60. _TOKENINFO_URL = 'https://www.googleapis.com/oauth2/v1/tokeninfo'
  61. _MAX_AGE_REGEX = re.compile(r'\s*max-age\s*=\s*(\d+)\s*')
  62. _CERT_NAMESPACE = '__verify_jwt'
  63. class _AppIdentityError(Exception):
  64. pass
  65. class InvalidGetUserCall(Exception):
  66. """Called get_current_user when the environment was not set up for it."""
  67. def get_current_user():
  68. """Get user information from the id_token or oauth token in the request.
  69. This should only be called from within an Endpoints request handler,
  70. decorated with an @endpoints.method decorator. The decorator should include
  71. the https://www.googleapis.com/auth/userinfo.email scope.
  72. If the current request uses an id_token, this validates and parses the token
  73. against the info in the current request handler and returns the user.
  74. Or, for an Oauth token, this call validates the token against the tokeninfo
  75. endpoint and oauth.get_current_user with the scopes provided in the method's
  76. decorator.
  77. Returns:
  78. None if there is no token or it's invalid. If the token was valid, this
  79. returns a User. Only the user's email field is guaranteed to be set.
  80. Other fields may be empty.
  81. Raises:
  82. InvalidGetUserCall: if the environment variables necessary to determine the
  83. endpoints user are not set. These are typically set when processing a
  84. request using an Endpoints handler. If they are not set, it likely
  85. indicates that this function was called from outside an Endpoints request
  86. handler.
  87. """
  88. if not _is_auth_info_available():
  89. raise InvalidGetUserCall('No valid endpoints user in environment.')
  90. if _ENV_USE_OAUTH_SCOPE in os.environ:
  91. return oauth.get_current_user(os.environ[_ENV_USE_OAUTH_SCOPE])
  92. if (_ENV_AUTH_EMAIL in os.environ and
  93. _ENV_AUTH_DOMAIN in os.environ):
  94. if not os.environ[_ENV_AUTH_EMAIL]:
  95. return None
  96. return users.User(os.environ[_ENV_AUTH_EMAIL],
  97. os.environ[_ENV_AUTH_DOMAIN] or None)
  98. return None
  99. def _is_auth_info_available():
  100. """Check if user auth info has been set in environment variables."""
  101. return ((_ENV_AUTH_EMAIL in os.environ and
  102. _ENV_AUTH_DOMAIN in os.environ) or
  103. _ENV_USE_OAUTH_SCOPE in os.environ)
  104. def _maybe_set_current_user_vars(method, api_info=None, request=None):
  105. """Get user information from the id_token or oauth token in the request.
  106. Used internally by Endpoints to set up environment variables for user
  107. authentication.
  108. Args:
  109. method: The class method that's handling this request. This method
  110. should be annotated with @endpoints.method.
  111. api_info: An api_config._ApiInfo instance. Optional. If None, will attempt
  112. to parse api_info from the implicit instance of the method.
  113. request: The current request, or None.
  114. """
  115. if _is_auth_info_available():
  116. return
  117. os.environ[_ENV_AUTH_EMAIL] = ''
  118. os.environ[_ENV_AUTH_DOMAIN] = ''
  119. try:
  120. api_info = api_info or method.im_self.api_info
  121. except AttributeError:
  122. logging.warning('AttributeError when accessing %s.im_self. An unbound '
  123. 'method was probably passed as an endpoints handler.',
  124. method.__name__)
  125. scopes = method.method_info.scopes
  126. audiences = method.method_info.audiences
  127. allowed_client_ids = method.method_info.allowed_client_ids
  128. else:
  129. scopes = (method.method_info.scopes
  130. if method.method_info.scopes is not None
  131. else api_info.scopes)
  132. audiences = (method.method_info.audiences
  133. if method.method_info.audiences is not None
  134. else api_info.audiences)
  135. allowed_client_ids = (method.method_info.allowed_client_ids
  136. if method.method_info.allowed_client_ids is not None
  137. else api_info.allowed_client_ids)
  138. if not scopes and not audiences and not allowed_client_ids:
  139. return
  140. token = _get_token(request)
  141. if not token:
  142. return None
  143. if ((scopes == [_EMAIL_SCOPE] or scopes == (_EMAIL_SCOPE,)) and
  144. allowed_client_ids):
  145. logging.debug('Checking for id_token.')
  146. time_now = long(time.time())
  147. user = _get_id_token_user(token, audiences, allowed_client_ids, time_now,
  148. memcache)
  149. if user:
  150. os.environ[_ENV_AUTH_EMAIL] = user.email()
  151. os.environ[_ENV_AUTH_DOMAIN] = user.auth_domain()
  152. return
  153. if scopes:
  154. logging.debug('Checking for oauth token.')
  155. if _is_local_dev():
  156. _set_bearer_user_vars_local(token, allowed_client_ids, scopes)
  157. else:
  158. _set_bearer_user_vars(allowed_client_ids, scopes)
  159. def _get_token(request):
  160. """Get the auth token for this request.
  161. Auth token may be specified in either the Authorization header or
  162. as a query param (either access_token or bearer_token). We'll check in
  163. this order:
  164. 1. Authorization header.
  165. 2. bearer_token query param.
  166. 3. access_token query param.
  167. Args:
  168. request: The current request, or None.
  169. Returns:
  170. The token in the request or None.
  171. """
  172. auth_header = os.environ.get('HTTP_AUTHORIZATION')
  173. if auth_header:
  174. allowed_auth_schemes = ('OAuth', 'Bearer')
  175. for auth_scheme in allowed_auth_schemes:
  176. if auth_header.startswith(auth_scheme):
  177. return auth_header[len(auth_scheme) + 1:]
  178. return None
  179. if request:
  180. for key in ('bearer_token', 'access_token'):
  181. token, _ = request.get_unrecognized_field_info(key)
  182. if token:
  183. return token
  184. def _get_id_token_user(token, audiences, allowed_client_ids, time_now, cache):
  185. """Get a User for the given id token, if the token is valid.
  186. Args:
  187. token: The id_token to check.
  188. audiences: List of audiences that are acceptable.
  189. allowed_client_ids: List of client IDs that are acceptable.
  190. time_now: The current time as a long (eg. long(time.time())).
  191. cache: Cache to use (eg. the memcache module).
  192. Returns:
  193. A User if the token is valid, None otherwise.
  194. """
  195. try:
  196. parsed_token = _verify_signed_jwt_with_certs(token, time_now, cache)
  197. except _AppIdentityError, e:
  198. logging.debug('id_token verification failed: %s', e)
  199. return None
  200. except:
  201. logging.debug('id_token verification failed.')
  202. return None
  203. if _verify_parsed_token(parsed_token, audiences, allowed_client_ids):
  204. email = parsed_token['email']
  205. return users.User(email)
  206. def _set_oauth_user_vars(token_info, audiences, allowed_client_ids, scopes,
  207. local_dev):
  208. logging.warning('_set_oauth_user_vars is deprecated and will be removed '
  209. 'soon.')
  210. return _set_bearer_user_vars(allowed_client_ids, scopes)
  211. def _set_bearer_user_vars(allowed_client_ids, scopes):
  212. """Validate the oauth bearer token and set endpoints auth user variables.
  213. If the bearer token is valid, this sets ENDPOINTS_USE_OAUTH_SCOPE. This
  214. provides enough information that our endpoints.get_current_user() function
  215. can get the user.
  216. Args:
  217. allowed_client_ids: List of client IDs that are acceptable.
  218. scopes: List of acceptable scopes.
  219. """
  220. for scope in scopes:
  221. try:
  222. client_id = oauth.get_client_id(scope)
  223. except oauth.Error:
  224. continue
  225. if (list(allowed_client_ids) != SKIP_CLIENT_ID_CHECK and
  226. client_id not in allowed_client_ids):
  227. logging.warning('Client ID is not allowed: %s', client_id)
  228. return
  229. os.environ[_ENV_USE_OAUTH_SCOPE] = scope
  230. logging.debug('Returning user from matched oauth_user.')
  231. return
  232. logging.debug('Oauth framework user didn\'t match oauth token user.')
  233. return None
  234. def _set_bearer_user_vars_local(token, allowed_client_ids, scopes):
  235. """Validate the oauth bearer token on the dev server.
  236. Since the functions in the oauth module return only example results in local
  237. development, this hits the tokeninfo endpoint and attempts to validate the
  238. token. If it's valid, we'll set _ENV_AUTH_EMAIL and _ENV_AUTH_DOMAIN so we
  239. can get the user from the token.
  240. Args:
  241. token: String with the oauth token to validate.
  242. allowed_client_ids: List of client IDs that are acceptable.
  243. scopes: List of acceptable scopes.
  244. """
  245. result = urlfetch.fetch(
  246. '%s?%s' % (_TOKENINFO_URL, urllib.urlencode({'access_token': token})))
  247. if result.status_code != 200:
  248. try:
  249. error_description = json.loads(result.content)['error_description']
  250. except (ValueError, KeyError):
  251. error_description = ''
  252. logging.error('Token info endpoint returned status %s: %s',
  253. result.status_code, error_description)
  254. return
  255. token_info = json.loads(result.content)
  256. if 'email' not in token_info:
  257. logging.warning('Oauth token doesn\'t include an email address.')
  258. return
  259. if not token_info.get('verified_email'):
  260. logging.warning('Oauth token email isn\'t verified.')
  261. return
  262. client_id = token_info.get('issued_to')
  263. if (list(allowed_client_ids) != SKIP_CLIENT_ID_CHECK and
  264. client_id not in allowed_client_ids):
  265. logging.warning('Client ID is not allowed: %s', client_id)
  266. return
  267. token_scopes = token_info.get('scope', '').split(' ')
  268. if not any(scope in scopes for scope in token_scopes):
  269. logging.warning('Oauth token scopes don\'t match any acceptable scopes.')
  270. return
  271. os.environ[_ENV_AUTH_EMAIL] = token_info['email']
  272. os.environ[_ENV_AUTH_DOMAIN] = ''
  273. logging.debug('Local dev returning user from token.')
  274. return
  275. def _is_local_dev():
  276. return os.environ.get('SERVER_SOFTWARE', '').startswith('Development')
  277. def _verify_parsed_token(parsed_token, audiences, allowed_client_ids):
  278. if parsed_token.get('iss') != 'accounts.google.com':
  279. logging.warning('Issuer was not valid: %s', parsed_token.get('iss'))
  280. return False
  281. aud = parsed_token.get('aud')
  282. if not aud:
  283. logging.warning('No aud field in token')
  284. return False
  285. cid = parsed_token.get('azp')
  286. if aud != cid and aud not in audiences:
  287. logging.warning('Audience not allowed: %s', aud)
  288. return False
  289. if list(allowed_client_ids) == SKIP_CLIENT_ID_CHECK:
  290. logging.warning('Client ID check can\'t be skipped for ID tokens. '
  291. 'Id_token cannot be verified.')
  292. return False
  293. elif not cid or cid not in allowed_client_ids:
  294. logging.warning('Client ID is not allowed: %s', cid)
  295. return False
  296. if 'email' not in parsed_token:
  297. return False
  298. return True
  299. def _urlsafe_b64decode(b64string):
  300. b64string = b64string.encode('ascii')
  301. padded = b64string + '=' * ((4 - len(b64string)) % 4)
  302. return base64.urlsafe_b64decode(padded)
  303. def _get_cert_expiration_time(headers):
  304. """Get the expiration time for a cert, given the response headers.
  305. Get expiration time from the headers in the result. If we can't get
  306. a time from the headers, this returns 0, indicating that the cert
  307. shouldn't be cached.
  308. Args:
  309. headers: A dict containing the response headers from the request to get
  310. certs.
  311. Returns:
  312. An integer with the number of seconds the cert should be cached. This
  313. value is guaranteed to be >= 0.
  314. """
  315. cache_control = headers.get('Cache-Control', '')
  316. for entry in cache_control.split(','):
  317. match = _MAX_AGE_REGEX.match(entry)
  318. if match:
  319. cache_time_seconds = int(match.group(1))
  320. break
  321. else:
  322. return 0
  323. age = headers.get('Age')
  324. if age is not None:
  325. try:
  326. age = int(age)
  327. except ValueError:
  328. age = 0
  329. cache_time_seconds -= age
  330. return max(0, cache_time_seconds)
  331. def _get_cached_certs(cert_uri, cache):
  332. certs = cache.get(cert_uri, namespace=_CERT_NAMESPACE)
  333. if certs is None:
  334. logging.debug('Cert cache miss')
  335. try:
  336. result = urlfetch.fetch(cert_uri)
  337. except AssertionError:
  338. return None
  339. if result.status_code == 200:
  340. certs = json.loads(result.content)
  341. expiration_time_seconds = _get_cert_expiration_time(result.headers)
  342. if expiration_time_seconds:
  343. cache.set(cert_uri, certs, time=expiration_time_seconds,
  344. namespace=_CERT_NAMESPACE)
  345. else:
  346. logging.error(
  347. 'Certs not available, HTTP request returned %d', result.status_code)
  348. return certs
  349. def _b64_to_long(b):
  350. b = b.encode('ascii')
  351. b += '=' * ((4 - len(b)) % 4)
  352. b = base64.b64decode(b)
  353. return long(b.encode('hex'), 16)
  354. def _verify_signed_jwt_with_certs(
  355. jwt, time_now, cache,
  356. cert_uri=_DEFAULT_CERT_URI):
  357. """Verify a JWT against public certs.
  358. See http://self-issued.info/docs/draft-jones-json-web-token.html.
  359. The PyCrypto library included with Google App Engine is severely limited and
  360. so you have to use it very carefully to verify JWT signatures. The first
  361. issue is that the library can't read X.509 files, so we make a call to a
  362. special URI that has the public cert in modulus/exponent form in JSON.
  363. The second issue is that the RSA.verify method doesn't work, at least for
  364. how the JWT tokens are signed, so we have to manually verify the signature
  365. of the JWT, which means hashing the signed part of the JWT and comparing
  366. that to the signature that's been encrypted with the public key.
  367. Args:
  368. jwt: string, A JWT.
  369. time_now: The current time, as a long (eg. long(time.time())).
  370. cache: Cache to use (eg. the memcache module).
  371. cert_uri: string, URI to get cert modulus and exponent in JSON format.
  372. Returns:
  373. dict, The deserialized JSON payload in the JWT.
  374. Raises:
  375. _AppIdentityError: if any checks are failed.
  376. """
  377. segments = jwt.split('.')
  378. if len(segments) != 3:
  379. raise _AppIdentityError('Token is not an id_token (Wrong number of '
  380. 'segments)')
  381. signed = '%s.%s' % (segments[0], segments[1])
  382. signature = _urlsafe_b64decode(segments[2])
  383. lsignature = long(signature.encode('hex'), 16)
  384. header_body = _urlsafe_b64decode(segments[0])
  385. try:
  386. header = json.loads(header_body)
  387. except:
  388. raise _AppIdentityError("Can't parse header")
  389. if header.get('alg') != 'RS256':
  390. raise _AppIdentityError('Unexpected encryption algorithm: %r' %
  391. header.get('alg'))
  392. json_body = _urlsafe_b64decode(segments[1])
  393. try:
  394. parsed = json.loads(json_body)
  395. except:
  396. raise _AppIdentityError("Can't parse token body")
  397. certs = _get_cached_certs(cert_uri, cache)
  398. if certs is None:
  399. raise _AppIdentityError(
  400. 'Unable to retrieve certs needed to verify the signed JWT')
  401. if not _CRYPTO_LOADED:
  402. raise _AppIdentityError('Unable to load pycrypto library. Can\'t verify '
  403. 'id_token signature. See http://www.pycrypto.org '
  404. 'for more information on pycrypto.')
  405. local_hash = SHA256.new(signed).hexdigest()
  406. verified = False
  407. for keyvalue in certs['keyvalues']:
  408. modulus = _b64_to_long(keyvalue['modulus'])
  409. exponent = _b64_to_long(keyvalue['exponent'])
  410. key = RSA.construct((modulus, exponent))
  411. hexsig = '%064x' % key.encrypt(lsignature, '')[0]
  412. hexsig = hexsig[-64:]
  413. verified = (hexsig == local_hash)
  414. if verified:
  415. break
  416. if not verified:
  417. raise _AppIdentityError('Invalid token signature')
  418. iat = parsed.get('iat')
  419. if iat is None:
  420. raise _AppIdentityError('No iat field in token')
  421. earliest = iat - _CLOCK_SKEW_SECS
  422. exp = parsed.get('exp')
  423. if exp is None:
  424. raise _AppIdentityError('No exp field in token')
  425. if exp >= time_now + _MAX_TOKEN_LIFETIME_SECS:
  426. raise _AppIdentityError('exp field too far in future')
  427. latest = exp + _CLOCK_SKEW_SECS
  428. if time_now < earliest:
  429. raise _AppIdentityError('Token used too early, %d < %d' %
  430. (time_now, earliest))
  431. if time_now > latest:
  432. raise _AppIdentityError('Token used too late, %d > %d' %
  433. (time_now, latest))
  434. return parsed