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
309 lines
9.2 KiB
Markdown
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/
|