Skip to content

Login with Funtico

This guide shows you how to implement Login with Funtico in your application using a secure backend approach. We’ll cover the complete authentication flow from initial setup to token management using secure HTTP-only cookies.

Before implementing Login with Funtico, ensure you have:

  • Auth Client Credentials - Client ID and Secret from the Funtico Developer Dashboard
  • Redirect URI - Configured callback URL in your auth client settings
  • SDK Installation - Install the appropriate SDK for your platform
Terminal window
npm install @pillarex/funtico-sdk

Initialize the Funtico SDK with your auth client credentials:

import { FunticoSDK } from '@pillarex/funtico-sdk';
const sdk = new FunticoSDK({
authClientId: process.env.FUNTICO_AUTH_CLIENT_ID,
authClientSecret: process.env.FUNTICO_AUTH_CLIENT_SECRET,
env: process.env.NODE_ENV === 'production' ? 'production' : 'staging'
});

Create a backend endpoint to start the authentication flow:

// Backend: POST /auth/login
app.post('/auth/login', async (req, res) => {
try {
const { codeVerifier, redirectUrl, state } = await sdk.signInWithFuntico({
callbackUrl: 'https://your-app.com/auth/callback',
scopes: ['openid', 'profile', 'email', 'offline_access', 'balance:read', 'transactions:read', 'tournaments:read', 'tournaments:play']
});
// Store codeVerifier in secure cookie using state as key
res.cookie(`state_${state}`, codeVerifier, {
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: 10 * 60 * 1000 // 10 minutes
});
// Return redirect URL to frontend
res.json({ redirectUrl });
} catch (error) {
res.status(500).json({ error: 'Failed to initiate login' });
}
});

Create a backend endpoint to handle the OAuth callback:

// Backend: GET /auth/callback
app.get('/auth/callback', async (req, res) => {
try {
const state = req.query.state as string;
const code = req.query.code as string;
if (!state || !code) {
return res.status(400).json({ error: 'Missing state or code parameter' });
}
// Retrieve codeVerifier from cookie
const codeVerifier = req.cookies[`state_${state}`];
if (!codeVerifier) {
return res.status(400).json({ error: 'Invalid or expired state' });
}
// Clear the state cookie
res.clearCookie(`state_${state}`);
// Exchange code for tokens
const { accessToken, refreshToken } = await sdk.getTokens({
codeVerifier,
url: req.url // full callback URL with query parameters
});
// Store tokens in secure cookies
res.cookie('access_token', accessToken, {
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: 60 * 60 * 1000 // 1 hour
});
res.cookie('refresh_token', refreshToken, {
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days
});
// Redirect to success page
res.redirect('https://your-app.com/dashboard?login=success');
} catch (error) {
res.status(500).json({ error: 'Failed to handle callback' });
}
});

Your frontend would interact with these endpoints like this:

// Frontend: Initiate login
async function startLogin() {
try {
const response = await fetch('/auth/login', { method: 'POST' });
const { redirectUrl } = await response.json();
// Redirect user to Funtico
window.location.href = redirectUrl;
} catch (error) {
console.error('Failed to start authentication:', error);
}
}

Create a backend endpoint to retrieve user information:

// Backend: GET /auth/profile
app.get('/auth/profile', async (req, res) => {
try {
const accessToken = req.cookies.access_token;
if (!accessToken) {
return res.status(401).json({ error: 'Not authenticated' });
}
const userInfo = await sdk.getUserInfo({ accessToken });
const balance = await sdk.getUserBalance({ accessToken });
res.json({ userInfo, balance });
} catch (error) {
if (error.status === 401) {
res.status(401).json({ error: 'Token expired' });
} else {
res.status(500).json({ error: 'Failed to get user profile' });
}
}
});

Implement automatic token refresh endpoint:

// Backend: POST /auth/refresh
app.post('/auth/refresh', async (req, res) => {
try {
const refreshToken = req.cookies.refresh_token;
if (!refreshToken) {
return res.status(401).json({ error: 'No refresh token' });
}
const { accessToken, refreshToken: newRefreshToken } = await sdk.refreshTokens({
refreshToken
});
// Update tokens in cookies
res.cookie('access_token', accessToken, {
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: 60 * 60 * 1000 // 1 hour
});
res.cookie('refresh_token', newRefreshToken, {
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days
});
res.json({ success: true });
} catch (error) {
res.status(401).json({ error: 'Failed to refresh token' });
}
});

