diff --git a/docs/plans/2026-02-06-oauth2-integration-design.md b/docs/plans/2026-02-06-oauth2-integration-design.md new file mode 100644 index 0000000..031ba36 --- /dev/null +++ b/docs/plans/2026-02-06-oauth2-integration-design.md @@ -0,0 +1,308 @@ +# 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/