mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2025-12-06 03:26:44 +03:00
Compare commits
1 Commits
749ad4a694
...
v3-sso
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5b4482c8a |
@@ -29,6 +29,8 @@ This guide explains how to test SSO authentication locally with Gitea Mirror.
|
||||
- Client Secret: (from Google Console)
|
||||
- Save the provider
|
||||
|
||||
Note: Provider creation uses Better Auth's SSO registration under the hood. Do not call the legacy `POST /api/sso/providers` endpoint directly; it is deprecated and reserved for internal mirroring. Use the UI or Better Auth client/server registration APIs instead.
|
||||
|
||||
## Option 2: Using Keycloak (Local Identity Provider)
|
||||
|
||||
### Setup with Docker:
|
||||
@@ -113,8 +115,8 @@ npm start
|
||||
|
||||
2. **Provider not showing in login**
|
||||
- Check browser console for errors
|
||||
- Verify provider was saved successfully
|
||||
- Check `/api/sso/providers` returns your providers
|
||||
- Verify provider was saved successfully (via UI)
|
||||
- Check `/api/sso/providers` (or `/api/sso/providers/public`) returns your providers. This list mirrors what was registered with Better Auth.
|
||||
|
||||
3. **Redirect URI mismatch**
|
||||
- Ensure the redirect URI in your OAuth app matches exactly:
|
||||
@@ -190,4 +192,4 @@ After successful SSO setup:
|
||||
1. Test user attribute mapping
|
||||
2. Configure role-based access
|
||||
3. Set up SAML if needed
|
||||
4. Test with your organization's actual IdP
|
||||
4. Test with your organization's actual IdP
|
||||
|
||||
31756
docs/better-auth-docs.md
Normal file
31756
docs/better-auth-docs.md
Normal file
File diff suppressed because it is too large
Load Diff
1
drizzle/0006_illegal_spyke.sql
Normal file
1
drizzle/0006_illegal_spyke.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE `sso_providers` ADD `saml_config` text;
|
||||
1948
drizzle/meta/0006_snapshot.json
Normal file
1948
drizzle/meta/0006_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -43,6 +43,13 @@
|
||||
"when": 1757786449446,
|
||||
"tag": "0005_polite_preak",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 6,
|
||||
"version": "6",
|
||||
"when": 1757825311459,
|
||||
"tag": "0006_illegal_spyke",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -70,6 +70,8 @@ export function LoginForm() {
|
||||
domain: domain,
|
||||
providerId: providerId,
|
||||
callbackURL: `${baseURL}/`,
|
||||
errorCallbackURL: `${baseURL}/auth-error`,
|
||||
newUserCallbackURL: `${baseURL}/`,
|
||||
scopes: ['openid', 'email', 'profile'], // TODO: This is not being respected by the SSO plugin.
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -14,6 +14,7 @@ import { Badge } from '../ui/badge';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { MultiSelect } from '@/components/ui/multi-select';
|
||||
import { authClient } from '@/lib/auth-client';
|
||||
|
||||
function isTrustedIssuer(issuer: string, allowedHosts: string[]): boolean {
|
||||
try {
|
||||
@@ -158,50 +159,146 @@ export function SSOSettings() {
|
||||
const createProvider = async () => {
|
||||
setAddingProvider(true);
|
||||
try {
|
||||
const requestData: any = {
|
||||
providerId: providerForm.providerId,
|
||||
issuer: providerForm.issuer,
|
||||
domain: providerForm.domain,
|
||||
organizationId: providerForm.organizationId || undefined,
|
||||
providerType,
|
||||
};
|
||||
|
||||
if (providerType === 'oidc') {
|
||||
requestData.clientId = providerForm.clientId;
|
||||
requestData.clientSecret = providerForm.clientSecret;
|
||||
requestData.authorizationEndpoint = providerForm.authorizationEndpoint;
|
||||
requestData.tokenEndpoint = providerForm.tokenEndpoint;
|
||||
requestData.jwksEndpoint = providerForm.jwksEndpoint;
|
||||
requestData.userInfoEndpoint = providerForm.userInfoEndpoint;
|
||||
requestData.discoveryEndpoint = providerForm.discoveryEndpoint;
|
||||
requestData.scopes = providerForm.scopes;
|
||||
requestData.pkce = providerForm.pkce;
|
||||
} else {
|
||||
requestData.entryPoint = providerForm.entryPoint;
|
||||
requestData.cert = providerForm.cert;
|
||||
requestData.callbackUrl = providerForm.callbackUrl || `${window.location.origin}/api/auth/sso/saml2/callback/${providerForm.providerId}`;
|
||||
requestData.audience = providerForm.audience || window.location.origin;
|
||||
requestData.wantAssertionsSigned = providerForm.wantAssertionsSigned;
|
||||
requestData.signatureAlgorithm = providerForm.signatureAlgorithm;
|
||||
requestData.digestAlgorithm = providerForm.digestAlgorithm;
|
||||
requestData.identifierFormat = providerForm.identifierFormat;
|
||||
}
|
||||
|
||||
if (editingProvider) {
|
||||
// Update existing provider
|
||||
const updatedProvider = await apiRequest<SSOProvider>(`/sso/providers?id=${editingProvider.id}`, {
|
||||
method: 'PUT',
|
||||
data: requestData,
|
||||
});
|
||||
setProviders(providers.map(p => p.id === editingProvider.id ? updatedProvider : p));
|
||||
toast.success('SSO provider updated successfully');
|
||||
// Delete and recreate to align with Better Auth docs
|
||||
try {
|
||||
await apiRequest(`/sso/providers?id=${editingProvider.id}`, { method: 'DELETE' });
|
||||
} catch (e) {
|
||||
// Continue even if local delete fails; registration will mirror latest
|
||||
console.warn('Failed to delete local provider before recreate', e);
|
||||
}
|
||||
|
||||
// Recreate via Better Auth registration
|
||||
try {
|
||||
if (providerType === 'oidc') {
|
||||
await authClient.sso.register({
|
||||
providerId: providerForm.providerId,
|
||||
issuer: providerForm.issuer,
|
||||
domain: providerForm.domain,
|
||||
organizationId: providerForm.organizationId || undefined,
|
||||
oidcConfig: {
|
||||
clientId: providerForm.clientId || undefined,
|
||||
clientSecret: providerForm.clientSecret || undefined,
|
||||
authorizationEndpoint: providerForm.authorizationEndpoint || undefined,
|
||||
tokenEndpoint: providerForm.tokenEndpoint || undefined,
|
||||
jwksEndpoint: providerForm.jwksEndpoint || undefined,
|
||||
userInfoEndpoint: providerForm.userInfoEndpoint || undefined,
|
||||
discoveryEndpoint: providerForm.discoveryEndpoint || undefined,
|
||||
scopes: providerForm.scopes,
|
||||
pkce: providerForm.pkce,
|
||||
},
|
||||
mapping: {
|
||||
id: 'sub',
|
||||
email: 'email',
|
||||
emailVerified: 'email_verified',
|
||||
name: 'name',
|
||||
image: 'picture',
|
||||
},
|
||||
} as any);
|
||||
} else {
|
||||
await authClient.sso.register({
|
||||
providerId: providerForm.providerId,
|
||||
issuer: providerForm.issuer,
|
||||
domain: providerForm.domain,
|
||||
organizationId: providerForm.organizationId || undefined,
|
||||
samlConfig: {
|
||||
entryPoint: providerForm.entryPoint,
|
||||
cert: providerForm.cert,
|
||||
callbackUrl:
|
||||
providerForm.callbackUrl ||
|
||||
`${window.location.origin}/api/auth/sso/saml2/callback/${providerForm.providerId}`,
|
||||
audience: providerForm.audience || window.location.origin,
|
||||
wantAssertionsSigned: providerForm.wantAssertionsSigned,
|
||||
signatureAlgorithm: providerForm.signatureAlgorithm,
|
||||
digestAlgorithm: providerForm.digestAlgorithm,
|
||||
identifierFormat: providerForm.identifierFormat,
|
||||
},
|
||||
mapping: {
|
||||
id: 'nameID',
|
||||
email: 'email',
|
||||
name: 'displayName',
|
||||
firstName: 'givenName',
|
||||
lastName: 'surname',
|
||||
},
|
||||
} as any);
|
||||
}
|
||||
toast.success('SSO provider recreated');
|
||||
} catch (e: any) {
|
||||
console.error('Recreate failed', e);
|
||||
const msg = typeof e?.message === 'string' ? e.message : String(e);
|
||||
// Common case: providerId already exists in Better Auth
|
||||
if (msg.toLowerCase().includes('already exists')) {
|
||||
toast.error('Provider ID already exists in auth server. Choose a new Provider ID and try again.');
|
||||
} else {
|
||||
showErrorToast(e, toast);
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh providers from our API after registration mirrors into DB
|
||||
const refreshed = await apiRequest<SSOProvider[] | { providers: SSOProvider[] }>(
|
||||
'/sso/providers'
|
||||
);
|
||||
setProviders(Array.isArray(refreshed) ? refreshed : refreshed?.providers || []);
|
||||
} else {
|
||||
// Create new provider
|
||||
const newProvider = await apiRequest<SSOProvider>('/sso/providers', {
|
||||
method: 'POST',
|
||||
data: requestData,
|
||||
});
|
||||
setProviders([...providers, newProvider]);
|
||||
// Create new provider - follow Better Auth docs using the SSO client
|
||||
if (providerType === 'oidc') {
|
||||
await authClient.sso.register({
|
||||
providerId: providerForm.providerId,
|
||||
issuer: providerForm.issuer,
|
||||
domain: providerForm.domain,
|
||||
organizationId: providerForm.organizationId || undefined,
|
||||
oidcConfig: {
|
||||
clientId: providerForm.clientId || undefined,
|
||||
clientSecret: providerForm.clientSecret || undefined,
|
||||
authorizationEndpoint: providerForm.authorizationEndpoint || undefined,
|
||||
tokenEndpoint: providerForm.tokenEndpoint || undefined,
|
||||
jwksEndpoint: providerForm.jwksEndpoint || undefined,
|
||||
userInfoEndpoint: providerForm.userInfoEndpoint || undefined,
|
||||
discoveryEndpoint: providerForm.discoveryEndpoint || undefined,
|
||||
scopes: providerForm.scopes,
|
||||
pkce: providerForm.pkce,
|
||||
},
|
||||
mapping: {
|
||||
id: 'sub',
|
||||
email: 'email',
|
||||
emailVerified: 'email_verified',
|
||||
name: 'name',
|
||||
image: 'picture',
|
||||
},
|
||||
} as any);
|
||||
} else {
|
||||
await authClient.sso.register({
|
||||
providerId: providerForm.providerId,
|
||||
issuer: providerForm.issuer,
|
||||
domain: providerForm.domain,
|
||||
organizationId: providerForm.organizationId || undefined,
|
||||
samlConfig: {
|
||||
entryPoint: providerForm.entryPoint,
|
||||
cert: providerForm.cert,
|
||||
callbackUrl:
|
||||
providerForm.callbackUrl ||
|
||||
`${window.location.origin}/api/auth/sso/saml2/callback/${providerForm.providerId}`,
|
||||
audience: providerForm.audience || window.location.origin,
|
||||
wantAssertionsSigned: providerForm.wantAssertionsSigned,
|
||||
signatureAlgorithm: providerForm.signatureAlgorithm,
|
||||
digestAlgorithm: providerForm.digestAlgorithm,
|
||||
identifierFormat: providerForm.identifierFormat,
|
||||
},
|
||||
mapping: {
|
||||
id: 'nameID',
|
||||
email: 'email',
|
||||
name: 'displayName',
|
||||
firstName: 'givenName',
|
||||
lastName: 'surname',
|
||||
},
|
||||
} as any);
|
||||
}
|
||||
|
||||
// Refresh providers from our API after registration mirrors into DB
|
||||
const refreshed = await apiRequest<SSOProvider[] | { providers: SSOProvider[] }>(
|
||||
'/sso/providers'
|
||||
);
|
||||
setProviders(Array.isArray(refreshed) ? refreshed : refreshed?.providers || []);
|
||||
toast.success('SSO provider created successfully');
|
||||
}
|
||||
|
||||
@@ -724,4 +821,4 @@ export function SSOSettings() {
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -617,6 +617,7 @@ export const ssoProviders = sqliteTable("sso_providers", {
|
||||
issuer: text("issuer").notNull(),
|
||||
domain: text("domain").notNull(),
|
||||
oidcConfig: text("oidc_config").notNull(), // JSON string with OIDC configuration
|
||||
samlConfig: text("saml_config"), // JSON string with SAML configuration (optional)
|
||||
userId: text("user_id").notNull(), // Admin who created this provider
|
||||
providerId: text("provider_id").notNull().unique(), // Unique identifier for the provider
|
||||
organizationId: text("organization_id"), // Optional - if provider is linked to an organization
|
||||
|
||||
@@ -2,6 +2,9 @@ import type { APIContext } from "astro";
|
||||
import { createSecureErrorResponse } from "@/lib/utils";
|
||||
import { requireAuth } from "@/lib/utils/auth-helpers";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { db, ssoProviders } from "@/lib/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
// POST /api/auth/sso/register - Register a new SSO provider using Better Auth
|
||||
export async function POST(context: APIContext) {
|
||||
@@ -168,7 +171,47 @@ export async function POST(context: APIContext) {
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
|
||||
// Mirror provider into our local sso_providers table for UI listing
|
||||
try {
|
||||
const existing = await db
|
||||
.select()
|
||||
.from(ssoProviders)
|
||||
.where(eq(ssoProviders.providerId, providerId))
|
||||
.limit(1);
|
||||
|
||||
const values: any = {
|
||||
issuer: registrationBody.issuer,
|
||||
domain: registrationBody.domain,
|
||||
organizationId: registrationBody.organizationId,
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
if (registrationBody.oidcConfig) {
|
||||
values.oidcConfig = JSON.stringify(registrationBody.oidcConfig);
|
||||
}
|
||||
if (registrationBody.samlConfig) {
|
||||
values.samlConfig = JSON.stringify(registrationBody.samlConfig);
|
||||
}
|
||||
|
||||
if (existing.length > 0) {
|
||||
await db.update(ssoProviders).set(values).where(eq(ssoProviders.id, existing[0].id));
|
||||
} else {
|
||||
await db.insert(ssoProviders).values({
|
||||
id: nanoid(),
|
||||
issuer: registrationBody.issuer,
|
||||
domain: registrationBody.domain,
|
||||
oidcConfig: JSON.stringify(registrationBody.oidcConfig || {}),
|
||||
samlConfig: registrationBody.samlConfig ? JSON.stringify(registrationBody.samlConfig) : undefined,
|
||||
userId: user.id,
|
||||
providerId: registrationBody.providerId,
|
||||
organizationId: registrationBody.organizationId,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// Do not fail the main request if mirroring to local table fails
|
||||
console.warn("Failed to mirror SSO provider to local DB:", e);
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify(result), {
|
||||
status: 201,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
@@ -199,4 +242,4 @@ export async function GET(context: APIContext) {
|
||||
} catch (error) {
|
||||
return createSecureErrorResponse(error, "SSO provider listing");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { requireAuth } from "@/lib/utils/auth-helpers";
|
||||
import { db, ssoProviders } from "@/lib/db";
|
||||
import { nanoid } from "nanoid";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { auth } from "@/lib/auth";
|
||||
|
||||
// GET /api/sso/providers - List all SSO providers
|
||||
export async function GET(context: APIContext) {
|
||||
@@ -29,7 +30,11 @@ export async function GET(context: APIContext) {
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/sso/providers - Create a new SSO provider
|
||||
// POST /api/sso/providers - DEPRECATED legacy create (use Better Auth registration)
|
||||
// This route remains for backward-compatibility only. Preferred flow:
|
||||
// - Client/UI calls authClient.sso.register(...) to register with Better Auth
|
||||
// - Server mirrors provider into local DB for listing
|
||||
// Creation via this route is discouraged and may be removed in a future version.
|
||||
export async function POST(context: APIContext) {
|
||||
try {
|
||||
const { user, response } = await requireAuth(context);
|
||||
@@ -45,10 +50,12 @@ export async function POST(context: APIContext) {
|
||||
tokenEndpoint,
|
||||
jwksEndpoint,
|
||||
userInfoEndpoint,
|
||||
discoveryEndpoint,
|
||||
mapping,
|
||||
providerId,
|
||||
organizationId,
|
||||
scopes,
|
||||
pkce,
|
||||
} = body;
|
||||
|
||||
// Validate required fields
|
||||
@@ -62,6 +69,32 @@ export async function POST(context: APIContext) {
|
||||
);
|
||||
}
|
||||
|
||||
// Clean issuer URL (remove trailing slash); validate URL format
|
||||
let cleanIssuer = issuer;
|
||||
try {
|
||||
const issuerUrl = new URL(issuer.toString().trim());
|
||||
cleanIssuer = issuerUrl.toString().replace(/\/$/, "");
|
||||
} catch {
|
||||
return new Response(
|
||||
JSON.stringify({ error: `Invalid issuer URL format: ${issuer}` }),
|
||||
{ status: 400, headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
}
|
||||
|
||||
// Validate OIDC endpoints: require discoveryEndpoint or at least authorization+token
|
||||
const hasDiscovery = typeof discoveryEndpoint === 'string' && discoveryEndpoint.trim() !== '';
|
||||
const hasCoreEndpoints = typeof authorizationEndpoint === 'string' && authorizationEndpoint.trim() !== ''
|
||||
&& typeof tokenEndpoint === 'string' && tokenEndpoint.trim() !== '';
|
||||
if (!hasDiscovery && !hasCoreEndpoints) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: "Invalid OIDC configuration",
|
||||
details: "Provide discoveryEndpoint, or both authorizationEndpoint and tokenEndpoint."
|
||||
}),
|
||||
{ status: 400, headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
}
|
||||
|
||||
// Check if provider ID already exists
|
||||
const existing = await db
|
||||
.select()
|
||||
@@ -79,15 +112,27 @@ export async function POST(context: APIContext) {
|
||||
);
|
||||
}
|
||||
|
||||
// Create OIDC config object
|
||||
// Helper to validate and normalize URL strings (optional fields allowed)
|
||||
const validateUrl = (value?: string) => {
|
||||
if (!value || typeof value !== 'string' || value.trim() === '') return undefined;
|
||||
try {
|
||||
return new URL(value.trim()).toString();
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
// Create OIDC config object (store as-is for UI and for Better Auth registration)
|
||||
const oidcConfig = {
|
||||
clientId,
|
||||
clientSecret,
|
||||
authorizationEndpoint,
|
||||
tokenEndpoint,
|
||||
jwksEndpoint,
|
||||
userInfoEndpoint,
|
||||
authorizationEndpoint: validateUrl(authorizationEndpoint),
|
||||
tokenEndpoint: validateUrl(tokenEndpoint),
|
||||
jwksEndpoint: validateUrl(jwksEndpoint),
|
||||
userInfoEndpoint: validateUrl(userInfoEndpoint),
|
||||
discoveryEndpoint: validateUrl(discoveryEndpoint),
|
||||
scopes: scopes || ["openid", "email", "profile"],
|
||||
pkce: pkce !== false,
|
||||
mapping: mapping || {
|
||||
id: "sub",
|
||||
email: "email",
|
||||
@@ -97,12 +142,55 @@ export async function POST(context: APIContext) {
|
||||
},
|
||||
};
|
||||
|
||||
// First, register with Better Auth so the SSO plugin has the provider
|
||||
try {
|
||||
const headers = new Headers();
|
||||
const cookieHeader = context.request.headers.get("cookie");
|
||||
if (cookieHeader) headers.set("cookie", cookieHeader);
|
||||
|
||||
const res = await auth.api.registerSSOProvider({
|
||||
body: {
|
||||
providerId,
|
||||
issuer: cleanIssuer,
|
||||
domain,
|
||||
organizationId,
|
||||
oidcConfig: {
|
||||
clientId: oidcConfig.clientId,
|
||||
clientSecret: oidcConfig.clientSecret,
|
||||
authorizationEndpoint: oidcConfig.authorizationEndpoint,
|
||||
tokenEndpoint: oidcConfig.tokenEndpoint,
|
||||
jwksEndpoint: oidcConfig.jwksEndpoint,
|
||||
discoveryEndpoint: oidcConfig.discoveryEndpoint,
|
||||
userInfoEndpoint: oidcConfig.userInfoEndpoint,
|
||||
scopes: oidcConfig.scopes,
|
||||
pkce: oidcConfig.pkce,
|
||||
},
|
||||
mapping: oidcConfig.mapping,
|
||||
},
|
||||
headers,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const errText = await res.text();
|
||||
return new Response(
|
||||
JSON.stringify({ error: `Failed to register SSO provider: ${errText}` }),
|
||||
{ status: res.status || 500, headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
return new Response(
|
||||
JSON.stringify({ error: `Better Auth registration failed: ${message}` }),
|
||||
{ status: 500, headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
}
|
||||
|
||||
// Insert new provider
|
||||
const [newProvider] = await db
|
||||
.insert(ssoProviders)
|
||||
.values({
|
||||
id: nanoid(),
|
||||
issuer,
|
||||
issuer: cleanIssuer,
|
||||
domain,
|
||||
oidcConfig: JSON.stringify(oidcConfig),
|
||||
userId: user.id,
|
||||
@@ -259,4 +347,4 @@ export async function DELETE(context: APIContext) {
|
||||
} catch (error) {
|
||||
return createSecureErrorResponse(error, "SSO providers API");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,9 @@ import MainLayout from '../../layouts/main.astro';
|
||||
{ var: 'PORT', desc: 'Server port', default: '4321' },
|
||||
{ var: 'HOST', desc: 'Server host', default: '0.0.0.0' },
|
||||
{ var: 'BETTER_AUTH_SECRET', desc: 'Authentication secret key', default: 'Auto-generated' },
|
||||
{ var: 'BETTER_AUTH_URL', desc: 'Authentication base URL', default: 'http://localhost:4321' },
|
||||
{ var: 'BETTER_AUTH_URL', desc: 'Authentication base URL (public origin)', default: 'http://localhost:4321' },
|
||||
{ var: 'PUBLIC_BETTER_AUTH_URL', desc: 'Optional: public URL used by the client', default: 'Unset' },
|
||||
{ var: 'BETTER_AUTH_TRUSTED_ORIGINS', desc: 'Comma-separated list of additional trusted origins', default: 'Unset' },
|
||||
{ var: 'NODE_EXTRA_CA_CERTS', desc: 'Path to CA certificate file', default: 'None' },
|
||||
{ var: 'DATABASE_URL', desc: 'SQLite database path', default: './data/gitea-mirror.db' },
|
||||
].map((item, i) => (
|
||||
@@ -464,4 +466,4 @@ ls -t "$BACKUP_DIR"/backup_*.tar.gz | tail -n +8 | xargs rm -f`}</code></pre>
|
||||
</section>
|
||||
</article>
|
||||
</main>
|
||||
</MainLayout>
|
||||
</MainLayout>
|
||||
|
||||
@@ -216,7 +216,7 @@ import MainLayout from '../../layouts/main.astro';
|
||||
<div class="space-y-3">
|
||||
{[
|
||||
'User accounts and authentication data (Better Auth)',
|
||||
'OAuth applications and SSO provider configurations',
|
||||
'OAuth applications and SSO provider configurations (providers registered via Better Auth; mirrored locally for UI)',
|
||||
'GitHub and Gitea configuration',
|
||||
'Repository and organization information',
|
||||
'Mirroring job history and status',
|
||||
@@ -336,4 +336,4 @@ import MainLayout from '../../layouts/main.astro';
|
||||
</section>
|
||||
</article>
|
||||
</main>
|
||||
</MainLayout>
|
||||
</MainLayout>
|
||||
|
||||
@@ -107,6 +107,23 @@ import MainLayout from '../../layouts/main.astro';
|
||||
</p>
|
||||
|
||||
<h3 class="text-xl font-semibold mb-4">Adding an SSO Provider</h3>
|
||||
<div class="bg-blue-500/10 border border-blue-500/20 rounded-lg p-4 mb-6">
|
||||
<div class="flex gap-3">
|
||||
<div class="text-blue-600 dark:text-blue-500">
|
||||
<svg class="w-5 h-5 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-semibold text-blue-600 dark:text-blue-500 mb-1">Better Auth Registration</p>
|
||||
<p class="text-sm">
|
||||
Provider creation uses Better Auth's SSO registration under the hood. The legacy API route
|
||||
<code class="bg-muted px-1 rounded">POST /api/sso/providers</code> is deprecated and not required for setup.
|
||||
To change Provider ID or endpoints, delete and recreate the provider from the UI.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-card rounded-lg border border-border p-6 mb-6">
|
||||
<h4 class="font-semibold mb-4">Required Information</h4>
|
||||
@@ -147,11 +164,11 @@ import MainLayout from '../../layouts/main.astro';
|
||||
|
||||
<h3 class="text-xl font-semibold mb-4">Redirect URL Configuration</h3>
|
||||
|
||||
<div class="bg-muted/30 rounded-lg p-4">
|
||||
<p class="text-sm mb-2">When configuring your SSO provider, use this redirect URL:</p>
|
||||
<code class="bg-muted rounded px-3 py-2 block">https://your-domain.com/api/auth/sso/callback/{`{provider-id}`}</code>
|
||||
<p class="text-xs text-muted-foreground mt-2">Replace <code>{`{provider-id}`}</code> with your chosen Provider ID (e.g., google-sso)</p>
|
||||
</div>
|
||||
<div class="bg-muted/30 rounded-lg p-4">
|
||||
<p class="text-sm mb-2">When configuring your SSO provider, use this redirect URL:</p>
|
||||
<code class="bg-muted rounded px-3 py-2 block">https://your-domain.com/api/auth/sso/callback/{`{provider-id}`}</code>
|
||||
<p class="text-xs text-muted-foreground mt-2">Replace <code>{`{provider-id}`}</code> with your chosen Provider ID (e.g., google-sso)</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="my-12 h-px bg-border/50"></div>
|
||||
@@ -532,4 +549,4 @@ import MainLayout from '../../layouts/main.astro';
|
||||
</section>
|
||||
</article>
|
||||
</main>
|
||||
</MainLayout>
|
||||
</MainLayout>
|
||||
|
||||
@@ -4,7 +4,10 @@ import MainLayout from '../../layouts/main.astro';
|
||||
const envVars = [
|
||||
{ name: 'NODE_ENV', desc: 'Runtime environment', default: 'development', example: 'production' },
|
||||
{ name: 'DATABASE_URL', desc: 'SQLite database URL', default: 'file:data/gitea-mirror.db', example: 'file:path/to/database.db' },
|
||||
{ name: 'JWT_SECRET', desc: 'Secret key for JWT auth', default: 'Auto-generated', example: 'your-secure-string' },
|
||||
{ name: 'BETTER_AUTH_SECRET', desc: 'Authentication secret key', default: 'Auto-generated', example: 'generate a strong random string' },
|
||||
{ name: 'BETTER_AUTH_URL', desc: 'Authentication base URL (public origin)', default: 'http://localhost:4321', example: 'https://gitea-mirror.example.com' },
|
||||
{ name: 'PUBLIC_BETTER_AUTH_URL', desc: 'Optional: public URL used by the client', default: 'Unset', example: 'https://gitea-mirror.example.com' },
|
||||
{ name: 'BETTER_AUTH_TRUSTED_ORIGINS', desc: 'Comma-separated list of additional trusted origins', default: 'Unset', example: 'https://gitea-mirror.example.com,https://alt.example.com' },
|
||||
{ name: 'HOST', desc: 'Server host', default: 'localhost', example: '0.0.0.0' },
|
||||
{ name: 'PORT', desc: 'Server port', default: '4321', example: '8080' }
|
||||
];
|
||||
@@ -509,4 +512,4 @@ curl http://your-server:port/api/health`}</code></pre>
|
||||
</section>
|
||||
</article>
|
||||
</main>
|
||||
</MainLayout>
|
||||
</MainLayout>
|
||||
|
||||
@@ -260,6 +260,16 @@ bun run start</code></pre>
|
||||
},
|
||||
{
|
||||
num: '3',
|
||||
title: 'Optional: Configure SSO',
|
||||
items: [
|
||||
'Open Configuration → Authentication',
|
||||
'Click “Add Provider” and enter your OIDC details',
|
||||
'Use redirect URL: https://<your-domain>/api/auth/sso/callback/{provider-id}',
|
||||
'Edits are handled as delete & recreate (Better Auth registration)'
|
||||
]
|
||||
},
|
||||
{
|
||||
num: '4',
|
||||
title: 'Configure Gitea Connection',
|
||||
items: [
|
||||
'Enter your Gitea server URL',
|
||||
@@ -269,7 +279,7 @@ bun run start</code></pre>
|
||||
]
|
||||
},
|
||||
{
|
||||
num: '4',
|
||||
num: '5',
|
||||
title: 'Set Up Scheduling',
|
||||
items: [
|
||||
'Enable automatic mirroring',
|
||||
@@ -434,4 +444,4 @@ bun run start</code></pre>
|
||||
</section>
|
||||
</article>
|
||||
</main>
|
||||
</MainLayout>
|
||||
</MainLayout>
|
||||
|
||||
Reference in New Issue
Block a user