Your frontend would handle user data and token refresh like this:

// Frontend: Get user profile
async function getProfile() {
const response = await fetch('/auth/profile', {
credentials: 'include' // Include cookies
});
if (response.ok) {
const { userInfo, balance } = await response.json();
return { userInfo, balance };
} else if (response.status === 401) {
// Try to refresh the token
const refreshed = await refreshToken();
if (refreshed) {
// Retry the request
return getProfile();
}
}
}
// Frontend: Refresh token
async function refreshToken() {
const response = await fetch('/auth/refresh', {
method: 'POST',
credentials: 'include'
});
return response.ok;
}
// Handle authentication errors
function handleAuthError(error: unknown) {
if (isSDKError(error)) {
switch (error.name) {
case 'invalid_client':
console.error('Invalid client credentials');
break;
case 'invalid_grant':
console.error('Invalid authorization code or refresh token');
break;
case 'access_denied':
console.error('User denied authorization');
break;
default:
console.error('Authentication error:', error);
}
} else {
console.error('Unexpected error:', error);
}
}
// User-friendly error messages
function getErrorMessage(error: unknown): string {
if (isSDKError(error)) {
switch (error.name) {
case 'invalid_client':
return 'Authentication service configuration error';
case 'invalid_grant':
return 'Authentication session expired. Please try again.';
case 'access_denied':
return 'Login was cancelled. Please try again.';
default:
return 'Authentication failed. Please try again.';
}
}
return 'An unexpected error occurred. Please try again.';
}
  • Secure Cookies: Use HTTP-only, secure cookies with proper SameSite settings
  • No Frontend Storage: Avoid storing tokens in localStorage or sessionStorage
  • Expiration: Set appropriate expiration times matching token lifespans
  • HTTPS Only: Ensure all communication uses HTTPS in production

State validation is handled securely on the backend using temporary cookies:

// Backend state validation
const codeVerifier = req.cookies[`state_${state}`];
if (!codeVerifier) {
return res.status(400).json({ error: 'Invalid or expired state' });
}
  • Graceful degradation: Handle authentication failures gracefully
  • User feedback: Provide clear error messages to users
  • Logging: Log authentication events for debugging
  • Retry logic: Implement retry mechanisms for transient failures

Here’s a complete example with backend and frontend components:

// Backend Express server example
import express from 'express';
import cookieParser from 'cookie-parser';
import { FunticoSDK } from '@pillarex/funtico-sdk';
const app = express();
app.use(cookieParser());
const sdk = new FunticoSDK({
authClientId: process.env.FUNTICO_AUTH_CLIENT_ID!,
authClientSecret: process.env.FUNTICO_AUTH_CLIENT_SECRET!,
env: process.env.NODE_ENV === 'production' ? 'production' : 'staging'
});
// Initiate login
app.post('/auth/login', async (req, res) => {
try {
const { codeVerifier, redirectUrl, state } = await sdk.signInWithFuntico({
callbackUrl: 'https://your-app.com/auth/callback',
scopes: ['openid', 'profile', 'email', 'offline_access', 'balance:read', 'transactions:read', 'tournaments:read', 'tournaments:play']
});
res.cookie(`state_${state}`, codeVerifier, {
httpOnly: true, secure: true, sameSite: 'lax', maxAge: 10 * 60 * 1000
});
res.json({ redirectUrl });
} catch (error) {
res.status(500).json({ error: 'Failed to initiate login' });
}
});
// Handle callback
app.get('/auth/callback', async (req, res) => {
try {
const state = req.query.state as string;
const codeVerifier = req.cookies[`state_${state}`];
if (!codeVerifier) {
return res.status(400).json({ error: 'Invalid state' });
}
res.clearCookie(`state_${state}`);
const { accessToken, refreshToken } = await sdk.getTokens({
codeVerifier, url: req.url
});
res.cookie('access_token', accessToken, {
httpOnly: true, secure: true, sameSite: 'lax', maxAge: 60 * 60 * 1000
});
res.cookie('refresh_token', refreshToken, {
httpOnly: true, secure: true, sameSite: 'lax', maxAge: 7 * 24 * 60 * 60 * 1000
});
res.redirect('https://your-app.com/dashboard');
} catch (error) {
res.status(500).json({ error: 'Authentication failed' });
}
});