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.
Prerequisites
Section titled “Prerequisites”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
npm install @pillarex/funtico-sdkSDK Initialization
Section titled “SDK Initialization”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'});Starting the Authentication Flow
Section titled “Starting the Authentication Flow”1. Initiate Login (Backend)
Section titled “1. Initiate Login (Backend)”Create a backend endpoint to start the authentication flow:
// Backend: POST /auth/loginapp.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' }); }});2. Handle the Callback (Backend)
Section titled “2. Handle the Callback (Backend)”Create a backend endpoint to handle the OAuth callback:
// Backend: GET /auth/callbackapp.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' }); }});3. Frontend Integration
Section titled “3. Frontend Integration”Your frontend would interact with these endpoints like this:
// Frontend: Initiate loginasync 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); }}Token Management
Section titled “Token Management”Accessing User Information (Backend)
Section titled “Accessing User Information (Backend)”Create a backend endpoint to retrieve user information:
// Backend: GET /auth/profileapp.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' }); } }});Token Refresh (Backend)
Section titled “Token Refresh (Backend)”Implement automatic token refresh endpoint:
// Backend: POST /auth/refreshapp.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' }); }});Frontend Token Management
Section titled “Frontend Token Management”Your frontend would handle user data and token refresh like this:
// Frontend: Get user profileasync 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 tokenasync function refreshToken() { const response = await fetch('/auth/refresh', { method: 'POST', credentials: 'include' });
return response.ok;}Error Handling
Section titled “Error Handling”Common Authentication Errors
Section titled “Common Authentication Errors”// Handle authentication errorsfunction 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 messagesfunction 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.';}Security Best Practices
Section titled “Security Best Practices”Token Storage
Section titled “Token Storage”- 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
Section titled “State Validation”State validation is handled securely on the backend using temporary cookies:
// Backend state validationconst codeVerifier = req.cookies[`state_${state}`];if (!codeVerifier) { return res.status(400).json({ error: 'Invalid or expired state' });}Error Handling
Section titled “Error Handling”- 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
Complete Integration Example
Section titled “Complete Integration Example”Here’s a complete example with backend and frontend components:
// Backend Express server exampleimport 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 loginapp.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 callbackapp.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' }); }});// React component exampleimport React, { useState } from 'react';
const LoginComponent: React.FC = () => { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState<string | null>(null);
const handleLogin = async () => { setIsLoading(true); setError(null);
try { const response = await fetch('/auth/login', { method: 'POST' }); const { redirectUrl } = await response.json();
// Redirect user to Funtico window.location.href = redirectUrl; } catch (error) { setError('Failed to start authentication. Please try again.'); console.error('Login error:', error); } finally { setIsLoading(false); } };
return ( <div className="login-container"> <h2>Login with Funtico</h2> {error && <div className="error">{error}</div>} <button onClick={handleLogin} disabled={isLoading} className="login-button" > {isLoading ? 'Connecting...' : 'Login with Funtico'} </button> </div> );};
export default LoginComponent;Next Steps
Section titled “Next Steps”- User Data Access - Retrieve and manage user information
- Payment Integration - Integrate payments with authenticated users