(A non-me-di-um version can be found on my blog)
How would you explain a typical website experience to your cat?
- First, you go to a webpage that asks for a bunch of your personal information. And you give it to them.
- Then, every time you want to come back to this page to do something, you have to remember some of this information.
- And then long after you stop using this site, there’s a good chance your information will either be sold to someone else or hacked.
This is how things have been for 20 years!
Is there a better way? Who knows. But at the very least, here’s an attempt to rethink how we set up accounts and what happens to them over time.
- Signing up for a website should be as easy as visiting a URL. No forms, no personal information, nothing.
- Passwords are optional.
- You can access your account instantly from anywhere, anytime. No logging in, no app, just visit a URL.
- Your account self-destructs eventually (or is adversely possessed). No more waiting around for an old account to get hacked.
Would I recommend this setup for your bank’s web app? Probably not. But my guess is there are other websites out there that could benefit from taking this lighter and more disposable approach.
Just visit a URL and I have a website? Sure thing. Our demo app URL will be sdnotes.com (short for standard definition notes).
- Have a yard sale next Sunday? Great, go to sdnotes.com/jimsyardsale
- Want a to-do list saved to the cloud? sdnotes.com/tim2do
- A shared wiki among friends? sdnotes.com/ourprivatesite
In all of these examples, your account is quietly created.
Start adding stuff to your page. At this point, no password has been added, but your site already works. Later on, if you want, you can add a password, but it isn’t required.
But won’t someone steal my site? Sure, that’s a possibility if you don’t add a password. The security model for this system is one part security-by-obscurity and one part embracing your site’s temporary nature.
Why don’t we just mandate a password? Well, we want our sites to be easy to update from anywhere, anytime, and possibly by multiple people (without having to share a password)
Since we’ve made creating a site so easy, what prevents people from hoarding websites? This is where self-destruction comes into play. If 30 days pass without a new post, your site is deleted from the record. Anyone can now claim it.
Use-it-or-lose-it applied to websites. Just because bits are so easy to preserve doesn’t mean we have to preserve them.
Implementation using Ruby on Rails
Now we’ll shift gears to see how this could be accomplished using Ruby on Rails.
If you’ve suddenly lost interest, here’s the live site: sdnotes.com/yoursitename
First, let’s generate a
Site model. These will be our implicitly-created accounts — ie sdnotes.com/jimsyardsale.
rails g model Site name:string password_digest:string locked:boolean
Note that the only required field is
presence: true, uniqueness:true … and some REGEX to make sure we only allow letters, numbers, and dashes in our URL.
One of our initial goals was to not require a password. This allows us to update our site from wherever, whenever just by remembering our URL. To accomplish this, we add
validations: false to the
has_secure_password method — which is Rails’ built-in password authenticator. It takes in a
password in our form, and saves it as
password_digest in our database.
locked attribute (from the above
generate command) will just tell us whether a site has decided to add a password. We set this to default
false before creating a new site via the
lock_init private method, because we want our initial sites to live free and easy at first.
Now that we have our
Site model up and running, we can create new sites, but we’ll need to make this site creation as easy as going to a URL — remember we said no forms!
To do so, we’ll set up our first route:
get '/:id', to: 'sites#show', as: 'main'
So, when the browser makes a
GET request to sdnotes.com/yoursitename, yoursitename will become available to us via a
params[:id] in the
show action of the
Now let’s create our
Sites controller, and a few actions we’ll need.
rails g controller Sites show add_password remove_password
As mentioned above, yoursitename is passed into our
show action as
params[:id] via the
before_action. Great, let’s check whether this site exists yet. If it’s
nil, let’s create a new site, and all that’s required is
name, which we already have from the URL. That’s it, your site is already created! No password, email, pet’s name, phone number, etc.
What can I do on my site? Well, whatever you want your app to do. You’ll just need to create the corresponding models and associations. For the demo app, we have posts which belong to sites (
rails g model Post body:text site:references
And we’ll just need to add
has_many :posts, dependent: :destroy to our
Site model. (Because of the
site: references command above, our
Post model already has the necessary
belongs_to :site .)
Now you’ve added some posts to your site. Maybe it’s a to-do list. Maybe it’s your wedding invitation. Maybe it’s your dream journal. Whatever it is, you’ve decided it needs a password. Your page will still be public on the web, you’ll just need a password to add posts.
To create our password, we’ll be updating our site’s record to include a password, and will have to provide a route.
patch '/:id', to: 'sites#add_password', as: 'site_pass'
Our form below matches the route created above — and since the
add_password action lives inside our
Sites controller, we can give it access to
Sites controller can update our record with a new password.
Note that before we add our password, we first need to make a couple checks —
unlocked? method checks to make sure we have access to add a password— more on this later. The
have_posts? method only allows us to add a password if there are some posts. We don’t need anyone cybersquatting empty sites!
Once we’ve determined it’s okay to update a password, we
pass_params method, which is just a way for Rails to protect what data a user can pass into a form. We also run the
lock_site method to make sure our site is now locked.
Alright, so our site is password protected! Now we have to enforce this password-protection for site visitors.
Now that a user can password-protect write-access to a site, we’ll need to add a way for someone to log in.
So far, the only
view that we’ve been interested in is our
show.html.erb and that trend continues. (And ultimately that’s the only view we’ll need)
We have this line of code in the
<% if private? %>
... login form ...
<% end %>
private? method lives inside our
SitesHelper module (so we have access to it in all views — in our case, the
If the site is locked, and the site session is not
session-unlocked, let’s render the
login form. In other words, if this site has a password, and you haven’t logged in, we’ll show you a login form.
It’s important to note that
@site.locked looks at the database for its value, whereas the session looks into the browser cookies.
Let’s take a step back — what is
session[…]? It’s a way for our app to store temporary data on the local browser, so we can remember when someone logs in. Luckily, Rails has some helpful
session methods to help us out (which is different from the below
rails g controller Sessions create
We’ve created a
Sessions controller, but no
Session model. This is okay! As mentioned above, we’ll be storing sessions in browser cookies and not in our database.
When you login, we’ll be creating a session, and when close your browser, your session is automatically deleted by your browser. Here’s our new route:
post '/unlock', to: 'sessions#create', as: 'unlock'
And our corresponding login form:
Great, so our form will
POST by default to the path specified in our route (
unlock_path), and send all the important login info to the
create action of our
To create a session, we first find the
site using the
hidden_field from the form, and then we run the
authenticate method on our
authenticate method is available from
has_secure_password, and it returns true if you entered the right password.
Assuming we’ve entered the right password, we’re now going to set a session cookie. To do this, we use Rails’
session[…] method. Session cookies take a key-value pair, so we set the key to a symbol representing the current site’s name(
site.name.to_sym), and then set the value to
It’s important that we set the key of the session cookie to be specific to the site name, as we could potentially be logged into multiple sites at once and not logged into others. Your browser can hold onto multiple session cookies.
Great! So now we have a way for people to log in to individual sites. Your browser now holds information telling us whether you’ve logged in.
But we haven’t fully addressed enabling and disabling access to site functionality depending on login status. We hinted at it above — remember that before we allowed a user to add a password, we ran
before_action :unlocked? in our
And here’s our
private? method again:
Now we understand what’s happening here — if the site has a password (
true), and the session cookie for the specific site (
session[@site.name.to_sym]) is not session-unlocked, we prevent you from updating a password (by redirecting you elsewhere).
Quick note regarding helper modules: to use our
private? method in our controllers (previously we were only using it in views), we have to
SitesHelper module, either in a specific controller, or in
ApplicationController where it’ll be accessible in all controllers.
So where else should we restrict user access if a site is password protected? Our posts! Remember that only logged in users can add (and delete) posts. We’ll have to address this in two places, first in the view, and then later in the controller.
show.html.erb view, we have:
<%= render 'posts/new_post' if postable? %>
postable? method is added to the
Let’s take a look at our
postable? method. We want to render our
new_post form under certain circumstances:
- If a site does not have a password (
!@site.locked), let’s show the new post form!
- If a site does have a password (
@site.locked) and the user has logged in to this specific site (
session[@site.name.to_sym] == “session-unlocked”), let’s show the new post form!
Right now, a site visitor should never see a way to add a post if they don’t have access. But this isn’t fool-proof just yet. Perhaps something goes wrong and the form is displayed, or maybe a sophisticated computer user injects form code. We need back-up in our
Posts controller to make sure posts are not added by users without access.
Before we create a new post, we run the private method
editable?. This method looks a lot like some of our previous methods — we check the same
private? method from earlier to make sure we have access to add a new post.
- We’ve been able to create accounts merely by visiting a URL.
- We can modify our site (add/delete posts) just by visiting the URL.
- And we can even add passwords later on if we desire.
The only part that’s missing is account self-destruction.
Don’t you love when you get an email from some website you last logged into 5 years ago informing you that your personal information has been hacked? Well, nice to hear from you again!
As mentioned earlier, our accounts will self-destruct after 30 days of inactivity.
While you could query the database on a regular basis and delete expired accounts, we’ll just handle site deletion from our controller, and only delete sites when necessary.
To do so, we’ve added an additional check to our
find_site method in our
Sites controller. If our site exists, we run another private method
Essentially, if a site has expired (30 days since a post), we want to delete the site right before someone tries to visit it. To recap — our initial
GET route sends us to the
show action in the
Sites controller. Before we run the
show action, we check
find_site, and then
expired? calculates that 30 days have passed since the last post, we delete the site from our database immediately. Then, when we run the
show action, we don’t find a site record with the
@slug name, so we create a new record.
If you’re still reading, maybe you’re wondering about our homepage? Well, we don’t have one! Instead, we’ll redirect users to their own personal page should they try to access the homepage. To do this, we add this route:
Which takes us to the
home action of the
Here we use Ruby’s handy method chaining to randomly grab an 8 character string, and redirect you to that page, where we’ll begin the whole process from scratch.
So what was the point of this again?
- We have too many accounts/passwords online.
- Account creation flows are boring and/or unnecessary.
- Accounts aren’t secure (eventually).
Could there be different ways to handle all of this?
Yes! Our little thought experiment is alive in the wild — sdnotes.com , short for standard definition notes. Go make a site!
Here’s the Github repo.