Skip to content
Fully Managed Users

Fully Managed Users

Use Roled’s complete authentication solution. Users log in through Roled’s hosted page with email and password. This guide implements OAuth 2.0 Authorization Code Flow with PKCE for secure, browser-based authentication.

What you’ll build

StepOutcome
1Configure project with redirect URI
2Start OAuth flow from your app
3Handle callback and exchange authorization code for tokens
4Use access token to call protected endpoints
5Implement refresh token flow
6Implement secure logout with token revocation

Prerequisites

  • A Roled Console account with a project created (see Project Setup)
  • A local app running on http://localhost:4000 (or your domain)

1. Configure redirect URI

Configure where Roled should send users after they authenticate successfully.

  1. Open your Business App project in Roled Console.

  2. Under Redirect URIs, add your application’s callback URL:

    • Development: http://localhost:4000/auth/callback
    • Production: https://example.com/auth/callback

    Redirect URIs configuration in Roled Console

  3. Click Save.

  4. Copy your Client ID from the Clients section, you’ll need it for the OAuth flow.

Your app must provide and use the redirect URI that you configured in the Roled Console. It is recommended to set the Login URL to get a better user experience on the authentication flow. See Redirect URIs for details.

2. Initiate the OAuth flow

Roled requires PKCE (Proof Key for Code Exchange) for all Authorization Code flows to prevent authorization code interception attacks. Your application must:

  1. Generate a random code_verifier (43–128 URL-safe characters)
  2. Compute code_challenge = Base64URL(SHA-256(code_verifier))
  3. Store both code_verifier and a random state value for validation
  4. Redirect the user’s browser to Roled’s authorization endpoint

Run this JavaScript in the browser when users click Login:

const ROLED_AUTH_URL = 'https://auth.roled.io/authorize';
const CLIENT_ID = '2JVGkuDwkFRmcj4wKAGHos'; // from Roled Console
const REDIRECT_URI = 'http://localhost:4000/auth/callback';

function base64UrlEncode(bytes) {
  let binary = '';
  for (const byte of bytes) {
    binary += String.fromCharCode(byte);
  }
  return btoa(binary)
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=+$/, '');
}

function generateCodeVerifier() {
  const bytes = new Uint8Array(32);
  crypto.getRandomValues(bytes);
  return base64UrlEncode(bytes);
}

async function generateCodeChallenge(codeVerifier) {
  const digest = await crypto.subtle.digest(
    'SHA-256',
    new TextEncoder().encode(codeVerifier)
  );
  return base64UrlEncode(new Uint8Array(digest));
}

async function loginWithRoled() {
  const codeVerifier = generateCodeVerifier();
  const codeChallenge = await generateCodeChallenge(codeVerifier);
  const state = generateCodeVerifier();

  // Store for the callback handler
  sessionStorage.setItem('roled_code_verifier', codeVerifier);
  sessionStorage.setItem('roled_oauth_state', state);

  const params = new URLSearchParams({
    client_id: CLIENT_ID,
    redirect_uri: REDIRECT_URI,
    response_type: 'code',
    code_challenge_method: 'S256',
    code_challenge: codeChallenge,
    state: state,
  });

  window.location.href = `${ROLED_AUTH_URL}?${params.toString()}`;
}

// Attach to your login button
document.getElementById('login-button').addEventListener('click', loginWithRoled);

This redirects users to a URL like:

https://auth.roled.io/authorize
  ?client_id=2JVGkuDwkFRmcj4wKAGHos
  &redirect_uri=http://localhost:4000/auth/callback
  &response_type=code
  &code_challenge_method=S256
  &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
  &state=xcoiv98y2kd22vusuye3kch

Users see Roled’s hosted login page:

Roled hosted login page for Business App

After successful authentication, Roled redirects back with an authorization code:

http://localhost:4000/auth/callback
  ?code=6qxJpdxpyuAoSpfzkEbJUpJvBoncYM3Cnxfmj4WznZty78YpMd7VSZ7mE4KZTAH3
  &state=xcoiv98y2kd22vusuye3kch

3. Exchange authorization code for tokens

Exchange the authorization code for access and refresh tokens using the Exchange Credentials endpoint. You can implement this on your backend (recommended for security) or client-side (suitable for SPAs).

Option A: Backend exchange (recommended)

Use Node.js with Express:

import express from 'express';

const app = express();
const CLIENT_ID = '2JVGkuDwkFRmcj4wKAGHos'; // from Roled Console
const REDIRECT_URI = 'http://localhost:4000/auth/callback';
const TOKEN_URL = 'https://auth.roled.io/api/v1/tokens';

