def account_check(username, password, request): settings = request.registry.settings hmac_secret = settings['userid_hmac_secret'] cache_key = utils.hmac_digest(hmac_secret, ACCOUNT_CACHE_KEY.format(username)) cache_ttl = int(settings.get('account_cache_ttl_seconds', 30)) hashed_password = utils.hmac_digest(cache_key, password) # Check cache to see whether somebody has recently logged in with the same # username and password. cache = request.registry.cache cache_result = cache.get(cache_key) # Username and password have been verified previously. No need to compare hashes if cache_result == hashed_password: # Refresh the cache TTL. cache.expire(cache_key, cache_ttl) return True # Back to standard procedure parent_id = username try: existing = request.registry.storage.get(parent_id=parent_id, collection_id='account', object_id=username) except storage_exceptions.RecordNotFoundError: return None hashed = existing['password'].encode(encoding='utf-8') pwd_str = password.encode(encoding='utf-8') # Check if password is valid (it is a very expensive computation) if bcrypt.checkpw(pwd_str, hashed): cache.set(cache_key, hashed_password, ttl=cache_ttl) return True
def user_checker(username, password, request): """LDAP user_checker Let you validate your Basic Auth users to a configured LDAP server. Use it like that:: config = Configurator( authentication_policy=BasicAuthenticationPolicy(check=user_checker)) """ settings = request.registry.settings cache_ttl = settings['ldap.cache_ttl_seconds'] hmac_secret = settings['userid_hmac_secret'] cache_key = utils.hmac_digest(hmac_secret, '%s:%s' % (username, password)) cache = request.registry.cache cache_result = cache.get(cache_key) ldap_fqn = settings['ldap.fqn'] if cache_result is None: cm = request.registry.ldap_cm try: with cm.connection(ldap_fqn.format(mail=username), password): cache.set(cache_key, "1", ttl=cache_ttl) return [] except INVALID_CREDENTIALS: cache.set(cache_key, "0", ttl=cache_ttl) elif cache_result == "1": return [] return None
def hawk_sessions(request): """Grab the Hawk Session from another Authentication backend.""" authn = AccountsAuthenticationPolicy() user = authn.authenticated_userid(request) if user is None: response = httpexceptions.HTTPUnauthorized() response.headers.update(authn.forget(request)) return response settings = request.registry.settings hmac_secret = settings['userid_hmac_secret'] algorithm = settings['hawk.algorithm'] token = os.urandom(32).hex() hawk_auth = HawkAuth(hawk_session=token, algorithm=algorithm) credentials = hawk_auth.credentials encoded_id = utils.hmac_digest(hmac_secret, credentials['id'].decode('utf-8')) cache_key = HAWK_SESSION_KEY.format(encoded_id) cache_ttl = int(settings['hawk.session_ttl_seconds']) session = utils.json.dumps({ "key": credentials["key"], "algorithm": credentials["algorithm"], "user_id": user }) request.registry.cache.set(cache_key, session, cache_ttl) headers = {'Hawk-Session-Token': token} return Response(headers=headers, status_code=201)
def reset_password_flow(username, password, request): cache_key = get_account_cache_key(username, request.registry) hashed_password = utils.hmac_digest(cache_key, password) pwd_str = password.encode(encoding="utf-8") cached_password = get_cached_reset_password(username, request.registry) if not cached_password: return None # The temporary reset password is only available for changing a user's password. if request.method.lower() not in ["post", "put", "patch"]: return None # Only allow modifying a user account, no other resource. uri = utils.strip_uri_prefix(request.path) resource_name, _ = utils.view_lookup(request, uri) if resource_name != "account": return None try: data = request.json["data"] except (ValueError, KeyError): return None # Request one and only one data field: the `password`. if not data or "password" not in data or len(data.keys()) > 1: return None cached_password_str = cached_password.encode(encoding="utf-8") if bcrypt.checkpw(pwd_str, cached_password_str): # Remove the temporary reset password from the cache. delete_cached_reset_password(username, request.registry) cache_account(hashed_password, username, request.registry) return True
def unauthenticated_userid(self, request): """Return the userid or ``None`` if token could not be verified. """ settings = request.registry.settings hmac_secret = settings["userid_hmac_secret"] authorization = request.headers.get("Authorization", "") try: authmeth, access_token = authorization.split(" ", 1) except ValueError: return None if authmeth.lower() != self.header_type.lower(): return None # XXX JWT Access token # https://auth0.com/docs/tokens/access-token#access-token-format # Check cache if these tokens were already verified. hmac_tokens = core_utils.hmac_digest(hmac_secret, access_token) cache_key = f"openid:verify:{hmac_tokens}" payload = request.registry.cache.get(cache_key) if payload is None: # This can take some time. payload = self._verify_token(access_token) if payload is None: return None # Save for next time / refresh ttl. request.registry.cache.set(cache_key, payload, ttl=self.verification_ttl) request.bound_data["user_profile"] = payload # Extract meaningful field from userinfo (eg. email or sub) return payload.get(self.userid_field)
def _verify_token(self, user_token, request): """Verify the token extracted from the Authorization header. This method stores the result in two locations to avoid hitting the auth remote server as much as possible: - on the request object, in case the Pyramid authentication methods like `effective_principals()` or `authenticated_userid()` are called several times during the request cycle; - in the cache backend, to reuse validated token from one request to another (during ``cache_ttl_seconds`` seconds.) """ # First check if this request was already verified. # `request.bound_data` is an attribute provided by Kinto to store # some data that is shared among sub-requests (e.g. default bucket # or batch requests) key = 'portier_verified_token' if key in request.bound_data: return request.bound_data[key] hmac_secret = request.registry.settings['userid_hmac_secret'] userID = utils.hmac_digest(hmac_secret, user_token) auth_cache = request.registry.cache encrypted_email = auth_cache.get("portier:%s" % userID) if encrypted_email is None: return None email = decrypt(encrypted_email, user_token) # Save for next call. request.bound_data[key] = email return email
def unauthenticated_userid(self, request): """Return the userid or ``None`` if token could not be verified. """ settings = request.registry.settings hmac_secret = settings['userid_hmac_secret'] authorization = request.headers.get('Authorization', '') try: authmeth, access_token = authorization.split(' ', 1) except ValueError: return None if authmeth.lower() != self.header_type.lower(): return None # XXX JWT Access token # https://auth0.com/docs/tokens/access-token#access-token-format # Check cache if these tokens were already verified. hmac_tokens = core_utils.hmac_digest(hmac_secret, access_token) cache_key = 'openid:verify:{}'.format(hmac_tokens) payload = request.registry.cache.get(cache_key) if payload is None: # This can take some time. payload = self._verify_token(access_token) if payload is None: return None # Save for next time / refresh ttl. request.registry.cache.set(cache_key, payload, ttl=self.verification_ttl) # Extract meaningful field from userinfo (eg. email or sub) return payload.get(self.userid_field)
def get_account_cache_key(username, registry): """Given a username, return the cache key for this account.""" settings = registry.settings hmac_secret = settings["userid_hmac_secret"] cache_key = utils.hmac_digest(hmac_secret, ACCOUNT_CACHE_KEY.format(username)) return cache_key
def default_bucket_id(request): settings = request.registry.settings secret = settings.get("default_bucket_hmac_secret", settings["userid_hmac_secret"]) # Build the user unguessable bucket_id UUID from its user_id digest = hmac_digest(secret, request.prefixed_userid) return str(uuid.UUID(digest[:32]))
def delete_cached_account(username, registry): """Given a username, delete the account key from the cache.""" hmac_secret = registry.settings["userid_hmac_secret"] cache_key = utils.hmac_digest(hmac_secret, ACCOUNT_CACHE_KEY.format(username)) cache = registry.cache cache_result = cache.delete(cache_key) return cache_result
def get_cached_validation_key(username, registry): """Given a username, get the validation key from the cache.""" hmac_secret = registry.settings["userid_hmac_secret"] cache_key = utils.hmac_digest( hmac_secret, ACCOUNT_VALIDATION_CACHE_KEY.format(username)) cache = registry.cache activation_key = cache.get(cache_key) return activation_key
def test_default_bucket_exists_and_has_user_id(self): bucket = self.app.get(self.bucket_url, headers=self.headers) result = bucket.json settings = self.app.app.registry.settings hmac_secret = settings["userid_hmac_secret"] bucket_id = hmac_digest(hmac_secret, self.principal)[:32] self.assertEqual(result["data"]["id"], str(UUID(bucket_id))) self.assertEqual(result["permissions"]["write"], [self.principal])
def delete_cached_reset_password(username, registry): """Given a username, delete the reset-password from the cache.""" hmac_secret = registry.settings["userid_hmac_secret"] cache_key = utils.hmac_digest( hmac_secret, ACCOUNT_RESET_PASSWORD_CACHE_KEY.format(username)) cache = registry.cache cache_result = cache.delete(cache_key) return cache_result
def test_default_bucket_exists_and_has_user_id(self): bucket = self.app.get(self.bucket_url, headers=self.headers) result = bucket.json settings = self.app.app.registry.settings hmac_secret = settings['userid_hmac_secret'] bucket_id = hmac_digest(hmac_secret, self.principal)[:32] self.assertEqual(result['data']['id'], text_type(UUID(bucket_id))) self.assertEqual(result['permissions']['write'], [self.principal])
def _verify_token(self, access_token, request): """Verify the token extracted from the Authorization header. This method stores the result in two locations to avoid hitting the auth remote server as much as possible: - on the request object, in case the Pyramid authentication methods like `effective_principals()` or `authenticated_userid()` are called several times during the request cycle; - in the cache backend, to reuse validated token from one request to another (during ``cache_ttl_seconds`` seconds.) """ # First check if this request was already verified. # `request.bound_data` is an attribute provided by Kinto to store # some data that is shared among sub-requests (e.g. default bucket # or batch requests) if REIFY_KEY not in request.bound_data: settings = request.registry.settings hmac_secret = settings['userid_hmac_secret'] cache_ttl = float(facebook_conf(request, 'cache_ttl_seconds')) hmac_token = core_utils.hmac_digest(hmac_secret, access_token) cache_key = 'facebook:verify:{}'.format(hmac_token) payload = request.registry.cache.get(cache_key) if payload is None: # Verify token from Facebook url = facebook_conf(request, 'userinfo_endpoint') params = { 'input_token': access_token, 'access_token': facebook_conf(request, 'app_access_token'), } # XXX: Implement token validation for Facebook resp = requests.get(url, params=params) try: resp.raise_for_status() except requests.exceptions.HTTPError: logger.exception("Facebook Token Protocol Error") raise httpexceptions.HTTPServiceUnavailable() else: body = resp.json() if not body['data']['is_valid']: payload = {} else: payload = body['data'] request.registry.cache.set(cache_key, payload, ttl=cache_ttl) # Save for next call. request.bound_data[REIFY_KEY] = payload.get('user_id') return request.bound_data[REIFY_KEY]
def on_account_changed(event): request = event.request cache = request.registry.cache settings = request.registry.settings # Extract username and password from current user username = request.matchdict["id"] hmac_secret = settings["userid_hmac_secret"] cache_key = utils.hmac_digest(hmac_secret, ACCOUNT_CACHE_KEY.format(username)) # Delete cache cache.delete(cache_key)
def test_default_bucket_hmac_secret_not_define_fallback_to_userid_hmac_secret( self): settings = self.get_app_settings({"userid_hmac_secret": "secret"}) if "default_bucket_hmac_secret" in settings: del settings["default_bucket_hmac_secret"] secret = settings.get("default_bucket_hmac_secret", settings["userid_hmac_secret"]) value = hmac_digest(secret, "input data") self.assertEqual(secret, "secret") self.assertTrue(value.startswith("6b9fc51c8e13506e9aee1f4c4bdbb650cd"))
def test_default_bucket_hmac_secret_define(self): settings = self.get_app_settings({ "default_bucket_hmac_secret": "bucket_id_salt", "userid_hmac_secret": "secret" }) secret = settings.get("default_bucket_hmac_secret", settings["userid_hmac_secret"]) value = hmac_digest(secret, "input data") self.assertEqual(secret, "bucket_id_salt") self.assertTrue(value.startswith("70f09c2d07853887d6b72bc70517f96377"))
def unauthenticated_userid(self, request): settings = request.registry.settings credentials = base_auth.extract_http_basic_credentials(request) if credentials: username, password = credentials if not username: return hmac_secret = settings["userid_hmac_secret"] credentials = "{}:{}".format(*credentials) userid = utils.hmac_digest(hmac_secret, credentials) return userid
def unauthenticated_userid(self, request): settings = request.registry.settings credentials = base_auth.extract_http_basic_credentials(request) if credentials: username, password = credentials if not username: return hmac_secret = settings['userid_hmac_secret'] credentials = '{}:{}'.format(*credentials) userid = utils.hmac_digest(hmac_secret, credentials) return userid
def unauthenticated_userid(self, request): settings = request.registry.settings credentials = self._get_credentials(request) if credentials: username, password = credentials if not username: return hmac_secret = settings['userid_hmac_secret'] credentials = '%s:%s' % credentials userid = utils.hmac_digest(hmac_secret, credentials) return userid
def on_account_changed(event): request = event.request cache = request.registry.cache settings = request.registry.settings hmac_secret = settings["userid_hmac_secret"] for obj in event.impacted_objects: # Extract username and password from current user username = obj["old"]["id"] cache_key = utils.hmac_digest(hmac_secret, ACCOUNT_CACHE_KEY.format(username)) # Delete cache cache.delete(cache_key)
def unauthenticated_userid(self, request): settings = request.registry.settings credentials = base_auth.extract_http_basic_credentials(request) if credentials: username, password = credentials if not username: return hmac_secret = settings["userid_hmac_secret"] credentials = f"{credentials[0]}:{credentials[1]}" userid = utils.hmac_digest(hmac_secret, credentials) return userid
def setUp(self): self.policy = authentication.PortierOAuthAuthenticationPolicy() self.backend = memory_backend.Cache(cache_prefix="", cache_max_size_bytes=5123) self.user_hmac_secret = random_bytes_hex(16) # Setup user self.token = '4128913851c9c4305e43dba2a7e59baa5c2fe2b909c6b63d04668346c4fb1e7b' self.email = '*****@*****.**' encrypted_email = encrypt(self.email, self.token) self.user_key = hmac_digest(self.user_hmac_secret, self.token) print("portier:%s" % self.user_key) self.backend.set("portier:%s" % self.user_key, encrypted_email, ttl=300) self.request = self._build_request()
def cache_reset_password(reset_password, username, registry): """Store a reset-password in the cache.""" settings = registry.settings hmac_secret = settings["userid_hmac_secret"] cache_key = utils.hmac_digest( hmac_secret, ACCOUNT_RESET_PASSWORD_CACHE_KEY.format(username)) # Store a reset password for 7 days by default. cache_ttl = int( settings.get( "account_validation.reset_password_cache_ttl_seconds", DEFAULT_RESET_PASSWORD_CACHE_TTL_SECONDS, )) cache = registry.cache cache_result = cache.set(cache_key, reset_password, ttl=cache_ttl) return cache_result
def cache_validation_key(activation_key, username, registry): """Store a validation_key in the cache.""" settings = registry.settings hmac_secret = settings["userid_hmac_secret"] cache_key = utils.hmac_digest( hmac_secret, ACCOUNT_VALIDATION_CACHE_KEY.format(username)) # Store an activation key for 7 days by default. cache_ttl = int( settings.get( "account_validation.validation_key_cache_ttl_seconds", DEFAULT_VALIDATION_KEY_CACHE_TTL_SECONDS, )) cache = registry.cache cache_result = cache.set(cache_key, activation_key, ttl=cache_ttl) return cache_result
def test_authentication_refresh_the_cache_each_time_we_authenticate(self): hmac_secret = self.app.app.registry.settings["userid_hmac_secret"] cache_key = utils.hmac_digest(hmac_secret, ACCOUNT_CACHE_KEY.format("me")) self.app.post_json("/accounts", {"data": {"id": "me", "password": "******"}}, status=201) resp = self.app.get("/", headers=get_user_headers("me", "bouh")) assert resp.json["user"]["id"] == "account:me" self.app.app.registry.cache.expire(cache_key, 10) resp = self.app.get("/", headers=get_user_headers("me", "bouh")) assert resp.json["user"]["id"] == "account:me" assert self.app.app.registry.cache.ttl(cache_key) >= 20 resp = self.app.get("/", headers=get_user_headers("me", "blah")) assert "user" not in resp.json
def test_authentication_refresh_the_cache_each_time_we_authenticate(self): hmac_secret = self.app.app.registry.settings['userid_hmac_secret'] cache_key = utils.hmac_digest(hmac_secret, ACCOUNT_CACHE_KEY.format('me')) self.app.post_json('/accounts', {'data': {'id': 'me', 'password': '******'}}, status=201) resp = self.app.get('/', headers=get_user_headers('me', 'bouh')) assert resp.json['user']['id'] == 'account:me' self.app.app.registry.cache.expire(cache_key, 10) resp = self.app.get('/', headers=get_user_headers('me', 'bouh')) assert resp.json['user']['id'] == 'account:me' assert self.app.app.registry.cache.ttl(cache_key) >= 20 resp = self.app.get('/', headers=get_user_headers('me', 'blah')) assert 'user' not in resp.json
def lookup_credentials(self, request, sender_id): settings = request.registry.settings hmac_secret = settings['userid_hmac_secret'] algorithm = settings['hawk.algorithm'] cache_key = HAWK_SESSION_KEY.format(utils.hmac_digest(hmac_secret, sender_id)) # Check cache to see if we know this session. cache = request.registry.cache session = cache.get(cache_key) cache_ttl = int(settings['hawk.session_ttl_seconds']) if session: cache.expire(cache_key, cache_ttl) request.bound_data['info'] = utils.json.loads(session) return {'id': sender_id, 'key': request.bound_data['info']['key'], 'algorithm': algorithm} raise LookupError('unknown sender')
def portier_verify(request): """Helper to redirect client towards Portier login form.""" broker_uri = portier_conf(request, 'broker_uri') token = request.validated['body']['id_token'] # Get the data from the config because the request might only # have local network information and not the public facing ones. audience = '{scheme}://{host}'.format( scheme=request.registry.settings['http_scheme'], host=request.registry.settings['http_host']) try: email, stored_redirect = get_verified_email( broker_url=broker_uri, token=token, audience=audience, issuer=broker_uri, cache=request.registry.cache) except ValueError as exc: error_details = 'Portier token validation failed: %s' % exc return http_error(httpexceptions.HTTPBadRequest(), errno=ERRORS.INVALID_AUTH_TOKEN, error='Invalid Auth Token', message=error_details) # Generate a random token user_token = codecs.encode(os.urandom(32), 'hex').decode('utf-8') # Encrypt the email with the token encrypted_email = encrypt(email, user_token) # Generate a user ID from the token hmac_secret = request.registry.settings['userid_hmac_secret'] userID = utils.hmac_digest(hmac_secret, user_token) # Store the encrypted user ID with the token session_ttl = portier_conf(request, 'session_ttl_seconds') request.registry.cache.set('portier:' + userID, encrypted_email, session_ttl) location = '%s%s' % (stored_redirect, user_token) return httpexceptions.HTTPFound(location=location)
def account_check(username, password, request): settings = request.registry.settings validation_enabled = settings.get("account_validation", False) cache_key = get_account_cache_key(username, request.registry) hashed_password = utils.hmac_digest(cache_key, password) # Check cache to see whether somebody has recently logged in with the same # username and password. cache_result = get_cached_account(username, request.registry) # Username and password have been verified previously. No need to compare hashes if cache_result == hashed_password: # Refresh the cache TTL. refresh_cached_account(username, request.registry) return True # Back to standard procedure parent_id = username try: existing = request.registry.storage.get(parent_id=parent_id, resource_name="account", object_id=username) except storage_exceptions.ObjectNotFoundError: return None if validation_enabled and not is_validated(existing): return None hashed = existing["password"].encode(encoding="utf-8") pwd_str = password.encode(encoding="utf-8") # Check if password is valid (it is a very expensive computation) if bcrypt.checkpw(pwd_str, hashed): cache_account(hashed_password, username, request.registry) return True # Last chance, is this a "reset password" flow? return reset_password_flow(username, password, request)
def test_supports_secret_as_bytes(self): value = hmac_digest(b"blah", "input data") self.assertTrue(value.startswith("d4f5c51db246c7faeb42240545b47274b6"))
def user_checker(username, password, request): """LDAP user_checker Let you validate your Basic Auth users to a configured LDAP server. Use it like that:: config = Configurator( authentication_policy=BasicAuthenticationPolicy(check=user_checker)) """ settings = request.registry.settings cache_ttl = settings['ldap.cache_ttl_seconds'] hmac_secret = settings['userid_hmac_secret'] cache_key = utils.hmac_digest(hmac_secret, '{}:{}'.format(username, password)) cache = request.registry.cache cache_result = cache.get(cache_key) bind_dn = settings.get('ldap.bind_dn') bind_password = settings.get('ldap.bind_password') if cache_result is None: cm = request.registry.ldap_cm # 0. Generate a search filter by combining the attribute and # filter provided in the ldap.fqn_filters directive with the # username passed by the HTTP client. base_dn = settings['ldap.base_dn'] filters = settings['ldap.filters'].format(mail=username) # 1. Search for the user try: with cm.connection(bind_dn, bind_password) as conn: # import pdb; pdb.set_trace() results = conn.search_s(base_dn, SCOPE_SUBTREE, filters) except BackendError: logger.exception("LDAP error") return None if len(results) != 1: # If the search does not return exactly one entry, deny or decline access. return None dn, entry = results[0] user_dn = str(dn) # 2. Fetch the distinguished name of the entry retrieved from # the search and attempt to bind to the LDAP server using that # DN and the password passed by the HTTP client. If the bind # is unsuccessful, deny or decline access. try: with cm.connection(user_dn, password): cache.set(cache_key, "1", ttl=cache_ttl) return [] except BackendError: logger.exception("LDAP error") except INVALID_CREDENTIALS: cache.set(cache_key, "0", ttl=cache_ttl) elif cache_result == "1": return [] return None
def get_user_id(self, credentials): hmac_secret = self.config.get('app:main', 'kinto.userid_hmac_secret') credentials = '%s:%s' % credentials digest = kinto_core_utils.hmac_digest(hmac_secret, credentials) return 'basicauth:%s' % digest
def get_default_bucket_id(config, uid): secret = config['registry'].settings['userid_hmac_secret'] digest = hmac_digest(secret, uid) return str(uuid.UUID(digest[:32]))
def default_bucket_id(request): settings = request.registry.settings secret = settings['userid_hmac_secret'] # Build the user unguessable bucket_id UUID from its user_id digest = hmac_digest(secret, request.prefixed_userid) return str(uuid.UUID(digest[:32]))