"""Handlers. All the different endpoints are used to identify a user based on a login and a password. """ import base64 from flask import abort from flask import g from flask import jsonify from flask import render_template from flask import request from auth import config from auth.app import app from auth.logic import client from auth.logic import encryption from auth.logic import user from auth.logic import token @app.route(config.HEALTH_CHECK) def hello_world(): """Health check.""" return 'Hello World', 200 @app.route('/authorize') def authorize(): """Authorize a client for a user. Each request generates a new RSA key (no value is stored in the cookie). We send back to the page the RSA Public Key (Pem format) and we also provide the token of the key. Params: client_id (int): the client id. state (str): the state to send back to the application with the token, which allows the application to verify the validity of the code sent. Raises: Exception: if the token (for any reason) cannot be created, there might be an issue somewhere. Returns: tuple: the response and the http status. """ client_id = request.args.get('client_id') state = request.args.get('state') keys = encryption.create() redirect = '' if config.ENVIRONMENT != config.PROD_ENVIRONMENT: redirect = request.args.get('redirect_uri', '') if not keys.success: raise Exception( 'Generating the keys dot not seem to work. Please ' 'investigate.') keys.message.update(public_key=keys.public_key.decode('utf8')) return render_template( 'login.html', keys=keys.message, client_id=client_id, state=state, redirect=redirect) @app.route('/login', methods=['POST']) def login(): """Login a user. This endpoint is hit by an xhr that contains all the necessary information to authenticate the user. When the user has been authenticated, a success message with the CODE is returned to the frontend, which starts a redirect. Params: login (str): the user's login. client_id (int): the client id. password (str): the user's password. request_id (str): the request id (which corresponds to the id used by the private key). Returns: tuple: the response and the http status. """ encrypted_password = base64.b64decode(request.form['password']) request_id = request.form['request_id'] # Validate the password information password = encryption.decode(request_id, encrypted_password) if not password.success: return jsonify(password.errors), password.status # Validate the client. client_id = int(request.form['client_id']) current_client = client.get_client(client_id) if not current_client.success: return jsonify(current_client.errors), current_client.status login = request.form['login'] response = user.login(login, password.message.decode('utf8'), client_id) if not response.success: return jsonify(response.errors), response.status redirect = current_client.message.redirect_url if config.ENVIRONMENT != config.PROD_ENVIRONMENT: redirect = request.form.get('redirect', '') or redirect return jsonify( dict( code=response.token, redirect=redirect)), response.status @app.route('/token', methods=['POST']) def access_token(): """Provide to the application the access token. The access token can only be accessed by the application when the code (generated by the authorize) is sent back to the application. This token is a one-time use, and expires quickly after. Params: client_id (int): the client id. client_secret (str): the client secret. code (str): the code to grant access. Headers: X-Forwarded-For: the original ip of the user that initiated this request. Throws: Exception: If the client information are not valid, we immediately stop the request and trigger a 500 (which triggers a set of alarms + emails). Returns: tuple: the response and the http status. """ client_id = int(request.form.get('client_id', 0)) client_secret = request.form.get('client_secret') code = request.form.get('code') user_ip = request.headers.get('X-Forwarded-For') # Throws an exception if the client id and the secret does not match. # None of those errors should happen since this will only be consumed # by a very limited set of applications. client.confirm(client_id, client_secret) current_token = token.validate_code_token(code, client_id) if not current_token.success: abort(current_token.status) # This token is not valid. current_user = user.get_user(current_token.user_login) # token.revoke(code, client_id) current_token = token.create_connection_token( current_user.login, current_user.id, client_id, user_ip=user_ip) if not current_token.success: return jsonify(current_token.errors), current_token.status return jsonify( dict( access_token=current_token.token, expires_in=current_token.expires_in, token_type='bearer')), current_token.status @app.route('/revoke') def revoke(): """Revoke a token. If a token needs to be revoked, the application can send a revoke request for this specific token. User intervention is not necessary for those requests. Params: client_id (int): the client id. client_secret (str): the client secret. token (str): the token itself. Returns: Response: the response and the http status. """ client_id = int(request.args.get('client_id', 0)) client_secret = request.args.get('client_secret') token_param = request.args.get('token') # Using confirm also checks if the client credentials are valid (such as # the client id and client secret). current_client = client.confirm(client_id, client_secret) if not current_client.success: return jsonify(current_client.errors), current_client.status current_token = token.validate_connection_token(token_param, client_id) if not current_token.success: return '', 403 token_revoke = token.revoke(token_param) if not token_revoke.success: return jsonify(token_revoke.errors), token_revoke.status return jsonify( dict(revoked=current_token.revoked)), 200 @app.route('/me') def me(): """Get the current user information. On request (supposing the client id, client secret and token provided are valid), we return some basic user information such as the user id, user email and user login. Params: client_id (str): the client id (need to be castable into an int) client_secret (str): the client secret. token (str): the user token (do not use the code). Returns: Response: contains the user id, login and email (on success). """ client_id = int(request.args.get('client_id')) client_secret = request.args.get('client_secret') token_param = request.args.get('token') resp = client.confirm(client_id, client_secret) if not resp.success: return jsonify(resp.errors), resp.status current_token = token.validate_connection_token(token_param, client_id) if not current_token.success: return '', 403 current_user = user.get_user(current_token.user_login) if not current_user.success: return jsonify(current_user.errors), current_user.status # Adds the user id into our logs which allows us to trace any issues they # may have ran into. g.log.info( 'User "{user}" authenticated by ows-auth with token "{token}".'.format( user=current_user.login, token=token_param )) return jsonify( dict( user_id=current_user.id, login=current_user.login, email=current_user.email, groups=current_user.groups or [])), 200 @app.route('/forgot_password', methods=['POST']) def forgotpassword(): """Initiate forgotten password flow. This is the first request to make as part of the flow to reset a forgotten password. It looks up the user by provided login and returns a token with a 24 hour expiry. Params: client_id (int): the client id. login (str): the user's username Returns: tuple: the response and the http status. """ # Validate the client client_id = int(request.form['client_id']) current_client = client.get_client(client_id) if not current_client.success: return jsonify(current_client.errors), current_client.status # Validate the user login = request.form['login'] current_user = user.get_user(login) # TODO: confirm response output for error message # why empty dict and 400 when user not found? if not current_user.success: return jsonify(current_user.errors), 404 user_ip = request.headers.get('X-Forwarded-For') password_token_response = token.create_password_token( current_user.login, current_user.id, client_id, user_ip) if not password_token_response.success: return ( jsonify(password_token_response.errors), password_token_response.status) return '', 201 @app.route('/reset/') def reset(password_token=None): """Reset a password for a user. After generating a lost password token, a user should be able to continue to this url and receive an interface for setting a new password Params: password_token (str): the forgotten password token Returns: tuple: the response and the http status. """ current_token = token.fetch_token(password_token) if not current_token.success: return '', 404 # Update the token to expire in 20 minutes token.update_token_expiration(password_token) return jsonify({'client_id': int(current_token.client_id)}), 200 @app.route('/user/password', methods=['POST']) def update_user_password(): """Update a user's password. Once a user has a password token, they can use this to submit a new password. This new password should then be stored and the user is then logged in with it. Params: password_token (str): the forgotten password token password (str): the user's password. client_id (int): the client id. Returns: tuple: the response and the http status. """ client_id = int(request.form['client_id']) encrypted_password = base64.b64decode(request.form['password']) request_id = request.form['request_id'] # Validate the password information decrypted_password = encryption.decode(request_id, encrypted_password) if not decrypted_password.success: return jsonify(decrypted_password.errors), decrypted_password.status decrypted_password = decrypted_password.message.decode('utf8') # Retrieve the user token from the password token current_token = token.fetch_token(request.form['password_token']) if not current_token.success: return '', 404 # Update the user with the new password current_user = user.get_user(current_token.user_login) update_resp = user.update_user_password( current_user.id, decrypted_password, clean=False) if not update_resp.success: return '', 400 # Log in the user with the new password response = user.login( current_user.login, decrypted_password, client_id) if not response.success: return jsonify(response.errors), response.status # # Retrieve the redirect URL from the current client current_client = client.get_client(client_id) if not current_client.success: return jsonify(current_client.errors), current_client.status redirect = current_client.message.redirect_url if config.ENVIRONMENT != config.PROD_ENVIRONMENT: redirect = request.form.get('redirect', '') or redirect return jsonify( dict( code=response.token, redirect=redirect)), 200