Design document for integrating Vben Admin with Fengling.AuthService using OAuth2 Authorization Code flow with PKCE. Key features: - OAuth2 Authorization Code flow with PKCE - JWT token management with automatic refresh - Single Logout (SLO) support - Multi-tenant and RBAC support via claims - Full error handling and security considerations
9.2 KiB
OAuth2 Integration Design: Vben Admin + Fengling.AuthService
Date: 2026-02-06 Status: Approved Author: Design Session
Overview
Integrate Vben Admin 5.0 with Fengling.AuthService using OAuth2 Authorization Code flow with PKCE. The auth service provides centralized authentication with JWT token issuance, multi-tenant support, and role-based access control.
Authentication Flow
OAuth2 Authorization Code with PKCE
-
Authorization Request: Redirect user to auth service
http://auth-service:5000/connect/authorize? client_id=vben-web redirect_uri=http://vben-app:5173/auth/callback response_type=code scope=api offline_access openid profile email roles code_challenge={generated from code_verifier} code_challenge_method=S256 state={random string} -
Authentication: User logs in on auth service's login page
- Auth service handles authentication via ASP.NET Core Identity
- Login page:
/account/login
-
Authorization Code: Auth service redirects back with code
http://vben-app:5173/auth/callback?code=...&state=... -
Token Exchange: Exchange code for tokens
POST /connect/token grant_type=authorization_code code={auth_code} redirect_uri=http://vben-app:5173/auth/callback client_id=vben-web code_verifier={random string} -
Store Tokens: Receive and store token response
{ "access_token": "jwt_token", "refresh_token": "refresh_token", "token_type": "Bearer", "expires_in": 3600, "scope": "api offline_access ..." }
Token Management
JWT Token Handling
Token Storage (localStorage):
interface TokenStorage {
accessToken: string;
refreshToken: string;
expiresAt: number;
}
Authorization Headers:
Authorization: Bearer {access_token}
Automatic Token Refresh:
- Trigger: 401 response OR 5 min before expiry
- Exchange refresh token for new access token
- Retry failed request with new token
- If refresh fails: redirect to login
Token Expiry Check:
const isTokenExpired = (expiresAt: number) =>
Date.now() >= expiresAt - 5 * 60 * 1000; // 5 min buffer
User Claims from JWT:
interface UserClaims {
sub: string; // user ID
name: string; // username
email: string;
role: string[];
tenant_id: string;
}
Frontend Changes
File Modifications
-
New OAuth2 Service (
#/api/core/oauth.ts):generateCodeVerifier(): Generate random code verifierredirectToAuth(): Initiate OAuth2 flowexchangeCodeForToken(code, state): Exchange code for tokensrefreshAccessToken(): Refresh expired access token
-
Update Auth Store (
#/store/auth.ts):- Replace
authLogin()withhandleAuthCallback(code, state) - Store both
accessTokenandrefreshToken - Parse JWT claims to extract user info locally
- Remove
loginApi()- no longer needed
- Replace
-
New Callback Page (
#/views/_core/authentication/callback.vue):- Parse URL params:
codeandstate - Validate state matches stored value
- Call
exchangeCodeForToken() - Extract user info from JWT
- Redirect to home or return_url
- Handle OAuth2 errors (access_denied, invalid_request)
- Parse URL params:
-
Update Login Page (
#/views/_core/authentication/login.vue):- Remove form fields (username, password, captcha)
- Replace with "Login with Fengling Auth" button
- Call
redirectToAuth()on click
-
Update Request Interceptor (
#/api/request.ts):- Handle
access_tokenin OAuth2 format - Implement automatic refresh on 401
- Store
refresh_token
- Handle
-
Update Auth API (
#/api/core/auth.ts):- Remove
loginApi()- replaced by OAuth2 flow - Update
logoutApi()to call/connect/revocation - Update
refreshTokenApi()for OAuth2 format
- Remove
-
Update User API (
#/api/core/user.ts):- Remove or mock
getUserInfoApi()- use JWT claims instead - Optionally fetch additional user details if needed
- Remove or mock
Route Configuration
New Route (#/router/routes/core.ts):
{
path: '/auth/callback',
name: 'OAuthCallback',
component: () => import('#/views/_core/authentication/callback.vue'),
meta: {
title: 'OAuth Callback',
hideInMenu: true,
ignoreAuth: true,
}
}
Logout Flow (Single Logout)
- Revoke Token: POST to
/connect/revocationwith refresh token - Clear Local State: Reset all stores, clear tokens
- Redirect to Auth Service Logout:
http://auth-service:5000/connect/logout? post_logout_redirect_uri=http://vben-app:5173
Logout API:
export async function logoutApi(refreshToken: string) {
return requestClient.post('/connect/revocation', {
token: refreshToken,
token_type_hint: 'refresh_token',
client_id: import.meta.env.VITE_OAUTH_CLIENT_ID
}, { withCredentials: true });
}
Auth Store Logout:
async function logout(redirect: boolean = true) {
const refreshToken = accessStore.refreshToken;
if (refreshToken) {
await logoutApi(refreshToken);
}
resetAllStores();
const logoutUrl = `${oauthConfig.authUrl}/connect/logout?` +
`post_logout_redirect_uri=${encodeURIComponent(oauthConfig.redirectUri)}`;
window.location.href = logoutUrl;
}
Environment Configuration
.env.development:
VITE_OAUTH_CLIENT_ID=vben-web
VITE_OAUTH_REDIRECT_URI=http://localhost:5173/auth/callback
VITE_AUTH_SERVICE_URL=http://localhost:5000
VITE_OAUTH_SCOPE=api offline_access openid profile email roles
.env.production:
VITE_OAUTH_CLIENT_ID=vben-web
VITE_OAUTH_REDIRECT_URI=https://your-app.com/auth/callback
VITE_AUTH_SERVICE_URL=https://auth.your-domain.com
VITE_OAUTH_SCOPE=api offline_access openid profile email roles
OAuth Config (#/config/oauth.ts):
export const oauthConfig = {
clientId: import.meta.env.VITE_OAUTH_CLIENT_ID,
redirectUri: import.meta.env.VITE_OAUTH_REDIRECT_URI,
authUrl: import.meta.env.VITE_AUTH_SERVICE_URL,
scope: import.meta.env.VITE_OAUTH_SCOPE,
endpoints: {
authorize: '/connect/authorize',
token: '/connect/token',
logout: '/connect/logout',
revocation: '/connect/revocation',
}
};
Auth Service Configuration
Register Vben Client (in SeedData or manual):
new OAuthApplication
{
ClientId = "vben-web",
ClientSecret = null,
DisplayName = "Vben Admin",
RedirectUris = new[] { "http://localhost:5173/auth/callback" },
PostLogoutRedirectUris = new[] { "http://localhost:5173" },
Permissions = new[]
{
OpenIddictConstants.Permissions.Endpoints.Authorization,
OpenIddictConstants.Permissions.Endpoints.Token,
OpenIddictConstants.Permissions.Endpoints.Logout,
OpenIddictConstants.Permissions.Endpoints.Revocation,
OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode,
OpenIddictConstants.Permissions.ResponseTypes.Code,
OpenIddictConstants.Permissions.Scopes.Profile,
OpenIddictConstants.Permissions.Scopes.Email,
OpenIddictConstants.Permissions.Scopes.Roles,
OpenIddictConstants.Permissions.Prefixes.Scope + "api"
}
}
Error Handling
OAuth2 Errors in Callback:
access_denied: User denied authorizationinvalid_request: Malformed requestunauthorized_client: Client not registeredunsupported_response_type: Invalid response_typeinvalid_scope: Invalid scopeserver_error: Auth service errortemporarily_unavailable: Auth service unavailable
Token Refresh Errors:
- 400/401 from
/connect/token: Refresh token invalid/expired - Redirect to login with error message
Security Considerations
- PKCE: Use code verifier and code challenge for public client security
- State Parameter: Prevent CSRF by validating state
- HTTPS: Required for production
- Token Storage: Consider using httpOnly cookies for additional security
- Token Expiry: Short-lived access tokens (1 hour), long-lived refresh tokens (30 days)
- Scope Limitation: Request minimum required scopes
Implementation Checklist
- Create
#/api/core/oauth.tsservice - Update
#/store/auth.tsfor OAuth2 flow - Create
#/views/_core/authentication/callback.vue - Update
#/views/_core/authentication/login.vue - Update
#/api/request.tsfor OAuth2 tokens - Update
#/api/core/auth.tsfor logout - Add OAuth callback route
- Create
#/config/oauth.tsconfiguration - Update environment files
- Register Vben client in auth service
- Test login flow
- Test token refresh
- Test logout (SLO)
- Test error handling
Migration Notes
- Existing mock users (vben, admin, jack) will no longer work
- Use auth service users (admin/Admin@123, testuser/Test@123)
- Existing API endpoints must accept JWT Bearer tokens
- Any custom user info fetching must be updated
- Remove any local user registration - now handled by auth service
References
- OAuth 2.1 RFC: https://datatracker.ietf.org/doc/html/rfc6749
- PKCE RFC: https://datatracker.ietf.org/doc/html/rfc7636
- OpenIddict Documentation: https://documentation.openiddict.com/
- Vben Admin Documentation: https://doc.vben.pro/