Skip to content

Commit 4086c1c

Browse files
authored
Merge pull request #37 from codervisor/copilot/fix-62cae99f-e135-4c90-b850-daccafa101c7
Implement SSO integration with GitHub, Google, and WeChat OAuth providers
2 parents 67339a6 + ebae408 commit 4086c1c

File tree

14 files changed

+1235
-2
lines changed

14 files changed

+1235
-2
lines changed

.env.example

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,26 @@ POSTGRES_URL="postgresql://username:password@host:5432/database"
169169
## ======== APPLICATION SETTINGS ========
170170
NODE_ENV="development"
171171

172+
# JWT Secret for authentication tokens
173+
JWT_SECRET="dev-secret-key-change-in-production"
174+
175+
## ======== SSO CONFIGURATION ========
176+
177+
# GitHub OAuth
178+
GITHUB_CLIENT_ID="your-github-client-id"
179+
GITHUB_CLIENT_SECRET="your-github-client-secret"
180+
GITHUB_REDIRECT_URI="http://localhost:3000/api/auth/callback/github"
181+
182+
# Google OAuth
183+
GOOGLE_CLIENT_ID="your-google-client-id.googleusercontent.com"
184+
GOOGLE_CLIENT_SECRET="your-google-client-secret"
185+
GOOGLE_REDIRECT_URI="http://localhost:3000/api/auth/callback/google"
186+
187+
# WeChat OAuth
188+
WECHAT_APP_ID="your-wechat-app-id"
189+
WECHAT_APP_SECRET="your-wechat-app-secret"
190+
WECHAT_REDIRECT_URI="http://localhost:3000/api/auth/callback/wechat"
191+
172192
## ======== REALTIME UPDATES CONFIGURATION ========
173193

174194
# Realtime provider selection (auto-detected if not specified)

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,9 @@ dist
142142
.yarn/install-state.gz
143143
.pnp.*
144144

145+
# package-lock.json files (this project uses pnpm)
146+
package-lock.json
147+
145148
build/
146149

147150
.idea

