ldap
This commit is contained in:
73
back/src/ldap/auth.ts
Normal file
73
back/src/ldap/auth.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
// src/ldap/auth.ts
|
||||
import ldap from "ldapjs";
|
||||
import { createClient, BASE_DN, BIND_DN, BIND_PASSWORD } from "./client";
|
||||
|
||||
export interface AuthResult {
|
||||
dn: string;
|
||||
username: string;
|
||||
}
|
||||
|
||||
function bindClient(client: ldap.Client, dn: string, password: string) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
client.bind(dn, password, (err) => {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function searchUserDN(
|
||||
client: ldap.Client,
|
||||
username: string
|
||||
): Promise<string | null> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const filter = `(uid=${username})` as const
|
||||
const opts = {
|
||||
filter,
|
||||
scope: "sub" as const,
|
||||
};
|
||||
|
||||
client.search(`ou=people,${BASE_DN}`, opts, (err, res) => {
|
||||
if (err) return reject(err);
|
||||
|
||||
let userDN: string | null = null;
|
||||
|
||||
res.on("searchEntry", (entry) => {
|
||||
userDN = entry.objectName?.toString() ?? null;
|
||||
});
|
||||
|
||||
res.on("error", reject);
|
||||
|
||||
res.on("end", () => resolve(userDN));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function authenticate(
|
||||
username: string,
|
||||
password: string
|
||||
): Promise<AuthResult> {
|
||||
const client = createClient();
|
||||
|
||||
try {
|
||||
// 1. bind service account
|
||||
await bindClient(client, BIND_DN, BIND_PASSWORD);
|
||||
|
||||
// 2. find user DN
|
||||
const userDN = await searchUserDN(client, username);
|
||||
|
||||
if (!userDN) {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
|
||||
// 3. re-bind as user (password check)
|
||||
await bindClient(client, userDN, password);
|
||||
|
||||
return {
|
||||
dn: userDN,
|
||||
username,
|
||||
};
|
||||
} finally {
|
||||
client.unbind();
|
||||
}
|
||||
}
|
||||
15
back/src/ldap/client.ts
Normal file
15
back/src/ldap/client.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
// src/ldap/client.ts
|
||||
import ldap from "ldapjs";
|
||||
|
||||
export const LDAP_URL = process.env.LDAP_URL!;
|
||||
export const BASE_DN = process.env.LDAP_BASE_DN!;
|
||||
export const BIND_DN = process.env.LDAP_BIND_DN!;
|
||||
export const BIND_PASSWORD = process.env.LDAP_BIND_PASSWORD!;
|
||||
|
||||
export function createClient() {
|
||||
return ldap.createClient({
|
||||
url: LDAP_URL,
|
||||
timeout: 5000,
|
||||
reconnect: true,
|
||||
});
|
||||
}
|
||||
22
back/src/middleware/auth.ts
Normal file
22
back/src/middleware/auth.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import jwt from "jsonwebtoken";
|
||||
import type { Request, Response, NextFunction } from "express";
|
||||
|
||||
export function authMiddleware(req: Request, res: Response, next: NextFunction) {
|
||||
const header = req.headers.authorization;
|
||||
const error = "You must be logged in to access this page"
|
||||
|
||||
if (!header) {
|
||||
return res.status(401).json({ error });
|
||||
}
|
||||
|
||||
const token = header.split(" ")[1]; // "Bearer TOKEN"
|
||||
|
||||
try {
|
||||
if (!token) throw new Error("No Bearer Token")
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET!);
|
||||
(req as any).user = decoded;
|
||||
next();
|
||||
} catch (err) {
|
||||
return res.status(401).json({ error });
|
||||
}
|
||||
}
|
||||
36
back/src/routes/auth.ts
Normal file
36
back/src/routes/auth.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
// src/routes/auth.ts
|
||||
import { Router } from "express";
|
||||
import jwt from "jsonwebtoken";
|
||||
import { authenticate } from "../ldap/auth";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.post("/login", async (req, res) => {
|
||||
const { username, password } = req.body;
|
||||
|
||||
try {
|
||||
const user = await authenticate(username, password);
|
||||
|
||||
const token = jwt.sign(
|
||||
{
|
||||
sub: user.dn,
|
||||
username: user.username,
|
||||
},
|
||||
process.env.JWT_SECRET!,
|
||||
{ expiresIn: "1h" }
|
||||
);
|
||||
|
||||
res.json({
|
||||
token,
|
||||
user,
|
||||
});
|
||||
} catch (err: any) {
|
||||
console.error(err)
|
||||
res.status(401).json({
|
||||
error: "Invalid credentials",
|
||||
details: err.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
12
back/src/routes/protected.ts
Normal file
12
back/src/routes/protected.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
// src/routes/auth.ts
|
||||
import { Router } from "express";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get("/video", async (req, res) => {
|
||||
res.json({
|
||||
tajne: "XD"
|
||||
});
|
||||
})
|
||||
|
||||
export default router;
|
||||
32
back/src/server.ts
Normal file
32
back/src/server.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
// src/server.ts
|
||||
import express from "express";
|
||||
import dotenv from "dotenv";
|
||||
import authRoutes from "./routes/auth";
|
||||
import protectedRoutes from "./routes/protected";
|
||||
import cors from "cors";
|
||||
import { authMiddleware } from "./middleware/auth";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(cors({
|
||||
origin: "http://localhost:5173",
|
||||
methods: ["GET", "POST"],
|
||||
credentials: true
|
||||
}));
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
app.use("/auth", authRoutes);
|
||||
app.use("/protected", authMiddleware, protectedRoutes);
|
||||
|
||||
app.get("/health", (_, res) => {
|
||||
res.json({ status: "ok" });
|
||||
});
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server running on http://localhost:${PORT}`);
|
||||
});
|
||||
Reference in New Issue
Block a user