FastAPI OAuth2 Authentication Failed when data sent from HTML Form - python

I am using OAuth2 to store the authentication JWT access_token for employee login. The code works as intended when performing authentication through the FastAPI/docs page, but fails to do so when the username and password are sent through an HTML Login Form. In both these cases, the /token method properly receives the credentials and fetches (and returns) the access_token from the employee_login function (abstracted away and works as intended).
However, in the case of the HTML form, the oauth2 fails to store the token, and when navigating to /employees I get {"detail":"Not authenticated"}.
Would oAuth2 be the correct approach to use when HTML forms are involved or would a direct cookie-storing option be ideal? If oAuth2 can work in this manner, would you have any input on why the HTML form does not authenticate but going through the /docs page does? Thank you!
FAST API:
oAuth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
#app.get("/")
def employee_landing(request: Request, alert=None) -> dict:
'''Landing Page with auth options'''
return EMPLOYEE_TEMPLATES.TemplateResponse(
"landing.html",
{
"request": request,
"alert":alert
}
)
#app.post("/token")
async def login(request:Request, input_data: OAuth2PasswordRequestForm = Depends()):
''' Login a user '''
login_state = employee_login(input_data.username, input_data.password)
return {"access_token": login_state['token'], "token_type": "bearer"}
#app.get("/employee")
async def get_employee(token: str = Depends(oAuth2_scheme)):
''' Get employee info if logged in'''
return token
Landing Page HTML:
<!-- sign in form -->
<form action="/token" method="post">
<input type="text" name="username" placeholder="username" required>
<input type="password" name="password" placeholder="password" required>
<input type="submit" value="Sign In">
</form>

Related

Flask - how to get HTML form input as Flask-RESTful api data

I have this form in HTML
<form action="{{ url_for('transfer-api') }}" method="post">
<p>Sender account id:</p>
<p><input type="text" name="senderId" id="senderId" minlength="20" maxlength="20"/> * 20 characters</p>
<p>Receiver account id:</p>
<p><input type="text" name="receiverId" id="receiverId" minlength="20" maxlength="20"/> * 20 characters</p>
<p>Amount:</p>
<p><input type="text" name="amount" id="amount" /> * number</p>
<input type="submit" value="transfer" />
</form>
I need to send a post request to a flask-restful api I've written
class Transfer(Resource):
def post(self):
...
return resp
api.add_resource(Transfer, "/api/transfer", endpoint="transfer-api")
If I use the flask-restful api with the requests module, it works fine
response = requests.post(BASE + "/api/transfer", json={"from": "7cdfe1555c4543558887", "to": "f30d031d5f3b49c7b2ca", "amount": 5})
print(response.json())
I want to do the same thing, but with the form input data. My class Transfer doesn't have "request.form["senderId"]" etc, but it gets the args "from", "to" and "amount" using reqparse imported from the flask-restful module.
If I run the code as it is, it gives me the message "Did not attempt to load JSON data because the request Content-Type was not 'application/json'." because I'm not giving any data to the api post request.
Have you tried this:
def post(self):
json_data = request.get_json(force=True)
[1] flask-restful (python) fails to parse post data in form-data

Setting Authorization header - Flask app using Python Requests and JWT

