- Understand why authentication tokens are commonly used when interacting with APIs
- Add a token strategy to an application
- Authenticate a user based on their token
- Build a basic Rails API
- Understand foundational concepts in authentication & encryption
##The authentication problem The HTTP protocol is stateless. This means the server does not remember anything about a client between requests. So if we authenticate a user with a username and password, then on the next request, our application won't remember us.
The old way of dealing with this was to store who was logged in on the server. Every time the server received a request from a client, it would check to see if that client was logged in or not. This is not very efficient:
-
Every time a user logs in, the server has to create a record somewhere on the server. If lots of users are logging in, the overhead on our server increases.
-
If the logged in information is stored in local memory on a server, the user will only be able to make requests to that server. This is not ideal - what if we have a bunch of servers, but most of the users are forced into using the original server they logged in at?
There are a few other problems, but these two are the main ones.
Token-based authentication is stateless. We are not storing any information about a logged in user on the server. No stored information means your applicaiton can scale and add more machines as necessary without worrying about where a user is logged in.
Here is the JWT authentication flow:
User requests access with username and password
|
|
The app validates the credentials
|
|
The app gives a signed token to the client
|
|
The client stores the token and presents it with every request
Just three strings, separated by periods:
aaaaaaaaaaaaaaa.bbbbbbbbbbbbbbbbbbbbb.ccccccccccccccccccc
The first part (aaaaaaaaaaaa) is the header
The second part (bbbbbbbbbbbb) is the payload - the good stuff, like who this person is, and their id in our database.
The third part (ccccccccccccc) is the signature. The signature is a hash of the header and the payload. It is hashed with a secret key, that we will provide.
Head on over to jwt.io and see what I mean:
In the example above, you'll notice that there are 3 parts. The payload is the one we care the most about, and it holds whatever data we decide to put in there. It's very much like a cookie; we put as few things in there as possible – just the pieces we really need.
Applications can save a JWT somewhere on a user's computer, just like a cookie. Because JWTs can be encrypted into a single string, we can also send it over HTTP really, really easily. Which means it'll work in any server/client scenario you can imagine. Quite nice.
Let's take a few minutes to look at the rails api code.
What I want to do is limit our API so that you can only get the user data from it IF you have been authenticated.
We will need to build a current_user/authenticate route that will check a user's password against the stored (and hashed) version in our database. If they match, our API will return a JWT. We then need to take that JWT and include it in all our later requests to prove that we are logged in.
Open up rails console and get a user in there:
User.create({username: 'johannkerr',password:'mustlovecats'})
This authentication is going to take place in our auth controller and application controller file, where we previously said that authentication would take place.
Let's first install the necessary JWT gem:
$ gem 'jwt'
We also need to provide a string that will serve as the secret signing key used in the hashing of the header and payload, to make up the third part of the JWT string
secret = "supersecretcode"
The algorithim as well:
algorithm = "HS256"
Explain JWT.encode && JWT.decode
Let's make sure we have all the required routes. Auth is where a user will send a POST request with a username and password. I.e. this is the route they will use to login. I guess we could have called it http://localhost:3000/api/v1/login
So let's write the machinery to generate a JWT and validate it:
Here's some pseudo-code of what I want to do -
class ApplicationController < ActionController::API
before_action :authorized
# this will run before every single action gets called, make sure you skip_before_action in the appropriate places
def issue_token(payload)
JWT.encode(payload, ENV['secret'], 'HS256')
# your secret should be in another file that is .gitignore'd, use a gem like 'figaro' to manage
end
def current_user
@user ||= User.find_by(id: user_id)
end
def user_id
decoded_token.first['id']
end
def decoded_token
begin
JWT.decode(request.headers['Authorization'], ENV['secret'], true, { :algorithm => 'HS256' })
rescue JWT::DecodeError
[{}]
end
end
def authorized
render json: {message: "Not welcome" }, status: 401 unless logged_in?
end
def logged_in?
!!current_user
end
end
And here is what the sessions controller should look like -
class Api::V1::AuthController < ApplicationController
skip_before_action :authorized, only: [:create, :show]
def create
user = User.find_by(username: params[:username])
if user && user.authenticate(params[:password])
render json: {username: user.username, id: user.id, token: issue_token({id: user.id})}
else
render({json: {error: 'User is invalid'}, status: 401})
end
end
def show
if current_user
render json: {
id: current_user.id,
username: current_user.username
}
else
render json: {error: 'Invalid token'}, status: 401
end
end
end
Lets take a few minutes to checkout what was given to us on the client.
Explain local storage.
localStorage.getItem('key')
localStore.setItem(object)
View in application to in dev tools
Now lets write the code for the authAdapter, what should this class do for us?
static anyone?
Here is the completed code:
const baseUrl = 'http://localhost:3000/api/v1'
export default class AuthAdapter {
static login (loginParams) {
return fetch(`${baseUrl}/auth`, {
method: 'POST',
headers: headers(),
body: JSON.stringify(loginParams)
}).then(res => res.json())
}
static currentUser () {
return fetch(`${baseUrl}/current_user`, {
headers: headers()
}).then(res => res.json())
}
}
function headers () {
return {
'content-type': 'application/json',
'accept': 'application/json',
'Authorization': localStorage.getItem('jwt')
}
}
Our App.js will contain the main logic for login
logout
and checking for a logged in user.
logIn(loginParams){
Auth.login(loginParams)
.then( user => {
if (!user.error) {
this.setState({
auth: { isLoggedIn: true, user: user}
})
localStorage.setItem('jwt', user.jwt )
}
})
}
logout(){
localStorage.removeItem('jwt')
this.setState({ auth: { isLoggedIn: false, user:{}}})
}
//Navigation.js
<Menu.Item name='logout' onClick={logout} />
Next we want to grab and validate the user on initiale mount. Lets add:
componentWillMount(){
if (localStorage.getItem('jwt')) {
Auth.currentUser()
.then(user => {
if (!user.error) {
console.log("fetch user");
this.setState({
auth: {
isLoggedIn: true,
user: user
}
})
}
})
}
}
I want to restrict users from going to the cards or home page unless the are logged in.
There are a few different ways I can do this. For now we will stay away from HOCs.
First method using Redirect component:
<Route exact path='/' render={()=>{
return this.state.auth.isLoggedIn ? <Home /> : <Redirect to="/login"/>
}} />
Second method using browser history component:
App.js
import createBrowserHistory from 'history/createBrowserHistory'
const history = createBrowserHistory()
<Router history={history}>
OR
import PropTypes from 'prop-types'
static ContextTypes ={ router: PropTypes.Object }
explain how all route components now have this.props.history
Component to authorize
componentWillMount () {
console.log('Mounting')
if (!localStorage.getItem('jwt')) this.props.history.push('/login')
}
componentWillUpdate () {
console.log('Updating')
if (!localStorage.getItem('jwt')) this.props.history.push('/login')
}
import React from 'react'
import PropTypes from 'prop-types'
export default function (RenderedComponent, inheritedProps) {
return class extends React.Component {
static contextTypes = {
router: PropTypes.object
}
componentDidMount() {
if (!localStorage.getItem('jwt')) {
this.context.router.history.push('/')
}
}
render() {
return (
<RenderedComponent {...inheritedProps} />
)
}
}
}
- What is a JWT? Why is useful for authorizing an API?
- How do you create a JWT?
- How do you secure a react component?