In Flitter, a KeyAction is a single-use link that directly calls a single handler, passing along some context and, optionally, a user. KeyActions are useful for when you want to generate links from your application that, when clicked, invalidate the link. KeyActions also have an expiration date (by default, this is 24 hours from the create date).
There are two main types of KeyActions. Let's look at an example of both:
Unauthenticated KeyActions
Unauthenticated KeyActions are KeyActions that don't care whether a particular user is logged in or not. They are simply a single-use link that executes some method handler.
Let's create a simple example. Say your application administers exams and the teacher can click a button to send links to students so they can check their grades on a particular exam, but they can only view their score once. We'll start off by creating a dummy endpoint to generate a KeyAction:
// in some controller file. e.g. controllers/Home.controller.js
async get_score_link(req, res) {
const key_action = await req.security.key_action('controller::Home.view_exam_score')
// We don't want this KeyAction to be associated with the current user,
// since it can be shared with others.
delete key_action.user_id
// Store some context data in the KeyAction
key_action.data_set('student_id', 34)
key_action.data_set('exam_id', 884)
// Save the KeyAction and send the response with the auto-generated action link
await key_action.save()
return res.send(`Score sharing link: ${key_action.url()}`)
}
Here, we call the module:flitter-auth/SecurityContext~SecurityContext#key_action method, which is a helper method for generating key actions for the current request. We pass it in the fully-qualified name of a canonical resource. In most cases, this will be a controller method (e.g. controller::Home.view_exam_score
).
After setting some context and saving the key action, we send the URL to the user. Flitter Auth provides built-in endpoints and routing for handling KeyAction URL requests.
When the client navigates to the KeyAction URL, Flitter Auth loads the KeyAction into the request and calls the configured handler. So, we need to create another handler to display the users score. That is, the controller::Home.view_exam_score
handler:
view_exam_score(req, res) {
// The instance of the KeyAction is available in "request.key_action"
const key_action = req.key_action
// Now, we can retrieve the data we stored earlier
const student_id = key_action.data_get('student_id')
const exam_id = key_action.data_get('exam_id')
return res.send(`Hello Student #${student_id}. Your score on Exam #${exam_id} is: ${Math.random()}`)
}
Here, we're just sending a basic message back to the user as a proof-of-concept. The key takeaway here is that you can use all the normal Flitter tools within the handler.
Now, if you create the route for our get_score_link
method and navigate to it, you should see a message similar to this:
Score sharing link: http://localhost:8000/auth/action/e57f7250-ce04-473c-9efd-8dec7478a281
As you can see, Flitter Auth automatically generated the URL for you. If you navigate to that URL, you should be greeted with the randombly generated score we generated above:
Hello Student #34. Your score on Exam #884 is: 0.2618006943970719
But, if you refresh the page, or try to navigate to the link a second time, you will be greeted with the 401 Access Denied error page.
Authenticated KeyActions
The other type of KeyAction, authenticated KeyActions, are functionally identical to unauthenticated KeyActions in every way, except for one key (very powerful) additional factor. When a user navigates to an authenticated KeyAction, if they are not signed in, it will automatically sign them in as the User associated with the KeyAction for the duration of the request only.
This might be useful for something like a Password Reset function where you want the application to have access to the user as though they are logged in, but the user won't be able to access the full application. Only the handler associated with the KeyAction. Here's a basic flow of what happens to the request when a client navigates to an authenticated KeyAction:
- Client clicks link
- Request passes through the standard global middleware
- Request passes through Flitter Auth's KeyAction middleware which makes sure the KeyAction has not expired, and has not been used.
- The User is automatically logged in to the session through their default provider (without needing the challenge)
- The handler associated with the KeyAction is called (a response is sent)
- The User is automatically logged out of the session through their default provider
This is reasonably secure because the only point in time where the user is authenticated is when the KeyAction handler is called, and the User is logged out at the end of the request lifecycle.
Generating authenticated KeyActions is nearly identical to their unauthenticated counterparts. The only difference is you set the auto_login
property on the KeyAction model. (See: module:flitter-auth/model/KeyAction~KeyAction#schema) For example:
async get_password_reset_link(req, res) {
// Assuming you've validated this somewhere
const user_id = req.body.user_id // User ID to reset the password for
const key_action = await req.security.key_action('controller::auth:Password.show_reset_page')
key_action.user_id = user_id
key_action.auto_login = true // This is what makes the KeyAction "authenticated"
await key_action.save()
return res.send(`Password reset link: ${key_action.url()}`) // Obviously, you'd send this in an e-mail or something more secure
}
Persistent KeyActions (!!!DANGER!!!)
Optionally, you can disable step 6 in the flow above, which would leave the user authenticated in the session AFTER the KeyAction has completed. The security issue here is fairly obvious, but for the sake of supporting all use cases, you can disable the automatic logout of the user by setting the no_auto_logout
property on the KeyAction model to true
.
This is, in almost all cases, an anti-pattern however. Any case you would want this can be achieved by just generating another KeyAction. For example, you might be tempted to disable auto-logout so the password reset form has a user in the session when it submits.
However, this is dangerous because the user can just navigate away from the form. Instead, you should generate a second KeyAction when the form is created, and use that KeyAction's URL as the POST target for the form to submit to. Best of both worlds.
That means that KeyActions can be activated by either GET or POST requests to their URL.