Portal Community

Integration Direction

WorkDesk consumes Passport — Passport is the authority for identity. WorkDesk redirects unauthenticated users to Passport's login page, receives the JWT on successful login, and attaches it to every subsequent API call.

Login Flow

1

User Navigates to WorkDesk

Browser loads the WorkDesk React SPA. authStore.ts checks for an existing valid JWT in memory (or secure cookie). If none exists, the user is not authenticated.

2

Redirect to Passport

WorkDesk redirects to https://passport.bizfirstai.com/login?return={encodedWorkDeskUrl}. Passport handles the login UI — LDAP, SSO, MFA, social login, etc.

3

Passport Issues JWT

On successful authentication, Passport issues a signed JWT containing the user's claims and redirects back to WorkDesk with the token.

4

Token Stored in authStore

WorkDesk stores the JWT in authStore (Zustand). The Axios interceptor picks it up immediately and attaches it to all subsequent API calls as Authorization: Bearer {token}.

5

User Reaches WorkDesk

WorkDesk loads the inbox, notifications, and dashboard — all API calls are automatically authenticated. The original URL the user requested is restored from the return parameter.

JWT Claims Used by WorkDesk

WorkDesk reads the following claims from the Passport JWT to scope all queries and enforce data isolation:

sub
user-guid — the actorId used in all WorkDesk queries
tid
tenant-guid — tenantId; scopes all data to the user's organization
email
user@example.com — displayed in WorkDesk profile header
name
Full Name — displayed in avatar and greeting
roles
["employee", "manager"] — determines WorkDesk navigation items and admin access
locale
en-US — drives FormRenderer field formatting and date display
exp
Unix timestamp — token expiry; triggers silent refresh before deadline
iss
https://passport.bizfirstai.com — validated by WorkDesk backend on every request

Axios Interceptor — Auth Token Attachment

WorkDesk attaches the JWT to every outbound HTTP request through a central Axios request interceptor. This means no individual API call in WorkDesk ever needs to manually set the Authorization header:

// authInterceptor.ts — attaches JWT to all WorkDesk API calls
import axios from 'axios';
import { authStore } from './authStore';
import { refreshToken } from './passportClient';

// Request interceptor — attach token
axios.interceptors.request.use(async (config) => {
  let token = authStore.getState().token;

  // Proactively refresh if token expires within 60 seconds
  const exp = authStore.getState().expiresAt;
  if (exp && Date.now() / 1000 > exp - 60) {
    token = await refreshToken();
    authStore.getState().setToken(token);
  }

  if (token) {
    config.headers['Authorization'] = `Bearer ${token}`;
  }
  return config;
});

// Response interceptor — handle 401 (token expired or invalid)
axios.interceptors.response.use(
  response => response,
  async (error) => {
    if (error.response?.status === 401) {
      // Token is invalid — redirect to Passport login
      authStore.getState().clearToken();
      window.location.href = `/login?return=${encodeURIComponent(window.location.pathname)}`;
    }
    return Promise.reject(error);
  }
);

authStore — Identity State

The authStore.ts Zustand store is the single source of truth for identity state in WorkDesk:

// authStore.ts — Zustand identity store
interface AuthState {
  token:      string | null;
  userId:     string | null;   // = JWT sub claim
  tenantId:   string | null;   // = JWT tid claim
  name:       string | null;
  email:      string | null;
  roles:      string[];
  locale:     string;
  expiresAt:  number | null;   // Unix timestamp

  setToken: (token: string) => void;
  clearToken: () => void;
}

export const useAuthStore = create<AuthState>((set) => ({
  token:     null,
  userId:    null,
  tenantId:  null,
  name:      null,
  email:     null,
  roles:     [],
  locale:    'en-US',
  expiresAt: null,

  setToken: (token) => {
    const claims = parseJwt(token); // decode without verifying — verification is server-side
    set({
      token,
      userId:    claims.sub,
      tenantId:  claims.tid,
      name:      claims.name,
      email:     claims.email,
      roles:     claims.roles ?? [],
      locale:    claims.locale ?? 'en-US',
      expiresAt: claims.exp,
    });
  },

  clearToken: () => set({ token: null, userId: null, tenantId: null }),
}));

Server-Side Token Validation

WorkDesk's ASP.NET Core backend validates the Passport JWT on every request. Client-side parsing is only for reading claims — the backend is the security boundary:

// WorkDesk backend — JWT validation configuration (Program.cs)
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
  .AddJwtBearer(options =>
  {
      options.Authority = "https://passport.bizfirstai.com";
      options.TokenValidationParameters = new TokenValidationParameters
      {
          ValidateIssuer           = true,
          ValidIssuer              = "https://passport.bizfirstai.com",
          ValidateAudience         = true,
          ValidAudience            = "workdesk-api",
          ValidateLifetime         = true,
          ClockSkew                = TimeSpan.FromSeconds(30),
          ValidateIssuerSigningKey = true,
          // Signing key fetched from Passport JWKS endpoint automatically
      };
  });

// All WorkDesk controllers require authentication by default
builder.Services.AddAuthorization(options =>
{
    options.FallbackPolicy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .Build();
});

Token Refresh Strategy

ScenarioBehavior
Token expires within 60 secondsAxios request interceptor proactively calls Passport refresh endpoint before sending the request
Refresh token itself is expiredUser is redirected to Passport login page; current URL saved as return parameter
401 response from WorkDesk APIImmediate redirect to login — no retry (token is invalid, not just expired)
Multiple concurrent requests during refreshAll requests are queued; token is refreshed once; all queued requests proceed with new token
User is idle for extended periodToken expires naturally; next API call triggers 401 → login redirect

Tenant Isolation Enforcement

The tenantId claim from the JWT is extracted server-side and applied as a mandatory filter condition on every database query WorkDesk executes. This is not optional — it is enforced at the repository layer:

// WorkDeskTaskRepository.cs — tenant isolation
public async Task<IReadOnlyList<WorkDeskTask>> GetPendingTasksAsync(
    Guid actorId,
    Guid tenantId,   // always passed from JWT — never from request body
    CancellationToken ct)
{
    return await _db.WorkDeskTasks
        .Where(t => t.TenantId == tenantId   // enforced at repo layer
                 && t.ActorId  == actorId
                 && t.Status   == TaskStatus.Pending)
        .OrderBy(t => t.DueAt)
        .ToListAsync(ct);
}
tenantId is Never Trusted from the Client

WorkDesk backend never reads tenantId from the request body, query string, or headers provided by the client. It is always extracted exclusively from the validated JWT claims. This ensures tenant isolation cannot be bypassed by a malicious or misconfigured client.