DEV Community

Panchalee Hewage
Panchalee Hewage

Posted on

Building a Role-Governed Student Association Portal with WSO2 Asgardeo, React, and Cloud Firestore

ELA Portal Home Page
Login Page
User Dashboard
Admin DashboardStudent 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:

  1. Identity & Authentication (CIAM): WSO2 Asgardeo via @asgardeo/auth-react handling OpenID Connect (OIDC) Authorization Code Flow with PKCE.
  2. Frontend Client: React 18, Vite, Tailwind CSS (custom brand tokens for ELA), and Lucide icons.
  3. 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>
);
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

Key Application Features

With identity and authorization established, the portal delivers:

  1. Dynamic Member Command Center: Tracks upcoming meetings, latest executive board notices, reading goal progress, and live poll statuses.

  2. Fortnightly Polling Station with Vote-Locking: Members vote on literature themes with strict 1-vote-per-user constraints enforced via their Asgardeo userId.

  3. The Chronicler & Gathering Hub: Centralized archive for event recaps, discussion essays, and multi-image galleries.

  4. 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.

  5. Digital Member Literary Passport: A gamified personal dashboard showcasing books read from the club's curated catalogue, personal 5-star ratings, and achievement badges.

  6. 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)