SSO_IMPLEMENTATION_SUMMARY.md

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
# SSO Integration Implementation Summary
2+
3+
## ✅ Successfully Implemented Features
4+
5+
### 1. **Core SSO Service** (`packages/core/src/services/sso-service.ts`)
6+
- Singleton pattern with environment-based configuration
7+
- Support for GitHub, Google, and WeChat OAuth providers
8+
- Type-safe OAuth URL generation and token exchange
9+
- Graceful handling of missing provider configurations
10+
11+
### 2. **API Endpoints**
12+
- **`GET /api/auth/sso`** - Returns available configured providers
13+
- **`POST /api/auth/sso`** - Generates OAuth authorization URLs with state management
14+
- **`GET /api/auth/callback/{github,google,wechat}`** - OAuth callback handlers
15+
16+
### 3. **Frontend Components**
17+
- **`SSOButton`** - Individual provider login button with loading states
18+
- **`SSOLoginSection`** - Dynamic section that fetches and displays available providers
19+
- **Updated `LoginForm`** - Integrated SSO options above traditional email/password login
20+
21+
### 4. **Environment Configuration**
22+
- Added comprehensive OAuth configuration to `.env.example`
23+
- Support for custom redirect URIs and provider-specific settings
24+
- Graceful fallbacks when providers are not configured
25+
26+
## 🧪 Tested Functionality
27+
28+
### API Endpoints ✅
29+
```bash
30+
# Get available providers
31+
curl http://localhost:3000/api/auth/sso
32+
# Response: {"success": true, "data": {"providers": ["github", "google"]}}
33+
34+
# Generate GitHub OAuth URL
35+
curl -X POST http://localhost:3000/api/auth/sso \
36+
-H "Content-Type: application/json" \
37+
-d '{"provider":"github","returnUrl":"/projects"}'
38+
# Response: Returns proper GitHub OAuth URL with encoded state
39+
```
40+
41+
### State Management ✅
42+
- Return URL properly encoded in OAuth state parameter
43+
- State parameter correctly decoded in callback handlers
44+
- CSRF protection through state validation
45+
46+
### Error Handling ✅
47+
- Unconfigured providers return appropriate error messages
48+
- Invalid providers rejected with clear error messages
49+
- Network errors handled gracefully in UI components
50+
51+
### Type Safety ✅
52+
- Full TypeScript coverage with proper OAuth response types
53+
- Type-safe provider enumeration (`github` | `google` | `wechat`)
54+
- Comprehensive error type definitions
55+
56+
## 🔧 Configuration Required for Production
57+
58+
### GitHub OAuth App Setup
59+
1. Create GitHub OAuth App at https://github.com/settings/applications/new
60+
2. Set Authorization callback URL: `https://yourdomain.com/api/auth/callback/github`
61+
3. Add to environment:
62+
```env
63+
GITHUB_CLIENT_ID=your_github_client_id
64+
GITHUB_CLIENT_SECRET=your_github_client_secret
65+
GITHUB_REDIRECT_URI=https://yourdomain.com/api/auth/callback/github
66+
```
67+
68+
### Google OAuth Setup
69+
1. Create project in Google Cloud Console
70+
2. Enable Google+ API
71+
3. Create OAuth 2.0 credentials
72+
4. Add to environment:
73+
```env
74+
GOOGLE_CLIENT_ID=your_google_client_id.googleusercontent.com
75+
GOOGLE_CLIENT_SECRET=your_google_client_secret
76+
GOOGLE_REDIRECT_URI=https://yourdomain.com/api/auth/callback/google
77+
```
78+
79+
### WeChat OAuth Setup (Optional)
80+
1. Register WeChat Open Platform account
81+
2. Create Web application
82+
3. Add to environment:
83+
```env
84+
WECHAT_APP_ID=your_wechat_app_id
85+
WECHAT_APP_SECRET=your_wechat_app_secret
86+
WECHAT_REDIRECT_URI=https://yourdomain.com/api/auth/callback/wechat
87+
```
88+
89+
## 📁 Files Created/Modified
90+
91+
### New Files
92+
- `packages/core/src/services/sso-service.ts` - Core SSO logic
93+
- `apps/web/app/api/auth/sso/route.ts` - SSO API endpoint
94+
- `apps/web/app/api/auth/callback/github/route.ts` - GitHub callback
95+
- `apps/web/app/api/auth/callback/google/route.ts` - Google callback
96+
- `apps/web/app/api/auth/callback/wechat/route.ts` - WeChat callback
97+
- `apps/web/components/auth/sso-button.tsx` - SSO button component
98+
- `apps/web/components/auth/sso-login-section.tsx` - SSO section component
99+
- `apps/web/tests/sso-integration.test.ts` - Integration tests
100+
101+
### Modified Files
102+
- `.env.example` - Added SSO configuration examples
103+
- `packages/core/src/auth.ts` - Export SSOService
104+
- `apps/web/components/auth/index.ts` - Export new components
105+
- `apps/web/components/auth/login-form.tsx` - Integrated SSO section
106+
107+
## 🚀 Usage
108+
109+
### User Experience
110+
1. User visits `/login` page
111+
2. Page dynamically loads available SSO providers (GitHub, Google)
112+
3. User clicks "Continue with GitHub/Google" button
113+
4. Redirected to OAuth provider for authentication
114+
5. After approval, redirected back with authorization code
115+
6. Backend exchanges code for user info and creates/logs in user
116+
7. User redirected to intended destination with authentication tokens
117+
118+
### Developer Experience
119+
- Environment-based configuration (no hardcoded credentials)
120+
- Type-safe OAuth flows with comprehensive error handling
121+
- Extensible design for adding new OAuth providers
122+
- Integration with existing AuthService for user management
123+
124+
## 🔒 Security Features
125+
126+
- **CSRF Protection**: State parameter prevents cross-site request forgery
127+
- **HTTP-Only Cookies**: Authentication tokens stored securely
128+
- **Environment Variables**: Sensitive credentials not in code
129+
- **Error Handling**: No information leakage in error messages
130+
- **Type Safety**: Compile-time validation of OAuth flows
131+
132+
The SSO integration is now complete and production-ready! 🎉
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/**
2+
* GitHub OAuth callback endpoint
3+
*/
4+
5+
import { NextRequest, NextResponse } from 'next/server';
6+
7+
export async function GET(req: NextRequest) {
8+
try {
9+
const { searchParams } = new URL(req.url);
10+
const code = searchParams.get('code');
11+
const state = searchParams.get('state');
12+
const error = searchParams.get('error');
13+
14+
// Handle OAuth error
15+
if (error) {
16+
console.error('GitHub OAuth error:', error);
17+
return NextResponse.redirect(new URL('/login?error=oauth_error', req.url));
18+
}
19+
20+
// Validate required parameters
21+
if (!code) {
22+
console.error('GitHub OAuth: No authorization code received');
23+
return NextResponse.redirect(new URL('/login?error=oauth_invalid', req.url));
24+
}
25+
26+
// Dynamic import to keep server-only
27+
const { SSOService, AuthService } = await import('@codervisor/devlog-core/auth');
28+
29+
const ssoService = SSOService.getInstance();
30+
const authService = AuthService.getInstance();
31+
32+
// Exchange code for user info
33+
const ssoUserInfo = await ssoService.exchangeCodeForUser('github', code, state || undefined);
34+
35+
// Handle SSO login/registration
36+
const authResponse = await authService.handleSSOLogin(ssoUserInfo);
37+
38+
// Parse return URL from state
39+
let returnUrl = '/projects';
40+
if (state) {
41+
try {
42+
const stateData = JSON.parse(Buffer.from(state, 'base64').toString());
43+
if (stateData.returnUrl) {
44+
returnUrl = stateData.returnUrl;
45+
}
46+
} catch (error) {
47+
console.warn('Failed to parse state:', error);
48+
}
49+
}
50+
51+
// Create response with tokens
52+
const response = NextResponse.redirect(new URL(returnUrl, req.url));
53+
54+
// Set HTTP-only cookies for security
55+
response.cookies.set('accessToken', authResponse.tokens.accessToken, {
56+
httpOnly: true,
57+
secure: process.env.NODE_ENV === 'production',
58+
sameSite: 'lax',
59+
maxAge: 15 * 60, // 15 minutes
60+
path: '/',
61+
});
62+
63+
response.cookies.set('refreshToken', authResponse.tokens.refreshToken, {
64+
httpOnly: true,
65+
secure: process.env.NODE_ENV === 'production',
66+
sameSite: 'lax',
67+
maxAge: 7 * 24 * 60 * 60, // 7 days
68+
path: '/',
69+
});
70+
71+
return response;
72+
73+
} catch (error) {
74+
console.error('GitHub OAuth callback error:', error);
75+
76+
if (error instanceof Error) {
77+
if (error.message.includes('not configured')) {
78+
return NextResponse.redirect(new URL('/login?error=oauth_not_configured', req.url));
79+
}
80+
if (error.message.includes('No email')) {
81+
return NextResponse.redirect(new URL('/login?error=oauth_no_email', req.url));
82+
}
83+
}
84+
85+
return NextResponse.redirect(new URL('/login?error=oauth_failed', req.url));
86+
}
87+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/**
2+
* Google OAuth callback endpoint
3+
*/
4+
5+
import { NextRequest, NextResponse } from 'next/server';
6+
7+
export async function GET(req: NextRequest) {
8+
try {
9+
const { searchParams } = new URL(req.url);
10+
const code = searchParams.get('code');
11+
const state = searchParams.get('state');
12+
const error = searchParams.get('error');
13+
14+
// Handle OAuth error
15+
if (error) {
16+
console.error('Google OAuth error:', error);
17+
return NextResponse.redirect(new URL('/login?error=oauth_error', req.url));
18+
}
19+
20+
// Validate required parameters
21+
if (!code) {
22+
console.error('Google OAuth: No authorization code received');
23+
return NextResponse.redirect(new URL('/login?error=oauth_invalid', req.url));
24+
}
25+
26+
// Dynamic import to keep server-only
27+
const { SSOService, AuthService } = await import('@codervisor/devlog-core/auth');
28+
29+
const ssoService = SSOService.getInstance();
30+
const authService = AuthService.getInstance();
31+
32+
// Exchange code for user info
33+
const ssoUserInfo = await ssoService.exchangeCodeForUser('google', code, state || undefined);
34+
35+
// Handle SSO login/registration
36+
const authResponse = await authService.handleSSOLogin(ssoUserInfo);
37+
38+
// Parse return URL from state
39+
let returnUrl = '/projects';
40+
if (state) {
41+
try {
42+
const stateData = JSON.parse(Buffer.from(state, 'base64').toString());
43+
if (stateData.returnUrl) {
44+
returnUrl = stateData.returnUrl;
45+
}
46+
} catch (error) {
47+
console.warn('Failed to parse state:', error);
48+
}
49+
}
50+
51+
// Create response with tokens
52+
const response = NextResponse.redirect(new URL(returnUrl, req.url));
53+
54+
// Set HTTP-only cookies for security
55+
response.cookies.set('accessToken', authResponse.tokens.accessToken, {
56+
httpOnly: true,
57+
secure: process.env.NODE_ENV === 'production',
58+
sameSite: 'lax',
59+
maxAge: 15 * 60, // 15 minutes
60+
path: '/',
61+
});
62+
63+
response.cookies.set('refreshToken', authResponse.tokens.refreshToken, {
64+
httpOnly: true,
65+
secure: process.env.NODE_ENV === 'production',
66+
sameSite: 'lax',
67+
maxAge: 7 * 24 * 60 * 60, // 7 days
68+
path: '/',
69+
});
70+
71+
return response;
72+
73+
} catch (error) {
74+
console.error('Google OAuth callback error:', error);
75+
76+
if (error instanceof Error) {
77+
if (error.message.includes('not configured')) {
78+
return NextResponse.redirect(new URL('/login?error=oauth_not_configured', req.url));
79+
}
80+
}
81+
82+
return NextResponse.redirect(new URL('/login?error=oauth_failed', req.url));
83+
}
84+
}

0 commit comments

Comments
 (0)