Skip to content

User authorization

Act on behalf of a person with OAuth 2.0 Authorization Code and PKCE.

Use this flow when your application acts on behalf of a person: they sign in to Ecomiq and authorize the permissions on the application's store. If a server acts as the application, use client_credentials from Authentication.

User
Your application
Auth
Admin API
1Generate the PKCE
2Redirect to /connect/authorize
Sign in and authorize
Back to redirect_uri with code
3POST /connect/token with code_verifier
access_token and refresh_token
4Call with Bearer
5POST /connect/token with refresh_token
New tokens
The store and the authorized permissions are carried in the token.

Requirements

  • Create the application in the dashboard with the With sign-in connection and its redirect URLs. They are matched exactly.
  • The application can already renew access: request the offline_access scope to receive a refresh token.
  • A dashboard application can only be authorized on the store where it was created.
  • Mobile and browser apps check No server: they are public clients and have no client_secret.

1. Generate the PKCE

PKCE is required and only S256 is accepted. Store verifier and state in the user's session.

pkce.ts
import { createHash, randomBytes } from "node:crypto";

export const verifier = randomBytes(32).toString("base64url");
export const challenge = createHash("sha256").update(verifier).digest("base64url");
export const state = randomBytes(16).toString("base64url");

2. Redirect to authorize

Authorization URL
https://auth.ecomiq.pe/connect/authorize
  ?response_type=code
  &client_id=<client-id>
  &redirect_uri=https://your-app.com/oauth/callback
  &scope=api_admin%20offline_access
  &state=<state>
  &code_challenge=<challenge>
  &code_challenge_method=S256
ParameterUse
response_typeAlways code.
redirect_uriOne of the registered URLs, identical.
scopeapi_admin for the Admin API; offline_access for a refresh token.
stateA random value; compare it when they return.
code_challenge_methodOnly S256.
promptlogin forces a new sign-in.

The person signs in, picks the store, and sees your application's permissions. When they accept, they return to your redirect_uri with code and state.

3. Exchange the code

The code works once and expires after 5 minutes.

curl -X POST https://auth.ecomiq.pe/connect/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code" \
  -d "code=<code>" \
  -d "redirect_uri=https://your-app.com/oauth/callback" \
  -d "code_verifier=$VERIFIER" \
  -d "client_id=$ECOMIQ_CLIENT_ID" \
  -d "client_secret=$ECOMIQ_CLIENT_SECRET"
Response
{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "CfDJ8...",
  "scope": "api_admin offline_access"
}

A public client omits client_secret.

4. Call the Admin API

Send the token in Authorization: Bearer. The store comes from the token; if you send x-Store-Id, it must match or you get a 403. The permissions are your application's permissions that the person still has in that store.

5. Renew the token

The access token lasts 1 hour and the refresh token 14 days. Every renewal returns a new refresh token: always keep the latest one.

curl -X POST https://auth.ecomiq.pe/connect/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=refresh_token" \
  -d "refresh_token=$REFRESH_TOKEN" \
  -d "client_id=$ECOMIQ_CLIENT_ID" \
  -d "client_secret=$ECOMIQ_CLIENT_SECRET"

Renewal fails with invalid_grant if the person changed their password, lost access to the store, or the application was deleted. Go back to step 2.

Full example

A dependency-free Node.js server: /login redirects to Ecomiq and /oauth/callback checks state and exchanges the code.

server.mjs
import { createServer } from "node:http";
import { createHash, randomBytes } from "node:crypto";

const AUTH = "https://auth.ecomiq.pe";
const REDIRECT_URI = "https://your-app.com/oauth/callback";
const pending = new Map();

createServer(async (req, res) => {
  const url = new URL(req.url, REDIRECT_URI);

  if (url.pathname === "/login") {
    const verifier = randomBytes(32).toString("base64url");
    const state = randomBytes(16).toString("base64url");
    pending.set(state, verifier);

    const authorize = new URL("/connect/authorize", AUTH);
    authorize.search = new URLSearchParams({
      response_type: "code",
      client_id: process.env.ECOMIQ_CLIENT_ID,
      redirect_uri: REDIRECT_URI,
      scope: "api_admin offline_access",
      state,
      code_challenge: createHash("sha256").update(verifier).digest("base64url"),
      code_challenge_method: "S256",
    }).toString();

    res.writeHead(302, { Location: authorize.toString() }).end();
    return;
  }

  if (url.pathname === "/oauth/callback") {
    const state = url.searchParams.get("state");
    const verifier = pending.get(state);
    pending.delete(state);
    if (!verifier || url.searchParams.has("error")) {
      res.writeHead(400).end(url.searchParams.get("error") ?? "invalid_state");
      return;
    }

    const response = await fetch(`${AUTH}/connect/token`, {
      method: "POST",
      headers: { "Content-Type": "application/x-www-form-urlencoded" },
      body: new URLSearchParams({
        grant_type: "authorization_code",
        code: url.searchParams.get("code"),
        redirect_uri: REDIRECT_URI,
        code_verifier: verifier,
        client_id: process.env.ECOMIQ_CLIENT_ID,
        client_secret: process.env.ECOMIQ_CLIENT_SECRET,
      }),
    });
    const tokens = await response.json();
    // Save tokens.access_token and tokens.refresh_token in your database.

    res.writeHead(200).end("Store connected.");
    return;
  }

  res.writeHead(404).end();
}).listen(3000);

Errors

WhereerrorCause
authorizeaccess_deniedThe person declined or can't authorize on that store.
authorizeinvalid_scopeYou requested a scope the application doesn't have.
authorizeinvalid_requestA parameter, the redirect URL, or the store is not valid.
tokeninvalid_grantCode expired or reused, wrong code_verifier, or access revoked.

Last updated:

On this page