In a previous article, we were discussing how to work with Lokalise APIv2 and perform common actions, like project creation, task assignment, file upload, and so on. However, in that post we employed regular user tokens to authenticate with the API, which might not always be desirable. In certain cases, your third-party app might need to interact with the API on behalf of multiple users. Of course, you can simply ask users to copy-paste their tokens but it’s not very safe; these tokens do not expire and it’s impossible to choose which actions the app will be able to perform. What would you do this case? Set up an OAuth 2 flow! This way, users will have to log in to your application via Lokalise and explicitly state that they allow the app to perform certain actions on their behalf. The list of actions will be displayed upon every sign-in and can be customized as needed.
In this article, we will learn how OAuth 2 works and how to implement an authentication flow with JS/Node, Python/Flask, and Ruby on Rails.
The source code for this article can be found on GitHub.
How does OAuth 2 work?
OAuth 2 is an authorization protocol that allows a user to grant third-party applications access to their information on another site. Simply put, a user can allow your app to perform certain actions on his or her behalf, for example to create projects, update translations, add new orders, and so on.
The flow can be separated into three steps:
- The user initiates an OAuth 2 flow (usually by clicking some kind of a link saying “Log in via [service name]”). The user reviews the list of actions the app would like to perform on his/her behalf, clicks “Approve”, and obtains a special secret code. This code might be displayed right on the web page, or the user might be redirected to the third-party app along with the code.
- The third-party app obtains the secret code and sends it to the service in an HTTP POST request. The response contains an access code that can be used to act on the user’s behalf. Specifically, the token can be used to perform API authentication. The response might also contain additional info like a refresh token and expiration time.
- When the access token expires, the third-party app can use the refresh token to request a new access token.
It may sound a bit too theoretical, but don’t worry: you’ll get the idea after browsing the code samples. You can also find more technical information in our docs.
Registering a new Lokalise OAuth 2 app
First of all, you will need to register a new OAuth 2 app with Lokalise. To achieve this, you can reach out to the tech support team and ask them to create a new app for you. Please provide the following info:
- Your app title and description. While the description is optional, it would be very helpful to have a few sentences explaining what exactly you are going to build.
- Your app logo (in PNG or JPG format).
- A link to the app website/documentation (optional).
- Required scopes. This is important information because the list of scopes will determine which actions your app will be able to perform via the Lokalise API (you can also narrow this scope later when authenticating the user). For example, to view the projects you’ll need a
read_projects
permission, whereas to create a new order awrite_team_orders
scope is required. You can find the list of all the required scopes for the corresponding API endpoints in the Lokalise API docs.
Once you have supplied this info, you’ll be provided with a client ID and client secret — we’ll use these to set up the authentication flow. Please note that you should never expose your client secret (as the name suggests, duh).
Basically, this is it. Now we can proceed to the main part of this tutorial.
OAuth 2 flow with JavaScript and Node
Setting up the project
So, let’s start by implementing an OAuth 2 flow in a Node.js app. Create a new Node project and install the necessary dependencies:
npm init -y npm install @lokalise/node-api dotenv express hbs cookie-session
- @lokalise/node-api is the official API SDK for JS and TS.
- dotenv will be used to safely store environment variables.
- Express is a web server that we are going to use.
- HBS (Handlebars) is a templating engine.
- Cookie-session will be used to store data inside the session.
Create a new .env
file and provide your client ID and client secret inside:
OAUTH2_CLIENT_ID=123abc OAUTH2_CLIENT_SECRET=345xyz
Don’t forget to add this file to .gitignore
if you are using Git.
Now create an index.js
file inside the project root:
import cookieSession from 'cookie-session' import express from 'express' import { router } from "./config/routes.js" import { setupAssets } from "./config/assets.js" const app = express() app.use(cookieSession({ name: 'session', secret: 'my_super_secret', maxAge: 24 * 60 * 60 * 1000 })) app.use('/', router) const port = process.env.PORT || 3000 app.listen(port, () => { console.log(`Express web app on port ${port}`) }) setupAssets(app)
This file sets up all the necessary configuration and imports the libraries.
Let’s also tweak the package.json
in the following way:
{ "name": "oauth2", "version": "1.0.0", "description": "", "main": "index.js", "type": "module", "scripts": { "start": "node index.js", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "@lokalise/node-api": "^7.3.1", "cookie-session": "^2.0.0", "dotenv": "^16.0.1", "express": "^4.18.1", "hbs": "^4.2.0" } }
Specifically, I’ve added the type
attribute and a start
command to the scripts
.
Routes and assets
Next, create a new config
directory with an assets.js
file inside. This file will set up Handlebars templates for us:
export function setupAssets(app) { app.set('view engine', 'hbs') }
Also add a routes.js
file:
import express from "express" import { StaticPagesController } from "../controllers/staticPagesController.js" export const router = express.Router() router.get('/', (req, res) => { StaticPagesController.index(req, res) })
We’ll create the staticPagesController.js
file a bit later.
Also, let’s create a views
directory in the project root and add a layout.hbs
file within it:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>{{title}}</title> </head> <body> <div> {{{body}}} </div> </body> </html>
The ground work is done and now we can add an OAuth 2 flow to our app.
Implementing OAuth 2 flow
Okay, so let’s proceed to the actual OAuth 2 flow implementation. First of all, create a new controllers
directory inside the project root and add an applicationController.js
file inside:
import { LokaliseAuth } from '@lokalise/node-api' export class ApplicationController { static renderView(_req, res, view, data = {}) { res.render(view, data) } static lokaliseOAuth2() { return new LokaliseAuth(process.env.OAUTH2_CLIENT_ID, process.env.OAUTH2_CLIENT_SECRET) } }
The lokaliseOAuth2
static method will return an OAuth 2 client that we are going to use later.
Next, we’ll create a staticPagesController.js
file in the same directory:
import { ApplicationController } from "./applicationController.js" export class StaticPagesController extends ApplicationController { static index(req, res) { this.renderView(req, res, 'static_pages/index', { title: 'Lokalise OAuth 2', authUrl: ApplicationController.lokaliseOAuth2().auth( ["read_projects"], "http://localhost:3000/callback", "secret_state" ) }) } }
In this file, we are generating a “log in via Lokalise link” that the users will have to visit. We are providing a single read_projects
scope, passing a callback URL, and a secret state for protection from CSRF.
Now it’s time to display this link, therefore create a new views/static_pages/index.hbs
file:
<h1>Lokalise OAuth 2</h1> <a href="{{authUrl}}">Log in via Lokalise</a>
Once the user has visited this link and logged in via Lokalise, s/he will be redirected to the callback URL that we specified a moment ago. Our job is to grab a secret code sent by Lokalise and use it to request an access and a refresh token. Thus, create a new controllers/callbacksController.js
file:
import { ApplicationController } from "./applicationController.js" export class CallbacksController extends ApplicationController { static async index(req, res) { const response = await ApplicationController.lokaliseOAuth2().token(req.query.code); req.session.accessToken = response.access_token req.session.refreshToken = response.refresh_token res.redirect('/projects') } }
So, we are using the JS SDK to request a token using the provided secret code. Then we store both tokens inside the session, and redirect the user to the /projects
path.
Finally, let’s add a new route to the config/routes.js
file:
// ... import { CallbacksController } from "../controllers/callbacksController.js" router.get('/callback', (req, res) => { CallbacksController.index(req, res) })
Using the access token to work with the API
At this point we are ready to use the access token to communicate with the API and perform actions on the user’s behalf. For instance, let’s display all the projects a user has access to.
To achieve that, create a new controllers/projectsController.js
file:
import { LokaliseApiOAuth } from '@lokalise/node-api' import { ApplicationController } from "./applicationController.js" export class ProjectsController extends ApplicationController { static async index(req, res) { const lokaliseApi = new LokaliseApiOAuth({ apiKey: req.session.accessToken }) const projects = await lokaliseApi.projects().list() this.renderView(req, res, 'projects/index', {projects: projects.items}) } }
We use projects().list()
to return all the user projects and then render the projects/index
view.
Add a new route:
// ... import { ProjectsController } from "../controllers/projectsController.js" router.get('/projects', (req, res) => { ProjectsController.index(req, res) })
And lastly add a new views/projects/index.hbs
file:
<h1>Your projects</h1> <ul> {{#each projects}} <li>{{this.name}}</li> {{/each}} </ul>
Nice!
Using refresh tokens
The access token has an expiration time (usually it’s 60 minutes), therefore you should refresh it once it expires. Let’s see how to achieve this by tweaking the projectsController.js
file:
import { LokaliseApiOAuth } from '@lokalise/node-api' import { ApplicationController } from "./applicationController.js" export class ProjectsController extends ApplicationController { static async index(req, res) { const projects = await this._listProjects(req) this.renderView(req, res, 'projects/index', {projects: projects.items}) } static async _listProjects(req) { try { const lokaliseApi = new LokaliseApiOAuth({ apiKey: req.session.accessToken }) return await lokaliseApi.projects().list() } catch(e) { if (e.code === 401) { const response = await ApplicationController.lokaliseOAuth2().refresh(req.session.refreshToken) req.session.accessToken = response.access_token return this._listProjects(req) } else { throw e } } } }
So, if we get an exception when trying to list the projects, check the error code. If the code is 401 (“Unauthorized”), it probably means our token has expired. Thus, we refresh it using the refresh
method, store it in the session, and try again. Otherwise just throw an exception.
You may also want to limit the number of retries to avoid endless recursion.
Testing it out
Everything is ready, so you can boot up your server by running the below:
npm start
Proceed to the http://localhost:3000
, log in via Lokalise, and observe the list of all your projects.
Great job!
OAuth 2 flow with Python and Flask
Setting up the project
First of all, add a new directory for your project and create a virtual environment inside:
cd MY_PROJECT_NAME && python -m venv venv . venv/bin/activate
Next, install the necessary dependencies:
pip install Flask python-dotenv python-lokalise-api
Create an app.py
file the following contents:
import os from dotenv import load_dotenv load_dotenv() import lokalise from flask import Flask, render_template, request, redirect, url_for, session app = Flask(__name__) app.secret_key = b'_5#y2L"F4Q8z\n\xec]/'
Create a new .env
file and provide your client ID and client secret inside:
OAUTH2_CLIENT_ID=123abc OAUTH2_CLIENT_SECRET=345xyz
Don’t forget to add this file to .gitignore
if you are using Git.
Displaying a login link
Next we have to generate a proper “log in via Lokalise” link and display it to the users. Therefore, let’s add two new methods to the app.py
file:
# ... @app.route('/') def login(): login_url = __auth_client().auth( ["read_projects"], "http://localhost:5000/callback", "secret state" ) return render_template('login.html', login_url=login_url) def __auth_client(): return lokalise.Auth(os.getenv('OAUTH2_CLIENT_ID'), os.getenv('OAUTH2_CLIENT_SECRET'))
Here we are providing a single scope (read_projects
), a callback URL, and a secret state for protection from CSRF.
Create a new folder called templates
and add a login.html
file within it:
<!doctype html> <html lang="en"> <head> <title>Login to Lokalise</title> </head> <body> <h1>Lokalise OAuth 2 login</h1> <a href="{{ login_url }}">Login!</a> </body> </html>
After the user visits the link and logs in via Lokalise, s/he will be redirected to the callback URL that we’ve provided when generating a link. Thus, the next step is to code this callback route.
Callback route
So, let’s add a new method to the app.py
file:
@app.route('/callback') def callback(): code = request.args.get('code', '') response = __auth_client().token(code) session['token'] = response['access_token'] session['refresh_token'] = response['refresh_token'] return redirect(url_for('projects'))
We are reading the secret code sent by Lokalise and using it to request an access and a refresh token. We store these tokens inside the session and then redirect the user to the /projects
page.
Using an access token to list projects via the API
At this point, we can use the access token to work with the API. For example, let’s display the list of all projects our user has access to. Therefore, let’s add two more methods to the app.py
file:
@app.route('/projects') def projects(): projects = __oauth_client().projects().items return render_template('projects.html', projects=projects) def __oauth_client(): return lokalise.OAuthClient(session['token'])
Thus, we are using the projects()
method to list all the projects and then render a projects
template.
Let’s create this template within the templates/projects.html
file:
<!doctype html> <html lang="en"> <head> <title>Your Lokalise projects</title> </head> <body> <h1>Choose a Lokalise project to work with</h1> <ul> {% for project in projects %} <li>{{ project.name }}</li> {% endfor %} </ul> </body> </html>
In this file we iterate over the projects and display their names.
Using refresh tokens
The access token that Lokalise has generated for us usually expires in 60 minutes; consequently you have to refresh it on a regular basis. Let’s implement this feature inside the app.py
:
@app.route('/projects') def projects(): projects = __list_projects() return render_template('projects.html', projects=projects) def __list_projects(): try: return __oauth_client().projects().items except lokalise.errors.Unauthorized: response = __auth_client().refresh(session['refresh_token']) session['token'] = response['access_token'] return __list_projects()
So, if we get an Unauthorized
error, it probably means that the token has expired. Therefore, we refresh it using the corresponding method, store a new token inside the session, and retry the operation. You might think about introducing retry limits to avoid endless recursion.
Testing it out
Now everything is ready, therefore boot your server by running:
flask run
Proceed to http://localhost:5000
, log in via Lokalise, and observe the list of your projects.
Great job!
OAuth 2 flow with Ruby on Rails
Setting up the project
Now let’s see how to use Lokalise OAuth 2 with the Ruby on Rails framework! First of all, create a new Rails project as always:
rails new OAuth2Flow
I’m going to use Rails 7, but these instructions should be relevant for older versions as well.
Now let’s add the following gems to the Gemfile
:
gem "ruby-lokalise-api", "~> 6.0" gem "omniauth-lokalise", "~> 0.0.1" gem "omniauth-rails_csrf_protection", "~> 1.0" group :development, :test do gem "dotenv-rails", "~> 2.7" end
- ruby-lokalise-api is the official Ruby SDK for Lokalise API that we are going to use to send API requests and refresh tokens. It also allows you to set up an OAuth 2 flow from scratch, but for that purpose we are going to utilize another solution.
- omniauth-lokalise is the gem I wrote some time ago. It makes setting up OAuth 2 with Rails a breeze.
- omniauth-rails_csrf_protection is a special solution to protect from CSRF attacks in Omniauth.
- dotenv-rails will be used to safely store environment variables.
Run:
bundle
Then create a new .env
file and provide your client ID and client secret inside:
OAUTH2_CLIENT_ID=123abc OAUTH2_CLIENT_SECRET=345xyz
Don’t forget to add this file to .gitignore
if you are using Git!
Setting up OAuth 2 with Omniauth
Next, let’s create a new file called omniauth.rb
inside the config/initializers
folder:
Rails.application.config.middleware.use OmniAuth::Builder do provider :lokalise, ENV['OAUTH2_CLIENT_ID'], ENV['OAUTH2_CLIENT_SECRET'], scope: 'read_projects read_team_users' end
Here we are providing configuration for the omniauth-lokalise which is built on top of the Omniauth gem. This gem enables you to easily add multiple authentication providers to a Rails app, including Facebook, GitHub, LinkedIn, and many more.
Please note the scope
param which contains the list of permissions we are going to request from the users. You should use space as a separator.
Omniauth expects our app to have a special callback route — that’s the route the users will be redirected to after logging in via Lokalise. Therefore, let’s add this route to the config/routes.rb
:
get '/auth/:provider/callback', to: 'sessions#create'
Great! Now let’s proceed to the next section and create a new controller.
Adding a callback action
So we’ve created a callback route, but we still need to create the corresponding controller and an action. Thus, add a new sessions_controller.rb
file within the app/controllers
directory:
class SessionsController < ApplicationController def create session[:access_token] = request.env['omniauth.auth']['credentials']['token'] session[:refresh_token] = request.env['omniauth.auth']['credentials']['refresh_token'] redirect_to pages_path end end
So, after the user is navigated to the callback route, we can access their access and refresh token by reading the request.env
object. We store these tokens inside the session and redirect to another page which will be created in a moment.
Displaying a login link
Now we can display a “login” link. Therefore, we’ll create a new PagesController
inside the app/controllers/pages_controller.rb
file:
class PagesController < ApplicationController def index; end end
Add new routes to the config/routes.rb
file:
resources :pages, only: :index root 'pages#index'
Finally, create a new view inside the app/views/pages/index.html.erb
file:
<%= form_tag('/auth/lokalise', method: 'post', data: {turbo: false}) do %> <button type='submit'>Login with Lokalise</button> <% end %>
Please note that we must use the HTTP POST verb here. Also, if your app has Turbo framework set up, don’t forget to disable it for this exact link.
That’s it, the OAuth 2 flow is now completed!
Using access tokens to work with the API
Let’s try to use the obtained access token to list all the user’s translation projects. To achieve that, tweak the pages_controller.rb
file:
require 'ruby_lokalise_api' class PagesController < ApplicationController def index if session[:access_token] client = RubyLokaliseApi.oauth2_client session[:access_token] @projects = client.projects.collection end end end
So, if the token is present, we use it to create an API client and then load all the projects that the user has access to.
Finally, let’s modify the index
view:
<% if session[:access_token] %> Access token: <%= session[:access_token] %><br> Refresh token: <%= session[:refresh_token] %><br> Your projects: <ul> <% @projects.each do |project| %> <li><%= project.name %></li> <% end %> </ul> <% else %> <%= form_tag('/auth/lokalise', method: 'post', data: {turbo: false}) do %> <button type='submit'>Login with Lokalise</button> <% end %> <% end %>
We are simply displaying project names in a list.
Using refresh tokens
The access tokens that Lokalise generates for you usually expire in 60 minutes. Can we refresh them somehow? But of course! All we have to do is provide a refresh token to a refresh
method available in Ruby SDK. Let’s tweak the pages_controller.rb
in the following way:
require 'ruby_lokalise_api' class PagesController < ApplicationController def index if session[:access_token] begin client = RubyLokaliseApi.oauth2_client session[:access_token] @projects = client.projects.collection rescue RubyLokaliseApi::Error::Unauthorized auth_client = RubyLokaliseApi.auth_client ENV['OAUTH2_CLIENT_ID'], ENV['OAUTH2_CLIENT_SECRET'] response = auth_client.refresh session[:refresh_token] session[:access_token] = response['access_token'] retry end end end end
So, if we get an Unauthorized
error, we try to refresh our token with the Ruby SDK, store it in the session, and then retry the operation. You might also want to limit the number of retries to avoid an endless recursion.
Testing it out
Now everything is ready, therefore you can boot your server by running:
rails s
Proceed to http://localhost:3000
, log in via Lokalise, and observe the list of your projects.
Great job!
Conclusion
So, in this article we have seen how to set up the Lokalise OAuth 2 flow with Node, Flask, and Rails. Now you can implement the described concepts in your own apps to build something fancy and useful!
You can also visit our DevHub which contains even more helpful tutorials for developers.
I thank you for sticking with me today, and until next time!