Build a simple Rails API server + Auth0 JWT authentication + React from scratch in 30 minutes (or less)
TL;DR In a nutshell the following gif (it might take a minute to load) shows what this article is all about:
Demo app is located at http://react-auth0-api.herokuapp.com/
Note : If you access the site for the first time, it might take about 30 seconds to ping the API server as heroku needs to start up the app.
Github-Rails API server : https://github.com/iankhor/ror-auth0-api
Github-React Front-end : https://github.com/iankhor/react-auth0-api
A mate of mine,
— a great coder himself, got me thinking about authenticated API calls from a browser. So I thought to give it a crack myself !
Purpose
The intention of this guide is for newcomers or startups who wants to quickly bring up a platform to get users. It does not follow any general best practices but serves as a guide to get an API server with a front-end up and running up quickly. This article is not the only way to do things but one of the ways to get some traction on your current project. If you run into any issues, I’d be happy to assist but Google is always your best companion
Disclaimer
If you are reading this 1 or 2 years from the time of this article was published, it is highly likely that the following steps could be outdated. I will try to update it as often as I could so tread with caution !
The Very Simple Plan
We will start with a Rails API using Auth0 and Postman to send GET
& POST
requests to fetch authenticated data.
Auth0 handles the sign in/up process. It authenticates a user and supplies a JSON Web Token (JWT) that is will be sent in the header of a request to the API server.
The following simplified diagram below illustrates on a high level how the Rails API server will function initially without a front-end.
We will enhance the plan above by using a front-end framework. In this article we will be using React, however you can use any other framework to perform a similar logic.
The Rails API server is able to standalone without any dependancies on what front-end framework is being used.
The Next Simple Plan
Once we are confident that the Rails API server is functioning, we will use a front-end framework to perform the functionality of Postman as illustrated in a simplified diagram below:
So lets begin !
BACK END — Rails API Server
Prerequisites, tools and items needed
- Rails 5.0.0
- Ruby 2.3.1
- Auth0
client ID
- Auth0
client secret
- Auth0
domain ID
- Auth0 dummy username and password (set one up after you have signed up with Auth0, this article assumes you have one set up with the username
admin@admin.com
and passwordadmin
) - Ruby gem
rack-cors
- Ruby gem
bcrypt
3.1.11 - Ruby gem
knock
2.1.1(to handle jwt from Auth0) - Ruby gem
active_model_serializer
v.0.10.5 - Ruby gem
dotenv-rails
v2.2.0 (for development only) - Ruby gem
faker
v1.7.3 (optional and used to generate fake data) - Ruby gem
pg
v0.20.0 (for postgres database) - Ruby gem
rack-cors
v0.4.1 (to handle cross-origin requests) - Postman (optional and used to test APIs)
- Postgresapp database server (this will serve as our local database server)
- Rails API server will run on
localhost:3000
Steps
The following steps assumes you have Ruby, Rails, Postgresapp, Postman installed in your system. Make sure you have signed up for a Auth0 account and have a client ID
, domain
and client secret
. If you are still unsure what they are, read the Auth0 setup section of my other article here to get a feel how Auth0 works.
- Create a Rails API app, run
rails new ror-auth0-api --api --database=postgresql
on your terminal - Create an environment variables file called
.env
in the root level of your app and add the following information from Auth0
AUTH0_CLIENT_ID={CLIENT_ID}
AUTH0_DOMAIN={DOMAIN}
AUTH0_CLIENT_SECRET={CLIENT_SECRET}
3. We need to tell rails to use the Auth0 credentials. Update the secrets.yml
file in config
folder to look like below:
development:
secret_key_base: ...YOUR KEY GENERATED BY RAILS...
auth0_client_id: <%= ENV["AUTH0_CLIENT_ID"] %>
auth0_client_secret: <%= ENV["AUTH0_CLIENT_SECRET"] %>test:
secret_key_base: ...YOUR KEY GENERATED BY RAILS...
auth0_client_id: <%= ENV["AUTH0_CLIENT_ID"] %>
auth0_client_secret: <%= ENV["AUTH0_CLIENT_SECRET"] %># Do not keep production secrets in the repository,
# instead read values from the environment.
production:
secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
auth0_client_id: <%= ENV["AUTH0_CLIENT_ID"] %>
auth0_client_secret: <%= ENV["AUTH0_CLIENT_SECRET"] %>
4. Add the following gems to your Gemfile
:
gem 'bcrypt', '~> 3.1.7'
gem 'dotenv-rails', groups: [:development, :test]
gem 'faker', '~> 1.7.2'
gem 'rack-cors','~>0.4.1'
gem 'active_model_serializers', '~> 0.10.0'
gem 'knock', '~> 2.0'
5. Run bundle install
6. Create a user model, run rails g model User name email password_digest
7. Create and migrate your postgres database, run rails db:create && rails db:migrate
8. In the User model , add the following code snippet:
# app/models/user.rbclass User < ApplicationRecord
has_secure_password def self.from_token_payload payload
payload['sub']
endend
9. Install the gem knock by running rails g knock:install
10. In application controller
, add the following code snippet to include theknock
gem. This updates knock
's default error message when a user tries to fetch data from a controller that requires authentication.
# app/controllers/application_controller.rb
class ApplicationController < ActionController::API
include Knock::Authenticable private # Define unauthorized access json response
def unauthorized_entity(entity_name)
render json: { error: "Unauthorized request" }, status:
:unauthorized
endend
11. Now we will create two controllers called home
and profile
. The home
controller will be used to ping the API server without a JWT, whereas the profiles
controller would require a valid JWT to fetch data.
12. Run rails g controller Home
13. Runrails g scaffold profile first_name:string last_name:string middle_name:string username:string email:string address:string phone:string profession:string abn:string
14. Run rails db:migrate
13. In your home controller, add the following code snippet. The API server will respond with a simple message when the API server is called.
# app/controllers/home_controller.rbclass HomeController < ApplicationController def index
render json: { message: "Welcome to a simple API server with
Auth0"}
endend
14. In your profiles controller
, add before_action :authenticate_user
to the controller. Any API calls to profiles
will require authentication.
# app/controllers/profiles_controller.rbclass ProfilesController < ApplicationController
...
before_action :authenticate_user# GET /profiles
def index
...
end ....
end
16. To enable cross-origin requests, uncomment the following block of code in config/initializers/cors.rb
. Ensure that any origin is allowed in this case. (ie : origins '*'
)
#config/initializers/cors.rbRails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins '*'resource '*',
headers: :any,
methods: [:get, :post, :put, :patch, :delete, :options, :head]
end
end
17. We need to configure knock
to use Auth0 credentials. Uncomment the following code in config/initializers/knock.rb
# config/initializers/knock.rbconfig.token_audience = -> { Rails.application.secrets.auth0_client_id }# DO NOT USE THE DEFAULT CONFIG
# config.token_secret_signature_key = -> { JWT.base64url_decode Rails.application.secrets.auth0_client_secret }# USE THIS INSTEAD
config.token_secret_signature_key = -> { Rails.application.secrets.auth0_client_secret }
Note :
knock
assumes Auth0’sclient secret
is base64 encoded but as of writing it isn’t. Therefore we need do not need to decode it as shown in the comment code block above.
18. We need to configure our routes. Ensure in the config/routes.rb
has the following code snippet
#config/routes.rb Rails.application.routes.draw do
resources :profiles
root 'home#index'end
19. Lastly, lets add some fake data to simulate a list of profiles. In your db/seeds.rb
file, add the following code snippet.
#db/seeds.rb10.times do |i|
first_name = Faker::Name.first_name
last_name = Faker::Name.last_name
middle_name = Faker::Name.name_with_middle
username = Faker::Internet.user_name(first_name + " " + last_name, %w(. _ -))
email = Faker::Internet.free_email(first_name + "-" + last_name)
phone = Faker::PhoneNumber.cell_phone
profession = Faker::Company.profession
abn = Faker::Company.australian_business_numberProfile.create!(
first_name: first_name,
last_name: last_name,
middle_name: middle_name,
username: username ,
email: email,
phone: phone,
profession: profession,
abn: abn,
)
end
20. Run rails db:seed
to populate our profiles database
21. Now we are done setting up the Rails server ! Lets fire it up and see if its working. Ensure postgresapp
is running in the background. Start the rails API server by runingrails s
.
22. Now, we need to get a JSON Web Token (JWT) from Auth0. Start Postman, and send a POST
request to https://_your_auth0_domain_.com/oauth/ro
with the following in the body as JSON
{
"client_id": "___YOUR AUTH0 CLIENT ID___",
"username": "admin@admin.com",
"password": "admin",
"connection": "___YOUR AUTH0 DATABASE___",
"scope": "openid"
}
23. You should get a response with id_token
. That will serve as your JWT for authentication next. Copy it down.
24. Assuming your Rails API server is running on localhost:3000
, use Postman again to send a GET
request to localhost:3000
.You should receive a response similar to the figure below:
25. Now lets test an authenticated route — the profiles
controller. In Postman, send a GET
request to localhost:3000/profiles
with Authorization : Bearer __your id_token in step 23__
. You should receive a response similar to the figure below:
26. That’s it ! We now have a simple API server up and running !
Next, we will use React as our front-end framework to replace the functionality of Postman from step 22 to 26.
FRONT END — React
Prerequisites, tools and items needed
- My react boilerplate : react-vanilla-boilerplate-with-router
- Axios (to perform fetch request to the Rails API server)
- React Bootstrap 4 — reactstrap
- Font Awesome
- React front-end to run on
localhost:9000
Steps
- In your terminal, run
git clone https://github.com/iankhor/react-vanilla-boilerplate-with-router react-auth0-api
- Remove existing git repository and remotes from Step 1 by running
`git remote rm origin && rm -rf .git
.You can add your own git repository from here on if you desired. - In your root directory of
react-auth0-api
folder, runcp .env.example .env
- Ensure in the
.env
file, the following environment variables are populated. An example would be :
REACT_APP_API_URL=http://localhost:3000
REACT_APP_AUTH0_CLIENT_ID=___YOUR AUTH0 CLIENT ID___
REACT_APP_AUTH0_DOMAIN=___YOUR AUTH0 DOMAIN____
4. Install all packages required by the boilerplate by running npm i
5. Now lets install all the other packages. Run the following:
npm i auth0-lock --savenpm i axios --savenpm i bootstrap@4.0.0-alpha.6 --savenpm i --save reactstrap react-addons-transition-group react-addons-css-transition-group
6. We will use a spinner icon to simulate a loading component using icons from Font Awesome. Get the CDN html link from https://www.bootstrapcdn.com/fontawesome/
and paste the code snippet between the <head>
tag in the public/index.html
file.
8. To ensure bootstrap CSS is carried over, ensure the bootstrap.css
is imported in src/components/index.js
#src/components/index.jsimport 'bootstrap/dist/css/bootstrap.css'
7. We will use Auth0’s own Lock widget (auth0-lock) to sign in/up users. In the public/index.html
file, ensure the following code snippet is between the <head>
tags to ensure the Auth0 Lock widget will work on mobile devices
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"/>
8. Your public/index.html
file should look something like this now:
9. In src/utils
, create a file called init.js
and add the following code snippet. This is the initialisation methods for Auth0 and Axios.
import AuthService from './AuthService'
import axios from 'axios'export const auth = new AuthService(
process.env.REACT_APP_AUTH0_CLIENT_ID,
process.env.REACT_APP_AUTH0_DOMAIN)export const api = axios.create({
baseURL: process.env.REACT_APP_API_URL
})
10. In src/utils
, create a file called AuthService.js
and add the following code snippet. These are helper methods to use Auth0.
import Auth0Lock from 'auth0-lock'
import logo from './../../assets/img/logo.svg'export default class AuthService {
constructor(clientId, domain) {
// Configure Auth0
this.lock = new Auth0Lock(clientId, domain, {
auth: {
redirectUrl: `${window.location.origin}/auth`,
responseType: 'token'
},
theme: {
logo: logo,
primaryColor: '#7FDBFF'
},
languageDictionary: {
title: "React + Auth0 + Rails API"
}
})
// Add callback for lock `authenticated` event
this.lock.on('authenticated', this._doAuthentication.bind(this))
// binds login functions to keep this context
this.login = this.login.bind(this)
}_doAuthentication(authResult) {
// Saves the user token
this.setToken(authResult.idToken)
// navigate to the home route location.replace("/");}login() {
// Call the show method to display the widget.
this.lock.show()
}loggedIn() {
// Checks if there is a saved token and it's still valid
return !!this.getToken()
}setToken(idToken) {
// Saves user token to local storage
localStorage.setItem('id_token', idToken)
}getToken() {
// Retrieves the user token from local storage
return localStorage.getItem('id_token')
}logout() {
// Clear user token and profile data from local storage
localStorage.removeItem('id_token'); location.replace("/");
}
}
11. In src/utils
, create a file called API.js
and add the following code snippet. These are helper methods to fetch and ping the Rails API server.
import { api } from './init'export function pingApiServer(){
return api.get('/')
.then(function (response) {
return response.data
})
.catch(function (error) {
return error.response.data
})
}export function fetchProfilesNoAuth(){
return api.get('/profiles')
.then(function (response) {
return response.data
})
.catch(function (error) {
return error.response.data
})
}export function fetchProfilesWithAuth(token){
return api.get('/profiles', { headers: { 'Authorization': 'Bearer ' + token } })
.then(function (response) {
return response.data
})
.catch(function (error) {
return error.response.data
})
}
12. Update App.js
in src/components
with the following code snippet
import React, { Component } from 'react';
import logo from '../../assets/img/logo.svg'
import '../css/style.css'
import Home from './Home'
import { auth } from './../utils/init'class App extends Component {
constructor(props){
super(props)this.state = {
isLoggedIn: auth.loggedIn(),
}
}render() {
return (
<div className="App">
<div className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h2>Welcome to React + Auth0 + Rails API</h2>
</div><Home
auth={auth}
isLoggedIn={ this.state.isLoggedIn }
token={auth.getToken()}
/></div>
);
}
}export default App;
13. Create three components files in src/components
and named them Home.js
, LoginTransition.js
and Loading.js
14. Paste the following code snippet to Home.js
//src/components/Home.jsimport React, { Component } from 'react'
import JSONDebugger from '../utils/JSONDebugger'
import { pingApiServer,
fetchProfilesWithAuth,
fetchProfilesNoAuth } from './../utils/API'
import { Container,
Row,
Col,
Button,
ButtonGroup } from 'reactstrap'import Loading from './Loading'class Home extends Component {
constructor(props){
super(props)this.state = { status: "Idle",
pingData: "No data from server yet",
profileData: "No data from server yet"
}
}resetStates = () => {
this.setState({
status: "Idle",
pingData: "No data from server yet",
profileData: "No data from server yet"
})
}renderLoading = () => {
switch(this.state.status) {
case "Fetching":
return <Loading />
default:
return null
}
}pingApi = () => {
this.setState( { status: "Fetching" })
pingApiServer()
.then( data => {
this.setState({ status: "Fetch completed", pingData: data} )
} )
}
_fetchProfilesNoAuth = () => {
this.setState( { status: "Fetching" })
fetchProfilesNoAuth()
.then( data => {
this.setState({ status: "Fetch completed", profileData: data} )
})
}_fetchProfilesWithAuth = () => {
this.setState( { status: "Fetching" })
fetchProfilesWithAuth(this.props.token)
.then( data => {
this.setState({ status: "Fetch completed", profileData: data} )
})
}renderSignInUp = () => {
return <Button color="primary" onClick={ this.props.auth.login }>Sign In / Sign Up</Button>
}renderLogOut = () => {
return (
<div>
<Row>
<Col>
<Button color="primary" onClick={ this.props.auth.logout}>Sign out</Button>
<Button color="secondary" onClick={ this.pingApi }>Ping API Server</Button>
</Col>
</Row>
<Row>
<Col xs="12" sm="6">
<Button color="danger" onClick={this._fetchProfilesNoAuth }>Fetch Profile without Authentication</Button>
</Col>
<Col xs="12" sm="6">
<Button color="success" onClick={ this._fetchProfilesWithAuth }>Fetch Profile with Authentication</Button>
</Col>
</Row>
<Row>
<Col>
<Button outline color="danger" size="sm" onClick={ this.resetStates }>Reset</Button>
</Col>
</Row>
</div>
)
}render(){
return(
<Container>
<div className="buttons">
<ButtonGroup vertical>
{ this.props.isLoggedIn ? this.renderLogOut() : this.renderSignInUp() }
</ButtonGroup>
</div>
{ this.renderLoading() }
{ <JSONDebugger json={this.state} /> }
</Container>
)
}
}export default Home
15. Paste the following code snippet to LoginTransition.js
//src/components/LoginTransition.jsimport React, { Component } from 'react'
import { Container, Row, Col } from 'reactstrap'
import Loading from './Loading'class LoginTransition extends Component {
render(){
return(
<Container className="login-transition">
<Row>
<Col>
<Loading />
</Col>
</Row>
</Container>
)
}
}export default LoginTransition
16. Paste the following code snippet to Loading.js
//src/components/Loading.jsimport React from 'react'const Loading = (props) => {
return (
<div>
<i className='fa fa-spinner fa-spin fa-5x color-font-aqua'></i>
</div>
)
}export default Loading
17. We will also need to update some of our CSS as well. In src/css/style.styl
. Replace the existing CSS entires the following code snippet. Do not update the style.css
file. The scripts in the boilerplate will look after that.
//src/css/style.stylh1
font-size 2em.color-silver
background-color #DDDDDD.color-font-aqua
color #7FDBFF.color-aqua
background-color #7FDBFF.generic-center
margin auto
padding 2rem
border 2px solid #000
text-align center.dummy-height
min-height 500px.border
border 2px solid #000.buttons
border 2px solid #000
padding 20px.borderless
border none// App.css
.App
text-align center.App-logo
animation App-logo-spin infinite 20s linear
height 80px.App-header
height 150px
padding 20px.App-intro
font-size large.login-transition
text-align center
padding-top 50vh.debugger
text-align left@keyframes App-logo-spin
from { transform: rotate(0deg) }
to { transform: rotate(360deg) }
18. Lastly, update src/components/shared/Routes.js
with the following code snippet:
//src/components/shared/Routes.jsimport React from 'react'//Routes
import NotFound from './NotFound'
import App from './../App';
import LoginTransition from './../LoginTransition';import { BrowserRouter, Route, Switch } from 'react-router-dom'const Routes = (props) => {
return (
<BrowserRouter>
<Switch>
<Route path="/" exact component={App} />
<Route path="/auth" exact component={LoginTransition} />
<Route component={NotFound} />
</Switch>
</BrowserRouter>
)
}export default Routes
18. Just one more step to setup in Auth0. Log into your Auth0 account. Navigate to Clients
on the left menu panel. Click on the client ID
that you have used for the Rails API Server. Under settings
, ensure Allowed Callback URLs include:
http://localhost:9000
http://localhost:9000/auth
18. Now, we are ready to test our React front-end to see if it would interact with our Rails API server.
19. In your terminal, run npm run watch
20. In your favourite browser, navigate to localhost:9000
and click on the Sign In / Sign Up button. Sign in with the username admin@admin.com
and password admin
(Note: as mentioned earlier, it is assumed you have set a dummy username and password)
21. You can now play around with all the buttons as shown in the gif below !
If you run into issues in any of the steps, it is a good exercise to try to debug it on your own. I have an publication that suggests a couple of ways to problem solve issues here which may help.
Hope this little write up would give you a starting point for your app and some learning at the same time !
And till next time … Keep hacking !