Diagram showing how OAuth 2 connects with the Lokalise platform

Lokalise OAuth 2: Acting on the user’s behalf

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:

    1. 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.
    2. 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.
    3. 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 a write_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!

    Related articles
    Stop wasting time with manual localization tasks. 

    Launch global products days from now.