def password_email_consumer(channel, method, header, body): """Send a user the link to reset their password. This takes the information passed through the queue and creates a verification token to email to the user for resetting their password. * Record the Verification URL in the Token Store * Email the Verification URL to the User """ user = json.loads(body) user = get_collection("users").find_one({ "username": user["username"] }) verification = uuid.uuid4() get_keyspace("verifications").setex(verification, user["username"], datetime.timedelta(hours = 6)) send_user_email(user, verification) channel.basic_ack(delivery_tag = method.delivery_tag)
def delete(self, username): """Delete an User. .. note:: This is an authenticated action that requires an access token from the user's token property. Request ------- :: DELETE /alunduil X-Auth-Token: 6e585a2d-438d-4a33-856a-8a7c086421ee Response -------- :: HTTP/1.0 200 OK Possible Errors --------------- :401: Unauthorized—Requested a user be deleted that isn't associated with the passed token. """ if get_keyspace("tokens").get(request.headers.get("X-Auth-Token")) != username: # TODO Redirect to token URL? raise UnauthorizedError(username=username) # TODO Submit queued job and not write from this API? get_collection("users").remove({"username": username}) return ""
def login(username): """Get an authorized token for subsequent API calls. This is the login method and must be called to get the token required for all calls making a note that they require the X-Auth-Token header. This call does require a password to be provided (digest authentication is used to improve security). This also means that one cannot simply pass in their username and password and get the resulting token. This token request requires two invocations: 1. Returns the HTTP Digest parameters 2. Returns the X-Auth-Token value Challenge-Response ------------------ A challenge is sent every time the API returns a 401 Unauthoried. This is the first step in getting a token. Response (Challenge) '''''''''''''''''''' :: 401 Unauthorized Location: /alunduil/token WWW-Authenticate: Digest realm="margarine.api", qop="auth", nonce="0cc175b9c0f1b6a831c399e269772661", opaque="92eb5ffee6ae2fec3ad71c777531578f" Request (Client Authentication Response) '''''''''''''''''''''''''''''''''''''''' :: GET /alunduil/token HTTP/1.1 Host: www.example.com Authorization: Digest username="******", realm="margarine.api", nonce="0cc175b9c0f1b6a831c399e269772661", uri="/v1/users/alunduil/token", qop=auth, nc=00000001, cnonce="4a8a08f09d37b73795649038408b5f33", response="2370039ff8a9fb83b4293210b5fb53e3", opaque="92eb5ffee6ae2fec3ad71c777531578f" Response (Token) '''''''''''''''' :: HTTP/1.1 200 OK 0b4fb639-edd1-44fe-b757-589a099097a5 """ logger.info("Checking authentication!") if request.authorization is None or request.authorization.opaque != Parameters()["api.uuid"]: raise UnauthorizedError(username=username) user = get_collection("users").find_one({"username": username}) logger.debug("user: %s", user) if user is None: abort(404) h1 = user["hash"] _ = "{request.method}:{request.path}" h2 = hashlib.md5(_.format(request=request)).hexdigest() _ = "{h1}:{a.nonce}:{a.nc}:{a.cnonce}:{a.qop}:{h2}" h3 = hashlib.md5(_.format(h1=h1, a=request.authorization, h2=h2)).hexdigest() logger.debug("response: %s", request.authorization.response) logger.debug("h3: %s", h3) if request.authorization.response != h3: raise UnauthorizedError(username=username) token = uuid.uuid4() get_keyspace("tokens").setex(str(token), username, datetime.timedelta(hours=6)) return str(token)
def post(self, username): """Update the password for the user and complete the password process. Authentication is not handled the same for this method as it is for the GET method of the password mechanism. This method can only be invoked properly with the verification query parameter passed from the password change mechanism in the GET method. Request ------- :: POST /alunduil/password?verification=6e585a2d-438d-4a33-856a-8a7c086421ee Content-Type: multipart/form-data password-0=PASSWORD;password-1=PASSWORD Response -------- :: HTTP/1.0 202 Accepted """ verification = request.args.get("verification") if get_keyspace("verifications").get(verification) != username: logger.error("verification token not valid") logger.debug("verification: %s", verification) abort(400) password = request.form.get("password-0") if password is None or password != request.form.get("password-1"): logger.error("passwords did not match!") logger.debug("password-0: %s", password) logger.debug("password-1: %s", request.form.get("password-1")) abort(400) message_properties = pika.BasicProperties() message_properties.content_type = "application/json" message_properties.durable = False message = {"username": username, "password": password} message = json.dumps(message) channel = get_channel() channel.exchange_declare(exchange="margarine.users.topic", type="topic", auto_delete=False) channel.basic_publish( body=message, exchange="margarine.users.topic", properties=message_properties, routing_key="users.password" ) channel.close() logger.info("Sent Password Update") get_keyspace("verifications").delete(verification) return "", 202
def get(self, username): """Begin a password change or continue after email verification. If an X-Auth-Token header is found, no further validation needs to be performed and the password change mechanism is returned. If there is no X-Auth-Token header but a query parameter or header with an emailed verification code is found (examples shown below), then no further validation needs to be performed and the password change mechanism is again returned. Otherwise, the process is simply initiated and an email is sent with a valid URL (including validation token) to continue this process. Authenticated Requests ---------------------- :: GET /alunduil/password X-Auth-Token: 6e585a2d-438d-4a33-856a-8a7c086421ee :: GET /alunduil/password X-Validation-Token: 6e585a2d-438d-4a33-856a-8a7c086421ee :: GET /alunduil/password?verification=6e585a2d-438d-4a33-856a-8a7c086421ee Authenticated Response ---------------------- :: HTTP/1.0 200 OK <!DOCTYPE html> <html> <body> <form name="password" action="/alunduil/password?verification=6e585a2d-438d-4a33-856a-8a7c086421ee" method="post"> Password: <input type="password" name="password-0"> Password: <input type="password" name="password-1"> <input type="submit" value="password-submit"> </form> </body> </html> Unauthenticated Requests ------------------------ :: GET /alunduil/password Unauthenticated Response ------------------------ :: HTTP/1.0 202 Accepted """ tokens = [ _ for _ in [ request.headers.get("X-Auth-Token"), request.headers.get("X-Verification-Token"), request.args.get("verification"), ] if _ is not None ] logger.debug("len(tokens): %s", len(tokens)) if len(tokens) > 1: abort(400) elif len(tokens) == 1: verification = tokens[0] if get_keyspace("tokens").get(verification) == username: get_keyspace("verifications").setex(verification, username, datetime.timedelta(minutes=3)) logger.debug("verification token: %s", verification) return render_template("password_mechanism.html", username=username, verification=verification) message_properties = pika.BasicProperties() message_properties.content_type = "application/json" message_properties.durable = False message = {"username": username} message = json.dumps(message) channel = get_channel() channel.exchange_declare(exchange="margarine.users.topic", type="topic", auto_delete=False) channel.basic_publish( body=message, exchange="margarine.users.topic", properties=message_properties, routing_key="users.email" ) channel.close() return "", 202
def put(self, username): """Create an User or modify an existing User. Create an User ============= To create a new user in the system, perform a PUT on the particular user's URL that want created with any parameters (required and optional) specified in the form data. Request ------- :: PUT /alunduil Content-Type: application/x-www-form-urlencoded email=alunduil%40alunduil.com name=Alex%20Brandt Response -------- :: HTTP/1.0 202 Accepted Modify an User ============== This method can also be used to modify an existing user—not just for creating new users. Request ------- :: PUT /alunduil Content-Type: application/x-www-form-urlencoded X-Auth-Token: 6e585a2d-438d-4a33-856a-8a7c086421ee email=alunduil%40alunduil.com Response -------- :: HTTP/1.0 200 OK Possible Errors =============== :400: Bad Request—A required option was not passed or is improperly formatted :401: Unauthorized—An attempt to create an existing user was detected The following are also used when updating a user: :409: Conflict—The new username requested is already in use. """ user = get_collection("users").find_one({"username": username}) logger.debug("user: %s", user) message_properties = pika.BasicProperties() message_properties.content_type = "application/json" message_properties.durable = False message = { "username": request.form.get("username", username), "email": request.form.get("email"), "name": request.form.get("name"), } routing_key = "users.create" if user is not None: routing_key = "users.update" message["original_username"] = username logger.debug("X-Auth-Token: %s", request.headers.get("X-Auth-Token")) if get_keyspace("tokens").get(request.headers.get("X-Auth-Token")) != username: # TODO Redirect to token URL? raise UnauthorizedError(username=username) if message["email"] is None and routing_key == "users.create": abort(400) message = json.dumps(message) channel = get_channel() channel.exchange_declare(exchange="margarine.users.topic", type="topic", auto_delete=False) channel.basic_publish( body=message, exchange="margarine.users.topic", properties=message_properties, routing_key=routing_key ) channel.close() return "", 202
def put(self, username): '''Create an User or modify an existing User. Create an User ============= To create a new user in the system, perform a PUT on the particular user's URL that want created with any parameters (required and optional) specified in the form data. Request ------- :: PUT /alunduil Content-Type: application/x-www-form-urlencoded email=alunduil%40alunduil.com name=Alex%20Brandt Response -------- :: HTTP/1.0 202 Accepted Modify an User ============== This method can also be used to modify an existing user—not just for creating new users. Request ------- :: PUT /alunduil Content-Type: application/x-www-form-urlencoded X-Auth-Token: 6e585a2d-438d-4a33-856a-8a7c086421ee email=alunduil%40alunduil.com Response -------- :: HTTP/1.0 200 OK Possible Errors =============== :400: Bad Request—A required option was not passed or is improperly formatted :401: Unauthorized—An attempt to create an existing user was detected The following are also used when updating a user: :409: Conflict—The new username requested is already in use. ''' user = get_collection('users').find_one({ 'username': username }) routing_key = 'users.create' if user is not None: routing_key = 'users.update' if get_keyspace('tokens').get(request.headers.get('X-Auth-Token')) != username: # TODO Redirect to token URL? raise UnauthorizedError(username = username) message = { 'username': username, 'requested_username': request.form.get('username', username), 'email': request.form.get('email', user.get('email')), 'name': request.form.get('name', user.get('name')), } if message['email'] is None and routing_key == 'users.create': logger.error('400—Creation of a new user, %s, without an email', username) abort(400) message = json.dumps(message) message_properties = pika.BasicProperties() message_properties.content_type = 'application/json' # TODO Switch to binary format? message_properties.durable = False logger.info('blend.user.PUT—Sending Message (Type: %s)', routing_key) channel = get_channel() channel.exchange_declare(exchange = 'margarine.users.topic', type = 'topic', auto_delete = False) channel.basic_publish(body = message, exchange = 'margarine.users.topic', properties = message_properties, routing_key = routing_key) channel.close() return '', 202