feat(auth): support shared session tokens and device/node pairing integration
This commit is contained in:
parent
6b85280b3d
commit
01c879c143
@ -74,6 +74,12 @@ yarn dev
|
||||
yarn build
|
||||
```
|
||||
|
||||
## 认证配置 (Authentication Configuration)
|
||||
|
||||
有关如何配置 GitHub 和 Google OIDC 认证的详细步骤,请参阅 [OIDC 认证指南](./docs/integrations/oidc-auth.md)。
|
||||
|
||||
> For detailed steps on configuring GitHub and Google OIDC authentication, please refer to the [OIDC Authentication Guide](./docs/integrations/oidc-auth.md).
|
||||
|
||||
## 开发指南 (Development Guidelines)
|
||||
|
||||
有关详细的编码标准、架构规则和 Agent 特定说明,请参阅 [AGENTS.md](./AGENTS.md)。
|
||||
|
||||
88
docs/integrations/oidc-auth.md
Normal file
88
docs/integrations/oidc-auth.md
Normal file
@ -0,0 +1,88 @@
|
||||
# OIDC Authentication Configuration Guide
|
||||
|
||||
This guide describes how to configure GitHub and Google OIDC authentication for the Cloud Neutral Toolkit.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Access to GitHub Developer Settings.
|
||||
- Access to Google Cloud Console.
|
||||
- Properly configured `accounts.svc.plus` and `console.svc.plus` services.
|
||||
|
||||
---
|
||||
|
||||
## 1. GitHub Configuration
|
||||
|
||||
### 1.1 Create GitHub OAuth App
|
||||
|
||||
1. Log in to GitHub and go to **Settings** > **Developer Settings** > **OAuth Apps**.
|
||||
2. Click **New OAuth App**.
|
||||
3. **Application name**: e.g., `Cloud Neutral Console`
|
||||
4. **Homepage URL**: `https://console.svc.plus` (or your actual console domain)
|
||||
5. **Authorization callback URL**: `https://accounts.svc.plus/api/auth/oauth/callback/github`
|
||||
6. Click **Register application**.
|
||||
7. Copy the **Client ID**.
|
||||
8. Click **Generate a new client secret** and copy the **Client Secret**.
|
||||
|
||||
### 1.2 Configure Environment Variables
|
||||
|
||||
Set the following environment variables for **accounts.svc.plus**:
|
||||
|
||||
```bash
|
||||
GITHUB_CLIENT_ID=your_github_client_id
|
||||
GITHUB_CLIENT_SECRET=your_github_client_secret
|
||||
# Optional: if you want to override the default callback
|
||||
# GITHUB_REDIRECT_URL=https://accounts.svc.plus/api/auth/oauth/callback/github
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Google Configuration
|
||||
|
||||
### 2.1 Create Google OAuth Client ID
|
||||
|
||||
1. Log in to [Google Cloud Console](https://console.cloud.google.com/).
|
||||
2. Select or create a project.
|
||||
3. Go to **APIs & Services** > **Credentials**.
|
||||
4. Click **Create Credentials** > **OAuth client ID**.
|
||||
5. **Application type**: `Web application`.
|
||||
6. **Name**: e.g., `Cloud Neutral Console`.
|
||||
7. **Authorized JavaScript origins**:
|
||||
- `https://console.svc.plus`
|
||||
8. **Authorized redirect URIs**:
|
||||
- `https://accounts.svc.plus/api/auth/oauth/callback/google`
|
||||
9. Click **Create**.
|
||||
10. Copy the **Client ID** and **Client Secret**.
|
||||
|
||||
### 2.2 Configure Environment Variables
|
||||
|
||||
Set the following environment variables for **accounts.svc.plus**:
|
||||
|
||||
```bash
|
||||
GOOGLE_CLIENT_ID=your_google_client_id
|
||||
GOOGLE_CLIENT_SECRET=your_google_client_secret
|
||||
# Optional: if you want to override the default callback
|
||||
# GOOGLE_REDIRECT_URL=https://accounts.svc.plus/api/auth/oauth/callback/google
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. General OIDC Environment Variables
|
||||
|
||||
Ensure these are also set for **accounts.svc.plus**:
|
||||
|
||||
```bash
|
||||
OAUTH_REDIRECT_URL=https://accounts.svc.plus/api/auth/oauth/callback
|
||||
OAUTH_FRONTEND_URL=https://console.svc.plus
|
||||
```
|
||||
|
||||
**Note**: The backend automatically appends `/{provider}` to `OAUTH_REDIRECT_URL` if a provider-specific redirect URL is not provided.
|
||||
|
||||
---
|
||||
|
||||
## 4. Frontend Configuration
|
||||
|
||||
For **console.svc.plus**, ensure the following is set so it knows where to redirect for the initial OAuth step:
|
||||
|
||||
```bash
|
||||
NEXT_PUBLIC_ACCOUNTS_SVC_URL=https://accounts.svc.plus
|
||||
```
|
||||
48
src/app/api/auth/token/exchange/route.ts
Normal file
48
src/app/api/auth/token/exchange/route.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { applySessionCookie, deriveMaxAgeFromExpires } from '@lib/authGateway'
|
||||
import { getAccountServiceApiBaseUrl } from '@server/serviceConfig'
|
||||
|
||||
const ACCOUNT_API_BASE = getAccountServiceApiBaseUrl()
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const payload = await request.json()
|
||||
const { publicToken, userId, email, role } = payload
|
||||
|
||||
if (!publicToken || !userId || !email) {
|
||||
return NextResponse.json({ success: false, error: 'invalid_request' }, { status: 400 })
|
||||
}
|
||||
|
||||
const response = await fetch(`${ACCOUNT_API_BASE}/token/exchange`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
public_token: publicToken,
|
||||
user_id: userId,
|
||||
email,
|
||||
roles: role,
|
||||
}),
|
||||
cache: 'no-store',
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
return NextResponse.json({ success: false, error: errorData.error || 'exchange_failed' }, { status: response.status })
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const { access_token, expires_in } = data
|
||||
|
||||
const result = NextResponse.json({ success: true })
|
||||
// If backend returns expires_in (seconds), use it; otherwise derive from expiresAt if it exists
|
||||
const maxAge = typeof expires_in === 'number' ? expires_in : deriveMaxAgeFromExpires(data.expiresAt)
|
||||
applySessionCookie(result, access_token, maxAge)
|
||||
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error('Token exchange proxy failed', error)
|
||||
return NextResponse.json({ success: false, error: 'internal_error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@ -57,17 +57,41 @@ function shouldUseSecureCookies(): boolean {
|
||||
const secureCookieBase = {
|
||||
httpOnly: true,
|
||||
secure: shouldUseSecureCookies(),
|
||||
sameSite: 'strict' as const,
|
||||
sameSite: 'lax' as const, // Change to lax to support cross-subdomain
|
||||
path: '/',
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the cookie domain based on the current environment.
|
||||
* If running on a .svc.plus subdomain, returns '.svc.plus' to allow SSO.
|
||||
*/
|
||||
function resolveCookieDomain(): string | undefined {
|
||||
if (typeof window !== 'undefined') {
|
||||
const host = window.location.hostname
|
||||
if (host.endsWith('.svc.plus')) {
|
||||
return '.svc.plus'
|
||||
}
|
||||
}
|
||||
|
||||
// For server-side, check headers or environment
|
||||
const baseUrl = process.env.NEXT_PUBLIC_APP_BASE_URL || process.env.APP_BASE_URL || ''
|
||||
if (baseUrl.includes('.svc.plus')) {
|
||||
return '.svc.plus'
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function applySessionCookie(response: NextResponse, token: string, maxAge?: number) {
|
||||
const resolvedMaxAge = Number.isFinite(maxAge) && maxAge && maxAge > 0 ? Math.floor(maxAge) : SESSION_DEFAULT_MAX_AGE
|
||||
const domain = resolveCookieDomain()
|
||||
|
||||
response.cookies.set({
|
||||
name: SESSION_COOKIE_NAME,
|
||||
value: token,
|
||||
...secureCookieBase,
|
||||
maxAge: resolvedMaxAge,
|
||||
...(domain ? { domain } : {}),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user