Logging in is often the first feature built by new developers and new projects. The process of identifying users is called "authentication". Your applications might even require more than one way to authenticate! This post walks you through the most popular types of authentication and their common use cases.
Authentication vs Authorization
Authentication is verifying a users identity. Authorization is determining what that user can do. It's important to note that Authentication systems (Devise, Clearance) are different than Authorization systems (Pundit, Cancancan). A common mistake is not separating these into unique systems within your application.
Cookie based Authentication
The most common way to authenticate web applications has always been cookies. Cookies are little text files stored on your computer for each domain you make a request to. Cookies are sent on every request. It's not required to even visit their page! This is one of the foundations of internet tracking. If Facebook adds a cookie to your computer by you logging in, it can track you on every page where Facebook can host an asset. Those little "Share on Facebook" buttons become a way for you to make a request to Facebook when you aren't even on their site! Cookies used in this way are referred to as "third party cookies". (note: Facebook now has a solution to utilize first party cookies instead that circumvents most ad blockers).
Cookies can contain anything as their contents but most sites utilize a data structure like JSON or XML to populate their cookies with information about the user. Cookies do not have any native security features. Since these are simply plain text files on your disk, they can be stolen and moved from one computer to another.
How are cookies used to authenticate
The typical flow with cookies involves having a form on your website that makes a post request with the user name and password. The server is going to validate these credentials which allows the server to know that the person is most likely who they say they are.
The server will then respond with a header instructing the browser to add the user id into the cookie. On subsequent requests, the cookie will contain the user id instead of the email and password.
But what about security?
Cookies aren't secure by default. Nothing stops you from altering your cookie and switching the user_id to someone else or taking the cookies from your "friends" computer and putting them on your own.
Signed Cookies
Signing allows anyone to verify the message is in fact the same message when it was signed and not altered since it was generated. Signing a user_id contained in the cookie will ensure that someone can not change the payload. Signing is performed by hashing the cookies value. The hash is then encrypted using the applications private key along with the timestamp. The encrypted value is then later decrypted using the public key.
Caveat: In rails the signing is done with hashing the secret key base with the cookie value. It isn't quite a real "signature" since external parties are not able to verify the cookie.
Encrypted Cookies
The next item to help with security is encryption. Encryption prevents others from viewing the data. This is a benefit when your cookie is storing data that you do not want others to view. With a cookie that has been signed and encrypted we ensure that no one can view the data and we also are able to determine the data is the same as when it is generated.
Device fingerprinting
In order to ensure that a cookie is not moved from computer to computer device fingerprinting is often employed. By adding the fingerprint into the encrypted data of the original device, it can be verified to be sent from the same device at a later date.
Cookie based sign in solutions
Devise - Undoubtedly the most popular authentication solution for Rails
Clearance - A lighter weight version of devise.
It's important to use existing proven solutions to ensure that the data in your cookies is stored and utilized in a proven way. Building a secure login system is much more than hashing a password. These two libraries have been battle tested for security and developer friendliness.
API Authentication
But I have an API! Cookies won't work!
Not necessarily. Cookies are sent on every requests, including Ajax requests. If you are building an application that runs in the browser and talks to an API on the same domain there is no further configuration to do. It will really just work. When utilized with encryption and signing it is also the most secure.
When is it not possible to use cookies to authenticate a web based SaaS application?
- When your API must be on a different domain. This could be if you are building a new front end app for your company and the api has existed for many years at this point. Or perhaps the organizational divisions prevent the front end team from working on the API application.
- You're building an app that will also be packaged on a mobile device it is typically easier to implement token based auth from the start.
Cookie based API login flow:
- Use the Devise login page to login
- Direct to an authenticated page that serves your mobile app on the same domain
- Utilize cookie auth everywhere and Devise will have no problem handling cookie auth for your SaaS app's API.
Token based auth
But I want to build a mobile app too.
At some point, cookie based auth in isolation is not going to cut it. Often this happens when the team starts to build a mobile application. Another way to authenticate must be used since cookies do not work outside of browsers. That method is called tokens!
Tokens are like wristbands in a nightclub. After an initial authentication, you are handed a token that you hold onto. Present the token and gain access! You might be able to see a drawback here in that the token is much easier to lose than when you didn't need to manage anything yourself with cookies.
There is not any technology that makes a token a "token". In the simplest explanation, it is only a string or data structure that the server is checking.
In order for tokens to work in a SaaS application we need to build the following requirements:
- Confirm the users identity and issue tokens
- Expire / revoke tokens to force someone to log out
- Ability to check tokens validity
- Reissue tokens when they expire without authenticating again
Meet OAuth
Open Auth (OAuth) is a specification to handle multiple types of secure token flows. If you are implementing tokens, you need OAuth. OAuth was introduced formally in 2010 and a revised version in 2012. I'm going to go over the different flows for building authorizing your own users.
Note: OAuth doesn't actually authenticate your user. Devise (most likely) is still going to authenticate your users via username and password while using OAuth. Alternatively, a third party like Facebook or Google can authenticate the user for you. From that point on OAuth ensures that your users can do what they are intending to do with your app and third party apps.
Resource Owner Password Credentials Flow
In this flow, a username and password is provided and in exchange you get a token back to use. There is a lot of debate over this flow as it is not officially recommended to use and considered an insecure implementation of OAuth. The preferred flow to replace this one is the "authorization code flow" described further below. The cited reasons for removing this flow include:
- When a user logs into your app, they can not set which permissions to allow your application to have.
- Passwords could be leaked from the clients server (login server) via logs or other means.
- OAuth is Authorization and not Authentication.
For most SaaS apps, these concerns are not an issue. You are the one that controls their password anyways and since you aren't a third party you know exactly what permissions they should have. You're going to have to use this flow if your product owner doesn't want to accept a browser redirect to sign in.
Thus, given proper restrictions it can be a secure way to build your applications. If you want to be the absolute most semantically correct than go with an Authorization code + PKCE.
Authorization code + PCKE
This is the recommended flow for mobile apps in the browser. It uses browser based authentication and you first must redirect your app user to a browser to login (Devise!). At that point they are redirected back to your application with an Authorization code. This authorization code is then exchanged for the users access token. PCKE provides a check to ensure that in the case of the authorization being intercepted it can not be exchanged for an access token.
Important: This flow may not officially support refresh tokens for browser applications. For native mobile apps a refresh token is permitted. In order to use a refresh token you need to send the client id and secret. Since browsers aren't a secure environment there is no way to verify the client is real. Thus refresh tokens are not recommended for this reason in web apps.
Unfortunately this makes a bit of a compromised user experience. Without a refresh token the user is going to need to enter their password every expiration. Thus I think to be truly practical for most people a deviation from the OAuth spec needs to be made. People will either end up implementing the "resource owner password credential flow" or adding refresh token capability to the "authorization code + PCKE" flow. Both have the same security flaw when implemented for browser apps. Using cookie auth prevents these security flaws.
I didn't get any of that...
In short, implementing tokens in browser based applications is going to require compromises to user experience to also be secure. Building your web app with cookie auth is going to be faster, less code, more secure and ultimately cheaper.
But I want a mobile app too!
There is no reason you can't use cookie based auth for your web app and token based auth for your native apps. In either case, Devise is most likely going to front your login page (or at least process the authentication request).
OAuth Providers
Doorkeeper - Doorkeeper provides all the flows needed for issuing and revoking OAuth tokens and applications.
JSON Web Tokens
JSON Web Tokens (JWT) is a specification for authentication and authorization. It was invented to be a "stateless" way to authenticate. I strongly recommend to avoid this approach for most SaaS applications because by the time it's implemented you lose the "stateless" benefit with no added advantage.
Why JWT?
JWT was created to solve a problem with micro services. People realized that they often split their micro services out in such a way that each service needed to auth the user. This makes the authentication service heavily used and if you need to use more than one micro service to render a page you are doing multiple authentication requests per page render instead of one. This doesn't make a lot of sense. Instead, after the user is authenticated, we store their details in an encrypted bit of data and the other services can read the data instead of accessing the authentication service every times it needs to authorize.
That sounds great!
Yes... but it's going to cause bugs and edge cases that need addressed. For example:
- User 1 logs into your app at 10am and has a 4 hour session
- User 2 revokes User 1's access at 11am
- User 1 has 3 hours of elevated access
This can be solved by revoking the token when user 2 updates user 1's access. But it introduces the catch 22 of JWT. You need to check the tokens validity each request. Doing this invalidates the primary benefit of not having to check the current state on each request!
By the time it's all said and done and all the bugs are patched, you've built the "resource owner password credential" OAuth flow utilizing a JWT formatted token response. Save the time and just start with the OAuth flow.
Where are JWT's useful?
It is great where app's permissions and state is not very important.
- If your site doesn't have the ability to change logged in user permissions
- You never need to force log out your users
Potential use cases
High traffic apps with non critical data. The downsides to JWT inside the javascript app can be largely ignored to pick up some performance improvements in the backend systems.
Somewhat secure API's. Sometimes you just want a way to track usage of your API and the data is public and free anyways (weather etc). If there is a pay tier as well a JWT can encode that information for future requests to avoid having to auth every request.
Transactional access. Apps that sell transactional services. For instance, a package that gives me 30 days access to tutorials. The JWT can contain the expiration and permissions. Since the transaction was made up front and will not change, JWT's would work well assuming you don't need to issue refunds and cancel their service.
Behind an API gateway. The gateway can handle the authentication and store that data to distribute to other micro services that do not have to check the auth server for that request.
JWT Providers
Devise-JWT - Provides support for generating JWT tokens from a model.
I would not recommend Devise-jwt as your sole solution to generating tokens. There are too many pitfalls to fall into and if you are not aware of exactly what you are doing then Doorkeeper and an Authorization code flow with Devise is going to be more secure and easier to setup.
So which one do I use?
All of them. As your business gains complexity so does your authentication system. In the beginning you can typically get by with cookies. As you build mobile apps and API's onto your product you will need to add token auth. As you start to scale it's very possible your services move to a JWT auth to talk to each other.