fengling-console-web/docs/plans/2026-02-06-oauth2-integration-design.md
Sam c803c2a633 docs: add OAuth2 integration design for Fengling.AuthService
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
2026-02-06 01:24:18 +08:00

309 lines
9.2 KiB
Markdown

# 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/