Integrate CARTO in your existing application

Learn how to securely embed CARTO into your existing architecture providing fine-grained data access

When you build a solution from scratch using CARTO you can choose between the three different authentication strategies to secure your application. However, in many other cases the starting point is an existing application with its own login system where you want to add a geospatial component, such as a map visualization.

In this guide you will learn how to integrate CARTO with an existing application that has its own login system, implementing the machine-to-machine authentication in order to provide fine-grained data access to the end users.

Overview

In this guide, the ACME company has decided to add maps to their existing application and CARTO will be the platform used for it. They have two important requirements:

  • ACME wants to keep its own login system.

  • The maps will show different data based on the user, in this case, based on the user's group.

With this in mind, we are going to develop a backend endpoint in order to generate a CARTO API Access Token with only access to the data the user has permission for in our application, the frontend application will use that token to interact with the rest of the CARTO APIs.

The backend is responsible for generating an API Access Token without exceding the actual permissions of the users.

The data used for this guide will be the table retail_stores contains all the retails in the US, It's available at carto-demo-data.demo_tables.retail_stores.

In our demo application we will have two users:

{
  "username": "user.boston@acme.com",
  "password": "boston",
  "group": "BOSTON"
},
{
  "username": "user.ny@acme.com",
  "password": "ny",
  "group": "NEW YORK"
} 

The user in the group of BOSTON will only see the stores in the city of Boston, the user in group NEW YORK will be limited to New York.

Scaffolding your backend

Since this guide needs a backend and a frontend application, the first thing we have to do is create the structure to store the code for these components. Let's start with the backend side and install the needed dependencies to work with Express:

mkdir backend
cd backend
npm init --yes
npm install express dotenv cors jsonwebtoken

We are going to use Typescript on both the backend and frontend sides, so we need to install the required dependencies:

npm i -D typescript @types/express @types/node @types/cors @types/jsonwebtoken
npx tsc --init

Now, the next step is to uncomment the following line in the file (it specifies an output folder for all emitted files):

"outDir": "./dist",

As an optional step, you would like to add to have extra help to develop by restarting your application automatically when there are changes in your code. To do that, install the following dependencies:

npm install -D concurrently nodemon

And add the following content to the file package.json:

{
    "scripts": {
        "build": "npx tsc",
        "start": "node dist/index.js",
        "dev": "concurrently \"npx tsc --watch\" \"nodemon -q dist/index.js\""
    }
}

Now, just typing npm run dev the server will run up and you can see the changes in the code immediately.

Creating an Application in CARTO

In order to use the CARTO APIs you need to authenticate your requests. For a backend-side application, the recommended approach is to do so by creating a Machine to Machine application. To create this application, you need to go to CARTO Workspace -> Developers -> Built applications.

For development purposes, you can set the URL for localhost in the App URL and Application Login URI. Just set the URL https://127.0.0.1:8000 in both fields.

Once the application is created, you will have to get the Client ID and the Client Secret

Coding the endpoint

Now, we have to create a couple of endpoints: a basic login endpoint to authenticate the user and an endpoint to generate the CARTO token.

For the login endpoint, the responsibilities will be:

  • Check the login and password.

  • Get the group of the user and create a JWT token.

In a real production environment, you should already have a login endpoint.

While the endpoint to get the CARTO token will do:

  • Validate the JWT in the Authentication header.

  • Extract the group and create the token via CARTO API.

Replace the file index.ts with the following content:

import express, { Express, Request, Response } from 'express'
import dotenv from 'dotenv'
import cors from 'cors'
import jwt from 'jsonwebtoken'
import fetch from 'node-fetch'

dotenv.config()

const app: Express = express()
const port = process.env.PORT || 8000
const jwtSecret = process.env.JWT_SECRET as string

app.use(express.json())
app.use(cors())

interface LoginRequestBody {
  username: string
  password: string
}

interface LoginResponseBody {
  token: string
}

interface AccessApiTokenResponse {
  token: string
  error: string,
  description: string
}

// hard-coded users. Don't use this in production! This should come from an IdP or a database.
const users = [{
    username: 'user.boston@acme.com',
    password: 'boston',
    group: 'BOSTON'
  },
  {
    username: 'user.ny@acme.com',
    password: 'ny',
    group: 'NEW YORK'
  } 
]

// This endpoint simulates a login system. It checks the credentials and returns a JWT token
// with the user group as a claim.
app.post('/login', async (req: Request, res: Response) => {
  const { username, password } = req.body as LoginRequestBody
  const user = users.find((u) => u.username === username)

  if (!user || user.password !== password) {
    res.status(401).send({ 'error': 'Invalid credentials' })
    return
  }

  const loginToken = jwt.sign({ group: user.group }, jwtSecret, { expiresIn: '1h' })
  const loginResponse = { token: loginToken } as LoginResponseBody

  res.send(loginResponse)
})

// This endpoint returns a CARTO API Access Token for the user group. The token will be used
// to query the tables for the city of the user.
app.post('/carto-token', async (req: Request, res: Response) => {
  try {

    // Get the token from the Authorization header with Bearer prefix
    const authHeader = req.headers.authorization as string
    const loginToken = authHeader.replace('Bearer ', '')

    // Decode the token to get the group
    const tokenGroup = jwt.verify(loginToken, jwtSecret) as { group: string }
    const token = await getAPIAccessTokenForGroup(tokenGroup.group)
    const response = { token, city: tokenGroup.group } as LoginResponseBody
    res.send(response)
  } catch (error) {
    console.log(error)
    res.status(401).send({ 'error': 'Invalid token' })
  }
})

