an overview of how masks manages sessions...
Every attempt to access something covered by masks creates a session—plain old Ruby objects that coordinate, verify, and record the attempt.
In masks, there are different types of sessions used to control access in different contexts. The most common case is someone trying to access an HTTP endpoint in your Rails application, but there are others—background jobs, the command line, inline ruby code…
Masks includes built-in types for most of these situations along with the ability to create custom types.
masks includes middleware to create a session for each HTTP request.
Controllers that include Masks::Controller
can interact with the masked
session:
class MyController < ApplicationController
include Masks::Controller
def my_action
masked_session # => Masks::Sessions::Request
end
end
For reference, the middleware ends up calling the following public method to create a request-based session:
session = Masks.request(request) # => Masks::Session::Request
You can create a session around a block of Ruby code with the following:
Masks.mask(name, params: {}, **data) do |session|
# only called if session.passed? or session.mask.fail == false
end # => Masks::Session::Inline
This is useful in all sorts of situations, like feature flags, command-line interfaces, and testing.
Access classes are plain old Ruby classes that are masked by a session.
Once initialized an access class creates a new session derived from a given session:
access = masked_session.access(name) # => Masks::Accessible
access.session # => Masks::Session::Access
The access class will throw an error on initialization unless its session can be masked and passes credentials and checks (just like all other sessions in masks).
You can build custom session types by extending Masks::Session
. Only three methods are required to be implemented:
params
- returns a hash of incoming parametersdata
- returns a hash of reconstituted session data (if any)matches_mask?(mask)
- return true
if the mask is applicable to the sessionOnce your session is implemented, create an instance and call the mask!
method to find its mask, check credentials, and return a result.
class ExampleSession < Masks::Session
def params
{}
end
def data
{}
end
def matches_mask?(mask)
mask.custom&.fetch(:example, false)
end
end
session = ExampleSession.mask!(**attrs)
More advanced customization is outlined in the API docs.
After initializing a session several things occur:
The masks.session
event is emitted every time this happens (using the
ActiveSupport::Notifications
framework). There are other events available:
Event | Description |
---|---|
masks.session |
Fired for each masked session |
masks.cleanup |
Fired when cleaning up a masked session |
These events are useful for instrumentation, but they don’t affect the behaviour of masks. Look into custom session types (described above) or custom credentials if you want further control over the session lifecycle in Ruby.
In most applications a “session” is granted after authentication, with a lifetime of hours or days. The session might expire if the session owner logs out, cookies are deleted, their account is deactivated, or some other reason (like a breach).
A masked session’s checks dictate its lifetime.
By default, past checks are ignored and credentials must re-run checks for
every session. If a check is configured with a lifetime
, then past passed
checks are accepted, provided they occurred within the duration.
For example, consider a masks.json
with checks like this (excerpt only):
{
"masks": [
{
"checks": {
"actor": {
"lifetime": "P1W"
},
"password": {
"lifetime": "P1D"
}
}
}
]
}
Any session masked by this configuration will accept prior passed actor
and
password
checks. Since the password
check expires daily the overall session
lifetime is one day. Extending the actor
check to 1 week forces the user to
supply an identifying credential (like a nickname or email) on a weekly basis.
The Rails engine exposes a DELETE /session
endpoint that functions as a
traditional logout endpoint. It calls cleanup!
on the session, which gives
the mask credentials a chance to delete all related data.
The session.cleanup
event is called on cleanup!
.
Changing the base session class’ version
will expire any sessions created
using a different version:
Masks.configuration.version = "v2" # sets the version for all session types to "v2"
Actors expose a similar property at the instance level. Override or recompute their version to expire all sessions related to the actor:
actor = Masks::Rails::Actor.find_by(...)
actor.version = SecureRandom.hex
actor.save!
Similarly, if checking device credentials you can expire sessions related to a specific device:
device = Masks::Rails::Device.find_by(actor: actor, ...)
device.version = SecureRandom.hex
device.save!
Request-based sessions will write data to the Rails session, if configured. Rails sessions can be expired by changing the name of the cookie or deleting data in the storage backend.
Consult the Rails guides for information on how to do this.