In this article we build upon the work we did in our previous article where we created our first manual Hyperlambda endpoint wrapping an SQL query. This article also has an associated YouTube video you can find below.
Security and JWT tokens in Magic
Magic is built upon JWT. JWT is an Open Standard for authentication and authorisation. You can read more about JWT at JWT.io. JWT tokens are what's referred to as "transparent authorisation tokens". This implies you can parse them semantically in your frontend, to see things such as username, roles, and other claims associated with the user. In a later article we will look at how you can create JWT tokens in Magic to authenticate users, but for now realising how to secure your Hyperlambda endpoints such that only authorised users can invoke them is sufficient.
To demand that a user belongs to one or more roles, you'd typically use the [auth.ticket.verify] slot. This slot simply throws an exception unless your user belongs to one of the comma separated roles you pass in as an argument to its invocation. Below is the code we started out with in the above video for your convenience.
.arguments
genre:string
// This line throws unless user belongs to root or admin role
auth.ticket.verify:root, admin
sqlite.connect:chinook
sqlite.select:@"select distinct c.Email, c.FirstName, c.LastName, g.name
from Customer c
inner join Invoice i on c.CustomerId = i.CustomerId
inner join InvoiceLine ii on i.InvoiceId = ii.InvoiceId
inner join Track t ON ii.TrackId = t.TrackId
inner join Genre g ON t.GenreId = g.GenreId
where g.Name = @genre
order by c.Email"
@genre:x:@.arguments/*/genre
return:x:-/*
If you modify your Hyperlambda file from our previous article and exchange its content with the above, and you try to invoke the endpoint in an incognito browser window, you'll be given a 401 error saying "access denied". This is because your browser does not associate the correct JWT token with your request.
This allows you to apply RBAC for your endpoints, implying "Role Based Access Control", to control who's got access to invoke your endpoints. RBAC is a well known authorisation mechanism for controlling who's got access to your application, and of course managing users and roles in Magic is extremely easy. Below is a screenshot of adding a user to a role from your Magic dashboard.
A user can belong to one or more roles, and each endpoint can be locked for all users except those belonging to one or more roles. This pattern allows you to easily control who's got access to invoke what endpoint on your server.
Caching and the HTTP response object
In the last parts of the above video we go through how you can add HTTP headers that are returned back to the client. Specifically, we apply the Cache-Control
HTTP header, which allows the client to cache the response object for "n amount of seconds". For endpoints that rarely changes their result given the same QUERY parameters, this is an easy win in regards to optimisations, and makes sure you application becomes much faster, and that the frontend becomes much more responsive, since it allows the browser to cache the endpoint's result for some pre-defined amount of time. You can find the complete code we're using to apply cache below.
.arguments
genre:string
auth.ticket.verify:root, admin
// This line of code will cache your response for 200 seconds
response.headers.set
Cache-Control:private, max-age=200
sqlite.connect:chinook
sqlite.select:@"select distinct c.Email, c.FirstName, c.LastName, g.name
from Customer c
inner join Invoice i on c.CustomerId = i.CustomerId
inner join InvoiceLine ii on i.InvoiceId = ii.InvoiceId
inner join Track t ON ii.TrackId = t.TrackId
inner join Genre g ON t.GenreId = g.GenreId
where g.Name = @genre
order by c.Email"
@genre:x:@.arguments/*/genre
return:x:-/*
Validators
A validator is kind of like a "reusable business rule component", and in Magic there exists server side validators for most things you can imagine, such as email addresses, integer numbers, date and time objects, etc - And they are typically one liners and easily applied in your Hyperlambda. Below is the example code from our YouTube video where we apply a validator for our [foo] integer argument.
.arguments
genre:string
foo:int
auth.ticket.verify:root, admin
// This makes the genre argument mandatory.
validators.mandatory:x:@.arguments/*/genre
// This ensure the [foo] argument is between 100 and 200
validators.integer:x:@.arguments/*/foo
min:100
max:200
response.headers.set
Cache-Control:private, max-age=200
sqlite.connect:chinook
sqlite.select:@"select distinct c.Email, c.FirstName, c.LastName, g.name
from Customer c
inner join Invoice i on c.CustomerId = i.CustomerId
inner join InvoiceLine ii on i.InvoiceId = ii.InvoiceId
inner join Track t ON ii.TrackId = t.TrackId
inner join Genre g ON t.GenreId = g.GenreId
where g.Name = @genre
order by c.Email"
@genre:x:@.arguments/*/genre
return:x:-/*
Notice the subtle parts above where [genre] is mandatory, but [foo] is not mandatory. However, if [foo] is given, it has to have a value between 100 and 200. If you wanted the [foo] argument to also be mandatory, you'd have to add a mandatory validator for it.
In later articles we will dive deeper into Hyperlambda validators.
Top comments (3)
Can I ask you why you choose JWT ?
Great question, and there are many good reasons for choosing JWT.
Etc, etc, etc - It's not "perfect", but when I researched the issue, it was definitely the best auth mechanism at the time ... ^_^
🙏 thanks