


Student organizations and academic clubs generally experience numerous recurring issues. Attendance management, keeping track of members’ activity, making decisions through polls, and giving Executive Boards an exclusive administration panel are tasks that have been done using separate and independent Excel sheets and paper forms.
To solve this for the English Literary Association (ELA) of NSBM Green University, I have designed and implemented a full-stack web application: the ELA Club Management Portal which includes QR-based attendance verification, real-time polling, and role-based routing.
At the center of this architecture is WSO2 Asgardeo, which is an enterprise-grade Customer Identity and Access Management (CIAM) platform. In this blog post, I will break down how I have integrated WSO2 Asgardeo into a modern React (Vite) and Tailwind CSS based SPA with role-based access control (RBAC).
Architecture Overview
The system bridges identity management and club data operations across three tiers:
-
Identity & Authentication (CIAM): WSO2 Asgardeo via
@asgardeo/auth-reacthandling OpenID Connect (OIDC) Authorization Code Flow with PKCE. - Frontend Client: React 18, Vite, Tailwind CSS (custom brand tokens for ELA), and Lucide icons.
- Real-time Data Store: Google Cloud Firestore handling event recaps, member passports, and theme polls.
Configuring WSO2 Asgardeo for Single-Page Applications
1. Application Registration
In the Asgardeo Console, I registered a Single Page Application (SPA) named ELA Club Portal and configured:
-
Authorized Redirect URLs:
http://localhost:5173(and production deployment domains) -
Allowed Origins:
http://localhost:5173 - Grant Types: Code Grant with PKCE for single-page applications.
2. Role-Based Access Control (RBAC) Hierarchy
Student association governance requires clear segregation of duties. I defined roles in Asgardeo:
-
Admin(Executive Board): Full governance, publishing upcoming sessions, managing polls, manual attendance overrides, and CSV roster exports. -
Editorial & PR: Publishing session recaps and managing content galleries. -
Member: Casting votes on polls, checking into meetings, and managing a personal Literary Passport.
To pass these permissions to the client, I enabled Role / Group Sharing under the application's attribute settings and included them in the ID token claims.
Integrating the Asgardeo React SDK
Integrating Asgardeo into React is straightforward using @asgardeo/auth-react.
Step 1: Wrapping the Application with AuthProvider
In main.jsx, we initialize the authentication state:
import React from 'react';
import ReactDOM from 'react-dom/client';
import { AuthProvider } from '@asgardeo/auth-react';
import App from './App.jsx';
import './index.css';
const authConfig = {
signInRedirectURL: import.meta.env.VITE_ASGARDEO_REDIRECT_URL,
signOutRedirectURL: import.meta.env.VITE_ASGARDEO_REDIRECT_URL,
clientID: import.meta.env.VITE_ASGARDEO_CLIENT_ID,
baseUrl: import.meta.env.VITE_ASGARDEO_BASE_URL,
scope: ['openid', 'profile', 'roles', 'groups']
};
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<AuthProvider config="{authConfig}">
<App/>
</AuthProvider>
</React.StrictMode>
);
Step 2: Enforcing Granular Route Guarding with Decoded Claims
To restrict top-level features (like the Executive Board suite), we inspect the decoded ID token claims.
Here is how the custom <ProtectedRoute> component extracts and evaluates roles:
import { useEffect, useState } from 'react';
import { useAuthContext } from '@asgardeo/auth-react';
export default function ProtectedRoute({ children, requiredRole }) {
const { state, getDecodedIDToken, signIn } = useAuthContext();
const [userRoles, setUserRoles] = useState([]);
const [checkingRoles, setCheckingRoles] = useState(true);
useEffect(() => {
let isMounted = true;
async function fetchUserRoles() {
if (!state.isAuthenticated) {
if (!state.isLoading && isMounted) setCheckingRoles(false);
return;
}
try {
const decodedToken = await getDecodedIDToken();
if (isMounted) {
// Normalize role claims from Asgardeo ID token
const extracted = [
...(Array.isArray(decodedToken?.roles) ? decodedToken.roles : decodedToken?.roles ? [decodedToken.roles] : []),
...(Array.isArray(decodedToken?.groups) ? decodedToken.groups : decodedToken?.groups ? [decodedToken.groups] : []),
...(Array.isArray(decodedToken?.['[http://wso2.org/claims/role](http://wso2.org/claims/role)']) ? decodedToken['[http://wso2.org/claims/role](http://wso2.org/claims/role)'] : [])
];
setUserRoles(extracted);
setCheckingRoles(false);
}
} catch {
if (isMounted) setCheckingRoles(false);
}
}
fetchUserRoles();
return () => { isMounted = false; };
}, [state.isAuthenticated, state.isLoading, getDecodedIDToken]);
if (state.isLoading || checkingRoles) {
return <div className="p-10 text-center text-sm">Verifying credentials with Asgardeo...</div>;
}
if (!state.isAuthenticated) {
return (
<div className="p-10 text-center">
<h2 className="text-xl font-bold mb-4">Member Sign-In Required</h2>
<button onClick={() => signIn()} className="px-6 py-2.5 bg-orange-600 text-white rounded-xl">
Sign In Now
</button>
</div>
);
}
const hasAccess = !requiredRole || userRoles.some((r) =>
typeof r === 'string' && r.toLowerCase().includes(requiredRole.toLowerCase())
);
if (!hasAccess) {
return (
<div className="p-10 text-center max-w-md mx-auto">
<h2 className="text-xl font-bold text-red-600">Restricted Access</h2>
<p className="text-sm text-gray-600 mt-2">
This area requires the <strong>{requiredRole}</strong> role.
</p>
</div>
);
}
return children;
}
Key Application Features
With identity and authorization established, the portal delivers:
Dynamic Member Command Center: Tracks upcoming meetings, latest executive board notices, reading goal progress, and live poll statuses.
Fortnightly Polling Station with Vote-Locking: Members vote on literature themes with strict 1-vote-per-user constraints enforced via their Asgardeo userId.
The Chronicler & Gathering Hub: Centralized archive for event recaps, discussion essays, and multi-image galleries.
QR-Based Automated Self Check-In: Admins generate a session QR code from the Admin Panel; members scan it with their phone camera to instantly authenticate through Asgardeo and register verified attendance on their profile.
Digital Member Literary Passport: A gamified personal dashboard showcasing books read from the club's curated catalogue, personal 5-star ratings, and achievement badges.
Executive Governance Suite: Complete CRUD management for events, club notices, and reading lists, paired with a one-click CSV attendance exporter.
Key Takeaways & Lessons Learned
OIDC Streamlines Security: Using Asgardeo decoupled authentication from our database layer, eliminating the need to store passwords or implement custom JWT verification flows.
PKCE is Essential for Modern SPAs: With single-page applications operating in browser environments where client secrets cannot be securely kept, the Authorization Code Flow with PKCE provided robust security against interception attacks.
Asgardeo Branding Studio: Customizing the login experience with official association colors, logos, and typography made authentication feel native rather than an external redirect.
Project Links
GitHub Repository: https://github.com/panchaleehewage/ela-portal.git
Top comments (0)