mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2025-12-06 19:46:44 +03:00
Added Header Authentication
This commit is contained in:
13
.env.example
13
.env.example
@@ -66,3 +66,16 @@ DOCKER_TAG=latest
|
||||
# TLS/SSL Configuration
|
||||
# GITEA_SKIP_TLS_VERIFY=false # WARNING: Only use for testing
|
||||
|
||||
# ===========================================
|
||||
# AUTHENTICATION CONFIGURATION
|
||||
# ===========================================
|
||||
|
||||
# Header Authentication (for Reverse Proxy SSO)
|
||||
# Enable automatic authentication via reverse proxy headers
|
||||
# HEADER_AUTH_ENABLED=false
|
||||
# HEADER_AUTH_USER_HEADER=X-Authentik-Username
|
||||
# HEADER_AUTH_EMAIL_HEADER=X-Authentik-Email
|
||||
# HEADER_AUTH_NAME_HEADER=X-Authentik-Name
|
||||
# HEADER_AUTH_AUTO_PROVISION=false
|
||||
# HEADER_AUTH_ALLOWED_DOMAINS=example.com,company.org
|
||||
|
||||
|
||||
65
README.md
65
README.md
@@ -218,6 +218,71 @@ If upgrading from a version without token encryption:
|
||||
bun run migrate:encrypt-tokens
|
||||
```
|
||||
|
||||
## Authentication
|
||||
|
||||
Gitea Mirror supports multiple authentication methods. **Email/password authentication is the default and always enabled.**
|
||||
|
||||
### 1. Email & Password (Default)
|
||||
The standard authentication method. First user to sign up becomes the admin.
|
||||
|
||||
### 2. Single Sign-On (SSO) with OIDC
|
||||
Enable users to sign in with external identity providers like Google, Azure AD, Okta, Authentik, or any OIDC-compliant service.
|
||||
|
||||
**Configuration:**
|
||||
1. Navigate to Settings → Authentication & SSO
|
||||
2. Click "Add Provider"
|
||||
3. Enter your OIDC provider details:
|
||||
- Issuer URL (e.g., `https://accounts.google.com`)
|
||||
- Client ID and Secret from your provider
|
||||
- Use the "Discover" button to auto-fill endpoints
|
||||
|
||||
**Redirect URL for your provider:**
|
||||
```
|
||||
https://your-domain.com/api/auth/sso/callback/{provider-id}
|
||||
```
|
||||
|
||||
### 3. Header Authentication (Reverse Proxy)
|
||||
Perfect for automatic authentication when using reverse proxies like Authentik, Authelia, or Traefik Forward Auth.
|
||||
|
||||
**Environment Variables:**
|
||||
```bash
|
||||
# Enable header authentication
|
||||
HEADER_AUTH_ENABLED=true
|
||||
|
||||
# Header names (customize based on your proxy)
|
||||
HEADER_AUTH_USER_HEADER=X-Authentik-Username
|
||||
HEADER_AUTH_EMAIL_HEADER=X-Authentik-Email
|
||||
HEADER_AUTH_NAME_HEADER=X-Authentik-Name
|
||||
|
||||
# Auto-provision new users
|
||||
HEADER_AUTH_AUTO_PROVISION=true
|
||||
|
||||
# Restrict to specific email domains (optional)
|
||||
HEADER_AUTH_ALLOWED_DOMAINS=example.com,company.org
|
||||
```
|
||||
|
||||
**How it works:**
|
||||
- Users authenticated by your reverse proxy are automatically logged in
|
||||
- No additional login step required
|
||||
- New users can be auto-provisioned if enabled
|
||||
- Falls back to regular authentication if headers are missing
|
||||
|
||||
**Example Authentik Configuration:**
|
||||
```nginx
|
||||
# In your reverse proxy configuration
|
||||
proxy_set_header X-Authentik-Username $authentik_username;
|
||||
proxy_set_header X-Authentik-Email $authentik_email;
|
||||
proxy_set_header X-Authentik-Name $authentik_name;
|
||||
```
|
||||
|
||||
### 4. OAuth Applications (Act as Identity Provider)
|
||||
Gitea Mirror can also act as an OIDC provider for other applications. Register OAuth applications in Settings → Authentication & SSO → OAuth Applications tab.
|
||||
|
||||
**Use cases:**
|
||||
- Allow other services to authenticate using Gitea Mirror accounts
|
||||
- Create service-to-service authentication
|
||||
- Build integrations with your Gitea Mirror instance
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please read our [Contributing Guidelines](CONTRIBUTING.md) for details on our code of conduct and the process for submitting pull requests.
|
||||
|
||||
@@ -52,6 +52,13 @@ services:
|
||||
- DELAY=${DELAY:-3600}
|
||||
# Optional: Skip TLS verification (insecure, use only for testing)
|
||||
# - GITEA_SKIP_TLS_VERIFY=${GITEA_SKIP_TLS_VERIFY:-false}
|
||||
# Header Authentication (for Reverse Proxy SSO)
|
||||
- HEADER_AUTH_ENABLED=${HEADER_AUTH_ENABLED:-false}
|
||||
- HEADER_AUTH_USER_HEADER=${HEADER_AUTH_USER_HEADER:-X-Authentik-Username}
|
||||
- HEADER_AUTH_EMAIL_HEADER=${HEADER_AUTH_EMAIL_HEADER:-X-Authentik-Email}
|
||||
- HEADER_AUTH_NAME_HEADER=${HEADER_AUTH_NAME_HEADER:-X-Authentik-Name}
|
||||
- HEADER_AUTH_AUTO_PROVISION=${HEADER_AUTH_AUTO_PROVISION:-false}
|
||||
- HEADER_AUTH_ALLOWED_DOMAINS=${HEADER_AUTH_ALLOWED_DOMAINS:-}
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=3", "--spider", "http://localhost:4321/api/health"]
|
||||
interval: 30s
|
||||
|
||||
@@ -9,9 +9,10 @@ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, D
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { apiRequest, showErrorToast } from '@/lib/utils';
|
||||
import { toast } from 'sonner';
|
||||
import { Plus, Trash2, ExternalLink, Loader2, AlertCircle, Copy } from 'lucide-react';
|
||||
import { Plus, Trash2, ExternalLink, Loader2, AlertCircle, Copy, Shield, Info } from 'lucide-react';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Skeleton } from '../ui/skeleton';
|
||||
import { Badge } from '../ui/badge';
|
||||
|
||||
interface SSOProvider {
|
||||
id: string;
|
||||
@@ -43,6 +44,7 @@ export function SSOSettings() {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [showProviderDialog, setShowProviderDialog] = useState(false);
|
||||
const [isDiscovering, setIsDiscovering] = useState(false);
|
||||
const [headerAuthEnabled, setHeaderAuthEnabled] = useState(false);
|
||||
|
||||
// Form states for new provider
|
||||
const [providerForm, setProviderForm] = useState({
|
||||
@@ -66,8 +68,13 @@ export function SSOSettings() {
|
||||
const loadData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const providersRes = await apiRequest<SSOProvider[]>('/sso/providers');
|
||||
const [providersRes, headerAuthStatus] = await Promise.all([
|
||||
apiRequest<SSOProvider[]>('/sso/providers'),
|
||||
apiRequest<{ enabled: boolean }>('/auth/header-status').catch(() => ({ enabled: false }))
|
||||
]);
|
||||
|
||||
setProviders(providersRes);
|
||||
setHeaderAuthEnabled(headerAuthStatus.enabled);
|
||||
} catch (error) {
|
||||
showErrorToast(error, toast);
|
||||
} finally {
|
||||
@@ -183,16 +190,58 @@ export function SSOSettings() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Info Alert for Authentication Flow */}
|
||||
{providers.length === 0 && (
|
||||
<Alert>
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
<strong>Current authentication:</strong> Users sign in with email and password only.
|
||||
Add SSO providers to enable users to sign in with their existing accounts from external services like Google, Azure AD, or any OIDC-compliant provider.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
{/* Authentication Methods Overview */}
|
||||
<Card className="mb-6">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Active Authentication Methods</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{/* Email & Password - Always enabled */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-2 w-2 rounded-full bg-green-500" />
|
||||
<span className="text-sm font-medium">Email & Password</span>
|
||||
<Badge variant="secondary" className="text-xs">Default</Badge>
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">Always enabled</span>
|
||||
</div>
|
||||
|
||||
{/* Header Authentication Status */}
|
||||
{headerAuthEnabled && (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-2 w-2 rounded-full bg-green-500" />
|
||||
<span className="text-sm font-medium">Header Authentication</span>
|
||||
<Badge variant="secondary" className="text-xs">Auto-login</Badge>
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">Via reverse proxy</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* SSO Providers Status */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`h-2 w-2 rounded-full ${providers.length > 0 ? 'bg-green-500' : 'bg-muted'}`} />
|
||||
<span className="text-sm font-medium">SSO/OIDC Providers</span>
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{providers.length > 0 ? `${providers.length} provider${providers.length !== 1 ? 's' : ''} configured` : 'Not configured'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Header Auth Info */}
|
||||
{headerAuthEnabled && (
|
||||
<Alert className="mt-4">
|
||||
<Shield className="h-4 w-4" />
|
||||
<AlertDescription className="text-xs">
|
||||
Header authentication is enabled. Users authenticated by your reverse proxy will be automatically logged in.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* SSO Providers */}
|
||||
<Card>
|
||||
|
||||
135
src/lib/auth-header.ts
Normal file
135
src/lib/auth-header.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
import { db, users } from "./db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
export interface HeaderAuthConfig {
|
||||
enabled: boolean;
|
||||
userHeader: string;
|
||||
emailHeader?: string;
|
||||
nameHeader?: string;
|
||||
autoProvision: boolean;
|
||||
allowedDomains?: string[];
|
||||
}
|
||||
|
||||
// Default configuration - DISABLED by default
|
||||
export const defaultHeaderAuthConfig: HeaderAuthConfig = {
|
||||
enabled: false,
|
||||
userHeader: "X-Authentik-Username", // Common header name
|
||||
emailHeader: "X-Authentik-Email",
|
||||
nameHeader: "X-Authentik-Name",
|
||||
autoProvision: false,
|
||||
allowedDomains: [],
|
||||
};
|
||||
|
||||
// Get header auth config from environment or database
|
||||
export function getHeaderAuthConfig(): HeaderAuthConfig {
|
||||
// Check environment variables for header auth config
|
||||
const envConfig: Partial<HeaderAuthConfig> = {
|
||||
enabled: process.env.HEADER_AUTH_ENABLED === "true",
|
||||
userHeader: process.env.HEADER_AUTH_USER_HEADER || defaultHeaderAuthConfig.userHeader,
|
||||
emailHeader: process.env.HEADER_AUTH_EMAIL_HEADER || defaultHeaderAuthConfig.emailHeader,
|
||||
nameHeader: process.env.HEADER_AUTH_NAME_HEADER || defaultHeaderAuthConfig.nameHeader,
|
||||
autoProvision: process.env.HEADER_AUTH_AUTO_PROVISION === "true",
|
||||
allowedDomains: process.env.HEADER_AUTH_ALLOWED_DOMAINS?.split(",").map(d => d.trim()),
|
||||
};
|
||||
|
||||
return {
|
||||
...defaultHeaderAuthConfig,
|
||||
...envConfig,
|
||||
};
|
||||
}
|
||||
|
||||
// Check if header authentication is enabled
|
||||
export function isHeaderAuthEnabled(): boolean {
|
||||
const config = getHeaderAuthConfig();
|
||||
return config.enabled === true;
|
||||
}
|
||||
|
||||
// Extract user info from headers
|
||||
export function extractUserFromHeaders(headers: Headers): {
|
||||
username?: string;
|
||||
email?: string;
|
||||
name?: string;
|
||||
} | null {
|
||||
const config = getHeaderAuthConfig();
|
||||
|
||||
if (!config.enabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const username = headers.get(config.userHeader);
|
||||
const email = config.emailHeader ? headers.get(config.emailHeader) : undefined;
|
||||
const name = config.nameHeader ? headers.get(config.nameHeader) : undefined;
|
||||
|
||||
if (!username) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If allowed domains are configured, check email domain
|
||||
if (config.allowedDomains && config.allowedDomains.length > 0 && email) {
|
||||
const domain = email.split("@")[1];
|
||||
if (!config.allowedDomains.includes(domain)) {
|
||||
console.warn(`Header auth rejected: email domain ${domain} not in allowed list`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return { username, email, name };
|
||||
}
|
||||
|
||||
// Find or create user from header auth
|
||||
export async function authenticateWithHeaders(headers: Headers) {
|
||||
const userInfo = extractUserFromHeaders(headers);
|
||||
|
||||
if (!userInfo || !userInfo.username) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const config = getHeaderAuthConfig();
|
||||
|
||||
// Try to find existing user by username or email
|
||||
let existingUser = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.username, userInfo.username))
|
||||
.limit(1);
|
||||
|
||||
if (existingUser.length === 0 && userInfo.email) {
|
||||
existingUser = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.email, userInfo.email))
|
||||
.limit(1);
|
||||
}
|
||||
|
||||
if (existingUser.length > 0) {
|
||||
return existingUser[0];
|
||||
}
|
||||
|
||||
// If auto-provisioning is disabled, don't create new users
|
||||
if (!config.autoProvision) {
|
||||
console.warn(`Header auth: User ${userInfo.username} not found and auto-provisioning is disabled`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create new user if auto-provisioning is enabled
|
||||
try {
|
||||
const newUser = {
|
||||
id: nanoid(),
|
||||
username: userInfo.username,
|
||||
email: userInfo.email || `${userInfo.username}@header-auth.local`,
|
||||
emailVerified: true, // Trust the auth provider
|
||||
name: userInfo.name || userInfo.username,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
await db.insert(users).values(newUser);
|
||||
console.log(`Header auth: Auto-provisioned new user ${userInfo.username}`);
|
||||
|
||||
return newUser;
|
||||
} catch (error) {
|
||||
console.error("Failed to auto-provision user from header auth:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { startCleanupService, stopCleanupService } from './lib/cleanup-service';
|
||||
import { initializeShutdownManager, registerShutdownCallback } from './lib/shutdown-manager';
|
||||
import { setupSignalHandlers } from './lib/signal-handlers';
|
||||
import { auth } from './lib/auth';
|
||||
import { isHeaderAuthEnabled, authenticateWithHeaders } from './lib/auth-header';
|
||||
|
||||
// Flag to track if recovery has been initialized
|
||||
let recoveryInitialized = false;
|
||||
@@ -12,7 +13,7 @@ let cleanupServiceStarted = false;
|
||||
let shutdownManagerInitialized = false;
|
||||
|
||||
export const onRequest = defineMiddleware(async (context, next) => {
|
||||
// Handle Better Auth session
|
||||
// First, try Better Auth session (cookie-based)
|
||||
try {
|
||||
const session = await auth.api.getSession({
|
||||
headers: context.request.headers,
|
||||
@@ -22,8 +23,35 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
||||
context.locals.user = session.user;
|
||||
context.locals.session = session.session;
|
||||
} else {
|
||||
context.locals.user = null;
|
||||
context.locals.session = null;
|
||||
// No cookie session, check for header authentication
|
||||
if (isHeaderAuthEnabled()) {
|
||||
const headerUser = await authenticateWithHeaders(context.request.headers);
|
||||
if (headerUser) {
|
||||
// Create a session-like object for header auth
|
||||
context.locals.user = {
|
||||
id: headerUser.id,
|
||||
email: headerUser.email,
|
||||
emailVerified: headerUser.emailVerified,
|
||||
name: headerUser.name || headerUser.username,
|
||||
username: headerUser.username,
|
||||
createdAt: headerUser.createdAt,
|
||||
updatedAt: headerUser.updatedAt,
|
||||
};
|
||||
context.locals.session = {
|
||||
id: `header-${headerUser.id}`,
|
||||
userId: headerUser.id,
|
||||
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000), // 1 day
|
||||
ipAddress: context.request.headers.get('x-forwarded-for') || context.clientAddress,
|
||||
userAgent: context.request.headers.get('user-agent'),
|
||||
};
|
||||
} else {
|
||||
context.locals.user = null;
|
||||
context.locals.session = null;
|
||||
}
|
||||
} else {
|
||||
context.locals.user = null;
|
||||
context.locals.session = null;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// If there's an error getting the session, set to null
|
||||
|
||||
16
src/pages/api/auth/header-status.ts
Normal file
16
src/pages/api/auth/header-status.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { APIRoute } from "astro";
|
||||
import { getHeaderAuthConfig } from "@/lib/auth-header";
|
||||
|
||||
export const GET: APIRoute = async () => {
|
||||
const config = getHeaderAuthConfig();
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
enabled: config.enabled,
|
||||
userHeader: config.userHeader,
|
||||
autoProvision: config.autoProvision,
|
||||
hasAllowedDomains: config.allowedDomains && config.allowedDomains.length > 0,
|
||||
}), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user