mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2025-12-10 05:26:44 +03:00
more SSO and OIDC fixes
This commit is contained in:
@@ -1,13 +0,0 @@
|
||||
# Legacy Auth Routes Backup
|
||||
|
||||
These files are the original authentication routes before migrating to Better Auth.
|
||||
They are kept here as a reference during the migration process.
|
||||
|
||||
## Migration Notes
|
||||
|
||||
- `index.ts` - Handled user session validation and getting current user
|
||||
- `login.ts` - Handled user login with email/password
|
||||
- `logout.ts` - Handled user logout and session cleanup
|
||||
- `register.ts` - Handled new user registration
|
||||
|
||||
All these endpoints are now handled by Better Auth through the catch-all route `[...all].ts`.
|
||||
@@ -1,83 +0,0 @@
|
||||
import type { APIRoute } from "astro";
|
||||
import { db, users, configs } from "@/lib/db";
|
||||
import { eq, and, sql } from "drizzle-orm";
|
||||
import jwt from "jsonwebtoken";
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key";
|
||||
|
||||
export const GET: APIRoute = async ({ request, cookies }) => {
|
||||
const authHeader = request.headers.get("Authorization");
|
||||
const token = authHeader?.split(" ")[1] || cookies.get("token")?.value;
|
||||
|
||||
if (!token) {
|
||||
const userCountResult = await db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(users);
|
||||
const userCount = userCountResult[0].count;
|
||||
|
||||
if (userCount === 0) {
|
||||
return new Response(JSON.stringify({ error: "No users found" }), {
|
||||
status: 404,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({ error: "Unauthorized" }), {
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(token, JWT_SECRET) as { id: string };
|
||||
|
||||
const userResult = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.id, decoded.id))
|
||||
.limit(1);
|
||||
|
||||
if (!userResult.length) {
|
||||
return new Response(JSON.stringify({ error: "User not found" }), {
|
||||
status: 404,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
const { password, ...userWithoutPassword } = userResult[0];
|
||||
|
||||
const configResult = await db
|
||||
.select({
|
||||
scheduleConfig: configs.scheduleConfig,
|
||||
})
|
||||
.from(configs)
|
||||
.where(and(eq(configs.userId, decoded.id), eq(configs.isActive, true)))
|
||||
.limit(1);
|
||||
|
||||
const scheduleConfig = configResult[0]?.scheduleConfig;
|
||||
|
||||
const syncEnabled = scheduleConfig?.enabled ?? false;
|
||||
const syncInterval = scheduleConfig?.interval ?? 3600;
|
||||
const lastSync = scheduleConfig?.lastRun ?? null;
|
||||
const nextSync = scheduleConfig?.nextRun ?? null;
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
...userWithoutPassword,
|
||||
syncEnabled,
|
||||
syncInterval,
|
||||
lastSync,
|
||||
nextSync,
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ error: "Invalid token" }), {
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -1,62 +0,0 @@
|
||||
import type { APIRoute } from "astro";
|
||||
import bcrypt from "bcryptjs";
|
||||
import jwt from "jsonwebtoken";
|
||||
import { db, users } from "@/lib/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key";
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
const { username, password } = await request.json();
|
||||
|
||||
if (!username || !password) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: "Username and password are required" }),
|
||||
{
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const user = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.username, username))
|
||||
.limit(1);
|
||||
|
||||
if (!user.length) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: "Invalid username or password" }),
|
||||
{
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const isPasswordValid = await bcrypt.compare(password, user[0].password);
|
||||
|
||||
if (!isPasswordValid) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: "Invalid username or password" }),
|
||||
{
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const { password: _, ...userWithoutPassword } = user[0];
|
||||
const token = jwt.sign({ id: user[0].id }, JWT_SECRET, { expiresIn: "7d" });
|
||||
|
||||
return new Response(JSON.stringify({ token, user: userWithoutPassword }), {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Set-Cookie": `token=${token}; Path=/; HttpOnly; SameSite=Strict; Max-Age=${
|
||||
60 * 60 * 24 * 7
|
||||
}`,
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
import type { APIRoute } from "astro";
|
||||
|
||||
export const POST: APIRoute = async () => {
|
||||
return new Response(JSON.stringify({ success: true }), {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Set-Cookie": "token=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0",
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,72 +0,0 @@
|
||||
import type { APIRoute } from "astro";
|
||||
import bcrypt from "bcryptjs";
|
||||
import jwt from "jsonwebtoken";
|
||||
import { db, users } from "@/lib/db";
|
||||
import { eq, or } from "drizzle-orm";
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key";
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
const { username, email, password } = await request.json();
|
||||
|
||||
if (!username || !email || !password) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: "Username, email, and password are required" }),
|
||||
{
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Check if username or email already exists
|
||||
const existingUser = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(or(eq(users.username, username), eq(users.email, email)))
|
||||
.limit(1);
|
||||
|
||||
if (existingUser.length) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: "Username or email already exists" }),
|
||||
{
|
||||
status: 409,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Hash password
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
|
||||
// Generate UUID
|
||||
const id = crypto.randomUUID();
|
||||
|
||||
// Create user
|
||||
const newUser = await db
|
||||
.insert(users)
|
||||
.values({
|
||||
id,
|
||||
username,
|
||||
email,
|
||||
password: hashedPassword,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.returning();
|
||||
|
||||
const { password: _, ...userWithoutPassword } = newUser[0];
|
||||
const token = jwt.sign({ id: newUser[0].id }, JWT_SECRET, {
|
||||
expiresIn: "7d",
|
||||
});
|
||||
|
||||
return new Response(JSON.stringify({ token, user: userWithoutPassword }), {
|
||||
status: 201,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Set-Cookie": `token=${token}; Path=/; HttpOnly; SameSite=Strict; Max-Age=${
|
||||
60 * 60 * 24 * 7
|
||||
}`,
|
||||
},
|
||||
});
|
||||
};
|
||||
137
src/pages/api/auth/oauth2/register.ts
Normal file
137
src/pages/api/auth/oauth2/register.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import type { APIContext } from "astro";
|
||||
import { createSecureErrorResponse } from "@/lib/utils";
|
||||
import { requireAuth } from "@/lib/utils/auth-helpers";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
|
||||
// POST /api/auth/oauth2/register - Register a new OAuth2 application
|
||||
export async function POST(context: APIContext) {
|
||||
try {
|
||||
const { response: authResponse } = await requireAuth(context);
|
||||
if (authResponse) return authResponse;
|
||||
|
||||
const body = await context.request.json();
|
||||
|
||||
// Extract and validate required fields
|
||||
const {
|
||||
client_name,
|
||||
redirect_uris,
|
||||
token_endpoint_auth_method = "client_secret_basic",
|
||||
grant_types = ["authorization_code"],
|
||||
response_types = ["code"],
|
||||
client_uri,
|
||||
logo_uri,
|
||||
scope = "openid profile email",
|
||||
contacts,
|
||||
tos_uri,
|
||||
policy_uri,
|
||||
jwks_uri,
|
||||
jwks,
|
||||
metadata,
|
||||
software_id,
|
||||
software_version,
|
||||
software_statement,
|
||||
} = body;
|
||||
|
||||
// Validate required fields
|
||||
if (!client_name || !redirect_uris || !Array.isArray(redirect_uris) || redirect_uris.length === 0) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: "invalid_request",
|
||||
error_description: "client_name and redirect_uris are required"
|
||||
}),
|
||||
{
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// Use Better Auth client to register OAuth2 application
|
||||
const response = await authClient.oauth2.register({
|
||||
client_name,
|
||||
redirect_uris,
|
||||
token_endpoint_auth_method,
|
||||
grant_types,
|
||||
response_types,
|
||||
client_uri,
|
||||
logo_uri,
|
||||
scope,
|
||||
contacts,
|
||||
tos_uri,
|
||||
policy_uri,
|
||||
jwks_uri,
|
||||
jwks,
|
||||
metadata,
|
||||
software_id,
|
||||
software_version,
|
||||
software_statement,
|
||||
});
|
||||
|
||||
// Check if response is an error
|
||||
if ('error' in response && response.error) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: response.error.code || "registration_error",
|
||||
error_description: response.error.message || "Failed to register application"
|
||||
}),
|
||||
{
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// The response follows OAuth2 RFC format with snake_case
|
||||
return new Response(JSON.stringify(response), {
|
||||
status: 201,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Cache-Control": "no-store",
|
||||
"Pragma": "no-cache"
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
// Handle Better Auth errors
|
||||
if (error.message?.includes('already exists')) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: "invalid_client_metadata",
|
||||
error_description: "Client with this configuration already exists"
|
||||
}),
|
||||
{
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
} catch (error) {
|
||||
return createSecureErrorResponse(error, "OAuth2 registration");
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/auth/oauth2/register - Get all registered OAuth2 applications
|
||||
export async function GET(context: APIContext) {
|
||||
try {
|
||||
const { response: authResponse } = await requireAuth(context);
|
||||
if (authResponse) return authResponse;
|
||||
|
||||
// TODO: Implement listing of OAuth2 applications
|
||||
// This would require querying the database directly
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
applications: [],
|
||||
message: "OAuth2 application listing not yet implemented"
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
return createSecureErrorResponse(error, "OAuth2 application listing");
|
||||
}
|
||||
}
|
||||
163
src/pages/api/auth/sso/register.ts
Normal file
163
src/pages/api/auth/sso/register.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import type { APIContext } from "astro";
|
||||
import { createSecureErrorResponse } from "@/lib/utils";
|
||||
import { requireAuth } from "@/lib/utils/auth-helpers";
|
||||
import { auth } from "@/lib/auth";
|
||||
|
||||
// POST /api/auth/sso/register - Register a new SSO provider using Better Auth
|
||||
export async function POST(context: APIContext) {
|
||||
try {
|
||||
const { user, response: authResponse } = await requireAuth(context);
|
||||
if (authResponse) return authResponse;
|
||||
|
||||
const body = await context.request.json();
|
||||
|
||||
// Extract configuration based on provider type
|
||||
const { providerId, issuer, domain, organizationId, providerType = "oidc" } = body;
|
||||
|
||||
// Validate required fields
|
||||
if (!providerId || !issuer || !domain) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: "Missing required fields: providerId, issuer, and domain" }),
|
||||
{
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
let registrationBody: any = {
|
||||
providerId,
|
||||
issuer,
|
||||
domain,
|
||||
organizationId,
|
||||
};
|
||||
|
||||
if (providerType === "saml") {
|
||||
// SAML provider configuration
|
||||
const {
|
||||
entryPoint,
|
||||
cert,
|
||||
callbackUrl,
|
||||
audience,
|
||||
wantAssertionsSigned = true,
|
||||
signatureAlgorithm = "sha256",
|
||||
digestAlgorithm = "sha256",
|
||||
identifierFormat = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
||||
idpMetadata,
|
||||
spMetadata,
|
||||
mapping = {
|
||||
id: "nameID",
|
||||
email: "email",
|
||||
name: "displayName",
|
||||
firstName: "givenName",
|
||||
lastName: "surname",
|
||||
}
|
||||
} = body;
|
||||
|
||||
registrationBody.samlConfig = {
|
||||
entryPoint,
|
||||
cert,
|
||||
callbackUrl: callbackUrl || `${context.url.origin}/api/auth/sso/saml2/callback/${providerId}`,
|
||||
audience: audience || context.url.origin,
|
||||
wantAssertionsSigned,
|
||||
signatureAlgorithm,
|
||||
digestAlgorithm,
|
||||
identifierFormat,
|
||||
idpMetadata,
|
||||
spMetadata,
|
||||
};
|
||||
registrationBody.mapping = mapping;
|
||||
} else {
|
||||
// OIDC provider configuration
|
||||
const {
|
||||
clientId,
|
||||
clientSecret,
|
||||
authorizationEndpoint,
|
||||
tokenEndpoint,
|
||||
jwksEndpoint,
|
||||
discoveryEndpoint,
|
||||
userInfoEndpoint,
|
||||
scopes = ["openid", "email", "profile"],
|
||||
pkce = true,
|
||||
mapping = {
|
||||
id: "sub",
|
||||
email: "email",
|
||||
emailVerified: "email_verified",
|
||||
name: "name",
|
||||
image: "picture",
|
||||
}
|
||||
} = body;
|
||||
|
||||
registrationBody.oidcConfig = {
|
||||
clientId,
|
||||
clientSecret,
|
||||
authorizationEndpoint,
|
||||
tokenEndpoint,
|
||||
jwksEndpoint,
|
||||
discoveryEndpoint,
|
||||
userInfoEndpoint,
|
||||
scopes,
|
||||
pkce,
|
||||
};
|
||||
registrationBody.mapping = mapping;
|
||||
}
|
||||
|
||||
// Get the user's auth headers to make the request
|
||||
const headers = new Headers();
|
||||
const cookieHeader = context.request.headers.get("cookie");
|
||||
if (cookieHeader) {
|
||||
headers.set("cookie", cookieHeader);
|
||||
}
|
||||
|
||||
// Register the SSO provider using Better Auth's API
|
||||
const response = await auth.api.registerSSOProvider({
|
||||
body: registrationBody,
|
||||
headers,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
return new Response(
|
||||
JSON.stringify({ error: `Failed to register SSO provider: ${error}` }),
|
||||
{
|
||||
status: response.status,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
return new Response(JSON.stringify(result), {
|
||||
status: 201,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
} catch (error) {
|
||||
return createSecureErrorResponse(error, "SSO registration");
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/auth/sso/register - Get all registered SSO providers
|
||||
export async function GET(context: APIContext) {
|
||||
try {
|
||||
const { user, response: authResponse } = await requireAuth(context);
|
||||
if (authResponse) return authResponse;
|
||||
|
||||
// For now, we'll need to query the database directly since Better Auth
|
||||
// doesn't provide a built-in API to list SSO providers
|
||||
// This will be implemented once we update the database schema
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
message: "SSO provider listing not yet implemented",
|
||||
providers: []
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
return createSecureErrorResponse(error, "SSO provider listing");
|
||||
}
|
||||
}
|
||||
64
src/pages/api/auth/sso/sp-metadata.ts
Normal file
64
src/pages/api/auth/sso/sp-metadata.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import type { APIContext } from "astro";
|
||||
import { createSecureErrorResponse } from "@/lib/utils";
|
||||
import { auth } from "@/lib/auth";
|
||||
|
||||
// GET /api/auth/sso/sp-metadata - Get Service Provider metadata for SAML
|
||||
export async function GET(context: APIContext) {
|
||||
try {
|
||||
const url = new URL(context.request.url);
|
||||
const providerId = url.searchParams.get("providerId");
|
||||
const format = url.searchParams.get("format") || "xml";
|
||||
|
||||
if (!providerId) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: "Provider ID is required" }),
|
||||
{
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Get SP metadata using Better Auth's API
|
||||
const response = await auth.api.spMetadata({
|
||||
query: {
|
||||
providerId,
|
||||
format,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
return new Response(
|
||||
JSON.stringify({ error: `Failed to get SP metadata: ${error}` }),
|
||||
{
|
||||
status: response.status,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Return the metadata in the requested format
|
||||
if (format === "xml") {
|
||||
const metadataXML = await response.text();
|
||||
return new Response(metadataXML, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/samlmetadata+xml",
|
||||
"Cache-Control": "public, max-age=86400", // Cache for 24 hours
|
||||
},
|
||||
});
|
||||
} else {
|
||||
const metadataJSON = await response.json();
|
||||
return new Response(JSON.stringify(metadataJSON), {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Cache-Control": "public, max-age=86400",
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
return createSecureErrorResponse(error, "SP metadata");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user