I am just learning to code and this has been driving me crazy. I don't know if I am missing something very simple or have a total misunderstanding about how authentication works (very possibly both), however, i would appreciate someone help.
I have created a login route for my flask app which renders the following and successfully allows a user logging in.
Login Page
If a user successfully logs in, i want to set the Authorization header using a token generated using JWT. I am using the request libary to do this and get a 200 response but everytime i check the network tab, i can not see the token in the 'Authorization' Response Header.
The idea is that once this Header has been set, i can protect my apis by ensuring a token is present and also use that information to ensure that APIs only return data for that user i.e. decrypt the token to work out the user.
Network tab - No Authorization Header
This is my current code. Where am i going wrong? Does my approach sound correct?
#loginsBlueprint.route('/login', methods=['GET', 'POST'])
def login():
error = None
if request.method == 'POST':
user = User.query.filter_by(username=request.form['username']).first()
if user is not None and bcrypt.check_password_hash(user.password, request.form['password']):
token = jwt.encode(
{'user': request.form['username'], 'exp': datetime.datetime.utcnow() + datetime.timedelta(seconds=15)},
app.config['SECRET_KEY'])
url = 'http://127.0.0.1:5000/login'
headers = {'Authorization': 'Bearer ' + token}
requests.get(url, headers=headers)
return render_template('landing_page.html')
else:
return make_response('Could not verify', 403,
{'WWW-Authenticate': 'Basic realm ="Wrong Password !!"'})
return render_template('login.html', error=error)
login.html
<html>
<head>
<title>Flask Intro - login page</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="static/bootstrap.min.css" rel="stylesheet" media="screen">
</head>
<body>
<div class="container">
<h1>Please login</h1>
<br>
<form action="" method="post">
<input type="text" placeholder="Username" name="username" value="{{
request.form.username }}">
<input type="password" placeholder="Password" name="password" value="{{
request.form.password }}">
<input class="btn btn-default" type="submit" value="Login">
</form>
{% if error %}
<p class="error"><strong>Error:</strong> {{ error }}
{% endif %}
</div>
</body>
</html>
Note: I have tried changing to requests.post but get a 400:
127.0.0.1 - - [30/Mar/2021 21:53:29] "←[31m←[1mPOST /login HTTP/1.1←[0m" 400
I thought it might be because i'm using CORS and so added this in:
CORS(app, expose_headers='Authorization')
A few things which are confusing here:
In the login() method, you make a requests.get() call. So it looks like you're calling a new endpoint from your app. Is that how it should work? To be exact, you seem to check the user's credentials, issue a JWT and then call the /login endpoint of an app listening on port 5000, setting the JWT as the bearer token.
Authorization is a request header. You don't use it in responses to the client. If you want to return the JWT to the client use one of the OAuth flows, either the Code flow (preferably) or the Implicit flow.
Once you'll get the token to your client, then you should use it as you said - set in the Authorization header as a Bearer token. As you're using flask, you can use this filter to secure your APIs.

Gathering User Input Data in Flask

I am working on a flask app that will take a user input from a form and pass it on to the Todosit API to generate a task. I have been able to authenticate and verify the API version of the code is working, my only hangup now seems to be getting the data from the front end (jinja template) passed in as a variable to the Todoist API POST call.
Here is my Python code for the route:
#app.route('/', methods=['GET', 'POST'])
def home():
if request.form["submit"] == "submit":
task = request.form["task"]
due_date = request.form["dueDate"]
requests.post(
"https://api.todoist.com/rest/v1/tasks",
data=json.dumps({
"content": task,
"label_ids": [2154915513],
"due_string": due_date,
"due_lang": "en",
"priority": 1
}),
headers={
"Content-Type": "application/json",
"X-Request-Id": str(uuid.uuid4()),
"Authorization": f"Bearer {API_KEY}"
}).json()
Here is the HTML form code:
<form>
<label for="task">Enter Task:</label><br>
<input type="text" id="task" name="task"><br>
<label for="due_date">Enter Due Date (Leave Blank if None):</label><br>
<input type="text" id="due_date" name="due_date">
<input type="submit" value="Submit">
</form>
When I run this and add a task & due date, the results are not passed into the variables task & due date correctly. Does anyone have any advice on passing front end variables to the Flask route for processing? Thanks!
Add for form tag: action="{{ url_for('handle_data') }}" method="post"
And for dueDate use right key: request.form["due_date"]
Hope its help

Django GET request params from POST request

I have a form that you need to fill out using a POST request, but I want to the result to be a redirection to various pages on my site. Sometimes I want the user to be redirected to a profile page, sometimes to a purchase page.
So I put the redirect URI in the GET params of the URL of the form page that POSTs:
/form/?redirect_uri=/payments/
The url is correct upon landing on the forms page, with the appropriate redirect_uri. However, when I fill out the form and POST, Django doesn't seem to think there are any params in the GET request.
How can I get those params from the URL out on a POST request?
EDIT:
def form_view(request):
if request.method == "POST":
# Do stuff with form validation here
redirect_uri = "/home/"
if "redirect_uri" in request.GET:
redirect_uri = request.GET['redirect_uri']
if redirect_uri == request.path:
# avoid loops
redirect_uri = "/home/"
return redirect(redirect_uri)
This form page is loaded by accessing this url:
/form/?redirect_uri=/payments/
Form:
<form action="/form/" method="POST" class="wizard-form" enctype="application/x-www-form-urlencoded" autocomplete="off">
{% csrf_token %}
<fieldset class="form-group">
<label for="email">Email address</label>
<input name="email" type="text" class="form-control" id="email" placeholder="Enter email or username" required>
</fieldset>
<button type="submit" class="btn btn-primary">Go</button>
</form>
Your form has to be modified as follows:
<form action="/form/?redirect_uri={{ request.GET.redirect_url}}"
method="POST" class="wizard-form"
enctype="application/x-www-form-urlencoded" autocomplete="off">
Permit me to also suggest a slight optimization to your view
def form_view(request):
if request.method == "POST":
# Do stuff with form validation here
redirect_uri = request.GET.get('redirect_uri',"/home/")
if "redirect_uri" == request.path:
# avoid loops
redirect_uri = "/home/"
return redirect(redirect_uri)
In the POST request - i.e. when you submit the form - the url will be what you specified in the 'action' attribute in the form. If the parameters you require are not there, your view will not get them.
So either update your forms's action attribute to have these parameters or alternatively you can add thin in the form itself (as hidden inputs).
In GET request you will get attributes from url that you see in the browser.

How do I pass through the "next" URL with Flask and Flask-login?

The documentation for Flask-login talks about handling a "next" URL. The idea seems to be:
User goes to /secret
User is redirect to a login page (e.g. /login)
After a successful login, the user is redirected back to /secret
The only semi-full example using Flask-login that I've found is https://gist.github.com/bkdinoop/6698956 . It's helpful, but since it doesn't include the HTML template files, I'm seeing if I can recreate them as an self-training exercise.
Here's a simplified version of the /secret and /login section:
#app.route("/secret")
#fresh_login_required
def secret():
return render_template("secret.html")
#app.route("/login", methods=["GET", "POST"])
def login():
<...login-checking code omitted...>
if user_is_logged_in:
flash("Logged in!")
return redirect(request.args.get("next") or url_for("index"))
else:
flash("Sorry, but you could not log in.")
return render_template("login.html")
And here's login.html:
<form name="loginform" action="{{ url_for('login') }}" method="POST">
Username: <input type="text" name="username" size="30" /><br />
Password: <input type="password" name="password" size="30" /><br />
<input type="submit" value="Login" /><br />
Now, when the user visits /secret, he gets redirected to /login?next=%2Fsecret . So far, so good - the "next" parameter is in the query string.
However when the user submits the login form, he is redirected back to the index page, not back to the /secret URL.
I'm guessing the reason is because the "next' parameter, which was available on the incoming URL, is not incorporated into the login form and is therefore not being passed as a variable when the form is processed. But what's the right way to solve this?
One solution seems to work - change the <form> tag from
<form name="loginform" action="{{ url_for('login') }}" method="POST">
to:
<form name="loginform" method="POST">
With the "action" attribute removed, the browser (at least Firefox 45 on Windows) automatically uses the current URL, causing it to inherit the ?next=%2Fsecret query string, which successfully sends it on to the form processing handler.
But is omitting the "action" form attribute and letting the browser fill it in the right solution? Does it work across all browsers and platforms?
Or does Flask or Flask-login intend for this to be handled in a different way?
If you need to specify a different action attribute in your form you can't use the next parameter provided by Flask-Login. I'd recommend anyways to put the endpoint instead of the url into the url parameter since it is easier to validate. Here's some code from the application I'm working on, maybe this can help you.
Overwrite Flask-Login's unauthorized handler to use the endpoint instead of the url in the next parameter:
#login_manager.unauthorized_handler
def handle_needs_login():
flash("You have to be logged in to access this page.")
return redirect(url_for('account.login', next=request.endpoint))
Use request.endpoint in your own URLs too:
{# login form #}
<form action="{{ url_for('account.login', next=request.endpoint) }}" method="post">
...
</form>
Redirect to the endpoint in the next parameter if it exists and is valid, else redirect to a fallback.
def redirect_dest(fallback):
dest = request.args.get('next')
try:
dest_url = url_for(dest)
except:
return redirect(fallback)
return redirect(dest_url)
#app.route("/login", methods=["GET", "POST"])
def login():
...
if user_is_logged_in:
flash("Logged in!")
return redirect_dest(fallback=url_for('general.index'))
else:
flash("Sorry, but you could not log in.")
return render_template("login.html")
#timakro provides a neat solution.
If you want to handle a dynamic link such as
index/<user>
then using url_for(request.endpoint,**request.view_args) instead because request.endpoint will not contain the dynamic vairable info:
#login_manager.unauthorized_handler
def handle_needs_login():
flash("You have to be logged in to access this page.")
#instead of using request.path to prevent Open Redirect Vulnerability
next=url_for(request.endpoint,**request.view_args)
return redirect(url_for('account.login', next=next))
the following code shall be changed to:
def redirect_dest(home):
dest_url = request.args.get('next')
if not dest_url:
dest_url = url_for(home)
return redirect(dest_url)
#app.route("/login", methods=["GET", "POST"])
def login():
...
if user_is_logged_in:
flash("Logged in!")
return redirect_dest(home=anyViewFunctionYouWantToSendUser)
else:
flash("Sorry, but you could not log in.")
return render_template("login.html")
Just in case someone is trying to pass through the "next" URL with Flask-Login but with Flask_Restful, the workaround I've been using is passing the "next" argument from the GET method to the POST method. With flask_restful the "next_page" argument is set to "None" after clicking on the Login Button in the login.html
login.html
...
<!-- next_page came from "render_template(next_page=request.args.get('next') ...)" in the get() function -->
<!-- And also from render_template('login.html', next_page=next_page) in the post() function -->
<form action="{{ url_for('login', next=next_page) }}" method="POST" >
<div class="field">
<div class="control">
<input class="input is-large" type="email" name="email" placeholder="Your Email" autofocus="">
</div>
</div>
<div class="field">
<div class="control">
<input class="input is-large" type="password" name="password" placeholder="Your Password">
</div>
</div>
<div class="field">
<label class="checkbox">
<input type="checkbox" name="remember_me">
Remember me
</label>
</div>
<button class="button is-block is-info is-large is-fullwidth">Login</button>
</form>
...
auth.py
class Login(Resource):
def get(self):
if current_user.is_authenticated:
return redirect(url_for('home'))
headers = {'Content-Type': 'text/html'}
#1 --> # Here I pass the "next_page" to login.html
return make_response(render_template('login.html', next_page=request.args.get('next')), 200, headers)
def post(self):
#2 --> # After the user clicks the login button, I retrieve the next_page saved in the GET method
next_page = request.args.get('next')
if current_user.is_authenticated:
return redirect(url_for('home'))
# Check if account exists in the db
existing_account = Account.objects(email=request.form.get('email')).first()
# Only redirects when the URL is relative, which ensures that the redirect
# stays within the same site as the application.
if existing_account:
if existing_account.check_password(request.form.get('password')):
login_user(existing_account, remember=request.form.get('remember_me'))
if not next_page or url_parse(next_page).netloc != '':
return redirect(url_for('home'))
#3 --> # Here I use the retrieved next_page argument
return redirect(url_for(next_page))
# Account not recognized
flash('Please check your login details and try again.')
headers = {'Content-Type': 'text/html'}
#4 --> # I also pass the "next_page" here in case the user-provided data is wrong
return make_response(render_template('login.html', next_page=next_page), 200, headers)
Out of all the above answers I didn't find anything helpful for me.
But what I found, I will share with you..
In login.html just add the below code
<input
type="hidden"
name="next"
value="{{ request.args.get('next', '') }}"
/>
And login definition changes as follows
#app.route("/login", methods=["GET", "POST"])
def login():
if request.method == "POST":
username = request.form.get("username")
password = request.form.get("password")
next_url = request.form.get("next")
if username in users and users[username][1] == password:
session["username"] = username
if next_url:
return redirect(next_url)
return redirect(url_for("profile"))
return render_template("login.html")
And This worked perfectly for me...
Thank you ...
Simple Example
#app.route('/login', methods=['GET', 'POST'])
def login():
// do your stuff
next_page = request.form.get('next_page')
# next_page either be 'http://localhost:5000/None or 'http://localhost:5000/some_endpoints
return redirect(next_page) if 'None' not in next_page else redirect(url_for('default_action'))
Note
as Mentioned in Flask documentation
don't forget to pass next_url in html
<input type="hidden" value="{{ request.args.get('next') }}" name="next_page" id="next_page"/>
This is what worked for me, this is an addition to what #timakro and #Kurumi Tokisaki has provided in an earlier post.
#login_manager.unauthorized_handler
def handle_needs_login():
return redirect(url_for('login', next_page=request.endpoint))
#app.route('/login/<next_page>', methods=['GET', 'POST'])
def login(next_page):
if current_user.is_authenticated:
return redirect(url_for(next_page))
if is_a_successfull_login():
redirect(url_for(next_page))

Categories

Resources