Passport Integration
Passport is BizFirstGO's SSO and identity platform. WorkDesk delegates all authentication and authorization to Passport — it never stores passwords, manages sessions locally, or implements its own login UI. Every API call WorkDesk makes carries a Passport-issued JWT bearer token.
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
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.
Redirect to Passport
WorkDesk redirects to https://passport.bizfirstai.com/login?return={encodedWorkDeskUrl}. Passport handles the login UI — LDAP, SSO, MFA, social login, etc.
Passport Issues JWT
On successful authentication, Passport issues a signed JWT containing the user's claims and redirects back to WorkDesk with the token.
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}.
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:
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
| Scenario | Behavior |
|---|---|
| Token expires within 60 seconds | Axios request interceptor proactively calls Passport refresh endpoint before sending the request |
| Refresh token itself is expired | User is redirected to Passport login page; current URL saved as return parameter |
| 401 response from WorkDesk API | Immediate redirect to login — no retry (token is invalid, not just expired) |
| Multiple concurrent requests during refresh | All requests are queued; token is refreshed once; all queued requests proceed with new token |
| User is idle for extended period | Token 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);
}
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.