How to Create an Oauth2 Authentication Flow With Koa
Storyblok is the first headless CMS that works for developers & marketers alike.
Hello everyone, in this tutorial we will create a CRUD app in Storyblok and authenticate it with OAuth2. This is the app we will build:
You can clone this tutorial at https://github.com/storyblok/storyblok-koa-oauth-example
Environment Setup
Requirements
- Node.Js (opens in a new window) and NPM installed (Or yarn to manage your packages)
- A Storyblok (opens in a new window) account to manage content
- Basic knowledge about Oauth2 (opens in a new window)
- Read our documentation about the OAuth2 flow on Storyblok
Creating a Storyblok Application
To create an application in Storyblok, you need to be a part of our Partner Program (opens in a new window) . In our Partner portal, you will see the Apps section on the left panel. After clicking the New button, you will see the following modal:
Fill out all of the required information (for now, only name and slug is necessary) and click on Save. After this, you will see a detail page for your App. On this page, access the Edit page to view the OAuth credentials and configure the URLs to your App. For now, store your Client ID
and Client Secret
credentials. Above, you can see where you will find these credentials:
Starting the API Development
For this tutorial we will use the grant (opens in a new window) package and the Koa (opens in a new window) library to develop our API and abstract some tasks in the OAuth2 workflow.
Setup the project
First, create a directory and start a project with:
yarn init # or npm init
We will use yarn, but you can use npm as well :)
Then, we will install the dependencies:
yarn add koa &&
yarn add grant-koa &&
yarn add storyblok-js-client &&
yarn add axios &&
yarn add koa-router &&
yarn add koa-static &&
yarn add koa-ejs &&
yarn add koa-session &&
yarn add koa-qs &&
yarn add qs &&
yarn add dotenv &&
yarn add koa-bodyparser &&
yarn add crypto-js &&
yarn add uuid
After, create an .env
file to put your credentials. The .env
file should look like this:
CONFIDENTIAL_CLIENT_ID="Your-Client-ID"
CONFIDENTIAL_CLIENT_SECRET="Your-Client-Secret"
CONFIDENTIAL_CLIENT_REDIRECT_URI=http://yourid.ngrok.io/connect/storyblok/callback
With the basic settings let's develop our app already authenticating with Storyblok using OAuth protocol.
Develop the Application
Setup initial code
Create a app.js
file in the root folder on your project. Below is the initial code for this file:
// setting the environment variables
require('dotenv').config()
// the nodejs imports
const path = require('path')
// the koa imports
const Koa = require('koa')
const session = require('koa-session')
const Router = require('koa-router')
const koaqs = require('koa-qs')
const render = require('koa-ejs')
const serve = require('koa-static')
const bodyParser = require('koa-bodyparser')
// Instantiating our app
const app = new Koa()
// setup application key for session
app.keys = ['grant', 'storyblok']
// use koa middlewares
app.use(bodyParser())
app.use(session(app))
koaqs(app)
// setup the views folder as a folder for our templates
render(app, {
root: path.join(__dirname, 'views'),
layout: 'template',
viewExt: 'html',
cache: false,
debug: false,
async: true
})
// use koa-static middleware for serve the public folder as static folder
app.use(serve(path.join(__dirname, '/public')))
// Initializing the router
const router = new Router()
router
.get('/', async ctx => {
ctx.body = 'Server started!'
})
// use the router instance and initialize the server
app
.use(router.routes())
.use(router.allowedMethods())
.listen(3000)
console.log('Server listen on port 3000')
After this, run the node app.js
command in your terminal and open your browser at http://localhost:3000/ . You will see a Server Started! message.
Setup grant for OAuth2 flow
The first step is to create a grant (opens in a new window) configuration and setup the callback route. Let's code:
Create a utils
folder and put a factory-grant-config.js
file in it. This file will export a factory function for the grant config.
const SHA256 = require('crypto-js/sha256')
const { v4: uuid } = require('uuid')
const getConfig = () => {
const codeIdentifier = uuid()
return {
defaults: {
origin: 'http://localhost:3000'
},
// we need to create a custom provider
// https://github.com/simov/grant#custom-providers
storyblok: {
key: process.env.CONFIDENTIAL_CLIENT_ID,
secret: process.env.CONFIDENTIAL_CLIENT_SECRET,
redirect_uri: process.env.CONFIDENTIAL_CLIENT_REDIRECT_URI,
callback: '/callback',
authorize_url: 'https://app.storyblok.com/oauth/authorize',
access_url: 'https://app.storyblok.com/oauth/token',
oauth: 2,
scope: 'read_content write_content',
// create some custom parameters to send in URL
// https://github.com/simov/grant#custom-parameters
// this additional parameters are explain in Storyblok OAuth documentation
custom_params: {
code_challenge: SHA256(codeIdentifier).toString(),
code_challenge_method: 'S256',
state: codeIdentifier
}
}
}
}
module.exports = getConfig
After this, edit the app.js
file to import the grant-koa (opens in a new window) package and the factory function. The code will be the following:
// for example, after the previous imports
const grant = require('grant').koa()
const grantConfig = require('./utils/factory-grant-config')()
// after the koa middlewares
// use the grant middleware with the grantConfig
app.use(grant(grantConfig))
// in registering routes section
router
.get('/', async ctx => {
ctx.body = 'Server started!'
})
// let's register a callback route to get the token data
.get('/callback', ctx => {
ctx.body = {
data: ctx.query
}
})
Now install your application on Storyblok space. After you install the application, open your browser in http://localhost:3000/connect/storyblok (this URL is explained in the documentation (opens in a new window) ). Your browser will redirect to an authorization URL and open a page to request read and write approval in your space:
After approval, the browser will redirect to the callback route and will show the grant response data (opens in a new window) with the access and refresh tokens.
Recovering the space_id
When using the complete grant workflow, we don't have control of authorization and access routes. That's a problem because the access url redirects to the application with the space_id
parameter in the URL. The space_id
parameter is very important, so we need to make some changes in our backend to get and store the space_id
in the session.
First, we need to change the redirect_uri
variable on our .env
file. Change this variable to http://localhost:3000/callback . Set the same URL on the Edit Application form on Storyblok in the "Oauth2 callback url" field.
After this, rewrite the callback route to get the code
and space_id
from authorization URL and get the access and refresh tokens from access URL. Create a new file in utils folder called get-token-from-code.js
. This file will contain the following content:
const axios = require('axios')
const qs = require('qs')
/**
* The getTokenFromCode function will be receive an acess_url
* and a config object to make a request to the access_url
* to get access and refresh tokens
*
* An example of config to get access token:
* {
* grant_type: 'authorization_code',
* code: 'XXXXXXX',
* client_id: 'YYYYYYY',
* client_secret: 'ZZZZZZZ',
* redirect_uri: 'WWWWWWW'
* }
*
* An another example of config to refresh the acess token:
* {
* grant_type: 'refresh_token',
* refresh_token: 'XXXXXXX', // instead of using code
* client_id: 'YYYYYYY',
* client_secret: 'ZZZZZZZ',
* redirect_uri: 'WWWWWWW'
* }
*
* @method getTokenFromCode
* @param {String} access_url
* @param {Object} config
* @return {Promise<Object>}
*/
const getTokenFromCode = (access_url, config) => {
return new Promise((resolve, reject) => {
const requestConfig = {
url: access_url,
method: 'POST',
headers: {
'content-type': 'application/x-www-form-urlencoded'
},
data: qs.stringify({
...config
})
}
axios(requestConfig)
.then(response => {
const { access_token, refresh_token } = response.data
resolve({
access_token,
refresh_token: refresh_token || config.refresh_token
})
})
.catch(reject)
})
}
module.exports = getTokenFromCode
Then, edit the callback route to use this function to get access and refresh tokens:
// in top of your code
const getTokenFromCode = require('./utils/get-token-from-code')
// in routes declaration
.get('/callback', async ctx => {
// for now, we will not use the space_id
const { space_id, code } = ctx.query
try {
const config = {
grant_type: 'authorization_code',
code,
client_id: grantConfig.storyblok.key,
client_secret: grantConfig.storyblok.secret,
redirect_uri: grantConfig.storyblok.redirect_uri
}
const { access_token, refresh_token } = await getTokenFromCode(
grantConfig.storyblok.access_url,
config
)
ctx.session.application_code = code
ctx.session.access_token = access_token
ctx.session.refresh_token = refresh_token
ctx.body = {
data: {
access_token,
refresh_token
}
}
} catch (e) {
ctx.status = e.response.status
ctx.body = {
error: true,
message: e.message
}
}
})
Now, open your browser again in http://localhost:3000/connect/storyblok to authenticate and redirect to the callback URL. You will see the JSON with the access_token
and refresh_token
.
Displaying the home page with the Vuejs app
Now that we have our settings ready, let's create the view that will be rendered in the App we created in Storyblok. First, create a folder called views
, and inside it create two files, home.html
and template.html
.
In the home.html
file, paste the code below:
<h2>Explore the API</h2>
<p>Congratulations! You just authorized this client to fetch data from <code>mapi.storyblok.com</code> using OAuth2.</p>
<p>Click on the buttons below to explore <code>mapi.storyblok.com</code> API</p>
<div id="app">
<button @click="openNew"
class="btn btn-primary">
New
</button>
<form @submit.prevent="createOrUpdate"
v-if="showForm"
class="mt-3 mb-3">
<div class="form-group">
<label>Name</label>
<input class="form-control" type="text" v-model="story.name" />
</div>
<div class="form-group">
<label>Slug</label>
<input class="form-control" type="text" v-model="story.slug" />
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
<div>
<table class="table mt-3">
<tr v-for="story in stories"
:key="story.id">
<td>
{{ story.name }}
</td>
<td>
<button @click="editStory(story)"
class="btn btn-danger">
Edit
</button>
<button @click.prevent="remove(story.id)"
class="btn btn-danger">
Delete
</button>
</td>
</tr>
</table>
</div>
</div>
<script type="text/javascript">
var SPACE_ID = <%= space_id %>
</script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.0"></script>
<script type="module" src="/app.js"></script>
<p id="display-json">Click on the buttons above, the response will show here.</p>
<p>Your access token: <code><%= access_token %></code></p>
<p><a class="btn btn-outline-secondary" href="/refresh?space_id=<%= space_id %>">Refresh Access Token</a></p>
And in the template.html
file, paste the code below.
<html lang="en-us">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<title> Storyblok example </title>
<script type="text/javascript">
if (window.top == window.self) {
window.location.assign('https://app.storyblok.com/oauth/app_redirect')
}
</script>
</head>
<body>
<div class="container-fluid">
<div class="container py-md-3">
<div class="row">
<div class="col">
<%- body %>
</div>
</div>
</div>
</div>
</body>
</html>
Let's add some logic to our front end. Create a public
folder and an app.js
file with our Vue.js application with the following code:
var client = window.axios.create({
baseURL: 'http://localhost:3000/explore/' + window.SPACE_ID + '/',
timeout: 10000
})
new Vue({
el: '#app',
data() {
return {
stories: [],
story: {},
showForm: false
}
},
created() {
this.list()
},
methods: {
handleError(err) {
alert(err)
},
editStory(story) {
this.story = story
this.showForm = true
},
openNew() {
this.story = {}
this.showForm = true
},
list() {
this.showForm = false
client.get('stories')
.then((response) => {
this.stories = response.data.stories
})
.catch(this.handleError)
},
createOrUpdate() {
if (this.story.id) {
this.update(this.story.id)
} else {
client.post('stories', {story: this.story})
.then((response) => {
console.log(response)
this.list()
})
.catch(this.handleError)
}
},
remove(id) {
client.delete('stories/' + id)
.then((response) => {
console.log(response)
this.list()
})
.catch(this.handleError)
},
update(id) {
client.put('stories/' + id, {story: this.story})
.then((response) => {
console.log(response)
this.list()
})
.catch(this.handleError)
}
}
})
Now, in our backend, we need to create the routes for the CRUD.
Creating the CRUD routes
First, render the Home template when the application is authenticated. Edit the root route
with the following code:
.get('/', async ctx => {
// get the space_id from the URL
const { space_id } = ctx.query
// render the home page passing the space_id and access_token values
await ctx.render('home', {
space_id,
access_token: ctx.session.access_token
})
})
Then, edit the callback route to make a redirect to root route.
// at the end of the route, replace this
ctx.body = {
data: {
access_token,
refresh_token
}
}
// for this
ctx.redirect(`/?space_id=${space_id}`)
Add CRUD routes. First, create a function to instantiate the Storyblok JS client for us. Create a get-storyblok-client.js
in utils
folder with the following code:
const StoryblokClient = require('storyblok-js-client')
/**
* @method getStoryblokClient
* @param {String} token access_token
* @return {StoryblokJSClient}
*/
const getStoryblokClient = token => {
return new StoryblokClient({
oauthToken: `Bearer ${token}`
})
}
module.exports = getStoryblokClient
Finally, add these CRUD routes:
// on top of your code
const getStoryblokClient = require('./utils/get-storyblok-client')
// after all routes declarations
.get('/explore/:space_id/:resource', async ctx => {
const { space_id, resource } = ctx.params
const client = getStoryblokClient(ctx.session.access_token)
try {
const response = await client.get(`spaces/${space_id}/${resource}`)
ctx.body = response.data
} catch (e) {
ctx.status = e.response.status
ctx.body = {
error: true,
message: e.message
}
}
})
.post('/explore/:space_id/:resource', async ctx => {
const { space_id, resource } = ctx.params
const client = getStoryblokClient(ctx.session.access_token)
const body = ctx.request.body
try {
const response = await client.post(`spaces/${space_id}/${resource}`, body)
ctx.body = response.data
} catch (e) {
ctx.status = e.response.status
ctx.body = {
error: true,
message: e.message
}
}
})
.put('/explore/:space_id/:resource/:id', async ctx => {
const { space_id, resource, id } = ctx.params
const client = getStoryblokClient(ctx.session.access_token)
const body = ctx.request.body
try {
const response = await client.put(`spaces/${space_id}/${resource}/${id}`, body)
ctx.body = response.data
} catch (e) {
ctx.status = e.response.status
ctx.body = {
error: true,
message: e.message
}
}
})
.delete('/explore/:space_id/:resource/:id', async ctx => {
const { space_id, resource, id } = ctx.params
const client = getStoryblokClient(ctx.session.access_token)
try {
const response = await client.delete(`spaces/${space_id}/${resource}/${id}`)
ctx.body = response.data
} catch (e) {
ctx.status = e.response.status
ctx.body = {
error: true,
message: e.message
}
}
})
Testing the CRUD app
Now that everything is ready, start the server and type in your terminal:
node app.js
Open your browser in the http://localhost:3000/connect/storyblok URL. If everything goes as it should, you will be redirected to authentication. You should see a list of your stories in your space. An example is this:
Implementing the refresh token route
The last thing that we need to implement is the refresh token method. Add a new route with the following code:
// after all routes declarations
.get('/refresh', async ctx => {
const { space_id } = ctx.query
try {
const config = {
grant_type: 'refresh_token',
refresh_token: ctx.session.refresh_token,
client_id: grantConfig.storyblok.key,
client_secret: grantConfig.storyblok.secret,
redirect_uri: grantConfig.storyblok.redirect_uri
}
const { access_token, refresh_token } = await getTokenFromCode(
grantConfig.storyblok.access_url,
config
)
ctx.session.access_token = access_token
ctx.session.refresh_token = refresh_token
ctx.redirect(`/?space_id=${space_id}`)
} catch (e) {
ctx.status = e.response.status
ctx.body = {
error: true,
message: e.response.data.error_description
}
}
})
Conclusion
In this tutorial, we learned how to build an application to Storyblok authenticating it with an OAuth2 protocol. We used the grant (opens in a new window) package and koa (opens in a new window) to build our backend. For the frontend, we used Vue.js to perform a full CRUD application. Remember that you can check the code at https://github.com/storyblok/storyblok-koa-oauth-example .