Use JWT for Authentication with the Knock Gem for Ruby on Rails APIs
By Steve Carey - 8/8/2018
Final code: Github
This is part of a series of tutorials on using React with Ruby on Rails. Each section is self-contained. In this section we will restrict access to our app by adding a User model to the Rails back end, a Log In page to the React front end, and use JSON Web Tokens for authentication.
We will duplicate the app we built in previous tutorials of this series which consists of:
In a traditional Ruby on Rails web application you would have a User model with a signup page. Then you would manage authentication with a login page that checks the user's username or email and password against the database. When the user is successfully logged in, their user id is added to the secured session cookie that Rails passes back and forth with the user's browser for each request-response cycle. That way you can restrict access to certain pages and actions to only registered users.
With an API this works the same except for the session cookie part. By definition the API server and the Client application are on different domains. In the app we are about to build, in development they will be on localhost:3001 and localhost:3000 respectively. Rails and other web frameworks have a same-origin policy where they won't send session cookies to another domain. It exposes the server to Cross-Site-Request-Forgery attacks. We need another solution. In this tutorial we will use JSON Web Tokens.
A JSON Web Token or JWT (pronounced J-W-T or "jot") allows us to authenticate requests between the client and the server by encrypting authentication information into a compact JSON object called a token. Then that token can be included with HTTP requests that require authentication. Session cookies are automatically sent back and forth with every single request. JWTs on the other hand, must be specifically programmed to be included with a request, which you would do only when authentication is needed.
A JWT consists of three strings separated by dots - the header, the payload, and the signature.
Since the first two strings are encoded using Base64, they can be decoded. Later in this tutorial when we create JWTs, there is an online tool that can decode them so you can see the header and payload: jwt.io.
We'll only give explanations for steps not covered in Rails APIs Tutorial of this series.
Generate a new API-only application:
mkdir rails-react-jwt; cd rails-react-jwt
rails new . --api
Generate an Articles scaffold which will give us a database migration file, a model, a JSON-api ready controller, and a route resource.
rails generate scaffold Article title:string content:text
rails db:migrate
# db/seed.rb articles = Article.create([ {title: "Learn Ruby", content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."}, {title: "Learn Rails", content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."}, {title: "Learn React", content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."} ])
rails db:seed
Add the CORS gem to allow access to our API.
# Gemfile gem 'rack-cors'
bundle install
# config/initializers/cors.rb Rails.application.config.middleware.insert_before 0, Rack::Cors do allow do origins 'http://localhost:3000' resource '*', headers: :any, methods: [:get, :post, :put, :patch, :delete, :options, :head] end end
This is the same API we build in part 1. Run the server and view the JSON data at http://localhost:3001/articles to make sure it's working so far.
rails server -p 3001
Let's add two more gems. Bcrypt for password hashing and knock which is specifically for JWT authentication.
# Gemfile gem 'bcrypt', '~> 3.1.7' gem 'knock'
bundle install
Generate the User scaffold:
rails generate scaffold User name:string email:string password_digest:string admin:boolean
Bcrypt will encrypt the password for us. You need to add "has_secure_password" to the User class. We'll add some validations too:
# app/models/user.rb class User < ApplicationRecord has_secure_password validates :name, presence: true validates :email, presence: true, uniqueness: { case_sensitive: false } end
The users controller generated by the scaffold gave us a field called password_digest, just like we told it to. But Bcrypt, in addition to hashing the password, it also turns password_digest into two fields: password and password_confirmation. So we need to change the permitted params from :password_digest to those two fields:
# users_controller.rb def user_params params.require(:user).permit(:name, :email, :password, :password_confirmation, :admin) end
In the migration file make the default value for admin false:
# db/migrate/timestamp_create_users.rb ... t.boolean :admin, default: false ...
Now migrate the database:
rails db:migrate
Add some additional seed data with at least one admin and one non-admin, something like the below. Make sure to delete or comment out the articles seed data, otherwise it will add the same articles again!
# db/seeds.rb User.create!(name: "Joey Ramone", email: "joey@ramones.com", password: "ramones", password_confirmation: "ramones", admin: true) User.create!(name: "Johnny Ramone", email: "johnny@ramones.com", password: "ramones", password_confirmation: "ramones")
rails db:seed
We installed the Knock gem, now we need to configure it to work with our User model. You can read more about knock on their Github page.
Run the install generator:
rails generate knock:install
This will create an initializer file at config/initializers/knock.rb that contains the default configuration. By default, Knock will expire the token in 24 hours. If you want to change that then uncomment the line and make the appropriate adjustments:
# config/initializers/knock.rb config.token_lifetime = 1.day
Generate a controller for users to sign in through:
rails generate knock:token_controller user
This generates a controller called user_token_controller. It inherits from Knock::AuthTokenController which comes with a create action that will create a JWT when a user successfully logs in. The generator also inserts a route in the routes.rb file: post 'user_token' => 'user_token#create'
as an API endpoint for the client to call when logging in.
Include the Knock::Authenticable module in your ApplicationController so you can add authentication before filters to to any of your controllers:
# app/controllers/application_controller.rb class ApplicationController < ActionController::API include Knock::Authenticable end
Add an authenticate_user before filter to the articles controller. And let's say we only want admin users to be able to delete articles. In that case you can wrap the @article.destroy statement in a conditional.
# app/controllers/articles_controller.rb before_action :authenticate_user ... def destroy if current_user.admin? @article.destroy end end
Knock will add the User id to the JWT payload by default, assigned to a key called "sub." If you want to add any other information, you would add a method to the user model called to_token_payload and add it there. If you do that you need to also explicitly add sub: id
or it won't be included. The below is just for reference and not needed in our app.
# app/models/user.rb ... def to_token_payload { sub: id, name: name } end
If you are using Rails 5.2 or higher you need to take two more steps. First, protect_from_forgery is included in ActionController::Base by default now, so you need to skip that in the Knock controller we generated for logging in from our React client. API clients are from a different domain so they won't have the standard Rails authenticity token.
# app/controllers/user_token_controller.rb class UserTokenController < Knock::AuthTokenController skip_before_action :verify_authenticity_token, raise: false end
The other change is Rails no longer uses config/secrets.yml to hold the secret_key_base that is used for various security features, including generating JWTs with the Knock gem. Rails now uses an encoded file called config/credentials.yml.enc. Add the below line to the Knock configuration file. If there is a line like this in the knock.rb file that is commented out, don't just uncomment it. Make sure it points to Rails.application.credentials and not Rails.application.secrets. The latter is for Rails 5.1 and earlier apps.
# config/initializers/knock.rb ... config.token_secret_signature_key = -> { Rails.application.credentials.fetch(:secret_key_base) }
This time let's add an "api" scope to all of our api routes. That will add "api" to the path but not to the controller or model.
# config/routes.rb scope '/api' do post 'user_token' => 'user_token#create' resources :users resources :articles end
Now for example, once the user has been authenticated they would access the articles API with http://localhost:3001/api/articles
.
This is where an API testing app like Postman is useful. The Curl command line tool will also work but is less user friendly. Especially when you get an error since Rails will send back an HTML error page which is unreadable from the Terminal screen. If you don't have Postman and don't feel like spending an hour or more loading it and getting a feel for it, you can skip this step and go to the next section.
In the tool, create a new user as a JSON string:
Method: POST URL: http://localhost:3001/api/users Headers: Content-type: application/json Body: { "user": { "name": "DeeDee Ramone", "email": "deedee@ramones.com", "password": "ramones" } }
Make sure to restart the server since we made some configuration changes.
rails server -p 3001
If you send this request you should get a 201 successfully created response, and have a new User record in the database. You can see all the users in JSON format at http://localhost:3001/api/users
. We won't make a sign up page in our app though. This is just to see if the API endpoint is working.
We will, however, be requesting JSON Web Tokens from our React client as our log in process. We can test that the same way. We just change the URL and the body.
Method: POST URL: http://localhost:3001/api/user_token Headers: Content-type: application/json Body: { "auth": { "email": "deedee@ramones.com", "password": "ramones" } }
Send this request. Knock will check the database for the email and password. If they match it will generate and return a JWT token which you should see in the response panel of the tool. If so, all is working as it should. On to React.
This user interface will be similar to the one from part 2 of this Rails-React series. We'll only explain what's different. Start by generating a separate React application using the create-react-app Node package. While you can put this anywhere, we'll put it in our Rails API app in it's own folder called "client."
create-react-app client
cd client
Install the react-router and axios packages:
yarn add react-router-dom axios
It's a good idea to run the React App just to make sure it's working. We could do this with the yarn start
command, but let's make things a little easier with the Foreman gem.
Right now we need to use two separate commands to start the Rails API server, and the React client server. The Foreman gem will let us do that with one command. Let's set that up. Per the Foreman setup instructions, don't add the gem to the Gemfile. Rather just install it directly on your system with gem install foreman
(if you haven't installed it already).
By default, when you issue the foreman start
command it will look for instructions in a file called Procfile. That same file is used in production as well (Heroku uses Procfile in production). Since our server start commands won't apply to the production environment, let's create a Procfile specifically for the dev environment:
touch Procfile.dev
Populate it with the following:
# Procfile.dev api: rails server -p 3001 web: cd client && PORT=3000 npm start
The first line will start our rails API server on port 3001. The second line will cd into the client folder then start our React server on port 3000.
Make sure to stop the Rails server you had running earlier. Now you can start both servers with one command:
foreman start -f Procfile.dev
The -f flag is forcing the use of the Procfile.dev file instead of the default Procfile.
In your browser if you go to localhost:3000 you should see the Welcome to React default page. React is working. And the Rails API should also be running on port 3001. If you go to localhost:3001/api/articles you should get a 401 error because we restricted access in the controller. But if you go to http://localhost:3001/api/users you should see a JSON rendering of your Users since we didn't restrict access in the users controller (which we would in a production app).
Lets add some structure to your React folders. From the "client" directory add the components, stylesheets, and images directories, move some of the default files, and add the component files that we'll populate later.
mkdir public/images mv public/favicon.ico public/images/favicon.ico mkdir src/stylesheets mkdir src/components rm src/logo.svg mv src/index.css src/stylesheets/index.css mv src/App.css src/stylesheets/App.css mv src/App.js src/components/App.jsx mv src/App.test.js src/components/App.test.js touch src/components/Home.jsx touch src/components/Login.jsx touch src/components/Logout.jsx touch src/components/ArticleList.jsx touch src/components/ArticleInfo.jsx touch src/components/ArticleAdd.jsx touch src/components/ArticleEdit.jsx
Fix the links to reflect the new folders and moved files:
# public/index.html <link rel="shortcut icon" href="%PUBLIC_URL%/images/favicon.ico">
# src/index.js import './stylesheets/index.css'; import App from './components/App';
Add the Bootstrap CDN to the index.css file for convenience.
# client/src/stylesheets/index.css @import url('https://stackpath.bootstrapcdn.com/bootstrap/4.1.2/css/bootstrap.min.css');
In the package.json file add a proxy. This will allow us to use shortcuts in our api calls within our React code. So instead of calling 'http://localhost:3001/api/articles', you only need to call '/api/articles'. Make sure to add the comma before the preceding property name as shown below.
# package.json ... }, "proxy": { "/api": { "target": "http://localhost:3001" } } }
Now let's set up the central component of our React client, the App.jsx file. This will hold our navigation bar and the custom element that will hold the content from each link. This is essentially the same as from our earlier app except we took the default header out and we're adding Login and Logout routes with corresponding components. Your code should look like this:
import React, { Component } from 'react'; import '../stylesheets/App.css'; import Home from './Home'; import Login from './Login'; import Logout from './Logout'; import ArticleList from './ArticleList'; import ArticleInfo from './ArticleInfo'; import ArticleAdd from './ArticleAdd'; import ArticleEdit from './ArticleEdit'; import {BrowserRouter as Router, Route, NavLink, Switch} from 'react-router-dom' class App extends Component { render() { return ( <Router> <div className="container"> <Navigation /> <Main /> </div> </Router> ); } } const Navigation = () => ( <nav className="navbar navbar-expand-lg navbar-dark bg-dark"> <ul className="navbar-nav mr-auto"> <li className="nav-item"><NavLink exact className="nav-link" activeClassName="active" to="/">Home</NavLink></li> <li className="nav-item"><NavLink exact className="nav-link" activeClassName="active" to="/articles">Articles</NavLink></li> { localStorage.getItem("jwt") ? <li className="nav-item"><NavLink exact className="nav-link" to="/logout">Log Out</NavLink></li> : <li className="nav-item"><NavLink exact className="nav-link" activeClassName="active" to="/login">Log In</NavLink></li> } </ul> </nav> ); const Main = () => ( <Switch> <Route exact path="/" component={Home} /> <Route exact path="/login" component={Login} /> <Route exact path="/logout" component={Logout} /> <Route exact path="/articles" component={ArticleList} /> <Route exact path="/articles/new" component={ArticleAdd} /> <Route exact path="/articles/:id" component={ArticleInfo} /> <Route exact path="/articles/:id/edit" component={ArticleEdit} /> </Switch> ); export default App;
There is something funny going on in the Navigation component with the Log Out and Log In NavLinks. We'll get to that later.
Populate the Home component:
# client/src/components/Home.jsx import React from 'react'; const Home = () => { return ( <div className="jumbotron"> <h1>Home Page</h1> </div> ); } export default Home;
You may need to restart the servers with foreman because of all the file changes: Ctrl+C
to stop.
foreman start -f Procfile.dev
Now in http://localhost:3000 you should see the Home Page. None of the other links will work yet. Next we'll hook up the Log In page.
Populate the Login.jsx file with:
import React, { Component } from 'react' import { post } from 'axios'; class Login extends Component { constructor() { super() this.handleSubmit = this.handleSubmit.bind(this); } handleSubmit (event) { event.preventDefault(); const email = document.getElementById('email').value; const password = document.getElementById('password').value; const request = {"auth": {"email": email, "password": password}}; post('/api/user_token', request) .then(response => { localStorage.setItem("jwt", response.data.jwt); this.props.history.push("/"); }) .catch(error => console.log('error', error)); } render() { return ( <div> <h1>Log In</h1> <form onSubmit={this.handleSubmit}> <div className="form-group"> <label htmlFor="email">Email: </label> <input name="email" id="email" type="email" className="form-control" /> </div> <div className="form-group"> <label htmlFor="password">Password:</label> <input name="password" id="password" type="password" className="form-control" /> </div> <button type="submit" className="btn btn-dark">Submit</button> </form> </div> ); } } export default Login;
Let's break down some of this code, starting with the render method:
render() { return ( <div> <h1>Log In</h1> <form onSubmit={this.handleSubmit}> #1a <div className="form-group"> <label htmlFor="email">Email: </label> <input name="email" id="email" type="email" className="form-control" /> #2 </div> <div className="form-group"> <label htmlFor="password">Password:</label> <input name="password" id="password" type="password" className="form-control" /> #3 </div> <button type="submit" className="btn btn-dark">Submit</button> #1b </form> </div> ); }
1) This is a React form. When the user clicks the submit button it triggers the onSubmit event which calls the handleSubmit handler function.
2&3) There are two form fields. We are approaching this form a little differently than we will with the article new and edit forms. Those use what React calls controlled components where the state of each form field is updated with every character typed. It requires a separate onChange attribute in the form fields that calls the handleChange function to setState with every keystroke. Here we are skipping the onChange event and just using the onSubmit event.
The handleSubmit handler function is where we send the login form data to the API.
handleSubmit (event) { event.preventDefault(); #1 const email = document.getElementById('email').value; #2 const password = document.getElementById('password').value; #3 const request = {"auth": {"email": email, "password": password}}; #4 post('/api/user_token', request) #5 .then(response => { #6 localStorage.setItem("jwt", response.data.jwt); #7 this.props.history.push("/"); #8 }) .catch(error => console.log('error', error)); #9 }
1) Since we are submitting a form we have to prevent the HTML default action which is to go to a new URL with the form data. Instead we are sending the from data via AJAX.
2&3) We added id attributes to the form fields. We use getElementById to get the form values.
4) Our API is using the Knock gem which will look to receive a login request with a property called "auth" set to an object containing the the log in form field names and values. If you want to see the request variable in the console at this point you can insert a console.log(request);
.
5) We are using the Axios package for our AJAX calls. Earlier we added a proxy to the package.json file allowing us to use just the path instead of the full URL. You could still use the full URL if you wanted to. The path we are using is the specially created route for the Knock gem's log in process. It contains the form data in the "request" variable.
6) If the email and password match a user in the database, Knock will send back a JWT as part of the response's data string. If you want to view the response in the console you can insert a console.log(response);
.
7) Once we get the JWT token we need to save it somewhere and send it with our requests. Two options are sessionStorage and localStorage. Whatever we put in sessionStorage will be stored in the browser until the user closes their browser. Then it is automatically erased. LocalStorage, on the other hand, will save it even after the user closes the browser. Which you use depends on how you want your app structured. By default, JWTs expire in 24 hours so it won't make much difference either way. But if you make your JWTs last much longer, then save the token in local storage, that will allow users to automatically be logged in whenever they visit your site.
8) We'll send the user to the home path when they log in.
9) In a real app you would want to give the user a useful message if there is an error when they log in. This is an introductory tutorial so we'll just log the error to the console.
Axios is a React package that simplifies making AJAX calls. If you want to know how to make the request using the fetch API instead, you would replace the lines 5-9 with:
fetch('/api/user_token', { method: 'POST', body: JSON.stringify(request), headers: {'Content-Type': 'application/json' } }) .then(response => response.json()) .then(data => { localStorage.setItem("jwt", data.jwt); this.props.history.push("/"); }) .catch(error => console.log('error', error));
Populate the logout component:
import React from 'react'; import { Redirect } from 'react-router-dom' const Logout = () => { localStorage.removeItem('jwt'); return <Redirect to='/' /> } export default Logout;
It only does two things.
1) It removes the jwt item from localStorage (or sessionStorage if you put it there).
2) It uses React Router's Redirect component to redirect to the home path.
Now let's revisit the Navigation component in the App.jsx file where something funny was going on.
const Navigation = () => ( <nav className="navbar navbar-expand-lg navbar-dark bg-dark"> <ul className="navbar-nav mr-auto"> <li className="nav-item"><NavLink exact className="nav-link" activeClassName="active" to="/">Home</NavLink></li> <li className="nav-item"><NavLink exact className="nav-link" activeClassName="active" to="/articles">Articles</NavLink></li> { #1 localStorage.getItem("jwt") ? #2 <li className="nav-item"><NavLink exact className="nav-link" to="/logout">Log Out</NavLink></li> #3 : <li className="nav-item"><NavLink exact className="nav-link" activeClassName="active" to="/login">Log In</NavLink></li> #4 } </ul> </nav> );
This component is our navigation bar which contains the React Router package's NavLink elements. In addition to the Home and Articles links, we want to display either a Log In or Log Out link as determined through a conditional statement. This might seem like a good place for an if-else statement. And it is. Except you can't put if statements within JSX. Here's the details as to why in case you're curious: react tips. But you can insert an expression in JSX including a ternary statement. A ternary statement is an if-else statement with short syntax: condition ? if true : if false
. So we'll use that.
1) To include an expression in JSX, wrap it in curly braces.
2) Our ternary statement will start with the condition of whether there is a jwt object in localStorage (or sessionStorage if you are using that).
3) If there is, display the NavLink element to the Logout path.
4) If not, display the NavLink element to the Login path.
Let's try it out to see if it works. In your browser go to the Login page. Enter a user email and password (from the db/seeds.rb file). Log in and Log out a few times. Hopefully, this works! If you are curious to see your local storage, after you log in, open Chrome Developer Tools and click on the "Application" tab. On the left panel you'll see a section labeled Storage. There you can click on Local Storage (or Session Storage) and you should see the jwt key and it's value. Then if you are really curious you can decode it online at jwt.io.
Populate the Articles resource components starting with ArticleList.
import React, { Component } from 'react'; import axios from 'axios'; import { Link } from 'react-router-dom'; class ArticleList extends Component { constructor() { super(); this.state = { articles: [] }; } componentDidMount() { let token = "Bearer " + localStorage.getItem("jwt"); axios({method: 'get', url: '/api/articles', headers: {'Authorization': token }}) .then(response => { this.setState({ articles: response.data }) }) .catch(error => console.log('error', error)); } render() { return ( <div> {this.state.articles.map((article) => { return( <div key={article.id}> <h2><Link to={`/articles/${article.id}`}>{article.title}</Link></h2> {article.content} <hr/> </div> ) })} <Link to="/articles/new" className="btn btn-outline-primary">Create Article</Link> </div> ) } } export default ArticleList;
Now in your browser if you click on the articles menu you should get the list of articles if you are logged in, and get nothing if you are logged out.
This component is mostly the same as we created in Part 2 of this series. But this time, on the back end we restricted access to the Articles resource to only logged in users. So now when we make the AJAX request we need to send the JWT token in the request headers. Remember that unlike session cookies, JWT tokens are not sent automatically. We have to add them to the headers.
componentDidMount() { let token = "Bearer " + localStorage.getItem("jwt"); #1 axios({method: 'get', url: '/api/articles', headers: {'Authorization': token }}) #2 .then(response => { #3 this.setState({ articles: response.data }) }) .catch(error => console.log('error', error)); }
1) Set a variable that gets the jwt string from localStorage (or sessionStorage). It should be in the format of "Bearer" then the string.
2) Use Axios to make the GET request. We must add the JWT to the request headers under the "Authorization" key.
3) Use ES6 promises to change the state for articles from an empty array to the response data when it is received.
If you want to see what this would look like using Fetch instead of Axios, here it is:
componentDidMount() { let token = "Bearer " + localStorage.getItem("jwt") fetch('/api/articles', { method: 'GET', headers: {'Authorization': token }}) .then(response => response.json()) .then(data => { this.setState({articles: data}); }) .catch(error => console.log('error', error)); }
Here's the ArticleInfo code:
import React, { Component } from 'react'; import axios from 'axios'; import { Link } from 'react-router-dom'; class ArticleInfo extends Component { constructor() { super(); this.state = { article: {} }; this.handleDelete = this.handleDelete.bind(this); } componentDidMount() { let token = "Bearer " + localStorage.getItem("jwt"); axios({method: 'get', url: `/api/articles/${this.props.match.params.id}`, headers: {'Authorization': token }}) .then((response) => { this.setState({ article: response.data }) }) .catch(error => console.log('error', error)); } handleDelete() { let token = "Bearer " + localStorage.getItem("jwt"); axios({ method: 'delete', url: `/api/articles/${this.props.match.params.id}`, headers: {'Authorization': token}}) .then(() => { this.props.history.push("/articles") }) .catch(error => console.log('error', error)); } render() { return ( <div> <h2>{this.state.article.id}: {this.state.article.title}</h2> <p>{this.state.article.content}</p> <p> <Link to={`/articles/${this.state.article.id}/edit`} className="btn btn-outline-dark">Edit</Link> <button onClick={this.handleDelete} className="btn btn-outline-dark">Delete</button> <Link to="/articles" className="btn btn-outline-dark">Close</Link> </p> <hr/> </div> ) } } export default ArticleInfo;
Here we are also adding the JWT token to the request headers. In the Rails articles_controller we restricted article deletes to admin users. In a real application we would only display the delete button to admin users, but we'll keep it simple here and leave it as is.
Populate the ArticleAdd component:
import React, { Component } from 'react'; import axios from 'axios'; class ArticleAdd extends Component { constructor() { super(); this.state = { title: '', content: ''}; this.handleChange = this.handleChange.bind(this); this.handleSubmit = this.handleSubmit.bind(this); this.handleCancel = this.handleCancel.bind(this); } handleSubmit(event) { event.preventDefault(); let token = "Bearer " + localStorage.getItem("jwt") axios({ method: 'post', url: '/api/articles', headers: {'Authorization': token }, data: this.state}) .then((response) => { this.props.history.push(`/articles/${response.data.id}`); }) .catch(error => console.log('error', error)); } handleChange(event) { this.setState({ [event.target.name]: event.target.value }); } handleCancel() { this.props.history.push("/articles"); } render() { return ( <div> <h1>Create Article Post</h1> <form onSubmit={this.handleSubmit}> <div className="form-group"> <label>Title</label> <input type="text" name="title" value={this.state.title} onChange={this.handleChange} className="form-control" /> </div> <div className="form-group"> <label>Content</label> <textarea name="content" rows="5" value={this.state.content} onChange={this.handleChange} className="form-control" /> </div> <div className="btn-group"> <button type="submit" className="btn btn-dark">Create</button> <button type="button" onClick={this.handleCancel} className="btn btn-secondary">Cancel</button> </div> </form> </div> ); } } export default ArticleAdd;
And finally, populate the ArticleEdit component:
import React from 'react'; import axios from 'axios'; class ArticleEdit extends React.Component { constructor() { super(); this.state = { title: '', content: ''}; this.handleChange = this.handleChange.bind(this); this.handleSubmit = this.handleSubmit.bind(this); this.handleCancel = this.handleCancel.bind(this); } componentDidMount() { let token = "Bearer " + localStorage.getItem("jwt"); axios({method: 'get', url: `/api/articles/${this.props.match.params.id}`, headers: {'Authorization': token }}) .then((response) => { this.setState(response.data) }) .catch(error => console.log('error', error)); } handleSubmit(event) { event.preventDefault(); let token = "Bearer " + localStorage.getItem("jwt") axios({ method: 'patch', url: `/api/articles/${this.state.id}`, headers: {'Authorization': token }, data: this.state}) .then(() => { this.props.history.push(`/articles/${this.state.id}`); }) .catch(error => console.log('error', error)); } handleChange(event) { this.setState({ [event.target.name]: event.target.value }); } handleCancel() { this.props.history.push(`/articles/${this.state.id}`); } render() { return ( <div> <h1>Edit {this.state.title}</h1> <form onSubmit={this.handleSubmit}> <div className="form-group"> <label>Title</label> <input type="text" name="title" value={this.state.title} onChange={this.handleChange} className="form-control" /> </div> <div className="form-group"> <label>Content</label> <textarea name="content" rows="5" value={this.state.content} onChange={this.handleChange} className="form-control" /> </div> <div className="btn-group"> <button type="submit" className="btn btn-dark">Update</button> <button type="button" onClick={this.handleCancel} className="btn btn-secondary">Cancel</button> </div> </form> </div> ); } } export default ArticleEdit;
Add, delete, and edit some articles. If you remove the headers: {'Authorization': token }
from any of the requests you should see a 401 (Unauthorized) error in the console. If it all works (and it should), you now have basic authentication with JSON Web Tokens!