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

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

  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

    {
      "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

  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):

{
  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:

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