app.listen(port, () => {
  console.log(`⚡️[server]: Server is running at http://localhost:${port}`)
})

Create a .env file located in the root of the backend with the following content:

PORT=8000
# The JWT Secret to sign the JWT token
JWT_SECRET=PUT_YOUR_SECRET_HERE
# API Base URL (copy this from CARTO Workspace -> Developers)
CARTO_BASE_URL=PUT_YOUR_API_BASE_URL_HERE
# ClientID and Secret of your Machine to Machine Application
CARTO_CLIENT_ID=PUT_YOUR_CLIENT_ID_HERE
CARTO_CLIENT_SECRET=PUT_YOUR_CLIENT_ID_HERE

The most important function involved in the endpoint is the one called getAPIAccessTokenForGroup that owns the logic to interact with CARTO in order to get a valid token for the frontend side. Let's see what its implementation looks like:

async function getAPIAccessTokenForGroup(group: string): Promise<string> {
  const cartoBaseUrl = process.env.CARTO_BASE_URL
  const clientId = process.env.CARTO_CLIENT_ID
  const clientSecret = process.env.CARTO_CLIENT_SECRET

  // Step 1: Get an OAuth Access Token using the clientId and clientSecret of a Machine to Machine Application.
  const accessTokenResponse = await fetch('https://auth.carto.com/oauth/token', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded'
    },
    body: `grant_type=client_credentials&client_id=${clientId}&client_secret=${clientSecret}&audience=carto-cloud-native-api`
  })

  const { access_token, error } = await accessTokenResponse.json() as { access_token: string, error: string }
  if (error) {
    console.log(error)
    throw new Error(error)
  }

  // Step 2: Generate an API Access Token for the logged user (with limited grants).
  const grants = [
    {
      'connection_name': 'carto_dw',
      'source': `SELECT * FROM \`carto-demo-data\`.demo_tables.retail_stores WHERE city = '${group}'`
    }
  ]

  // Call the tokens API using the OAuth Access Token from step 1.
  const accessApiTokenResponse = await fetch(`${cartoBaseUrl}/v3/tokens`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${access_token}`
    },
    body: JSON.stringify({
      'grants': grants,
      // Put here the referers of your frontend application in production
      'referers': [],
      'allowed_apis': [
        // 'sql', // Uncomment this line to allow SQL API calls
        'maps'
      ]
    })
  })

  const { token, error: tokenError, description: tokenDescription } = await accessApiTokenResponse.json() as AccessApiTokenResponse
  if (tokenError) {
    console.log(error)
    throw new Error(tokenError)
  }

  return token
}

Tokens are limited based on the quota of your plan. In a production environment, you should implement a mechanism to create only the tokens that you need:

  • API Access Tokens: avoid creating two tokens with the same grants for the same user.

  • OAuth Access Tokens: OAuth Access Tokens are valid for 24 hours, so you should avoid creating more than one token per day per application/API instance.

The function will take one parameter indicating the group in order to create the right token. The first thing to do is to get the environment variables to interact with the CARTO API. The next step is to obtain an OAuth Access Token to deal with the rest of the APIs. To get this first token you can see in the line 7 to 13 how to use the Client ID and the Client Secret to make the request and get this token. Once, you have a valid OAuth Access Token, the last step is to create an API Access Token for the specific grants. In this case, the grants include the permission to execute a specific SQL query to get all the retail for a specific city.

That means, that once the token is created, the client using this token only will have permission to execute the defined query and get only the data declared in the grants.

Finally, once the code for the backend is ready you can run the server just by typing in your terminal:

npm run dev

If your application requires dynamic queries, you can use query parameters in SQL and Maps API.

SELECT * FROM carto-demo-data.demo_tables.retail_stores

WHERE city = 'BOSTON' AND storetype = @storetype

Frontend side integration

At this point, our backend is ready to generate tokens for our users. In the following code you can see an example of a frontend application that does:

  • Login.

  • Call the previous endpoint to get an API Access Token with the permissions limited to the user.

  • Use the token to call CARTO APIs.

To run this, first, you need to clone the project:

git clone https://github.com/CartoDB/carto-for-developers-guides.git
cd carto-for-developers-guides/integrate-existing-app/frontend

Edit the .env file at set the VITE_API_BASE_URL:

# API Base URL (copy this from CARTO Workspace -> Developers)
VITE_CARTO_API_BASE_URL=https://gcp-us-east1.api.carto.com
# Custom company backend
VITE_COMPANY_API_BASE_URL=http://localhost:8000

If you have a different region, you need to modify VITE_CARTO_API_BASE_URL.

Run:

npm run dev

What's next?

All the code for this guide is available on GitHub.

git clone https://github.com/CartoDB/carto-for-developers-guides.git
cd carto-for-developers-guides/integrate-existing-app

We recommend you to visit CARTO + deck.gl to learn more about the different visualizations you can create using deck.gl. There are many examples in our gallery that might be useful to improve your application!

Last updated