app.get('/auth/callback', async (req, res) => {
  const { code, state } = req.query;

  // Validate state and retrieve code_verifier from the session
  const savedState = req.session.roled_oauth_state;
  const codeVerifier = req.session.roled_code_verifier;

  if (!code || state !== savedState || !codeVerifier) {
    return res.status(400).send('Invalid OAuth callback');
  }

  const body = new URLSearchParams({
    grant_type: 'authorization_code',
    client_id: CLIENT_ID,
    authorization_code: code,
    redirect_uri: REDIRECT_URI,
    code_verifier: codeVerifier,
  });

  const response = await fetch(TOKEN_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: body.toString(),
  });

  const result = await response.json();

  if (!result.success) {
    return res.status(400).json(result.error);
  }

  const { access_token, refresh_token } = result.data;

  // Store tokens in a secure, httpOnly session cookie
  req.session.accessToken = access_token;
  req.session.refreshToken = refresh_token;

  res.redirect('/dashboard');
});

Option B: Client-side exchange (SPA-friendly)

For single-page applications without a backend, handle the token exchange in the browser:

const ROLED_AUTH_URL = 'https://auth.roled.io/authorize';
const CLIENT_ID = '2JVGkuDwkFRmcj4wKAGHos'; // from Roled Console
const REDIRECT_URI = 'http://localhost:4000/auth/callback';
const TOKEN_URL = 'https://auth.roled.io/api/v1/tokens';

async function handleCallback() {
  const params = new URLSearchParams(window.location.search);
  const code = params.get('code');
  const state = params.get('state');

  // Retrieve stored values from sessionStorage
  const savedState = sessionStorage.getItem('roled_oauth_state');
  const codeVerifier = sessionStorage.getItem('roled_code_verifier');

  if (!code || state !== savedState || !codeVerifier) {
    console.error('Invalid OAuth callback');
    return;
  }

  const body = new URLSearchParams({
    grant_type: 'authorization_code',
    client_id: CLIENT_ID,
    authorization_code: code,
    redirect_uri: REDIRECT_URI,
    code_verifier: codeVerifier,
  });

  const response = await fetch(TOKEN_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: body.toString(),
  });

  const result = await response.json();

  if (!result.success) {
    console.error('Token exchange failed:', result.error);
    return;
  }

  const { access_token, refresh_token } = result.data;

  // Store tokens (note: localStorage is less secure)
  localStorage.setItem('roled_access_token', access_token);
  localStorage.setItem('roled_refresh_token', refresh_token);

  // Clean up
  sessionStorage.removeItem('roled_code_verifier');
  sessionStorage.removeItem('roled_oauth_state');

  // Redirect to dashboard
  window.location.href = '/dashboard';
}

// Call this on page load if the callback page detects the URL has a code
if (new URLSearchParams(window.location.search).get('code')) {
  handleCallback();
}

4. Use the access token

Include the access token in the Authorization header when calling protected endpoints.

You can use the Inspect Current Access Token or Get Current User Details endpoints to retrieve the current user’s details, including roles and permissions. Use the response to enforce permissions in your app, like showing different content/page/menu/feature based on user roles or permissions.

const accessToken = localStorage.getItem('roled_access_token');
// or get from session cookie if using backend

// inspect current token details
const response = await fetch('https://auth.roled.io/api/v1/tokens/current', {
  headers: {
    Authorization: `Bearer ${accessToken}`,
  },
});

const { data } = await response.json();
// data.user.display_name   → "Alice Smith"
// data.user.email          → "alice.smith@example.com"
// data.role.name           → "Store Manager"
// data.permissions         → ["products:create", "products:read", ...]

On the server side, you can use a middleware to enforce permissions.

// Middleware example (Node.js/Express) with simple in-memory caching
const permissionCache = new Map();
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes

async function getUserPermissions(accessToken) {
  const response = await fetch('https://auth.roled.io/api/v1/users/current?include_permissions=true', {
    headers: {
      Authorization: `Bearer ${accessToken}`,
    },
  });

  const result = await response.json();
  if (!result.success) {
    throw new Error('Invalid or expired token');
  }

  return {
    displayName: result.data.display_name,
    email: result.data.email,
    roleName: result.data.role_name,
    permissions: result.data.permissions || [],
  };
}

async function getPermissionsWithCache(accessToken) {
  const cached = permissionCache.get(accessToken);
  const now = Date.now();

  if (cached && (now - cached.timestamp) < CACHE_TTL) {
    return cached.data;
  }

  const permissions = await getUserPermissions(accessToken);

  permissionCache.set(accessToken, {
    data: permissions,
    timestamp: now,
  });

  return permissions;
}

function requirePermission(requiredPermission) {
  return async (req, res, next) => {
    try {
      const accessToken = req.session.accessToken;
      if (!accessToken) {
        return res.status(401).json({ error: 'Unauthorized' });
      }

      const { permissions, roleName } = await getPermissionsWithCache(accessToken);

      if (!permissions.includes(requiredPermission)) {
        return res.status(403).json({
          error: 'Forbidden',
          message: `Permission '${requiredPermission}' required for role '${roleName}'`,
        });
      }

      req.userPermissions = permissions;
      req.userRole = roleName;
      next();
    } catch (error) {
      res.status(500).json({ error: 'Authorization failed' });
    }
  };
}

