Tutorial 4¶
This tutorial will be based of the previous tutorial. Please complete it first.
Doing stuff that that actually matters¶
So as for the past few tutorials, we have done nothing that actually does stuff. So in this tutorial, we will cover the JWT system and use it to create an authentication flow. To start, lets create a JWT generator with keypairs, to do so, we need to run the following command:
python3 -m libmercury create jwt Main
When we run this, it will prompt us to choose betwen HMAC or RSA. We will choose RSA.
What kind of key do you want to generate? (HMAC/RSA): RSA
[KEYGEN] RSA private key saved to src/.vault/MainPublic_key.pem
[KEYGEN] RSA public key saved to src/.vault/MainPrivate_key.pem
[CODEGEN] Successfully created src/cargo/MainJwt.py
Once we do this, we can use the useAutherization decorator to protect our endpoints. So lets create all our templates. To start, lets create a signin template by creating a file in src/templates/signin.html:
<html>
<body>
{% if error %}
<div class="error">{{ error }}</div>
{% endif %}
<form action="/api/signin" method="post">
<input type="text" name="username">
<input type="password" name="password">
<input type="submit">
</form>
</body>
</html>
Then lets create a signup template by creating a file in src/templates/signup.html:
<html>
<body>
{% if error %}
<div class="error">{{ error }}</div>
{% endif %}
<form action="/api/signup" method="post">
<input type="text" name="username">
<input type="password" name="password">
<input type="submit">
</form>
</body>
</html>
Then lets create a protected endpoint that tells us some information about who is signed in. Lets create a file in src/templates/protected.html:
<html>
<body>
<h1>Hello {{username}}</h1>
</body>
</html>
Now lets rig our views up to a controller, we will name this controller “PagesController”. To do so run the following command:
python3 -m libmercury controller Pages
Then lets program our controller like so:
from libmercury import GETRoute, Request, Response, use_template, useAutherization, redirect
from src.security.MainJwt import MainJwt
class PagesController:
@staticmethod
@GETRoute("/signin")
def signin(request: Request) -> Response:
return use_template("signin.html")
@staticmethod
@GETRoute("/signup")
def signup(request: Request) -> Response:
return use_template("signin.html")
@staticmethod
@GETRoute("/protected")
@useAutherization(MainJwt, cookie="token", error=lambda: redirect("/signin"))
def protected(request: Request) -> Response:
return use_template("protected.html")
Note
You may get an error from your IDE along the lines of it failing to find src.security.MainJwt. This is not a bug and is completely expected.
Writing Controllers¶
Now currently, there is a big issue in the fact that anyone even if logged in can visit the /signin and /signup pages. To fix this, we can add a check to see if our user is logged in already, we can do this like so:
from libmercury import GETRoute, Request, Response, use_template, useAutherization
from libmercury.security.jwt import JWT
from src.security.MainJwt import MainJwt
class PagesController:
@staticmethod
@GETRoute("/signin")
def signin(request: Request) -> Response:
if mainJwt._verify(request.cookies.get("token")):
return redirect("/protected")
return use_template("signin.html", error=request.args.get("error"))
@staticmethod
@GETRoute("/signup")
def signup(request: Request) -> Response:
if mainJwt._verify(request.cookies.get("token")):
return redirect("/protected")
return use_template("signup.html", error=request.args.get("error"))
@staticmethod
@GETRoute("/dashboard")
@useAutherization(MainJwt, cookie="token", error=lambda: redirect("/signin"))
def protected(request: Request) -> Response:
return use_template("protected.html", username=JWT(request.cookies["token"]).payload["username"], error=request.args.get("error"))
Now we can implement a login POST by opening the already created src/controllers/AuthenticationFlowController.py:
from src.security.MainJwt import MainJwt
class AuthFlowController:
@staticmethod
@POSTRoute("/api/signin")
@useValidator(SigninValidator)
def signin(request: Request) -> Response:
if MainJwt._verify(request.cookies.get("token")):
return redirect("/dashboard")
#Check to see if the account exists
username = request.form["username"]
if not exists(user, username=username):
return redirect("/signin?error=User: {username} doesn't exist")
#Check to see if the password is correct
password = request.form["password"]
if not query(user, username=username).first().password == password:
return redirect("/signin?error=Incorrect password")
response = redirect("/dashboard")
response.set_cookie("token", MainJwt._makeJwt({"username": username, "exp": expires_in(3600)}))
return response
@staticmethod
@POSTRoute("/api/signup")
@useValidator(SignupValidator, error = lambda: redirect("/signup"))
def signup(request: Request) -> Response:
if MainJwt._verify(request.cookies.get("token")):
return redirect("/dashboard")
#Check to see if an account exists
username = request.form["username"]
if exists(user, username=username):
return redirect(f"/signup?error=Account: {username} already exists")
#Create the account
password = request.form["password"]
new_user = user(username=username, password=password)
Connection.Session.add_all([new_user,])
Connection.Session.commit()
#Redirect to the login page
return redirect("/signin")
Testing it out¶
If you run the server and then go to this page
Next tutorial: Tutorial 5