# 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 1. **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} ``` 2. **Authentication**: User logs in on auth service's login page - Auth service handles authentication via ASP.NET Core Identity - Login page: `/account/login` 3. **Authorization Code**: Auth service redirects back with code ``` http://vben-app:5173/auth/callback?code=...&state=... ``` 4. **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} ``` 5. **Store Tokens**: Receive and store token response ```json { "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): ```typescript 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**: ```typescript const isTokenExpired = (expiresAt: number) => Date.now() >= expiresAt - 5 * 60 * 1000; // 5 min buffer ``` **User Claims from JWT**: ```typescript interface UserClaims { sub: string; // user ID name: string; // username email: string; role: string[]; tenant_id: string; } ``` ## Frontend Changes ### File Modifications 1. **New OAuth2 Service** (`#/api/core/oauth.ts`): - `generateCodeVerifier()`: Generate random code verifier - `redirectToAuth()`: Initiate OAuth2 flow - `exchangeCodeForToken(code, state)`: Exchange code for tokens - `refreshAccessToken()`: Refresh expired access token 2. **Update Auth Store** (`#/store/auth.ts`): - Replace `authLogin()` with `handleAuthCallback(code, state)` - Store both `accessToken` and `refreshToken` - Parse JWT claims to extract user info locally - Remove `loginApi()` - no longer needed 3. **New Callback Page** (`#/views/_core/authentication/callback.vue`): - Parse URL params: `code` and `state` - 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) 4. **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 5. **Update Request Interceptor** (`#/api/request.ts`): - Handle `access_token` in OAuth2 format - Implement automatic refresh on 401 - Store `refresh_token` 6. **Update Auth API** (`#/api/core/auth.ts`): - Remove `loginApi()` - replaced by OAuth2 flow - Update `logoutApi()` to call `/connect/revocation` - Update `refreshTokenApi()` for OAuth2 format 7. **Update User API** (`#/api/core/user.ts`): - Remove or mock `getUserInfoApi()` - use JWT claims instead - Optionally fetch additional user details if needed ### Route Configuration **New Route** (`#/router/routes/core.ts`): ```typescript { path: '/auth/callback', name: 'OAuthCallback', component: () => import('#/views/_core/authentication/callback.vue'), meta: { title: 'OAuth Callback', hideInMenu: true, ignoreAuth: true, } } ``` ## Logout Flow (Single Logout) 1. **Revoke Token**: POST to `/connect/revocation` with refresh token 2. **Clear Local State**: Reset all stores, clear tokens 3. **Redirect to Auth Service Logout**: ``` http://auth-service:5000/connect/logout? post_logout_redirect_uri=http://vben-app:5173 ``` **Logout API**: ```typescript 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**: ```typescript 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**: ```bash 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**: ```bash 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`): ```typescript 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): ```csharp 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 authorization - `invalid_request`: Malformed request - `unauthorized_client`: Client not registered - `unsupported_response_type`: Invalid response_type - `invalid_scope`: Invalid scope - `server_error`: Auth service error - `temporarily_unavailable`: Auth service unavailable **Token Refresh Errors**: - 400/401 from `/connect/token`: Refresh token invalid/expired - Redirect to login with error message ## Security Considerations 1. **PKCE**: Use code verifier and code challenge for public client security 2. **State Parameter**: Prevent CSRF by validating state 3. **HTTPS**: Required for production 4. **Token Storage**: Consider using httpOnly cookies for additional security 5. **Token Expiry**: Short-lived access tokens (1 hour), long-lived refresh tokens (30 days) 6. **Scope Limitation**: Request minimum required scopes ## Implementation Checklist - [ ] Create `#/api/core/oauth.ts` service - [ ] Update `#/store/auth.ts` for OAuth2 flow - [ ] Create `#/views/_core/authentication/callback.vue` - [ ] Update `#/views/_core/authentication/login.vue` - [ ] Update `#/api/request.ts` for OAuth2 tokens - [ ] Update `#/api/core/auth.ts` for logout - [ ] Add OAuth callback route - [ ] Create `#/config/oauth.ts` configuration - [ ] 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/