// Protected endpoint example
app.get('/products', requirePermission('products:read'), async (req, res) => {
  // Only users with products:read permission reach here
  const products = await db.getProducts();
  res.json({ products });
});

app.delete('/products/:id', requirePermission('products:delete'), async (req, res) => {
  // Only users with products:delete permission reach here
  const { id } = req.params;
  await db.deleteProduct(id);
  res.json({ success: true });
});
Since permission checks happen on every request, it is recommended to implement a caching strategy to significantly reduce API latency and network calls to the Roled API, as user roles and permissions do not change frequently.

5. Refresh access tokens

Access tokens expire after 1 hour. Use the refresh token to obtain a new access token without requiring user re-authentication. This maintains seamless user experience while preserving security.

Call the Exchange Credentials endpoint withrefresh_token as the grant type.

const CLIENT_ID = '2JVGkuDwkFRmcj4wKAGHos'; // from Roled Console
const TOKEN_URL = 'https://auth.roled.io/api/v1/tokens';

async function refreshAccessToken() {
  const refreshToken = localStorage.getItem('roled_refresh_token');
  if (!refreshToken) {
    throw new Error('No refresh token available');
  }

  const body = new URLSearchParams({
    grant_type: 'refresh_token',
    client_id: CLIENT_ID,
    refresh_token: refreshToken,
  });

  const response = await fetch(TOKEN_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: body.toString(),
  });

  const result = await response.json();

  if (!result.success) {
    throw new Error('Token refresh failed');
  }

  const { access_token, refresh_token } = result.data;

  // Store new tokens
  localStorage.setItem('roled_access_token', access_token);
  localStorage.setItem('roled_refresh_token', refresh_token);

  return access_token;
}
Important: Refresh tokens are single-use. Each successful refresh returns both a new access token and a new refresh token. Always store the updated refresh token.

6. Implement secure logout

Secure logout requires revoking tokens server-side. Roled’s revocation endpoint immediately invalidates tokens, ensuring they cannot access protected resources.

Call the Revoke Current Access Token endpoint to invalidate both the access and refresh tokens:

const REVOKE_URL = 'https://auth.roled.io/api/v1/tokens/current/revoke';

async function logout() {
  const refreshToken = localStorage.getItem('roled_refresh_token');
  const accessToken = localStorage.getItem('roled_access_token');
  const clientId = '2JVGkuDwkFRmcj4wKAGHos'; // from Roled Console

  if (refreshToken) {
    try {
      // Revoke the token on the server
      await fetch(REVOKE_URL, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${accessToken}`,
        },
        body: JSON.stringify({
          client_id: clientId,
          refresh_token: refreshToken,
        }),
      });
    } catch (error) {
      console.error('Token revocation failed:', error);
      // Continue with local cleanup even if server revocation fails
    }
  }

  // Clear tokens from localStorage
  localStorage.removeItem('roled_access_token');
  localStorage.removeItem('roled_refresh_token');

  // Clear any sessionStorage items used during OAuth flow
  sessionStorage.removeItem('roled_code_verifier');
  sessionStorage.removeItem('roled_oauth_state');

  // Redirect to login page or home
  window.location.href = '/login';
}

// Attach to logout button
document.getElementById('logout-button').addEventListener('click', logout);
Always revoke tokens server-side before clearing them locally. Local-only cleanup leaves valid tokens that attackers can exploit if stolen.

Backend logout implementation

If you’re managing sessions on the backend, implement logout as follows:

// Express.js backend logout endpoint
app.post('/auth/logout', async (req, res) => {
  try {
    const refreshToken = req.session.refreshToken;
    const accessToken = req.session.accessToken;

    if (refreshToken) {
      // Revoke token on Roled server
      await fetch('https://auth.roled.io/api/v1/tokens/current/revoke', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${accessToken}`,
        },
        body: JSON.stringify({
          client_id: '2JVGkuDwkFRmcj4wKAGHos', // from Roled Console
          refresh_token: refreshToken,
        }),
      });
    }
  } catch (error) {
    console.error('Token revocation error:', error);
    // Continue with session destruction even if revocation fails
  }

  // Destroy session
  req.session.destroy((err) => {
    if (err) {
      console.error('Session destruction error:', err);
      return res.status(500).json({ error: 'Logout failed' });
    }

    // Clear session cookie
    res.clearCookie('connect.sid');
    res.json({ success: true, message: 'Logged out successfully' });
  });
});

Next steps

You’ve successfully integrated Roled authentication! Here’s what to explore next: