Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 21e7a1f8ac | |||
| b248ef8bba | |||
| 6c21809a24 |
63
.env.dev
Normal file
63
.env.dev
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
# OAuth 2.0 Authentication System - Development Environment
|
||||||
|
|
||||||
|
# Application Environment
|
||||||
|
ENVIRONMENT=dev
|
||||||
|
|
||||||
|
# Security
|
||||||
|
SECRET_KEY=3bf17c7f-5446-4a18-9cb3-f885eba501e8
|
||||||
|
JWT_ALGORITHM=HS256
|
||||||
|
JWT_ACCESS_TOKEN_EXPIRE_MINUTES=30
|
||||||
|
JWT_REFRESH_TOKEN_EXPIRE_DAYS=7
|
||||||
|
|
||||||
|
# MongoDB Configuration
|
||||||
|
MONGODB_URL=mongodb://admin:admin123@localhost:27017/oauth_db?authSource=admin
|
||||||
|
DATABASE_NAME=oauth_db
|
||||||
|
|
||||||
|
# Redis Configuration
|
||||||
|
REDIS_URL=redis://localhost:6379
|
||||||
|
REDIS_DB=0
|
||||||
|
|
||||||
|
# API Configuration
|
||||||
|
API_HOST=0.0.0.0
|
||||||
|
API_PORT=8000
|
||||||
|
API_PREFIX=/api/v1
|
||||||
|
|
||||||
|
# Frontend Configuration
|
||||||
|
FRONTEND_URL=http://localhost:5173
|
||||||
|
|
||||||
|
# CORS Settings
|
||||||
|
CORS_ORIGINS=["http://localhost:5173", "http://localhost:8000"]
|
||||||
|
CORS_ALLOW_CREDENTIALS=true
|
||||||
|
|
||||||
|
# Rate Limiting
|
||||||
|
RATE_LIMIT_REQUESTS=100
|
||||||
|
RATE_LIMIT_PERIOD=60
|
||||||
|
|
||||||
|
# Backup and Archive Paths
|
||||||
|
BACKUP_PATH=/var/backups/oauth
|
||||||
|
ARCHIVE_PATH=/var/archives/oauth
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
LOG_LEVEL=DEBUG
|
||||||
|
LOG_PATH=/var/log/oauth
|
||||||
|
|
||||||
|
# Email Configuration
|
||||||
|
EMAIL_ENABLED=false
|
||||||
|
SMTP_HOST=smtp.gmail.com
|
||||||
|
SMTP_PORT=587
|
||||||
|
SMTP_USER=
|
||||||
|
SMTP_PASSWORD=
|
||||||
|
EMAIL_FROM=noreply@oauth.local
|
||||||
|
|
||||||
|
# OAuth Client Defaults
|
||||||
|
DEFAULT_CLIENT_ACCESS_TOKEN_EXPIRE_MINUTES=60
|
||||||
|
DEFAULT_CLIENT_REFRESH_TOKEN_EXPIRE_DAYS=30
|
||||||
|
|
||||||
|
# Session Configuration
|
||||||
|
SESSION_SECRET_KEY=5d1cacb8-4d7e-4604-b553-e0251f8fbe7e
|
||||||
|
SESSION_COOKIE_NAME=oauth_session
|
||||||
|
SESSION_EXPIRE_MINUTES=1440
|
||||||
|
|
||||||
|
# Admin Configuration (Development Only)
|
||||||
|
ADMIN_EMAIL=admin@oauth.local
|
||||||
|
ADMIN_PASSWORD=admin123
|
||||||
64
.env.example
Normal file
64
.env.example
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
# OAuth 2.0 Authentication System Environment Variables
|
||||||
|
# Copy this file to .env.dev, .env.vei, or .env.prod and update values
|
||||||
|
|
||||||
|
# Application Environment
|
||||||
|
ENVIRONMENT=dev # dev, vei, prod
|
||||||
|
|
||||||
|
# Security
|
||||||
|
SECRET_KEY=your-secret-key-here-change-in-production
|
||||||
|
JWT_ALGORITHM=HS256
|
||||||
|
JWT_ACCESS_TOKEN_EXPIRE_MINUTES=30
|
||||||
|
JWT_REFRESH_TOKEN_EXPIRE_DAYS=7
|
||||||
|
|
||||||
|
# MongoDB Configuration
|
||||||
|
MONGODB_URL=mongodb://admin:admin123@localhost:27017/oauth_db?authSource=admin
|
||||||
|
DATABASE_NAME=oauth_db
|
||||||
|
|
||||||
|
# Redis Configuration
|
||||||
|
REDIS_URL=redis://localhost:6379
|
||||||
|
REDIS_DB=0
|
||||||
|
|
||||||
|
# API Configuration
|
||||||
|
API_HOST=0.0.0.0
|
||||||
|
API_PORT=8000
|
||||||
|
API_PREFIX=/api/v1
|
||||||
|
|
||||||
|
# Frontend Configuration
|
||||||
|
FRONTEND_URL=http://localhost:5173
|
||||||
|
|
||||||
|
# CORS Settings
|
||||||
|
CORS_ORIGINS=["http://localhost:5173", "http://localhost:9080"]
|
||||||
|
CORS_ALLOW_CREDENTIALS=true
|
||||||
|
|
||||||
|
# Rate Limiting
|
||||||
|
RATE_LIMIT_REQUESTS=100
|
||||||
|
RATE_LIMIT_PERIOD=60 # seconds
|
||||||
|
|
||||||
|
# Backup and Archive Paths
|
||||||
|
BACKUP_PATH=/var/backups/oauth
|
||||||
|
ARCHIVE_PATH=/var/archives/oauth
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
LOG_LEVEL=INFO
|
||||||
|
LOG_PATH=/var/log/oauth
|
||||||
|
|
||||||
|
# Email Configuration (Optional)
|
||||||
|
EMAIL_ENABLED=false
|
||||||
|
SMTP_HOST=smtp.gmail.com
|
||||||
|
SMTP_PORT=587
|
||||||
|
SMTP_USER=
|
||||||
|
SMTP_PASSWORD=
|
||||||
|
EMAIL_FROM=noreply@oauth.local
|
||||||
|
|
||||||
|
# OAuth Client Defaults
|
||||||
|
DEFAULT_CLIENT_ACCESS_TOKEN_EXPIRE_MINUTES=60
|
||||||
|
DEFAULT_CLIENT_REFRESH_TOKEN_EXPIRE_DAYS=30
|
||||||
|
|
||||||
|
# Session Configuration
|
||||||
|
SESSION_SECRET_KEY=session-secret-key-change-in-production
|
||||||
|
SESSION_COOKIE_NAME=oauth_session
|
||||||
|
SESSION_EXPIRE_MINUTES=1440 # 24 hours
|
||||||
|
|
||||||
|
# Admin Configuration
|
||||||
|
ADMIN_EMAIL=admin@oauth.local
|
||||||
|
ADMIN_PASSWORD=admin123 # Change in production!
|
||||||
11
CLAUDE.md
11
CLAUDE.md
@ -1,5 +1,12 @@
|
|||||||
# OAuth 2.0 인증 시스템
|
# OAuth 2.0 인증 시스템
|
||||||
|
|
||||||
|
## 📋 작업 관리 (필독)
|
||||||
|
- **[작업 체크리스트](CLAUDE/task-plan.md)** - 전체 프로젝트 작업 진행 상황 관리
|
||||||
|
- **새 세션 시작 시**: 반드시 `CLAUDE/task-plan.md` 파일을 먼저 읽고 현재 진행 상황 확인
|
||||||
|
- 각 Phase별 세부 작업을 체크리스트로 추적
|
||||||
|
- 완료된 작업은 [x]로 표시
|
||||||
|
- **현재 상태**: Phase 1, 2 완료 / Phase 3 (프론트엔드) 대기중
|
||||||
|
|
||||||
## 문서 작성 규칙
|
## 문서 작성 규칙
|
||||||
- 모든 다이어그램은 Mermaid 문법을 사용하여 작성
|
- 모든 다이어그램은 Mermaid 문법을 사용하여 작성
|
||||||
- 코드 블록은 언어별 하이라이팅 적용
|
- 코드 블록은 언어별 하이라이팅 적용
|
||||||
@ -84,8 +91,6 @@ docker-compose down -v
|
|||||||
```
|
```
|
||||||
|
|
||||||
#### 서비스 접속 URL
|
#### 서비스 접속 URL
|
||||||
- **API Gateway**: http://localhost:9080
|
|
||||||
- **APISIX Dashboard**: http://localhost:9000 (admin/admin123)
|
|
||||||
- **Frontend**: http://localhost:5173
|
- **Frontend**: http://localhost:5173
|
||||||
- **Backend API**: http://localhost:9080/api/v1 (through APISIX)
|
- **Backend API**: http://localhost:9080/api/v1 (through APISIX)
|
||||||
- **MongoDB**: mongodb://localhost:27017
|
- **MongoDB**: mongodb://localhost:27017
|
||||||
@ -94,8 +99,6 @@ docker-compose down -v
|
|||||||
### API 엔드포인트
|
### API 엔드포인트
|
||||||
- Health Check: `GET http://localhost:9080/health`
|
- Health Check: `GET http://localhost:9080/health`
|
||||||
- API Documentation: `http://localhost:9080/api/v1/docs`
|
- API Documentation: `http://localhost:9080/api/v1/docs`
|
||||||
- APISIX Admin API: `http://localhost:9092/apisix/admin`
|
|
||||||
- APISIX Dashboard: `http://localhost:9000`
|
|
||||||
|
|
||||||
## OAuth 인증 시스템
|
## OAuth 인증 시스템
|
||||||
|
|
||||||
|
|||||||
@ -5,67 +5,67 @@
|
|||||||
### Phase 1: 기본 구조 설정
|
### Phase 1: 기본 구조 설정
|
||||||
|
|
||||||
#### 1. 프로젝트 디렉토리 구조 생성
|
#### 1. 프로젝트 디렉토리 구조 생성
|
||||||
- [ ] 메인 디렉토리 생성 (oauth/, services/, .docker/, .k8s/)
|
- [x] 메인 디렉토리 생성 (oauth/, services/, .docker/, .k8s/)
|
||||||
- [ ] OAuth 하위 디렉토리 생성 (backend/, frontend/, docs/, configs/)
|
- [x] OAuth 하위 디렉토리 생성 (backend/, frontend/, docs/, configs/)
|
||||||
- [ ] 환경별 설정 디렉토리 생성 (configs/dev/, configs/vei/, configs/prod/)
|
- [x] 환경별 설정 디렉토리 생성 (configs/dev/, configs/vei/, configs/prod/)
|
||||||
|
|
||||||
#### 2. Docker Compose 파일 생성
|
#### 2. Docker Compose 파일 생성
|
||||||
- [ ] docker-compose.yml 기본 파일 작성
|
- [x] docker-compose.yml 기본 파일 작성
|
||||||
- [ ] MongoDB 7.0 서비스 정의
|
- [x] MongoDB 7.0 서비스 정의
|
||||||
- [ ] Redis 7 서비스 정의
|
- [x] Redis 7 서비스 정의
|
||||||
- [ ] APISIX Gateway 서비스 정의
|
- [ ] ~~APISIX Gateway 서비스 정의~~ (나중에 추가)
|
||||||
- [ ] OAuth Backend 서비스 정의
|
- [x] OAuth Backend 서비스 정의
|
||||||
- [ ] OAuth Frontend 서비스 정의
|
- [x] OAuth Frontend 서비스 정의
|
||||||
- [ ] 네트워크 및 볼륨 설정
|
- [x] 네트워크 및 볼륨 설정
|
||||||
- [ ] Health check 설정
|
- [x] Health check 설정
|
||||||
- [ ] 서비스 간 의존성 설정 (depends_on)
|
- [x] 서비스 간 의존성 설정 (depends_on)
|
||||||
|
|
||||||
#### 3. 환경 설정 파일 생성
|
#### 3. 환경 설정 파일 생성
|
||||||
- [ ] .env.example 파일 생성
|
- [x] .env.example 파일 생성
|
||||||
- [ ] .env.dev 파일 생성
|
- [x] .env.dev 파일 생성
|
||||||
- [ ] .gitignore 파일 생성
|
- [x] .gitignore 파일 생성
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Phase 2: OAuth 백엔드 구축
|
### Phase 2: OAuth 백엔드 구축
|
||||||
|
|
||||||
#### 1. FastAPI 프로젝트 초기화
|
#### 1. FastAPI 프로젝트 초기화
|
||||||
- [ ] backend 디렉토리 구조 생성
|
- [x] backend 디렉토리 구조 생성
|
||||||
- [ ] requirements.txt 파일 작성
|
- [x] requirements.txt 파일 작성
|
||||||
- [ ] Dockerfile 작성
|
- [x] Dockerfile 작성
|
||||||
- [ ] .env.example 작성
|
- [x] .env.example 작성
|
||||||
|
|
||||||
#### 2. 기본 앱 구조 생성
|
#### 2. 기본 앱 구조 생성
|
||||||
- [ ] app/__init__.py 생성
|
- [x] app/__init__.py 생성
|
||||||
- [ ] app/main.py (FastAPI 앱 진입점) 생성
|
- [x] app/main.py (FastAPI 앱 진입점) 생성
|
||||||
- [ ] app/config.py (환경 설정) 생성
|
- [x] app/config.py (환경 설정) 생성
|
||||||
|
|
||||||
#### 3. 데이터베이스 모델 정의
|
#### 3. 데이터베이스 모델 정의
|
||||||
- [ ] app/models/__init__.py 생성
|
- [x] app/models/__init__.py 생성
|
||||||
- [ ] app/models/user.py (사용자 모델) 생성
|
- [x] app/models/user.py (사용자 모델) 생성
|
||||||
- [ ] app/models/application.py (애플리케이션 모델) 생성
|
- [x] app/models/application.py (애플리케이션 모델) 생성
|
||||||
- [ ] app/models/auth_history.py (인증 히스토리 모델) 생성
|
- [x] app/models/auth_history.py (인증 히스토리 모델) 생성
|
||||||
|
|
||||||
#### 4. 유틸리티 및 서비스 생성
|
#### 4. 유틸리티 및 서비스 생성
|
||||||
- [ ] app/utils/__init__.py 생성
|
- [x] app/utils/__init__.py 생성
|
||||||
- [ ] app/utils/database.py (MongoDB 연결) 생성
|
- [x] app/utils/database.py (MongoDB 연결) 생성
|
||||||
- [ ] app/utils/security.py (보안 관련) 생성
|
- [x] app/utils/security.py (보안 관련) 생성
|
||||||
- [ ] app/services/__init__.py 생성
|
- [x] app/services/__init__.py 생성
|
||||||
- [ ] app/services/auth_service.py 생성
|
- [x] app/services/auth_service.py 생성
|
||||||
- [ ] app/services/token_service.py (JWT 처리) 생성
|
- [x] app/services/token_service.py (JWT 처리) 생성
|
||||||
|
|
||||||
#### 5. API 라우터 구현
|
#### 5. API 라우터 구현
|
||||||
- [ ] app/routers/__init__.py 생성
|
- [x] app/routers/__init__.py 생성
|
||||||
- [ ] app/routers/auth.py (인증 엔드포인트) 생성
|
- [x] app/routers/auth.py (인증 엔드포인트) 생성
|
||||||
- [ ] app/routers/users.py (사용자 관리) 생성
|
- [x] app/routers/users.py (사용자 관리) 생성
|
||||||
- [ ] app/routers/applications.py (애플리케이션 관리) 생성
|
- [x] app/routers/applications.py (애플리케이션 관리) 생성
|
||||||
|
|
||||||
#### 6. 핵심 기능 구현
|
#### 6. 핵심 기능 구현
|
||||||
- [ ] JWT 토큰 생성/검증 로직
|
- [x] JWT 토큰 생성/검증 로직
|
||||||
- [ ] 로그인/로그아웃 API
|
- [x] 로그인/로그아웃 API
|
||||||
- [ ] OAuth 2.0 Authorization Code Flow
|
- [x] OAuth 2.0 Authorization Code Flow
|
||||||
- [ ] Refresh Token 관리
|
- [x] Refresh Token 관리
|
||||||
- [ ] 3단계 권한 체계 (System Admin/Group Admin/User)
|
- [x] 3단계 권한 체계 (System Admin/Group Admin/User)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -186,16 +186,19 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 진행 상태
|
## 진행 상태 (2025-01-05 업데이트)
|
||||||
|
|
||||||
### 현재 진행 중인 작업
|
|
||||||
- Phase 1: 기본 구조 설정
|
|
||||||
|
|
||||||
### 완료된 작업
|
### 완료된 작업
|
||||||
- 작업 계획서 작성
|
- ✅ Phase 1: 기본 구조 설정 (완료)
|
||||||
|
- ✅ Phase 2: OAuth 백엔드 구축 (완료)
|
||||||
|
|
||||||
|
### 현재 대기 중인 작업
|
||||||
|
- ⏳ Phase 3: OAuth 프론트엔드 구축 (다음 세션에서 진행)
|
||||||
|
|
||||||
### 다음 작업
|
### 다음 작업
|
||||||
- 프로젝트 디렉토리 구조 생성
|
- React + Vite + TypeScript + MUI 프론트엔드 구축
|
||||||
|
- 로그인/회원가입 페이지 구현
|
||||||
|
- OAuth 2.0 인증 플로우 UI 구현
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
114
docker-compose.yml
Normal file
114
docker-compose.yml
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
# MongoDB Database
|
||||||
|
mongodb:
|
||||||
|
image: mongo:7.0
|
||||||
|
container_name: oauth-mongodb
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "27017:27017"
|
||||||
|
environment:
|
||||||
|
MONGO_INITDB_ROOT_USERNAME: admin
|
||||||
|
MONGO_INITDB_ROOT_PASSWORD: admin123
|
||||||
|
MONGO_INITDB_DATABASE: oauth_db
|
||||||
|
volumes:
|
||||||
|
- mongodb_data:/data/db
|
||||||
|
- mongodb_config:/data/configdb
|
||||||
|
networks:
|
||||||
|
- oauth-network
|
||||||
|
healthcheck:
|
||||||
|
test: echo 'db.runCommand("ping").ok' | mongosh localhost:27017/test --quiet
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 40s
|
||||||
|
|
||||||
|
# Redis Cache/Queue
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
container_name: oauth-redis
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
|
command: redis-server --appendonly yes
|
||||||
|
volumes:
|
||||||
|
- redis_data:/data
|
||||||
|
networks:
|
||||||
|
- oauth-network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 30s
|
||||||
|
|
||||||
|
# OAuth Backend (FastAPI)
|
||||||
|
oauth-backend:
|
||||||
|
build:
|
||||||
|
context: ./oauth/backend
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
target: development
|
||||||
|
container_name: oauth-backend
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
environment:
|
||||||
|
- ENVIRONMENT=dev
|
||||||
|
- MONGODB_URL=mongodb://admin:admin123@mongodb:27017/oauth_db?authSource=admin
|
||||||
|
- REDIS_URL=redis://redis:6379
|
||||||
|
- SECRET_KEY=${SECRET_KEY:-3bf17c7f-5446-4a18-9cb3-f885eba501e8}
|
||||||
|
- DATABASE_NAME=oauth_db
|
||||||
|
volumes:
|
||||||
|
- ./oauth/backend:/app
|
||||||
|
- backend_logs:/var/log/oauth
|
||||||
|
networks:
|
||||||
|
- oauth-network
|
||||||
|
depends_on:
|
||||||
|
mongodb:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
|
||||||
|
|
||||||
|
# OAuth Frontend (React + Vite)
|
||||||
|
oauth-frontend:
|
||||||
|
build:
|
||||||
|
context: ./oauth/frontend
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
target: development
|
||||||
|
container_name: oauth-frontend
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "5173:5173"
|
||||||
|
environment:
|
||||||
|
- VITE_API_URL=http://localhost:8000/api/v1
|
||||||
|
- VITE_ENV=dev
|
||||||
|
volumes:
|
||||||
|
- ./oauth/frontend:/app
|
||||||
|
- /app/node_modules
|
||||||
|
networks:
|
||||||
|
- oauth-network
|
||||||
|
depends_on:
|
||||||
|
oauth-backend:
|
||||||
|
condition: service_started
|
||||||
|
command: npm run dev -- --host 0.0.0.0
|
||||||
|
|
||||||
|
networks:
|
||||||
|
oauth-network:
|
||||||
|
driver: bridge
|
||||||
|
name: oauth-network
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mongodb_data:
|
||||||
|
driver: local
|
||||||
|
name: oauth-mongodb-data
|
||||||
|
mongodb_config:
|
||||||
|
driver: local
|
||||||
|
name: oauth-mongodb-config
|
||||||
|
redis_data:
|
||||||
|
driver: local
|
||||||
|
name: oauth-redis-data
|
||||||
|
backend_logs:
|
||||||
|
driver: local
|
||||||
|
name: oauth-backend-logs
|
||||||
25
oauth/backend/.env.example
Normal file
25
oauth/backend/.env.example
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# OAuth Backend Environment Variables
|
||||||
|
|
||||||
|
# Application
|
||||||
|
ENVIRONMENT=dev
|
||||||
|
SECRET_KEY=your-secret-key-here
|
||||||
|
API_PREFIX=/api/v1
|
||||||
|
|
||||||
|
# MongoDB
|
||||||
|
MONGODB_URL=mongodb://admin:admin123@localhost:27017/oauth_db?authSource=admin
|
||||||
|
DATABASE_NAME=oauth_db
|
||||||
|
|
||||||
|
# Redis
|
||||||
|
REDIS_URL=redis://localhost:6379
|
||||||
|
|
||||||
|
# JWT Settings
|
||||||
|
JWT_ALGORITHM=HS256
|
||||||
|
JWT_ACCESS_TOKEN_EXPIRE_MINUTES=30
|
||||||
|
JWT_REFRESH_TOKEN_EXPIRE_DAYS=7
|
||||||
|
|
||||||
|
# CORS
|
||||||
|
CORS_ORIGINS=["http://localhost:5173"]
|
||||||
|
CORS_ALLOW_CREDENTIALS=true
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
LOG_LEVEL=INFO
|
||||||
59
oauth/backend/Dockerfile
Normal file
59
oauth/backend/Dockerfile
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
# Multi-stage Dockerfile for OAuth Backend
|
||||||
|
|
||||||
|
# Base stage
|
||||||
|
FROM python:3.11-slim as base
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install system dependencies
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
gcc \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Copy requirements
|
||||||
|
COPY requirements.txt .
|
||||||
|
|
||||||
|
# Install Python dependencies
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Development stage
|
||||||
|
FROM base as development
|
||||||
|
|
||||||
|
# Set environment variables
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
ENV ENVIRONMENT=dev
|
||||||
|
|
||||||
|
# Copy application code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Create necessary directories
|
||||||
|
RUN mkdir -p /var/log/oauth
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
# Run with hot reload
|
||||||
|
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
|
||||||
|
|
||||||
|
# Production stage
|
||||||
|
FROM base as production
|
||||||
|
|
||||||
|
# Set environment variables
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
ENV ENVIRONMENT=prod
|
||||||
|
|
||||||
|
# Copy application code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Create necessary directories
|
||||||
|
RUN mkdir -p /var/log/oauth
|
||||||
|
|
||||||
|
# Create non-root user
|
||||||
|
RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app /var/log/oauth
|
||||||
|
USER appuser
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
# Run without reload
|
||||||
|
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]
|
||||||
3
oauth/backend/app/__init__.py
Normal file
3
oauth/backend/app/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
"""OAuth 2.0 Authentication System Backend"""
|
||||||
|
|
||||||
|
__version__ = "1.0.0"
|
||||||
62
oauth/backend/app/config.py
Normal file
62
oauth/backend/app/config.py
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
"""Configuration settings for OAuth backend"""
|
||||||
|
|
||||||
|
from typing import List, Optional
|
||||||
|
from pydantic_settings import BaseSettings
|
||||||
|
from pydantic import Field
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
class Settings(BaseSettings):
|
||||||
|
# Application
|
||||||
|
environment: str = Field(default="dev", env="ENVIRONMENT")
|
||||||
|
api_prefix: str = Field(default="/api/v1", env="API_PREFIX")
|
||||||
|
secret_key: str = Field(..., env="SECRET_KEY")
|
||||||
|
|
||||||
|
# MongoDB
|
||||||
|
mongodb_url: str = Field(..., env="MONGODB_URL")
|
||||||
|
database_name: str = Field(default="oauth_db", env="DATABASE_NAME")
|
||||||
|
|
||||||
|
# Redis
|
||||||
|
redis_url: str = Field(..., env="REDIS_URL")
|
||||||
|
redis_db: int = Field(default=0, env="REDIS_DB")
|
||||||
|
|
||||||
|
# JWT
|
||||||
|
jwt_algorithm: str = Field(default="HS256", env="JWT_ALGORITHM")
|
||||||
|
jwt_access_token_expire_minutes: int = Field(default=30, env="JWT_ACCESS_TOKEN_EXPIRE_MINUTES")
|
||||||
|
jwt_refresh_token_expire_days: int = Field(default=7, env="JWT_REFRESH_TOKEN_EXPIRE_DAYS")
|
||||||
|
|
||||||
|
# CORS
|
||||||
|
cors_origins: List[str] = Field(default=["http://localhost:5173"], env="CORS_ORIGINS")
|
||||||
|
cors_allow_credentials: bool = Field(default=True, env="CORS_ALLOW_CREDENTIALS")
|
||||||
|
|
||||||
|
# Rate Limiting
|
||||||
|
rate_limit_requests: int = Field(default=100, env="RATE_LIMIT_REQUESTS")
|
||||||
|
rate_limit_period: int = Field(default=60, env="RATE_LIMIT_PERIOD")
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
log_level: str = Field(default="INFO", env="LOG_LEVEL")
|
||||||
|
log_path: str = Field(default="/var/log/oauth", env="LOG_PATH")
|
||||||
|
|
||||||
|
# Admin
|
||||||
|
admin_email: Optional[str] = Field(default="admin@oauth.local", env="ADMIN_EMAIL")
|
||||||
|
admin_password: Optional[str] = Field(default="admin123", env="ADMIN_PASSWORD")
|
||||||
|
|
||||||
|
# Session
|
||||||
|
session_secret_key: str = Field(default="session-secret-key", env="SESSION_SECRET_KEY")
|
||||||
|
session_cookie_name: str = Field(default="oauth_session", env="SESSION_COOKIE_NAME")
|
||||||
|
session_expire_minutes: int = Field(default=1440, env="SESSION_EXPIRE_MINUTES")
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
env_file = ".env"
|
||||||
|
env_file_encoding = "utf-8"
|
||||||
|
case_sensitive = False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def parse_env_var(cls, field_name: str, raw_val: str):
|
||||||
|
if field_name == "cors_origins":
|
||||||
|
return json.loads(raw_val) if isinstance(raw_val, str) else raw_val
|
||||||
|
return raw_val
|
||||||
|
|
||||||
|
|
||||||
|
# Create settings instance
|
||||||
|
settings = Settings()
|
||||||
198
oauth/backend/app/main.py
Normal file
198
oauth/backend/app/main.py
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
"""Main FastAPI application"""
|
||||||
|
|
||||||
|
from fastapi import FastAPI, Request
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
import logging
|
||||||
|
from app.config import settings
|
||||||
|
from app.utils.database import connect_database, disconnect_database
|
||||||
|
from app.routers import auth_router, users_router, applications_router
|
||||||
|
from app.services.auth_service import AuthService
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(
|
||||||
|
level=getattr(logging, settings.log_level),
|
||||||
|
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||||
|
)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(app: FastAPI):
|
||||||
|
"""Manage application lifecycle"""
|
||||||
|
# Startup
|
||||||
|
logger.info("Starting OAuth 2.0 Authentication System")
|
||||||
|
await connect_database()
|
||||||
|
|
||||||
|
# Create admin user if not exists
|
||||||
|
if settings.environment == "dev":
|
||||||
|
await AuthService.create_admin_user()
|
||||||
|
|
||||||
|
yield
|
||||||
|
|
||||||
|
# Shutdown
|
||||||
|
logger.info("Shutting down OAuth 2.0 Authentication System")
|
||||||
|
await disconnect_database()
|
||||||
|
|
||||||
|
|
||||||
|
# Create FastAPI app
|
||||||
|
app = FastAPI(
|
||||||
|
title="OAuth 2.0 Authentication System",
|
||||||
|
description="Enterprise-grade OAuth 2.0 based central authentication system",
|
||||||
|
version="1.0.0",
|
||||||
|
docs_url=f"{settings.api_prefix}/docs",
|
||||||
|
redoc_url=f"{settings.api_prefix}/redoc",
|
||||||
|
openapi_url=f"{settings.api_prefix}/openapi.json",
|
||||||
|
lifespan=lifespan
|
||||||
|
)
|
||||||
|
|
||||||
|
# Configure CORS
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=settings.cors_origins,
|
||||||
|
allow_credentials=settings.cors_allow_credentials,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Health check endpoint
|
||||||
|
@app.get("/health")
|
||||||
|
async def health_check():
|
||||||
|
"""Health check endpoint"""
|
||||||
|
return {
|
||||||
|
"status": "healthy",
|
||||||
|
"environment": settings.environment,
|
||||||
|
"version": "1.0.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Root endpoint
|
||||||
|
@app.get("/")
|
||||||
|
async def root():
|
||||||
|
"""Root endpoint"""
|
||||||
|
return {
|
||||||
|
"message": "OAuth 2.0 Authentication System API",
|
||||||
|
"docs": f"{settings.api_prefix}/docs",
|
||||||
|
"health": "/health"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Include routers
|
||||||
|
app.include_router(auth_router, prefix=settings.api_prefix)
|
||||||
|
app.include_router(users_router, prefix=settings.api_prefix)
|
||||||
|
app.include_router(applications_router, prefix=settings.api_prefix)
|
||||||
|
|
||||||
|
|
||||||
|
# Global exception handler
|
||||||
|
@app.exception_handler(Exception)
|
||||||
|
async def global_exception_handler(request: Request, exc: Exception):
|
||||||
|
"""Global exception handler"""
|
||||||
|
logger.error(f"Unhandled exception: {exc}")
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=500,
|
||||||
|
content={"detail": "Internal server error"}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# OAuth 2.0 authorization endpoint
|
||||||
|
@app.get("/oauth/authorize")
|
||||||
|
async def oauth_authorize(
|
||||||
|
response_type: str,
|
||||||
|
client_id: str,
|
||||||
|
redirect_uri: str,
|
||||||
|
scope: str,
|
||||||
|
state: str = None
|
||||||
|
):
|
||||||
|
"""OAuth 2.0 authorization endpoint"""
|
||||||
|
# This would typically render a login page
|
||||||
|
# For now, return the parameters for the frontend to handle
|
||||||
|
return {
|
||||||
|
"response_type": response_type,
|
||||||
|
"client_id": client_id,
|
||||||
|
"redirect_uri": redirect_uri,
|
||||||
|
"scope": scope,
|
||||||
|
"state": state
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# OAuth 2.0 token endpoint
|
||||||
|
@app.post("/oauth/token")
|
||||||
|
async def oauth_token(
|
||||||
|
grant_type: str,
|
||||||
|
code: str = None,
|
||||||
|
client_id: str = None,
|
||||||
|
client_secret: str = None,
|
||||||
|
redirect_uri: str = None,
|
||||||
|
refresh_token: str = None
|
||||||
|
):
|
||||||
|
"""OAuth 2.0 token endpoint"""
|
||||||
|
from app.services.token_service import TokenService
|
||||||
|
|
||||||
|
if grant_type == "authorization_code":
|
||||||
|
if not all([code, client_id, client_secret, redirect_uri]):
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=400,
|
||||||
|
content={"error": "invalid_request", "error_description": "Missing required parameters"}
|
||||||
|
)
|
||||||
|
|
||||||
|
token = await TokenService.exchange_authorization_code(
|
||||||
|
code=code,
|
||||||
|
client_id=client_id,
|
||||||
|
client_secret=client_secret,
|
||||||
|
redirect_uri=redirect_uri
|
||||||
|
)
|
||||||
|
|
||||||
|
if not token:
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=400,
|
||||||
|
content={"error": "invalid_grant", "error_description": "Invalid authorization code"}
|
||||||
|
)
|
||||||
|
|
||||||
|
return token
|
||||||
|
|
||||||
|
elif grant_type == "refresh_token":
|
||||||
|
if not refresh_token:
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=400,
|
||||||
|
content={"error": "invalid_request", "error_description": "Missing refresh token"}
|
||||||
|
)
|
||||||
|
|
||||||
|
user_id = await TokenService.verify_refresh_token(refresh_token)
|
||||||
|
if not user_id:
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=400,
|
||||||
|
content={"error": "invalid_grant", "error_description": "Invalid refresh token"}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get user and create new tokens
|
||||||
|
from app.services.auth_service import AuthService
|
||||||
|
user = await AuthService.get_user_by_id(user_id)
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=400,
|
||||||
|
content={"error": "invalid_grant", "error_description": "User not found"}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Revoke old refresh token
|
||||||
|
await TokenService.revoke_refresh_token(refresh_token)
|
||||||
|
|
||||||
|
# Create new tokens
|
||||||
|
tokens = await TokenService.create_tokens(
|
||||||
|
str(user.id),
|
||||||
|
{
|
||||||
|
"username": user.username,
|
||||||
|
"role": user.role,
|
||||||
|
"email": user.email
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return tokens
|
||||||
|
|
||||||
|
else:
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=400,
|
||||||
|
content={"error": "unsupported_grant_type", "error_description": f"Grant type '{grant_type}' is not supported"}
|
||||||
|
)
|
||||||
5
oauth/backend/app/models/__init__.py
Normal file
5
oauth/backend/app/models/__init__.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
"""Data models for OAuth backend"""
|
||||||
|
|
||||||
|
from .user import User, UserCreate, UserUpdate, UserRole, UserInDB
|
||||||
|
from .application import Application, ApplicationCreate, ApplicationUpdate
|
||||||
|
from .auth_history import AuthHistory, AuthAction
|
||||||
79
oauth/backend/app/models/application.py
Normal file
79
oauth/backend/app/models/application.py
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
"""Application model definitions"""
|
||||||
|
|
||||||
|
from typing import List, Optional, Dict, Any
|
||||||
|
from pydantic import BaseModel, Field, ConfigDict
|
||||||
|
from datetime import datetime
|
||||||
|
from bson import ObjectId
|
||||||
|
|
||||||
|
|
||||||
|
class ApplicationTheme(BaseModel):
|
||||||
|
"""Application theme configuration"""
|
||||||
|
primary_color: str = "#1976d2"
|
||||||
|
secondary_color: str = "#dc004e"
|
||||||
|
logo_url: Optional[str] = None
|
||||||
|
favicon_url: Optional[str] = None
|
||||||
|
font_family: str = "Roboto, sans-serif"
|
||||||
|
custom_css: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class ApplicationBase(BaseModel):
|
||||||
|
"""Base application model"""
|
||||||
|
app_name: str = Field(..., min_length=3, max_length=100)
|
||||||
|
description: Optional[str] = None
|
||||||
|
redirect_uris: List[str] = []
|
||||||
|
allowed_origins: List[str] = []
|
||||||
|
theme: Optional[ApplicationTheme] = ApplicationTheme()
|
||||||
|
permissions: List[str] = ["sso", "name", "email"] # Default permissions
|
||||||
|
is_active: bool = True
|
||||||
|
|
||||||
|
|
||||||
|
class ApplicationCreate(ApplicationBase):
|
||||||
|
"""Application creation model"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ApplicationUpdate(BaseModel):
|
||||||
|
"""Application update model"""
|
||||||
|
app_name: Optional[str] = Field(None, min_length=3, max_length=100)
|
||||||
|
description: Optional[str] = None
|
||||||
|
redirect_uris: Optional[List[str]] = None
|
||||||
|
allowed_origins: Optional[List[str]] = None
|
||||||
|
theme: Optional[ApplicationTheme] = None
|
||||||
|
permissions: Optional[List[str]] = None
|
||||||
|
is_active: Optional[bool] = None
|
||||||
|
|
||||||
|
|
||||||
|
class Application(ApplicationBase):
|
||||||
|
"""Application response model"""
|
||||||
|
id: str = Field(alias="_id")
|
||||||
|
client_id: str
|
||||||
|
client_secret: str
|
||||||
|
created_by: str
|
||||||
|
created_at: datetime
|
||||||
|
updated_at: datetime
|
||||||
|
|
||||||
|
model_config = ConfigDict(
|
||||||
|
populate_by_name=True,
|
||||||
|
arbitrary_types_allowed=True,
|
||||||
|
json_encoders={ObjectId: str}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ApplicationInDB(Application):
|
||||||
|
"""Application model in database"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ApplicationPublic(BaseModel):
|
||||||
|
"""Public application information (no secret)"""
|
||||||
|
id: str = Field(alias="_id")
|
||||||
|
app_name: str
|
||||||
|
description: Optional[str] = None
|
||||||
|
theme: Optional[ApplicationTheme] = None
|
||||||
|
permissions: List[str] = []
|
||||||
|
|
||||||
|
model_config = ConfigDict(
|
||||||
|
populate_by_name=True,
|
||||||
|
arbitrary_types_allowed=True,
|
||||||
|
json_encoders={ObjectId: str}
|
||||||
|
)
|
||||||
53
oauth/backend/app/models/auth_history.py
Normal file
53
oauth/backend/app/models/auth_history.py
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
"""Authentication history model definitions"""
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
from pydantic import BaseModel, Field, ConfigDict
|
||||||
|
from datetime import datetime
|
||||||
|
from enum import Enum
|
||||||
|
from bson import ObjectId
|
||||||
|
|
||||||
|
|
||||||
|
class AuthAction(str, Enum):
|
||||||
|
"""Authentication action types"""
|
||||||
|
LOGIN = "login"
|
||||||
|
LOGOUT = "logout"
|
||||||
|
TOKEN_REFRESH = "token_refresh"
|
||||||
|
AUTHORIZATION_CODE = "authorization_code"
|
||||||
|
PASSWORD_RESET = "password_reset"
|
||||||
|
REGISTER = "register"
|
||||||
|
FAILED_LOGIN = "failed_login"
|
||||||
|
|
||||||
|
|
||||||
|
class AuthHistoryBase(BaseModel):
|
||||||
|
"""Base authentication history model"""
|
||||||
|
user_id: str
|
||||||
|
application_id: Optional[str] = None
|
||||||
|
action: AuthAction
|
||||||
|
ip_address: str
|
||||||
|
user_agent: Optional[str] = None
|
||||||
|
result: str = "success"
|
||||||
|
details: Optional[dict] = None
|
||||||
|
|
||||||
|
model_config = ConfigDict(use_enum_values=True)
|
||||||
|
|
||||||
|
|
||||||
|
class AuthHistoryCreate(AuthHistoryBase):
|
||||||
|
"""Authentication history creation model"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class AuthHistory(AuthHistoryBase):
|
||||||
|
"""Authentication history response model"""
|
||||||
|
id: str = Field(alias="_id")
|
||||||
|
created_at: datetime
|
||||||
|
|
||||||
|
model_config = ConfigDict(
|
||||||
|
populate_by_name=True,
|
||||||
|
arbitrary_types_allowed=True,
|
||||||
|
json_encoders={ObjectId: str}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AuthHistoryInDB(AuthHistory):
|
||||||
|
"""Authentication history model in database"""
|
||||||
|
pass
|
||||||
108
oauth/backend/app/models/user.py
Normal file
108
oauth/backend/app/models/user.py
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
"""User model definitions"""
|
||||||
|
|
||||||
|
from typing import Optional, List
|
||||||
|
from pydantic import BaseModel, EmailStr, Field, ConfigDict
|
||||||
|
from datetime import datetime
|
||||||
|
from enum import Enum
|
||||||
|
from bson import ObjectId
|
||||||
|
|
||||||
|
|
||||||
|
class UserRole(str, Enum):
|
||||||
|
"""User role enumeration"""
|
||||||
|
SYSTEM_ADMIN = "system_admin"
|
||||||
|
GROUP_ADMIN = "group_admin"
|
||||||
|
USER = "user"
|
||||||
|
|
||||||
|
|
||||||
|
class PyObjectId(ObjectId):
|
||||||
|
"""Custom ObjectId type for Pydantic"""
|
||||||
|
@classmethod
|
||||||
|
def __get_validators__(cls):
|
||||||
|
yield cls.validate
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def validate(cls, v):
|
||||||
|
if not ObjectId.is_valid(v):
|
||||||
|
raise ValueError("Invalid ObjectId")
|
||||||
|
return ObjectId(v)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def __get_pydantic_json_schema__(cls, field_schema):
|
||||||
|
field_schema.update(type="string")
|
||||||
|
|
||||||
|
|
||||||
|
class UserBase(BaseModel):
|
||||||
|
"""Base user model"""
|
||||||
|
email: EmailStr
|
||||||
|
username: str = Field(..., min_length=3, max_length=50)
|
||||||
|
full_name: Optional[str] = None
|
||||||
|
role: UserRole = UserRole.USER
|
||||||
|
profile_picture: Optional[str] = None
|
||||||
|
phone_number: Optional[str] = None
|
||||||
|
gender: Optional[str] = None
|
||||||
|
birth_date: Optional[str] = None
|
||||||
|
is_active: bool = True
|
||||||
|
|
||||||
|
model_config = ConfigDict(use_enum_values=True)
|
||||||
|
|
||||||
|
|
||||||
|
class UserCreate(UserBase):
|
||||||
|
"""User creation model"""
|
||||||
|
password: str = Field(..., min_length=8)
|
||||||
|
|
||||||
|
|
||||||
|
class UserUpdate(BaseModel):
|
||||||
|
"""User update model"""
|
||||||
|
email: Optional[EmailStr] = None
|
||||||
|
username: Optional[str] = Field(None, min_length=3, max_length=50)
|
||||||
|
full_name: Optional[str] = None
|
||||||
|
role: Optional[UserRole] = None
|
||||||
|
profile_picture: Optional[str] = None
|
||||||
|
phone_number: Optional[str] = None
|
||||||
|
gender: Optional[str] = None
|
||||||
|
birth_date: Optional[str] = None
|
||||||
|
is_active: Optional[bool] = None
|
||||||
|
password: Optional[str] = Field(None, min_length=8)
|
||||||
|
|
||||||
|
|
||||||
|
class User(UserBase):
|
||||||
|
"""User response model"""
|
||||||
|
id: str = Field(alias="_id")
|
||||||
|
created_at: datetime
|
||||||
|
updated_at: datetime
|
||||||
|
|
||||||
|
model_config = ConfigDict(
|
||||||
|
populate_by_name=True,
|
||||||
|
arbitrary_types_allowed=True,
|
||||||
|
json_encoders={ObjectId: str}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class UserInDB(User):
|
||||||
|
"""User model in database"""
|
||||||
|
hashed_password: str
|
||||||
|
|
||||||
|
|
||||||
|
class UserLogin(BaseModel):
|
||||||
|
"""User login model"""
|
||||||
|
username: str
|
||||||
|
password: str
|
||||||
|
|
||||||
|
|
||||||
|
class TokenData(BaseModel):
|
||||||
|
"""Token data model"""
|
||||||
|
user_id: Optional[str] = None
|
||||||
|
username: Optional[str] = None
|
||||||
|
role: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class Token(BaseModel):
|
||||||
|
"""Token response model"""
|
||||||
|
access_token: str
|
||||||
|
refresh_token: str
|
||||||
|
token_type: str = "bearer"
|
||||||
|
|
||||||
|
|
||||||
|
class TokenRefresh(BaseModel):
|
||||||
|
"""Token refresh request model"""
|
||||||
|
refresh_token: str
|
||||||
5
oauth/backend/app/routers/__init__.py
Normal file
5
oauth/backend/app/routers/__init__.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
"""API routers"""
|
||||||
|
|
||||||
|
from .auth import router as auth_router
|
||||||
|
from .users import router as users_router
|
||||||
|
from .applications import router as applications_router
|
||||||
256
oauth/backend/app/routers/applications.py
Normal file
256
oauth/backend/app/routers/applications.py
Normal file
@ -0,0 +1,256 @@
|
|||||||
|
"""Applications management router"""
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
||||||
|
from typing import List
|
||||||
|
from bson import ObjectId
|
||||||
|
from datetime import datetime
|
||||||
|
from app.models.application import Application, ApplicationCreate, ApplicationUpdate, ApplicationPublic
|
||||||
|
from app.models.user import User, UserRole
|
||||||
|
from app.routers.auth import get_current_user
|
||||||
|
from app.utils.database import get_database
|
||||||
|
from app.utils.security import generate_client_id, generate_client_secret
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/applications", tags=["Applications"])
|
||||||
|
|
||||||
|
|
||||||
|
def require_admin(current_user: User = Depends(get_current_user)) -> User:
|
||||||
|
"""Require admin role"""
|
||||||
|
if current_user.role not in [UserRole.SYSTEM_ADMIN, UserRole.GROUP_ADMIN]:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="Not enough permissions"
|
||||||
|
)
|
||||||
|
return current_user
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/", response_model=List[Application])
|
||||||
|
async def get_applications(
|
||||||
|
skip: int = Query(0, ge=0),
|
||||||
|
limit: int = Query(10, ge=1, le=100),
|
||||||
|
current_user: User = Depends(get_current_user)
|
||||||
|
):
|
||||||
|
"""Get applications (own applications for users, all for admins)"""
|
||||||
|
db = get_database()
|
||||||
|
|
||||||
|
query = {}
|
||||||
|
if current_user.role not in [UserRole.SYSTEM_ADMIN, UserRole.GROUP_ADMIN]:
|
||||||
|
query = {"created_by": str(current_user.id)}
|
||||||
|
|
||||||
|
apps = await db.applications.find(query).skip(skip).limit(limit).to_list(limit)
|
||||||
|
return [Application(**app) for app in apps]
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/", response_model=Application)
|
||||||
|
async def create_application(
|
||||||
|
app_data: ApplicationCreate,
|
||||||
|
current_user: User = Depends(get_current_user)
|
||||||
|
):
|
||||||
|
"""Create a new application"""
|
||||||
|
db = get_database()
|
||||||
|
|
||||||
|
# Generate client credentials
|
||||||
|
client_id = generate_client_id()
|
||||||
|
client_secret = generate_client_secret()
|
||||||
|
|
||||||
|
# Check if client_id already exists (unlikely but possible)
|
||||||
|
existing_app = await db.applications.find_one({"client_id": client_id})
|
||||||
|
if existing_app:
|
||||||
|
# Regenerate if collision
|
||||||
|
client_id = generate_client_id()
|
||||||
|
|
||||||
|
# Prepare application document
|
||||||
|
app_doc = {
|
||||||
|
**app_data.model_dump(),
|
||||||
|
"client_id": client_id,
|
||||||
|
"client_secret": client_secret,
|
||||||
|
"created_by": str(current_user.id),
|
||||||
|
"created_at": datetime.utcnow(),
|
||||||
|
"updated_at": datetime.utcnow()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Insert application
|
||||||
|
result = await db.applications.insert_one(app_doc)
|
||||||
|
app_doc["_id"] = result.inserted_id
|
||||||
|
|
||||||
|
return Application(**app_doc)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/public/{client_id}", response_model=ApplicationPublic)
|
||||||
|
async def get_application_public(client_id: str):
|
||||||
|
"""Get public application information (for OAuth flow)"""
|
||||||
|
db = get_database()
|
||||||
|
|
||||||
|
app = await db.applications.find_one({"client_id": client_id, "is_active": True})
|
||||||
|
if not app:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Application not found"
|
||||||
|
)
|
||||||
|
|
||||||
|
return ApplicationPublic(**app)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{app_id}", response_model=Application)
|
||||||
|
async def get_application(app_id: str, current_user: User = Depends(get_current_user)):
|
||||||
|
"""Get application by ID"""
|
||||||
|
db = get_database()
|
||||||
|
|
||||||
|
try:
|
||||||
|
app = await db.applications.find_one({"_id": ObjectId(app_id)})
|
||||||
|
if not app:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Application not found"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check permissions
|
||||||
|
if str(app["created_by"]) != str(current_user.id) and current_user.role not in [UserRole.SYSTEM_ADMIN, UserRole.GROUP_ADMIN]:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="Not enough permissions"
|
||||||
|
)
|
||||||
|
|
||||||
|
return Application(**app)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
if isinstance(e, HTTPException):
|
||||||
|
raise
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Invalid application ID"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/{app_id}", response_model=Application)
|
||||||
|
async def update_application(
|
||||||
|
app_id: str,
|
||||||
|
app_update: ApplicationUpdate,
|
||||||
|
current_user: User = Depends(get_current_user)
|
||||||
|
):
|
||||||
|
"""Update application"""
|
||||||
|
db = get_database()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Get existing application
|
||||||
|
app = await db.applications.find_one({"_id": ObjectId(app_id)})
|
||||||
|
if not app:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Application not found"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check permissions
|
||||||
|
if str(app["created_by"]) != str(current_user.id) and current_user.role not in [UserRole.SYSTEM_ADMIN, UserRole.GROUP_ADMIN]:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="Not enough permissions"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Prepare update data
|
||||||
|
update_data = app_update.model_dump(exclude_unset=True)
|
||||||
|
update_data["updated_at"] = datetime.utcnow()
|
||||||
|
|
||||||
|
# Update application
|
||||||
|
result = await db.applications.update_one(
|
||||||
|
{"_id": ObjectId(app_id)},
|
||||||
|
{"$set": update_data}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Return updated application
|
||||||
|
app = await db.applications.find_one({"_id": ObjectId(app_id)})
|
||||||
|
return Application(**app)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
if isinstance(e, HTTPException):
|
||||||
|
raise
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail=str(e)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{app_id}/regenerate-secret", response_model=Application)
|
||||||
|
async def regenerate_client_secret(
|
||||||
|
app_id: str,
|
||||||
|
current_user: User = Depends(get_current_user)
|
||||||
|
):
|
||||||
|
"""Regenerate client secret"""
|
||||||
|
db = get_database()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Get existing application
|
||||||
|
app = await db.applications.find_one({"_id": ObjectId(app_id)})
|
||||||
|
if not app:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Application not found"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check permissions
|
||||||
|
if str(app["created_by"]) != str(current_user.id) and current_user.role not in [UserRole.SYSTEM_ADMIN, UserRole.GROUP_ADMIN]:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="Not enough permissions"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Generate new secret
|
||||||
|
new_secret = generate_client_secret()
|
||||||
|
|
||||||
|
# Update application
|
||||||
|
await db.applications.update_one(
|
||||||
|
{"_id": ObjectId(app_id)},
|
||||||
|
{"$set": {
|
||||||
|
"client_secret": new_secret,
|
||||||
|
"updated_at": datetime.utcnow()
|
||||||
|
}}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Return updated application
|
||||||
|
app = await db.applications.find_one({"_id": ObjectId(app_id)})
|
||||||
|
return Application(**app)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
if isinstance(e, HTTPException):
|
||||||
|
raise
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail=str(e)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{app_id}")
|
||||||
|
async def delete_application(
|
||||||
|
app_id: str,
|
||||||
|
current_user: User = Depends(get_current_user)
|
||||||
|
):
|
||||||
|
"""Delete application"""
|
||||||
|
db = get_database()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Get existing application
|
||||||
|
app = await db.applications.find_one({"_id": ObjectId(app_id)})
|
||||||
|
if not app:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Application not found"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check permissions
|
||||||
|
if str(app["created_by"]) != str(current_user.id) and current_user.role != UserRole.SYSTEM_ADMIN:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="Not enough permissions"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Delete application
|
||||||
|
result = await db.applications.delete_one({"_id": ObjectId(app_id)})
|
||||||
|
|
||||||
|
return {"message": "Application deleted successfully"}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
if isinstance(e, HTTPException):
|
||||||
|
raise
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Invalid application ID"
|
||||||
|
)
|
||||||
164
oauth/backend/app/routers/auth.py
Normal file
164
oauth/backend/app/routers/auth.py
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
"""Authentication router"""
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status, Request
|
||||||
|
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
||||||
|
from typing import Optional
|
||||||
|
from app.models.user import User, UserLogin, Token, TokenRefresh, UserCreate
|
||||||
|
from app.models.auth_history import AuthAction
|
||||||
|
from app.services.auth_service import AuthService
|
||||||
|
from app.services.token_service import TokenService
|
||||||
|
from app.utils.security import decode_token
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/auth", tags=["Authentication"])
|
||||||
|
|
||||||
|
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")
|
||||||
|
|
||||||
|
|
||||||
|
async def get_current_user(token: str = Depends(oauth2_scheme)) -> User:
|
||||||
|
"""Get current authenticated user"""
|
||||||
|
credentials_exception = HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Could not validate credentials",
|
||||||
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
|
)
|
||||||
|
|
||||||
|
payload = decode_token(token)
|
||||||
|
if payload is None or payload.get("type") != "access":
|
||||||
|
raise credentials_exception
|
||||||
|
|
||||||
|
user = await AuthService.get_user_by_id(payload.get("sub"))
|
||||||
|
if user is None:
|
||||||
|
raise credentials_exception
|
||||||
|
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/register", response_model=User)
|
||||||
|
async def register(user_data: UserCreate, request: Request):
|
||||||
|
"""Register a new user"""
|
||||||
|
try:
|
||||||
|
user = await AuthService.create_user(user_data)
|
||||||
|
|
||||||
|
# Log registration
|
||||||
|
await AuthService.log_auth_action(
|
||||||
|
user_id=str(user.id),
|
||||||
|
action=AuthAction.REGISTER,
|
||||||
|
ip_address=request.client.host,
|
||||||
|
user_agent=request.headers.get("User-Agent")
|
||||||
|
)
|
||||||
|
|
||||||
|
return user
|
||||||
|
except ValueError as e:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail=str(e)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/login", response_model=Token)
|
||||||
|
async def login(request: Request, form_data: OAuth2PasswordRequestForm = Depends()):
|
||||||
|
"""Login with username/email and password"""
|
||||||
|
user = await AuthService.authenticate_user(form_data.username, form_data.password)
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
# Log failed login attempt
|
||||||
|
await AuthService.log_auth_action(
|
||||||
|
user_id="unknown",
|
||||||
|
action=AuthAction.FAILED_LOGIN,
|
||||||
|
ip_address=request.client.host,
|
||||||
|
user_agent=request.headers.get("User-Agent"),
|
||||||
|
result="failed",
|
||||||
|
details={"username": form_data.username}
|
||||||
|
)
|
||||||
|
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Incorrect username or password",
|
||||||
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create tokens
|
||||||
|
tokens = await TokenService.create_tokens(
|
||||||
|
str(user.id),
|
||||||
|
{
|
||||||
|
"username": user.username,
|
||||||
|
"role": user.role,
|
||||||
|
"email": user.email
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Log successful login
|
||||||
|
await AuthService.log_auth_action(
|
||||||
|
user_id=str(user.id),
|
||||||
|
action=AuthAction.LOGIN,
|
||||||
|
ip_address=request.client.host,
|
||||||
|
user_agent=request.headers.get("User-Agent")
|
||||||
|
)
|
||||||
|
|
||||||
|
return tokens
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/refresh", response_model=Token)
|
||||||
|
async def refresh_token(request: Request, token_data: TokenRefresh):
|
||||||
|
"""Refresh access token using refresh token"""
|
||||||
|
user_id = await TokenService.verify_refresh_token(token_data.refresh_token)
|
||||||
|
|
||||||
|
if not user_id:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Invalid refresh token"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get user
|
||||||
|
user = await AuthService.get_user_by_id(user_id)
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="User not found"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Revoke old refresh token
|
||||||
|
await TokenService.revoke_refresh_token(token_data.refresh_token)
|
||||||
|
|
||||||
|
# Create new tokens
|
||||||
|
tokens = await TokenService.create_tokens(
|
||||||
|
str(user.id),
|
||||||
|
{
|
||||||
|
"username": user.username,
|
||||||
|
"role": user.role,
|
||||||
|
"email": user.email
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Log token refresh
|
||||||
|
await AuthService.log_auth_action(
|
||||||
|
user_id=str(user.id),
|
||||||
|
action=AuthAction.TOKEN_REFRESH,
|
||||||
|
ip_address=request.client.host,
|
||||||
|
user_agent=request.headers.get("User-Agent")
|
||||||
|
)
|
||||||
|
|
||||||
|
return tokens
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/logout")
|
||||||
|
async def logout(request: Request, current_user: User = Depends(get_current_user)):
|
||||||
|
"""Logout current user"""
|
||||||
|
# Revoke all user tokens
|
||||||
|
await TokenService.revoke_all_user_tokens(str(current_user.id))
|
||||||
|
|
||||||
|
# Log logout
|
||||||
|
await AuthService.log_auth_action(
|
||||||
|
user_id=str(current_user.id),
|
||||||
|
action=AuthAction.LOGOUT,
|
||||||
|
ip_address=request.client.host,
|
||||||
|
user_agent=request.headers.get("User-Agent")
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"message": "Successfully logged out"}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/me", response_model=User)
|
||||||
|
async def get_me(current_user: User = Depends(get_current_user)):
|
||||||
|
"""Get current user information"""
|
||||||
|
return current_user
|
||||||
156
oauth/backend/app/routers/users.py
Normal file
156
oauth/backend/app/routers/users.py
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
"""Users management router"""
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
||||||
|
from typing import List, Optional
|
||||||
|
from bson import ObjectId
|
||||||
|
from app.models.user import User, UserUpdate, UserRole
|
||||||
|
from app.routers.auth import get_current_user
|
||||||
|
from app.utils.database import get_database
|
||||||
|
from app.utils.security import hash_password
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/users", tags=["Users"])
|
||||||
|
|
||||||
|
|
||||||
|
def require_admin(current_user: User = Depends(get_current_user)) -> User:
|
||||||
|
"""Require admin role"""
|
||||||
|
if current_user.role not in [UserRole.SYSTEM_ADMIN, UserRole.GROUP_ADMIN]:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="Not enough permissions"
|
||||||
|
)
|
||||||
|
return current_user
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/", response_model=List[User])
|
||||||
|
async def get_users(
|
||||||
|
skip: int = Query(0, ge=0),
|
||||||
|
limit: int = Query(10, ge=1, le=100),
|
||||||
|
current_user: User = Depends(require_admin)
|
||||||
|
):
|
||||||
|
"""Get all users (admin only)"""
|
||||||
|
db = get_database()
|
||||||
|
|
||||||
|
users = await db.users.find().skip(skip).limit(limit).to_list(limit)
|
||||||
|
return [User(**user) for user in users]
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{user_id}", response_model=User)
|
||||||
|
async def get_user(user_id: str, current_user: User = Depends(get_current_user)):
|
||||||
|
"""Get user by ID"""
|
||||||
|
# Users can only view their own profile unless they're admin
|
||||||
|
if str(current_user.id) != user_id and current_user.role not in [UserRole.SYSTEM_ADMIN, UserRole.GROUP_ADMIN]:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="Not enough permissions"
|
||||||
|
)
|
||||||
|
|
||||||
|
db = get_database()
|
||||||
|
|
||||||
|
try:
|
||||||
|
user = await db.users.find_one({"_id": ObjectId(user_id)})
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="User not found"
|
||||||
|
)
|
||||||
|
return User(**user)
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Invalid user ID"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/{user_id}", response_model=User)
|
||||||
|
async def update_user(
|
||||||
|
user_id: str,
|
||||||
|
user_update: UserUpdate,
|
||||||
|
current_user: User = Depends(get_current_user)
|
||||||
|
):
|
||||||
|
"""Update user"""
|
||||||
|
# Users can only update their own profile unless they're admin
|
||||||
|
if str(current_user.id) != user_id and current_user.role not in [UserRole.SYSTEM_ADMIN, UserRole.GROUP_ADMIN]:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="Not enough permissions"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Only system admin can change roles
|
||||||
|
if user_update.role and current_user.role != UserRole.SYSTEM_ADMIN:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="Only system admin can change user roles"
|
||||||
|
)
|
||||||
|
|
||||||
|
db = get_database()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Prepare update data
|
||||||
|
update_data = user_update.model_dump(exclude_unset=True)
|
||||||
|
|
||||||
|
# Hash password if provided
|
||||||
|
if "password" in update_data:
|
||||||
|
update_data["hashed_password"] = hash_password(update_data.pop("password"))
|
||||||
|
|
||||||
|
# Update timestamp
|
||||||
|
update_data["updated_at"] = datetime.utcnow()
|
||||||
|
|
||||||
|
# Update user
|
||||||
|
result = await db.users.update_one(
|
||||||
|
{"_id": ObjectId(user_id)},
|
||||||
|
{"$set": update_data}
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.modified_count == 0:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="User not found"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Return updated user
|
||||||
|
user = await db.users.find_one({"_id": ObjectId(user_id)})
|
||||||
|
return User(**user)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
if isinstance(e, HTTPException):
|
||||||
|
raise
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail=str(e)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{user_id}")
|
||||||
|
async def delete_user(
|
||||||
|
user_id: str,
|
||||||
|
current_user: User = Depends(require_admin)
|
||||||
|
):
|
||||||
|
"""Delete user (admin only)"""
|
||||||
|
# Prevent self-deletion
|
||||||
|
if str(current_user.id) == user_id:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Cannot delete your own account"
|
||||||
|
)
|
||||||
|
|
||||||
|
db = get_database()
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = await db.users.delete_one({"_id": ObjectId(user_id)})
|
||||||
|
|
||||||
|
if result.deleted_count == 0:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="User not found"
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"message": "User deleted successfully"}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
if isinstance(e, HTTPException):
|
||||||
|
raise
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Invalid user ID"
|
||||||
|
)
|
||||||
4
oauth/backend/app/services/__init__.py
Normal file
4
oauth/backend/app/services/__init__.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
"""Service layer for business logic"""
|
||||||
|
|
||||||
|
from .auth_service import AuthService
|
||||||
|
from .token_service import TokenService
|
||||||
135
oauth/backend/app/services/auth_service.py
Normal file
135
oauth/backend/app/services/auth_service.py
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
"""Authentication service"""
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from bson import ObjectId
|
||||||
|
from app.utils.database import get_database
|
||||||
|
from app.utils.security import verify_password, hash_password
|
||||||
|
from app.models.user import UserCreate, User, UserInDB, UserRole
|
||||||
|
from app.models.auth_history import AuthHistoryCreate, AuthAction
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class AuthService:
|
||||||
|
"""Service for handling authentication logic"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def authenticate_user(username: str, password: str) -> Optional[UserInDB]:
|
||||||
|
"""Authenticate a user with username/email and password"""
|
||||||
|
db = get_database()
|
||||||
|
|
||||||
|
# Try to find user by username or email
|
||||||
|
user = await db.users.find_one({
|
||||||
|
"$or": [
|
||||||
|
{"username": username},
|
||||||
|
{"email": username}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not verify_password(password, user["hashed_password"]):
|
||||||
|
return None
|
||||||
|
|
||||||
|
return UserInDB(**user)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def create_user(user_data: UserCreate) -> User:
|
||||||
|
"""Create a new user"""
|
||||||
|
db = get_database()
|
||||||
|
|
||||||
|
# Check if user already exists
|
||||||
|
existing_user = await db.users.find_one({
|
||||||
|
"$or": [
|
||||||
|
{"email": user_data.email},
|
||||||
|
{"username": user_data.username}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
if existing_user:
|
||||||
|
raise ValueError("User with this email or username already exists")
|
||||||
|
|
||||||
|
# Hash password
|
||||||
|
hashed_password = hash_password(user_data.password)
|
||||||
|
|
||||||
|
# Prepare user document
|
||||||
|
user_doc = {
|
||||||
|
**user_data.model_dump(exclude={"password"}),
|
||||||
|
"hashed_password": hashed_password,
|
||||||
|
"created_at": datetime.utcnow(),
|
||||||
|
"updated_at": datetime.utcnow()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Insert user
|
||||||
|
result = await db.users.insert_one(user_doc)
|
||||||
|
user_doc["_id"] = result.inserted_id
|
||||||
|
|
||||||
|
return User(**user_doc)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def get_user_by_id(user_id: str) -> Optional[User]:
|
||||||
|
"""Get user by ID"""
|
||||||
|
db = get_database()
|
||||||
|
|
||||||
|
try:
|
||||||
|
user = await db.users.find_one({"_id": ObjectId(user_id)})
|
||||||
|
if user:
|
||||||
|
return User(**user)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error getting user by ID: {e}")
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def log_auth_action(
|
||||||
|
user_id: str,
|
||||||
|
action: AuthAction,
|
||||||
|
ip_address: str,
|
||||||
|
user_agent: Optional[str] = None,
|
||||||
|
application_id: Optional[str] = None,
|
||||||
|
result: str = "success",
|
||||||
|
details: Optional[dict] = None
|
||||||
|
):
|
||||||
|
"""Log authentication action"""
|
||||||
|
db = get_database()
|
||||||
|
|
||||||
|
history_doc = {
|
||||||
|
"user_id": user_id,
|
||||||
|
"application_id": application_id,
|
||||||
|
"action": action.value,
|
||||||
|
"ip_address": ip_address,
|
||||||
|
"user_agent": user_agent,
|
||||||
|
"result": result,
|
||||||
|
"details": details,
|
||||||
|
"created_at": datetime.utcnow()
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.auth_history.insert_one(history_doc)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def create_admin_user():
|
||||||
|
"""Create default admin user if not exists"""
|
||||||
|
from app.config import settings
|
||||||
|
|
||||||
|
db = get_database()
|
||||||
|
|
||||||
|
# Check if admin exists
|
||||||
|
admin = await db.users.find_one({"email": settings.admin_email})
|
||||||
|
|
||||||
|
if not admin:
|
||||||
|
admin_data = UserCreate(
|
||||||
|
email=settings.admin_email,
|
||||||
|
username="admin",
|
||||||
|
password=settings.admin_password,
|
||||||
|
full_name="System Administrator",
|
||||||
|
role=UserRole.SYSTEM_ADMIN
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await AuthService.create_user(admin_data)
|
||||||
|
logger.info("Admin user created successfully")
|
||||||
|
except ValueError:
|
||||||
|
logger.info("Admin user already exists")
|
||||||
201
oauth/backend/app/services/token_service.py
Normal file
201
oauth/backend/app/services/token_service.py
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
"""Token management service"""
|
||||||
|
|
||||||
|
from typing import Optional, Dict, Any
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from bson import ObjectId
|
||||||
|
from app.utils.database import get_database
|
||||||
|
from app.utils.security import create_access_token, create_refresh_token, decode_token
|
||||||
|
from app.models.user import Token
|
||||||
|
import logging
|
||||||
|
import secrets
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class TokenService:
|
||||||
|
"""Service for handling token operations"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def create_tokens(user_id: str, user_data: Dict[str, Any]) -> Token:
|
||||||
|
"""Create access and refresh tokens for a user"""
|
||||||
|
# Create access token
|
||||||
|
access_token = create_access_token(
|
||||||
|
data={
|
||||||
|
"sub": user_id,
|
||||||
|
"username": user_data.get("username"),
|
||||||
|
"role": user_data.get("role"),
|
||||||
|
"email": user_data.get("email")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create refresh token
|
||||||
|
refresh_token = create_refresh_token(
|
||||||
|
data={
|
||||||
|
"sub": user_id,
|
||||||
|
"username": user_data.get("username")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Store refresh token in database
|
||||||
|
await TokenService.store_refresh_token(user_id, refresh_token)
|
||||||
|
|
||||||
|
return Token(
|
||||||
|
access_token=access_token,
|
||||||
|
refresh_token=refresh_token,
|
||||||
|
token_type="bearer"
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def store_refresh_token(user_id: str, token: str):
|
||||||
|
"""Store refresh token in database"""
|
||||||
|
db = get_database()
|
||||||
|
|
||||||
|
from app.config import settings
|
||||||
|
expires_at = datetime.utcnow() + timedelta(
|
||||||
|
days=settings.jwt_refresh_token_expire_days
|
||||||
|
)
|
||||||
|
|
||||||
|
token_doc = {
|
||||||
|
"token": token,
|
||||||
|
"user_id": user_id,
|
||||||
|
"created_at": datetime.utcnow(),
|
||||||
|
"expires_at": expires_at,
|
||||||
|
"is_active": True
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.refresh_tokens.insert_one(token_doc)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def verify_refresh_token(token: str) -> Optional[str]:
|
||||||
|
"""Verify refresh token and return user_id"""
|
||||||
|
db = get_database()
|
||||||
|
|
||||||
|
# Decode token
|
||||||
|
payload = decode_token(token)
|
||||||
|
if not payload or payload.get("type") != "refresh":
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Check if token exists in database
|
||||||
|
token_doc = await db.refresh_tokens.find_one({
|
||||||
|
"token": token,
|
||||||
|
"is_active": True
|
||||||
|
})
|
||||||
|
|
||||||
|
if not token_doc:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Check if token is expired
|
||||||
|
if token_doc["expires_at"] < datetime.utcnow():
|
||||||
|
# Mark token as inactive
|
||||||
|
await db.refresh_tokens.update_one(
|
||||||
|
{"_id": token_doc["_id"]},
|
||||||
|
{"$set": {"is_active": False}}
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
return payload.get("sub")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def revoke_refresh_token(token: str):
|
||||||
|
"""Revoke a refresh token"""
|
||||||
|
db = get_database()
|
||||||
|
|
||||||
|
await db.refresh_tokens.update_one(
|
||||||
|
{"token": token},
|
||||||
|
{"$set": {"is_active": False}}
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def revoke_all_user_tokens(user_id: str):
|
||||||
|
"""Revoke all refresh tokens for a user"""
|
||||||
|
db = get_database()
|
||||||
|
|
||||||
|
await db.refresh_tokens.update_many(
|
||||||
|
{"user_id": user_id},
|
||||||
|
{"$set": {"is_active": False}}
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def create_authorization_code(
|
||||||
|
user_id: str,
|
||||||
|
client_id: str,
|
||||||
|
redirect_uri: str,
|
||||||
|
scope: str,
|
||||||
|
state: Optional[str] = None
|
||||||
|
) -> str:
|
||||||
|
"""Create and store authorization code"""
|
||||||
|
db = get_database()
|
||||||
|
|
||||||
|
code = secrets.token_urlsafe(32)
|
||||||
|
expires_at = datetime.utcnow() + timedelta(minutes=10) # Code valid for 10 minutes
|
||||||
|
|
||||||
|
code_doc = {
|
||||||
|
"code": code,
|
||||||
|
"user_id": user_id,
|
||||||
|
"client_id": client_id,
|
||||||
|
"redirect_uri": redirect_uri,
|
||||||
|
"scope": scope,
|
||||||
|
"state": state,
|
||||||
|
"created_at": datetime.utcnow(),
|
||||||
|
"expires_at": expires_at,
|
||||||
|
"used": False
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.authorization_codes.insert_one(code_doc)
|
||||||
|
|
||||||
|
return code
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def exchange_authorization_code(
|
||||||
|
code: str,
|
||||||
|
client_id: str,
|
||||||
|
client_secret: str,
|
||||||
|
redirect_uri: str
|
||||||
|
) -> Optional[Token]:
|
||||||
|
"""Exchange authorization code for tokens"""
|
||||||
|
db = get_database()
|
||||||
|
|
||||||
|
# Find and validate code
|
||||||
|
code_doc = await db.authorization_codes.find_one({
|
||||||
|
"code": code,
|
||||||
|
"client_id": client_id,
|
||||||
|
"redirect_uri": redirect_uri,
|
||||||
|
"used": False
|
||||||
|
})
|
||||||
|
|
||||||
|
if not code_doc:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Check if code is expired
|
||||||
|
if code_doc["expires_at"] < datetime.utcnow():
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Validate client secret
|
||||||
|
app = await db.applications.find_one({
|
||||||
|
"client_id": client_id,
|
||||||
|
"client_secret": client_secret
|
||||||
|
})
|
||||||
|
|
||||||
|
if not app:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Mark code as used
|
||||||
|
await db.authorization_codes.update_one(
|
||||||
|
{"_id": code_doc["_id"]},
|
||||||
|
{"$set": {"used": True}}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get user
|
||||||
|
user = await db.users.find_one({"_id": ObjectId(code_doc["user_id"])})
|
||||||
|
if not user:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Create tokens
|
||||||
|
return await TokenService.create_tokens(
|
||||||
|
str(user["_id"]),
|
||||||
|
{
|
||||||
|
"username": user["username"],
|
||||||
|
"role": user["role"],
|
||||||
|
"email": user["email"]
|
||||||
|
}
|
||||||
|
)
|
||||||
1
oauth/backend/app/utils/__init__.py
Normal file
1
oauth/backend/app/utils/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
"""Utility modules for OAuth backend"""
|
||||||
87
oauth/backend/app/utils/database.py
Normal file
87
oauth/backend/app/utils/database.py
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
"""MongoDB database connection and utilities"""
|
||||||
|
|
||||||
|
from motor.motor_asyncio import AsyncIOMotorClient
|
||||||
|
from typing import Optional
|
||||||
|
from app.config import settings
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Database:
|
||||||
|
client: Optional[AsyncIOMotorClient] = None
|
||||||
|
database = None
|
||||||
|
|
||||||
|
|
||||||
|
db = Database()
|
||||||
|
|
||||||
|
|
||||||
|
async def connect_database():
|
||||||
|
"""Create database connection"""
|
||||||
|
try:
|
||||||
|
db.client = AsyncIOMotorClient(settings.mongodb_url)
|
||||||
|
db.database = db.client[settings.database_name]
|
||||||
|
|
||||||
|
# Test connection
|
||||||
|
await db.client.server_info()
|
||||||
|
logger.info("Successfully connected to MongoDB")
|
||||||
|
|
||||||
|
# Create indexes
|
||||||
|
await create_indexes()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to connect to MongoDB: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
async def disconnect_database():
|
||||||
|
"""Close database connection"""
|
||||||
|
if db.client:
|
||||||
|
db.client.close()
|
||||||
|
logger.info("Disconnected from MongoDB")
|
||||||
|
|
||||||
|
|
||||||
|
async def create_indexes():
|
||||||
|
"""Create database indexes for better performance"""
|
||||||
|
try:
|
||||||
|
# Users collection indexes
|
||||||
|
users_collection = db.database["users"]
|
||||||
|
await users_collection.create_index("email", unique=True)
|
||||||
|
await users_collection.create_index("username", unique=True)
|
||||||
|
await users_collection.create_index("created_at")
|
||||||
|
|
||||||
|
# Applications collection indexes
|
||||||
|
apps_collection = db.database["applications"]
|
||||||
|
await apps_collection.create_index("client_id", unique=True)
|
||||||
|
await apps_collection.create_index("created_by")
|
||||||
|
await apps_collection.create_index("created_at")
|
||||||
|
|
||||||
|
# Auth history collection indexes
|
||||||
|
history_collection = db.database["auth_history"]
|
||||||
|
await history_collection.create_index("user_id")
|
||||||
|
await history_collection.create_index("application_id")
|
||||||
|
await history_collection.create_index("created_at")
|
||||||
|
await history_collection.create_index(
|
||||||
|
[("created_at", 1)],
|
||||||
|
expireAfterSeconds=2592000 # 30 days
|
||||||
|
)
|
||||||
|
|
||||||
|
# Refresh tokens collection indexes
|
||||||
|
tokens_collection = db.database["refresh_tokens"]
|
||||||
|
await tokens_collection.create_index("token", unique=True)
|
||||||
|
await tokens_collection.create_index("user_id")
|
||||||
|
await tokens_collection.create_index("expires_at")
|
||||||
|
await tokens_collection.create_index(
|
||||||
|
[("expires_at", 1)],
|
||||||
|
expireAfterSeconds=0
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info("Database indexes created successfully")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to create indexes: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def get_database():
|
||||||
|
"""Get database instance"""
|
||||||
|
return db.database
|
||||||
93
oauth/backend/app/utils/security.py
Normal file
93
oauth/backend/app/utils/security.py
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
"""Security utilities for password hashing and JWT tokens"""
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from typing import Optional, Dict, Any
|
||||||
|
from passlib.context import CryptContext
|
||||||
|
from jose import JWTError, jwt
|
||||||
|
from app.config import settings
|
||||||
|
import secrets
|
||||||
|
import string
|
||||||
|
|
||||||
|
# Password hashing context
|
||||||
|
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||||
|
|
||||||
|
|
||||||
|
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||||
|
"""Verify a plain password against a hashed password"""
|
||||||
|
return pwd_context.verify(plain_password, hashed_password)
|
||||||
|
|
||||||
|
|
||||||
|
def hash_password(password: str) -> str:
|
||||||
|
"""Hash a password"""
|
||||||
|
return pwd_context.hash(password)
|
||||||
|
|
||||||
|
|
||||||
|
def create_access_token(data: Dict[str, Any], expires_delta: Optional[timedelta] = None) -> str:
|
||||||
|
"""Create a JWT access token"""
|
||||||
|
to_encode = data.copy()
|
||||||
|
|
||||||
|
if expires_delta:
|
||||||
|
expire = datetime.now(timezone.utc) + expires_delta
|
||||||
|
else:
|
||||||
|
expire = datetime.now(timezone.utc) + timedelta(
|
||||||
|
minutes=settings.jwt_access_token_expire_minutes
|
||||||
|
)
|
||||||
|
|
||||||
|
to_encode.update({"exp": expire, "type": "access"})
|
||||||
|
encoded_jwt = jwt.encode(
|
||||||
|
to_encode,
|
||||||
|
settings.secret_key,
|
||||||
|
algorithm=settings.jwt_algorithm
|
||||||
|
)
|
||||||
|
return encoded_jwt
|
||||||
|
|
||||||
|
|
||||||
|
def create_refresh_token(data: Dict[str, Any], expires_delta: Optional[timedelta] = None) -> str:
|
||||||
|
"""Create a JWT refresh token"""
|
||||||
|
to_encode = data.copy()
|
||||||
|
|
||||||
|
if expires_delta:
|
||||||
|
expire = datetime.now(timezone.utc) + expires_delta
|
||||||
|
else:
|
||||||
|
expire = datetime.now(timezone.utc) + timedelta(
|
||||||
|
days=settings.jwt_refresh_token_expire_days
|
||||||
|
)
|
||||||
|
|
||||||
|
to_encode.update({"exp": expire, "type": "refresh"})
|
||||||
|
encoded_jwt = jwt.encode(
|
||||||
|
to_encode,
|
||||||
|
settings.secret_key,
|
||||||
|
algorithm=settings.jwt_algorithm
|
||||||
|
)
|
||||||
|
return encoded_jwt
|
||||||
|
|
||||||
|
|
||||||
|
def decode_token(token: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""Decode and verify a JWT token"""
|
||||||
|
try:
|
||||||
|
payload = jwt.decode(
|
||||||
|
token,
|
||||||
|
settings.secret_key,
|
||||||
|
algorithms=[settings.jwt_algorithm]
|
||||||
|
)
|
||||||
|
return payload
|
||||||
|
except JWTError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def generate_client_secret() -> str:
|
||||||
|
"""Generate a secure client secret"""
|
||||||
|
alphabet = string.ascii_letters + string.digits
|
||||||
|
return ''.join(secrets.choice(alphabet) for _ in range(32))
|
||||||
|
|
||||||
|
|
||||||
|
def generate_client_id() -> str:
|
||||||
|
"""Generate a unique client ID"""
|
||||||
|
alphabet = string.ascii_lowercase + string.digits
|
||||||
|
return ''.join(secrets.choice(alphabet) for _ in range(16))
|
||||||
|
|
||||||
|
|
||||||
|
def generate_authorization_code() -> str:
|
||||||
|
"""Generate a secure authorization code"""
|
||||||
|
alphabet = string.ascii_letters + string.digits
|
||||||
|
return ''.join(secrets.choice(alphabet) for _ in range(32))
|
||||||
36
oauth/backend/requirements.txt
Normal file
36
oauth/backend/requirements.txt
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
# Core
|
||||||
|
fastapi==0.109.0
|
||||||
|
uvicorn[standard]==0.27.0
|
||||||
|
python-multipart==0.0.6
|
||||||
|
|
||||||
|
# Database
|
||||||
|
motor==3.3.2
|
||||||
|
pymongo==4.6.1
|
||||||
|
|
||||||
|
# Redis
|
||||||
|
redis==5.0.1
|
||||||
|
aioredis==2.0.1
|
||||||
|
|
||||||
|
# Security
|
||||||
|
passlib[bcrypt]==1.7.4
|
||||||
|
python-jose[cryptography]==3.3.0
|
||||||
|
cryptography==41.0.7
|
||||||
|
|
||||||
|
# Validation
|
||||||
|
pydantic==2.5.3
|
||||||
|
pydantic-settings==2.1.0
|
||||||
|
email-validator==2.1.0
|
||||||
|
|
||||||
|
# CORS
|
||||||
|
python-dotenv==1.0.0
|
||||||
|
|
||||||
|
# Date/Time
|
||||||
|
python-dateutil==2.8.2
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
pytest==7.4.4
|
||||||
|
pytest-asyncio==0.23.3
|
||||||
|
httpx==0.26.0
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
loguru==0.7.2
|
||||||
Reference in New Issue
Block a user