Restructuring the Project
Updating dependencies
This commit is contained in:
191
Backend/src/api/admin/client.ts
Normal file
191
Backend/src/api/admin/client.ts
Normal file
@ -0,0 +1,191 @@
|
||||
import { Router, Request } from "express";
|
||||
import RequestError, { HttpStatusCode } from "../../helper/request_error";
|
||||
import promiseMiddleware from "../../helper/promiseMiddleware";
|
||||
import Client from "../../models/client";
|
||||
import verify, { Types } from "../middlewares/verify";
|
||||
import { randomBytes } from "crypto";
|
||||
|
||||
const ClientRouter: Router = Router();
|
||||
ClientRouter.route("/")
|
||||
/**
|
||||
* @api {get} /admin/client
|
||||
* @apiName AdminGetClients
|
||||
*
|
||||
* @apiGroup admin_client
|
||||
* @apiPermission admin
|
||||
*
|
||||
* @apiSuccess {Object[]} clients
|
||||
* @apiSuccess {String} clients._id The internally used id
|
||||
* @apiSuccess {String} clients.maintainer
|
||||
* @apiSuccess {Boolean} clients.internal
|
||||
* @apiSuccess {String} clients.name
|
||||
* @apiSuccess {String} clients.redirect_url
|
||||
* @apiSuccess {String} clients.website
|
||||
* @apiSuccess {String} clients.logo
|
||||
* @apiSuccess {String} clients.client_id Client ID used outside of DB
|
||||
* @apiSuccess {String} clients.client_secret
|
||||
*/
|
||||
.get(
|
||||
promiseMiddleware(async (req, res) => {
|
||||
let clients = await Client.find({});
|
||||
//ToDo check if user is required!
|
||||
res.json(clients);
|
||||
})
|
||||
)
|
||||
/**
|
||||
* @api {get} /admin/client
|
||||
* @apiName AdminAddClients
|
||||
*
|
||||
* @apiGroup admin_client
|
||||
* @apiPermission admin
|
||||
*
|
||||
* @apiParam {Boolean} internal Is it an internal app
|
||||
* @apiParam {String} name
|
||||
* @apiParam {String} redirect_url
|
||||
* @apiParam {String} website
|
||||
* @apiParam {String} logo
|
||||
*
|
||||
* @apiSuccess {Object[]} clients
|
||||
* @apiSuccess {String} clients._id The internally used id
|
||||
* @apiSuccess {String} clients.maintainer
|
||||
* @apiSuccess {Boolean} clients.internal
|
||||
* @apiSuccess {String} clients.name
|
||||
* @apiSuccess {String} clients.redirect_url
|
||||
* @apiSuccess {String} clients.website
|
||||
* @apiSuccess {String} clients.logo
|
||||
* @apiSuccess {String} clients.client_id Client ID used outside of DB
|
||||
* @apiSuccess {String} clients.client_secret
|
||||
*/
|
||||
.post(
|
||||
verify(
|
||||
{
|
||||
internal: {
|
||||
type: Types.BOOLEAN,
|
||||
optional: true,
|
||||
},
|
||||
name: {
|
||||
type: Types.STRING,
|
||||
},
|
||||
redirect_url: {
|
||||
type: Types.STRING,
|
||||
},
|
||||
website: {
|
||||
type: Types.STRING,
|
||||
},
|
||||
logo: {
|
||||
type: Types.STRING,
|
||||
optional: true,
|
||||
},
|
||||
featured: {
|
||||
type: Types.BOOLEAN,
|
||||
optional: true,
|
||||
},
|
||||
description: {
|
||||
type: Types.STRING,
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
true
|
||||
),
|
||||
promiseMiddleware(async (req, res) => {
|
||||
req.body.client_secret = randomBytes(32).toString("hex");
|
||||
let client = Client.new(req.body);
|
||||
client.maintainer = req.user._id;
|
||||
await Client.save(client);
|
||||
res.json(client);
|
||||
})
|
||||
);
|
||||
|
||||
ClientRouter.route("/:id")
|
||||
/**
|
||||
* @api {delete} /admin/client/:id
|
||||
* @apiParam {String} id Client _id
|
||||
* @apiName AdminDeleteClient
|
||||
*
|
||||
* @apiGroup admin_client
|
||||
* @apiPermission admin
|
||||
*
|
||||
* @apiSuccess {Boolean} success
|
||||
*/
|
||||
.delete(
|
||||
promiseMiddleware(async (req, res) => {
|
||||
let { id } = req.params;
|
||||
await Client.delete(id);
|
||||
res.json({ success: true });
|
||||
})
|
||||
)
|
||||
/**
|
||||
* @api {put} /admin/client/:id
|
||||
* @apiParam {String} id Client _id
|
||||
* @apiName AdminUpdateClient
|
||||
*
|
||||
* @apiGroup admin_client
|
||||
* @apiPermission admin
|
||||
*
|
||||
* @apiParam {Boolean} internal Is it an internal app
|
||||
* @apiParam {String} name
|
||||
* @apiParam {String} redirect_url
|
||||
* @apiParam {String} website
|
||||
* @apiParam {String} logo
|
||||
*
|
||||
* @apiSuccess {String} _id The internally used id
|
||||
* @apiSuccess {String} maintainer UserID of client maintainer
|
||||
* @apiSuccess {Boolean} internal Defines if it is a internal client
|
||||
* @apiSuccess {String} name The name of the Client
|
||||
* @apiSuccess {String} redirect_url Redirect URL after login
|
||||
* @apiSuccess {String} website Website of Client
|
||||
* @apiSuccess {String} logo The Logo of the Client (optional)
|
||||
* @apiSuccess {String} client_id Client ID used outside of DB
|
||||
* @apiSuccess {String} client_secret The client secret, that can be used to obtain token
|
||||
*/
|
||||
.put(
|
||||
verify(
|
||||
{
|
||||
internal: {
|
||||
type: Types.BOOLEAN,
|
||||
optional: true,
|
||||
},
|
||||
name: {
|
||||
type: Types.STRING,
|
||||
optional: true,
|
||||
},
|
||||
redirect_url: {
|
||||
type: Types.STRING,
|
||||
optional: true,
|
||||
},
|
||||
website: {
|
||||
type: Types.STRING,
|
||||
optional: true,
|
||||
},
|
||||
logo: {
|
||||
type: Types.STRING,
|
||||
optional: true,
|
||||
},
|
||||
featured: {
|
||||
type: Types.BOOLEAN,
|
||||
optional: true,
|
||||
},
|
||||
description: {
|
||||
type: Types.STRING,
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
true
|
||||
),
|
||||
promiseMiddleware(async (req, res) => {
|
||||
let { id } = req.query as { [key: string]: string };
|
||||
let client = await Client.findById(id);
|
||||
if (!client)
|
||||
throw new RequestError(
|
||||
req.__("Client not found"),
|
||||
HttpStatusCode.BAD_REQUEST
|
||||
);
|
||||
for (let key in req.body) {
|
||||
client[key] = req.body[key];
|
||||
}
|
||||
await Client.save(client);
|
||||
res.json(client);
|
||||
})
|
||||
);
|
||||
|
||||
export default ClientRouter;
|
24
Backend/src/api/admin/index.ts
Normal file
24
Backend/src/api/admin/index.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { Request, Router } from "express";
|
||||
import ClientRoute from "./client";
|
||||
import UserRoute from "./user";
|
||||
import RegCodeRoute from "./regcode";
|
||||
import PermissionRoute from "./permission";
|
||||
import { GetUserMiddleware } from "../middlewares/user";
|
||||
import RequestError, { HttpStatusCode } from "../../helper/request_error";
|
||||
|
||||
const AdminRoute: Router = Router();
|
||||
|
||||
AdminRoute.use(GetUserMiddleware(true, true), (req: Request, res, next) => {
|
||||
if (!req.isAdmin)
|
||||
throw new RequestError(
|
||||
"You have no permission to access this API",
|
||||
HttpStatusCode.FORBIDDEN
|
||||
);
|
||||
else next();
|
||||
});
|
||||
|
||||
AdminRoute.use("/client", ClientRoute);
|
||||
AdminRoute.use("/regcode", RegCodeRoute);
|
||||
AdminRoute.use("/user", UserRoute);
|
||||
AdminRoute.use("/permission", PermissionRoute);
|
||||
export default AdminRoute;
|
111
Backend/src/api/admin/permission.ts
Normal file
111
Backend/src/api/admin/permission.ts
Normal file
@ -0,0 +1,111 @@
|
||||
import { Request, Router } from "express";
|
||||
import { GetUserMiddleware } from "../middlewares/user";
|
||||
import RequestError, { HttpStatusCode } from "../../helper/request_error";
|
||||
import promiseMiddleware from "../../helper/promiseMiddleware";
|
||||
import Permission from "../../models/permissions";
|
||||
import verify, { Types } from "../middlewares/verify";
|
||||
import Client from "../../models/client";
|
||||
import { ObjectID } from "bson";
|
||||
|
||||
const PermissionRoute: Router = Router();
|
||||
PermissionRoute.route("/")
|
||||
/**
|
||||
* @api {get} /admin/permission
|
||||
* @apiName AdminGetPermissions
|
||||
*
|
||||
* @apiParam client Optionally filter by client _id
|
||||
*
|
||||
* @apiGroup admin_permission
|
||||
* @apiPermission admin
|
||||
*
|
||||
* @apiSuccess {Object[]} permissions
|
||||
* @apiSuccess {String} permissions._id The ID
|
||||
* @apiSuccess {String} permissions.name Permission name
|
||||
* @apiSuccess {String} permissions.description A description, that makes it clear to the user, what this Permission allows to do
|
||||
* @apiSuccess {String} permissions.client The ID of the owning client
|
||||
*/
|
||||
.get(
|
||||
promiseMiddleware(async (req, res) => {
|
||||
let query = {};
|
||||
if (req.query.client) {
|
||||
query = { client: new ObjectID(req.query.client as string) };
|
||||
}
|
||||
let permissions = await Permission.find(query);
|
||||
res.json(permissions);
|
||||
})
|
||||
)
|
||||
/**
|
||||
* @api {post} /admin/permission
|
||||
* @apiName AdminAddPermission
|
||||
*
|
||||
* @apiParam client The ID of the owning client
|
||||
* @apiParam name Permission name
|
||||
* @apiParam description A description, that makes it clear to the user, what this Permission allows to do
|
||||
*
|
||||
* @apiGroup admin_permission
|
||||
* @apiPermission admin
|
||||
*
|
||||
* @apiSuccess {Object[]} permissions
|
||||
* @apiSuccess {String} permissions._id The ID
|
||||
* @apiSuccess {String} permissions.name Permission name
|
||||
* @apiSuccess {String} permissions.description A description, that makes it clear to the user, what this Permission allows to do
|
||||
* @apiSuccess {String} permissions.client The ID of the owning client
|
||||
* @apiSuccess {String} permissions.grant_type The type of the permission. "user" | "client" granted
|
||||
*/
|
||||
.post(
|
||||
verify(
|
||||
{
|
||||
client: {
|
||||
type: Types.STRING,
|
||||
},
|
||||
name: {
|
||||
type: Types.STRING,
|
||||
},
|
||||
description: {
|
||||
type: Types.STRING,
|
||||
},
|
||||
type: {
|
||||
type: Types.ENUM,
|
||||
values: ["user", "client"],
|
||||
},
|
||||
},
|
||||
true
|
||||
),
|
||||
promiseMiddleware(async (req, res) => {
|
||||
let client = await Client.findById(req.body.client);
|
||||
if (!client) {
|
||||
throw new RequestError(
|
||||
"Client not found",
|
||||
HttpStatusCode.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
let permission = Permission.new({
|
||||
description: req.body.description,
|
||||
name: req.body.name,
|
||||
client: client._id,
|
||||
grant_type: req.body.type,
|
||||
});
|
||||
await Permission.save(permission);
|
||||
res.json(permission);
|
||||
})
|
||||
)
|
||||
/**
|
||||
* @api {delete} /admin/permission
|
||||
* @apiName AdminDeletePermission
|
||||
*
|
||||
* @apiParam id The permission ID
|
||||
*
|
||||
* @apiGroup admin_permission
|
||||
* @apiPermission admin
|
||||
*
|
||||
* @apiSuccess {Boolean} success
|
||||
*/
|
||||
.delete(
|
||||
promiseMiddleware(async (req, res) => {
|
||||
let { id } = req.query as { [key: string]: string };
|
||||
await Permission.delete(id);
|
||||
res.json({ success: true });
|
||||
})
|
||||
);
|
||||
|
||||
export default PermissionRoute;
|
69
Backend/src/api/admin/regcode.ts
Normal file
69
Backend/src/api/admin/regcode.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import { Request, Router } from "express";
|
||||
import promiseMiddleware from "../../helper/promiseMiddleware";
|
||||
import RegCode from "../../models/regcodes";
|
||||
import { randomBytes } from "crypto";
|
||||
import moment = require("moment");
|
||||
import { GetUserMiddleware } from "../middlewares/user";
|
||||
import { HttpStatusCode } from "../../helper/request_error";
|
||||
|
||||
const RegCodeRoute: Router = Router();
|
||||
RegCodeRoute.route("/")
|
||||
/**
|
||||
* @api {get} /admin/regcode
|
||||
* @apiName AdminGetRegcodes
|
||||
*
|
||||
* @apiGroup admin_regcode
|
||||
* @apiPermission admin
|
||||
*
|
||||
* @apiSuccess {Object[]} regcodes
|
||||
* @apiSuccess {String} permissions._id The ID
|
||||
* @apiSuccess {String} permissions.token The Regcode Token
|
||||
* @apiSuccess {String} permissions.valid Defines if the Regcode is valid
|
||||
* @apiSuccess {String} permissions.validTill Expiration date of RegCode
|
||||
*/
|
||||
.get(
|
||||
promiseMiddleware(async (req, res) => {
|
||||
let regcodes = await RegCode.find({});
|
||||
res.json(regcodes);
|
||||
})
|
||||
)
|
||||
/**
|
||||
* @api {delete} /admin/regcode
|
||||
* @apiName AdminDeleteRegcode
|
||||
*
|
||||
* @apiParam {String} id The id of the RegCode
|
||||
*
|
||||
* @apiGroup admin_regcode
|
||||
* @apiPermission admin
|
||||
*
|
||||
* @apiSuccess {Boolean} success
|
||||
*/
|
||||
.delete(
|
||||
promiseMiddleware(async (req, res) => {
|
||||
let { id } = req.query as { [key: string]: string };
|
||||
await RegCode.delete(id);
|
||||
res.json({ success: true });
|
||||
})
|
||||
)
|
||||
/**
|
||||
* @api {post} /admin/regcode
|
||||
* @apiName AdminAddRegcode
|
||||
*
|
||||
* @apiGroup admin_regcode
|
||||
* @apiPermission admin
|
||||
*
|
||||
* @apiSuccess {String} code The newly created code
|
||||
*/
|
||||
.post(
|
||||
promiseMiddleware(async (req, res) => {
|
||||
let regcode = RegCode.new({
|
||||
token: randomBytes(10).toString("hex"),
|
||||
valid: true,
|
||||
validTill: moment().add("1", "month").toDate(),
|
||||
});
|
||||
await RegCode.save(regcode);
|
||||
res.json({ code: regcode.token });
|
||||
})
|
||||
);
|
||||
|
||||
export default RegCodeRoute;
|
93
Backend/src/api/admin/user.ts
Normal file
93
Backend/src/api/admin/user.ts
Normal file
@ -0,0 +1,93 @@
|
||||
import { Request, Router } from "express";
|
||||
import { GetUserMiddleware } from "../middlewares/user";
|
||||
import { HttpStatusCode } from "../../helper/request_error";
|
||||
import promiseMiddleware from "../../helper/promiseMiddleware";
|
||||
import User from "../../models/user";
|
||||
import Mail from "../../models/mail";
|
||||
import RefreshToken from "../../models/refresh_token";
|
||||
import LoginToken from "../../models/login_token";
|
||||
|
||||
const UserRoute: Router = Router();
|
||||
UserRoute.use(GetUserMiddleware(true, true), (req: Request, res, next) => {
|
||||
if (!req.isAdmin) res.sendStatus(HttpStatusCode.FORBIDDEN);
|
||||
else next();
|
||||
});
|
||||
|
||||
UserRoute.route("/")
|
||||
/**
|
||||
* @api {get} /admin/user
|
||||
* @apiName AdminGetUsers
|
||||
*
|
||||
* @apiGroup admin_user
|
||||
* @apiPermission admin
|
||||
* @apiSuccess {Object[]} user
|
||||
* @apiSuccess {String} user._id The internal id of the user
|
||||
* @apiSuccess {String} user.uid The public UID of the user
|
||||
* @apiSuccess {String} user.username The username
|
||||
* @apiSuccess {String} user.name The real name
|
||||
* @apiSuccess {Date} user.birthday The birthday
|
||||
* @apiSuccess {Number} user.gender 0 = none, 1 = male, 2 = female, 3 = other
|
||||
* @apiSuccess {Boolean} user.admin Is admin or not
|
||||
*/
|
||||
.get(
|
||||
promiseMiddleware(async (req, res) => {
|
||||
let users = await User.find({});
|
||||
users.forEach(
|
||||
(e) => delete e.password && delete e.salt && delete e.encryption_key
|
||||
);
|
||||
res.json(users);
|
||||
})
|
||||
)
|
||||
/**
|
||||
* @api {delete} /admin/user
|
||||
* @apiName AdminDeleteUser
|
||||
*
|
||||
* @apiParam {String} id The User ID
|
||||
*
|
||||
* @apiGroup admin_user
|
||||
* @apiPermission admin
|
||||
*
|
||||
* @apiSuccess {Boolean} success
|
||||
*/
|
||||
.delete(
|
||||
promiseMiddleware(async (req, res) => {
|
||||
let { id } = req.query as { [key: string]: string };
|
||||
let user = await User.findById(id);
|
||||
|
||||
await Promise.all([
|
||||
user.mails.map((mail) => Mail.delete(mail)),
|
||||
[
|
||||
RefreshToken.deleteFilter({ user: user._id }),
|
||||
LoginToken.deleteFilter({ user: user._id }),
|
||||
],
|
||||
]);
|
||||
|
||||
await User.delete(user);
|
||||
res.json({ success: true });
|
||||
})
|
||||
)
|
||||
/**
|
||||
* @api {put} /admin/user
|
||||
* @apiName AdminChangeUser
|
||||
*
|
||||
* @apiParam {String} id The User ID
|
||||
*
|
||||
* @apiGroup admin_user
|
||||
* @apiPermission admin
|
||||
*
|
||||
* @apiSuccess {Boolean} success
|
||||
*
|
||||
* @apiDescription Flipps the user role:
|
||||
* admin -> user
|
||||
* user -> admin
|
||||
*/
|
||||
.put(
|
||||
promiseMiddleware(async (req, res) => {
|
||||
let { id } = req.query as { [key: string]: string };
|
||||
let user = await User.findById(id);
|
||||
user.admin = !user.admin;
|
||||
await User.save(user);
|
||||
res.json({ success: true });
|
||||
})
|
||||
);
|
||||
export default UserRoute;
|
110
Backend/src/api/client/index.ts
Normal file
110
Backend/src/api/client/index.ts
Normal file
@ -0,0 +1,110 @@
|
||||
import { Request, Response, Router } from "express";
|
||||
import Stacker from "../middlewares/stacker";
|
||||
import {
|
||||
GetClientAuthMiddleware,
|
||||
GetClientApiAuthMiddleware,
|
||||
} from "../middlewares/client";
|
||||
import { GetUserMiddleware } from "../middlewares/user";
|
||||
import { createJWT } from "../../keys";
|
||||
import Client from "../../models/client";
|
||||
import RequestError, { HttpStatusCode } from "../../helper/request_error";
|
||||
import config from "../../config";
|
||||
import Mail from "../../models/mail";
|
||||
|
||||
const ClientRouter = Router();
|
||||
|
||||
/**
|
||||
* @api {get} /client/user
|
||||
*
|
||||
* @apiDescription Can be used for simple authentication of user. It will redirect the user to the redirect URI with a very short lived jwt.
|
||||
*
|
||||
* @apiParam {String} redirect_uri URL to redirect to on success
|
||||
* @apiParam {String} state A optional state, that will be included in the JWT and redirect_uri as parameter
|
||||
*
|
||||
* @apiName ClientUser
|
||||
* @apiGroup client
|
||||
*
|
||||
* @apiPermission user_client Requires ClientID and Authenticated User
|
||||
*/
|
||||
ClientRouter.get(
|
||||
"/user",
|
||||
Stacker(
|
||||
GetClientAuthMiddleware(false),
|
||||
GetUserMiddleware(false, false),
|
||||
async (req: Request, res: Response) => {
|
||||
let { redirect_uri, state } = req.query;
|
||||
|
||||
if (redirect_uri !== req.client.redirect_url)
|
||||
throw new RequestError(
|
||||
"Invalid redirect URI",
|
||||
HttpStatusCode.BAD_REQUEST
|
||||
);
|
||||
|
||||
let jwt = await createJWT(
|
||||
{
|
||||
client: req.client.client_id,
|
||||
uid: req.user.uid,
|
||||
username: req.user.username,
|
||||
state: state,
|
||||
},
|
||||
{
|
||||
expiresIn: 30,
|
||||
issuer: config.core.url,
|
||||
algorithm: "RS256",
|
||||
subject: req.user.uid,
|
||||
audience: req.client.client_id,
|
||||
}
|
||||
); //after 30 seconds this token is invalid
|
||||
res.redirect(
|
||||
redirect_uri + "?jwt=" + jwt + (state ? `&state=${state}` : "")
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
ClientRouter.get(
|
||||
"/account",
|
||||
Stacker(GetClientApiAuthMiddleware(), async (req: Request, res) => {
|
||||
let mails = await Promise.all(
|
||||
req.user.mails.map((id) => Mail.findById(id))
|
||||
);
|
||||
|
||||
let mail = mails.find((e) => e.primary) || mails[0];
|
||||
|
||||
res.json({
|
||||
user: {
|
||||
username: req.user.username,
|
||||
name: req.user.name,
|
||||
email: mail,
|
||||
},
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* @api {get} /client/featured
|
||||
*
|
||||
* @apiDescription Get a list of clients, that want to be featured on the home page
|
||||
*
|
||||
* @apiName GetFeaturedClients
|
||||
* @apiGroup client
|
||||
*/
|
||||
ClientRouter.get(
|
||||
"/featured",
|
||||
Stacker(async (req: Request, res) => {
|
||||
let clients = await Client.find({
|
||||
featured: true,
|
||||
});
|
||||
|
||||
res.json({
|
||||
clients: clients.map(({ name, logo, website, description }) => ({
|
||||
name,
|
||||
logo,
|
||||
website,
|
||||
description,
|
||||
})),
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
export default ClientRouter;
|
98
Backend/src/api/client/permissions.ts
Normal file
98
Backend/src/api/client/permissions.ts
Normal file
@ -0,0 +1,98 @@
|
||||
import { Request, Response } from "express";
|
||||
import Stacker from "../middlewares/stacker";
|
||||
import {
|
||||
ClientAuthMiddleware,
|
||||
GetClientAuthMiddleware,
|
||||
} from "../middlewares/client";
|
||||
import Permission from "../../models/permissions";
|
||||
import User from "../../models/user";
|
||||
|
||||
import RequestError, { HttpStatusCode } from "../../helper/request_error";
|
||||
import Grant from "../../models/grants";
|
||||
import { ObjectID } from "mongodb";
|
||||
|
||||
export const GetPermissions = Stacker(
|
||||
GetClientAuthMiddleware(true),
|
||||
async (req: Request, res: Response) => {
|
||||
const { user, permission } = req.query as { [key: string]: string };
|
||||
|
||||
let permissions: { id: string; name: string; description: string }[];
|
||||
let users: string[];
|
||||
|
||||
if (user) {
|
||||
const grant = await Grant.findOne({
|
||||
client: req.client._id,
|
||||
user: new ObjectID(user),
|
||||
});
|
||||
|
||||
permissions = await Promise.all(
|
||||
grant.permissions.map((perm) => Permission.findById(perm))
|
||||
).then((res) =>
|
||||
res
|
||||
.filter((e) => e.grant_type === "client")
|
||||
.map((e) => {
|
||||
return {
|
||||
id: e._id.toHexString(),
|
||||
name: e.name,
|
||||
description: e.description,
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (permission) {
|
||||
const grants = await Grant.find({
|
||||
client: req.client._id,
|
||||
permissions: new ObjectID(permission),
|
||||
});
|
||||
|
||||
users = grants.map((grant) => grant.user.toHexString());
|
||||
}
|
||||
|
||||
res.json({ permissions, users });
|
||||
}
|
||||
);
|
||||
|
||||
export const PostPermissions = Stacker(
|
||||
GetClientAuthMiddleware(true),
|
||||
async (req: Request, res: Response) => {
|
||||
const { permission, uid } = req.body;
|
||||
|
||||
const user = await User.findOne({ uid });
|
||||
if (!user) {
|
||||
throw new RequestError("User not found!", HttpStatusCode.BAD_REQUEST);
|
||||
}
|
||||
|
||||
const permissionDoc = await Permission.findById(permission);
|
||||
if (!permissionDoc || !permissionDoc.client.equals(req.client._id)) {
|
||||
throw new RequestError(
|
||||
"Permission not found!",
|
||||
HttpStatusCode.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
|
||||
let grant = await Grant.findOne({
|
||||
client: req.client._id,
|
||||
user: req.user._id,
|
||||
});
|
||||
|
||||
if (!grant) {
|
||||
grant = Grant.new({
|
||||
client: req.client._id,
|
||||
user: req.user._id,
|
||||
permissions: [],
|
||||
});
|
||||
}
|
||||
|
||||
//TODO: Fix clients getting user data without consent, when a grant is created and no additional permissions are requested, since for now, it is only checked for grant existance to make client access user data
|
||||
|
||||
if (grant.permissions.indexOf(permission) < 0)
|
||||
grant.permissions.push(permission);
|
||||
|
||||
await Grant.save(grant);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
});
|
||||
}
|
||||
);
|
33
Backend/src/api/index.ts
Normal file
33
Backend/src/api/index.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import * as express from "express";
|
||||
import AdminRoute from "./admin";
|
||||
import UserRoute from "./user";
|
||||
import InternalRoute from "./internal";
|
||||
import Login from "./user/login";
|
||||
import ClientRouter from "./client";
|
||||
import * as cors from "cors";
|
||||
import OAuthRoute from "./oauth";
|
||||
import config from "../config";
|
||||
|
||||
const ApiRouter: express.IRouter = express.Router();
|
||||
ApiRouter.use("/admin", AdminRoute);
|
||||
ApiRouter.use(cors());
|
||||
ApiRouter.use("/user", UserRoute);
|
||||
ApiRouter.use("/internal", InternalRoute);
|
||||
ApiRouter.use("/oauth", OAuthRoute);
|
||||
|
||||
ApiRouter.use("/client", ClientRouter);
|
||||
|
||||
// Legacy reasons (deprecated)
|
||||
ApiRouter.use("/", ClientRouter);
|
||||
|
||||
// Legacy reasons (deprecated)
|
||||
ApiRouter.post("/login", Login);
|
||||
|
||||
ApiRouter.get("/config.json", (req, res) => {
|
||||
return res.json({
|
||||
name: config.core.name,
|
||||
url: config.core.url,
|
||||
});
|
||||
});
|
||||
|
||||
export default ApiRouter;
|
30
Backend/src/api/internal/index.ts
Normal file
30
Backend/src/api/internal/index.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { Router } from "express";
|
||||
import { OAuthInternalApp } from "./oauth";
|
||||
import PasswordAuth from "./password";
|
||||
|
||||
const InternalRoute: Router = Router();
|
||||
/**
|
||||
* @api {get} /internal/oauth
|
||||
* @apiName ClientInteralOAuth
|
||||
*
|
||||
* @apiGroup client_internal
|
||||
* @apiPermission client_internal Only ClientID
|
||||
*
|
||||
* @apiParam {String} redirect_uri Redirect URI called after success
|
||||
* @apiParam {String} state State will be set in RedirectURI for the client to check
|
||||
*/
|
||||
InternalRoute.get("/oauth", OAuthInternalApp);
|
||||
|
||||
/**
|
||||
* @api {post} /internal/password
|
||||
* @apiName ClientInteralPassword
|
||||
*
|
||||
* @apiGroup client_internal
|
||||
* @apiPermission client_internal Requires ClientID and Secret
|
||||
*
|
||||
* @apiParam {String} username Username (either username or UID)
|
||||
* @apiParam {String} uid User ID (either username or UID)
|
||||
* @apiParam {String} password Hashed and Salted according to specification
|
||||
*/
|
||||
InternalRoute.post("/password", PasswordAuth);
|
||||
export default InternalRoute;
|
41
Backend/src/api/internal/oauth.ts
Normal file
41
Backend/src/api/internal/oauth.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import Stacker from "../middlewares/stacker";
|
||||
import { GetClientAuthMiddleware } from "../middlewares/client";
|
||||
import { UserMiddleware } from "../middlewares/user";
|
||||
import RequestError, { HttpStatusCode } from "../../helper/request_error";
|
||||
import ClientCode from "../../models/client_code";
|
||||
import moment = require("moment");
|
||||
import { randomBytes } from "crypto";
|
||||
export const OAuthInternalApp = Stacker(
|
||||
GetClientAuthMiddleware(false, true),
|
||||
UserMiddleware,
|
||||
async (req: Request, res: Response) => {
|
||||
let { redirect_uri, state } = req.query as { [key: string]: string };
|
||||
if (!redirect_uri) {
|
||||
throw new RequestError(
|
||||
"No redirect url set!",
|
||||
HttpStatusCode.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
|
||||
let sep = redirect_uri.indexOf("?") < 0 ? "?" : "&";
|
||||
|
||||
let code = ClientCode.new({
|
||||
user: req.user._id,
|
||||
client: req.client._id,
|
||||
validTill: moment().add(30, "minutes").toDate(),
|
||||
code: randomBytes(16).toString("hex"),
|
||||
permissions: [],
|
||||
});
|
||||
await ClientCode.save(code);
|
||||
|
||||
res.redirect(
|
||||
redirect_uri +
|
||||
sep +
|
||||
"code=" +
|
||||
code.code +
|
||||
(state ? "&state=" + state : "")
|
||||
);
|
||||
res.end();
|
||||
}
|
||||
);
|
35
Backend/src/api/internal/password.ts
Normal file
35
Backend/src/api/internal/password.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { GetClientAuthMiddleware } from "../middlewares/client";
|
||||
import Stacker from "../middlewares/stacker";
|
||||
import RequestError, { HttpStatusCode } from "../../helper/request_error";
|
||||
import User from "../../models/user";
|
||||
|
||||
const PasswordAuth = Stacker(
|
||||
GetClientAuthMiddleware(true, true),
|
||||
async (req: Request, res: Response) => {
|
||||
let {
|
||||
username,
|
||||
password,
|
||||
uid,
|
||||
}: { username: string; password: string; uid: string } = req.body;
|
||||
let query: any = { password: password };
|
||||
if (username) {
|
||||
query.username = username.toLowerCase();
|
||||
} else if (uid) {
|
||||
query.uid = uid;
|
||||
} else {
|
||||
throw new RequestError(
|
||||
req.__("No username or uid set"),
|
||||
HttpStatusCode.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
|
||||
let user = await User.findOne(query);
|
||||
if (!user) {
|
||||
res.json({ error: req.__("Password or username wrong") });
|
||||
} else {
|
||||
res.json({ success: true, uid: user.uid });
|
||||
}
|
||||
}
|
||||
);
|
||||
export default PasswordAuth;
|
110
Backend/src/api/middlewares/client.ts
Normal file
110
Backend/src/api/middlewares/client.ts
Normal file
@ -0,0 +1,110 @@
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import RequestError, { HttpStatusCode } from "../../helper/request_error";
|
||||
import Client from "../../models/client";
|
||||
import { validateJWT } from "../../keys";
|
||||
import User from "../../models/user";
|
||||
import Mail from "../../models/mail";
|
||||
import { OAuthJWT } from "../../helper/jwt";
|
||||
|
||||
export function GetClientAuthMiddleware(
|
||||
checksecret = true,
|
||||
internal = false,
|
||||
checksecret_if_available = false
|
||||
) {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
let client_id = req.query.client_id || req.body.client_id;
|
||||
let client_secret = req.query.client_secret || req.body.client_secret;
|
||||
|
||||
if (!client_id && !client_secret && req.headers.authorization) {
|
||||
let header = req.headers.authorization;
|
||||
let [type, val] = header.split(" ");
|
||||
if (val) {
|
||||
let str = Buffer.from(val, "base64").toString("utf-8");
|
||||
let [id, secret] = str.split(":");
|
||||
client_id = id;
|
||||
client_secret = secret;
|
||||
}
|
||||
}
|
||||
|
||||
if (!client_id || (!client_secret && checksecret)) {
|
||||
throw new RequestError(
|
||||
"No client credentials",
|
||||
HttpStatusCode.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
let w = { client_id: client_id, client_secret: client_secret };
|
||||
if (!checksecret && !(checksecret_if_available && client_secret))
|
||||
delete w.client_secret;
|
||||
|
||||
let client = await Client.findOne(w);
|
||||
|
||||
if (!client) {
|
||||
throw new RequestError(
|
||||
"Invalid client_id" + (checksecret ? "or client_secret" : ""),
|
||||
HttpStatusCode.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
|
||||
if (internal && !client.internal) {
|
||||
throw new RequestError(
|
||||
req.__("Client has no permission for access"),
|
||||
HttpStatusCode.FORBIDDEN
|
||||
);
|
||||
}
|
||||
req.client = client;
|
||||
next();
|
||||
} catch (e) {
|
||||
if (next) next(e);
|
||||
else throw e;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const ClientAuthMiddleware = GetClientAuthMiddleware();
|
||||
|
||||
export function GetClientApiAuthMiddleware(permissions?: string[]) {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const invalid_err = new RequestError(
|
||||
req.__("You are not logged in or your login is expired"),
|
||||
HttpStatusCode.UNAUTHORIZED
|
||||
);
|
||||
let token =
|
||||
(req.query.access_token as string) ||
|
||||
(req.headers.authorization as string);
|
||||
if (!token) throw invalid_err;
|
||||
|
||||
if (token.toLowerCase().startsWith("bearer "))
|
||||
token = token.substring(7);
|
||||
|
||||
let data: OAuthJWT;
|
||||
try {
|
||||
data = await validateJWT(token);
|
||||
} catch (err) {
|
||||
throw invalid_err;
|
||||
}
|
||||
|
||||
let user = await User.findOne({ uid: data.user });
|
||||
|
||||
if (!user) throw invalid_err;
|
||||
|
||||
let client = await Client.findOne({ client_id: data.application });
|
||||
if (!client) throw invalid_err;
|
||||
|
||||
if (
|
||||
permissions &&
|
||||
(!data.permissions ||
|
||||
!permissions.every((e) => data.permissions.indexOf(e) >= 0))
|
||||
)
|
||||
throw invalid_err;
|
||||
|
||||
req.user = user;
|
||||
req.client = client;
|
||||
next();
|
||||
} catch (e) {
|
||||
if (next) next(e);
|
||||
else throw e;
|
||||
}
|
||||
};
|
||||
}
|
28
Backend/src/api/middlewares/stacker.ts
Normal file
28
Backend/src/api/middlewares/stacker.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { Request, Response, NextFunction, RequestHandler } from "express";
|
||||
import promiseMiddleware from "../../helper/promiseMiddleware";
|
||||
|
||||
type RH = (req: Request, res: Response, next?: NextFunction) => any;
|
||||
|
||||
function call(handler: RH, req: Request, res: Response) {
|
||||
return new Promise<void>((yes, no) => {
|
||||
let p = handler(req, res, (err) => {
|
||||
if (err) no(err);
|
||||
else yes();
|
||||
});
|
||||
if (p && p.catch) p.catch((err) => no(err));
|
||||
});
|
||||
}
|
||||
|
||||
const Stacker = (...handler: RH[]) => {
|
||||
return promiseMiddleware(
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
let hc = handler.concat();
|
||||
while (hc.length > 0) {
|
||||
let h = hc.shift();
|
||||
await call(h, req, res);
|
||||
}
|
||||
next();
|
||||
}
|
||||
);
|
||||
};
|
||||
export default Stacker;
|
106
Backend/src/api/middlewares/user.ts
Normal file
106
Backend/src/api/middlewares/user.ts
Normal file
@ -0,0 +1,106 @@
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import LoginToken, { CheckToken } from "../../models/login_token";
|
||||
import Logging from "@hibas123/nodelogging";
|
||||
import RequestError, { HttpStatusCode } from "../../helper/request_error";
|
||||
import User from "../../models/user";
|
||||
import promiseMiddleware from "../../helper/promiseMiddleware";
|
||||
|
||||
class Invalid extends Error {}
|
||||
|
||||
/**
|
||||
* Returns customized Middleware function, that could also be called directly
|
||||
* by code and will return true or false depending on the token. In the false
|
||||
* case it will also send error and redirect if json is not set
|
||||
* @param json Default false. Checks if requests wants an json or html for returning errors
|
||||
* @param special_required Default false. If true, a special token is required
|
||||
* @param redirect_uri Default current uri. Sets the uri to redirect, if json is not set and user not logged in
|
||||
* @param validated Default true. If false, the token must not be validated
|
||||
*/
|
||||
export function GetUserMiddleware(
|
||||
json = false,
|
||||
special_required: boolean = false,
|
||||
redirect_uri?: string,
|
||||
validated = true
|
||||
) {
|
||||
return promiseMiddleware(async function (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next?: NextFunction
|
||||
) {
|
||||
const invalid = (message: string) => {
|
||||
throw new Invalid(req.__(message));
|
||||
};
|
||||
try {
|
||||
let { login, special } = req.query as { [key: string]: string };
|
||||
if (!login) {
|
||||
login = req.cookies.login;
|
||||
special = req.cookies.special;
|
||||
}
|
||||
if (!login) invalid("No login token");
|
||||
if (!special && special_required) invalid("No special token");
|
||||
|
||||
let token = await LoginToken.findOne({ token: login, valid: true });
|
||||
if (!(await CheckToken(token, validated)))
|
||||
invalid("Login token invalid");
|
||||
|
||||
let user = await User.findById(token.user);
|
||||
if (!user) {
|
||||
token.valid = false;
|
||||
await LoginToken.save(token);
|
||||
invalid("Login token invalid");
|
||||
}
|
||||
|
||||
let special_token;
|
||||
if (special) {
|
||||
Logging.debug("Special found");
|
||||
special_token = await LoginToken.findOne({
|
||||
token: special,
|
||||
special: true,
|
||||
valid: true,
|
||||
user: token.user,
|
||||
});
|
||||
if (!(await CheckToken(special_token, validated)))
|
||||
invalid("Special token invalid");
|
||||
req.special = true;
|
||||
}
|
||||
|
||||
req.user = user;
|
||||
req.isAdmin = user.admin;
|
||||
req.token = {
|
||||
login: token,
|
||||
special: special_token,
|
||||
};
|
||||
|
||||
if (next) next();
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (e instanceof Invalid) {
|
||||
if (req.method === "GET" && !json) {
|
||||
res.status(HttpStatusCode.UNAUTHORIZED);
|
||||
res.redirect(
|
||||
"/login?base64=true&state=" +
|
||||
Buffer.from(
|
||||
redirect_uri ? redirect_uri : req.originalUrl
|
||||
).toString("base64")
|
||||
);
|
||||
} else {
|
||||
throw new RequestError(
|
||||
req.__(
|
||||
"You are not logged in or your login is expired" +
|
||||
` (${e.message})`
|
||||
),
|
||||
HttpStatusCode.UNAUTHORIZED,
|
||||
undefined,
|
||||
{ auth: true }
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (next) next(e);
|
||||
else throw e;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export const UserMiddleware = GetUserMiddleware();
|
142
Backend/src/api/middlewares/verify.ts
Normal file
142
Backend/src/api/middlewares/verify.ts
Normal file
@ -0,0 +1,142 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import Logging from "@hibas123/nodelogging";
|
||||
import {
|
||||
isString,
|
||||
isDate,
|
||||
} from "util";
|
||||
import RequestError, { HttpStatusCode } from "../../helper/request_error";
|
||||
|
||||
export enum Types {
|
||||
STRING,
|
||||
NUMBER,
|
||||
BOOLEAN,
|
||||
EMAIL,
|
||||
OBJECT,
|
||||
DATE,
|
||||
ARRAY,
|
||||
ENUM,
|
||||
}
|
||||
|
||||
function isEmail(value: any): boolean {
|
||||
return /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(
|
||||
value
|
||||
);
|
||||
}
|
||||
|
||||
export interface CheckObject {
|
||||
type: Types;
|
||||
query?: boolean;
|
||||
optional?: boolean;
|
||||
|
||||
/**
|
||||
* Only when Type.ENUM
|
||||
*
|
||||
* values to check before
|
||||
*/
|
||||
values?: string[];
|
||||
|
||||
/**
|
||||
* Only when Type.STRING
|
||||
*/
|
||||
notempty?: boolean; // Only STRING
|
||||
}
|
||||
|
||||
export interface Checks {
|
||||
[index: string]: CheckObject; // | Types
|
||||
}
|
||||
|
||||
// req: Request, res: Response, next: NextFunction
|
||||
export default function (fields: Checks, noadditional = false) {
|
||||
return (req: Request, res: Response, next: NextFunction) => {
|
||||
let errors: { message: string; field: string }[] = [];
|
||||
|
||||
function check(data: any, field_name: string, field: CheckObject) {
|
||||
if (data !== undefined && data !== null) {
|
||||
switch (field.type) {
|
||||
case Types.STRING:
|
||||
if (isString(data)) {
|
||||
if (!field.notempty) return;
|
||||
if (data !== "") return;
|
||||
}
|
||||
break;
|
||||
case Types.NUMBER:
|
||||
if (typeof data == "number") return;
|
||||
break;
|
||||
case Types.EMAIL:
|
||||
if (isEmail(data)) return;
|
||||
break;
|
||||
case Types.BOOLEAN:
|
||||
if (typeof data == "boolean") return;
|
||||
break;
|
||||
case Types.OBJECT:
|
||||
if (typeof data == "object") return;
|
||||
break;
|
||||
case Types.ARRAY:
|
||||
if (Array.isArray(data)) return;
|
||||
break;
|
||||
case Types.DATE:
|
||||
if (isDate(data)) return;
|
||||
break;
|
||||
case Types.ENUM:
|
||||
if (typeof data == "string") {
|
||||
if (field.values.indexOf(data) >= 0) return;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
Logging.error(
|
||||
`Invalid type to check: ${field.type} ${Types[field.type]}`
|
||||
);
|
||||
}
|
||||
errors.push({
|
||||
message: res.__(
|
||||
"Field {{field}} has wrong type. It should be from type {{type}}",
|
||||
{ field: field_name, type: Types[field.type].toLowerCase() }
|
||||
),
|
||||
field: field_name,
|
||||
});
|
||||
} else {
|
||||
if (!field.optional)
|
||||
errors.push({
|
||||
message: res.__("Field {{field}} is not defined", {
|
||||
field: field_name,
|
||||
}),
|
||||
field: field_name,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (let field_name in fields) {
|
||||
let field = fields[field_name];
|
||||
let data = fields[field_name].query
|
||||
? req.query[field_name]
|
||||
: req.body[field_name];
|
||||
check(data, field_name, field);
|
||||
}
|
||||
|
||||
if (noadditional) {
|
||||
//Checks if the data given has additional parameters
|
||||
let should = Object.keys(fields);
|
||||
should = should.filter((e) => !fields[e].query); //Query parameters should not exist on body
|
||||
let has = Object.keys(req.body);
|
||||
|
||||
has.every((e) => {
|
||||
if (should.indexOf(e) >= 0) {
|
||||
return true;
|
||||
} else {
|
||||
errors.push({
|
||||
message: res.__("Field {{field}} should not be there", {
|
||||
field: e,
|
||||
}),
|
||||
field: e,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
let err = new RequestError(errors, HttpStatusCode.BAD_REQUEST, true);
|
||||
next(err);
|
||||
} else next();
|
||||
};
|
||||
}
|
249
Backend/src/api/oauth/auth.ts
Normal file
249
Backend/src/api/oauth/auth.ts
Normal file
@ -0,0 +1,249 @@
|
||||
import Stacker from "../middlewares/stacker";
|
||||
import { GetUserMiddleware } from "../middlewares/user";
|
||||
import { Request, Response } from "express";
|
||||
import Client from "../../models/client";
|
||||
import Logging from "@hibas123/nodelogging";
|
||||
import Permission, { IPermission } from "../../models/permissions";
|
||||
import ClientCode from "../../models/client_code";
|
||||
import moment = require("moment");
|
||||
import { randomBytes } from "crypto";
|
||||
// import { ObjectID } from "bson";
|
||||
import Grant, { IGrant } from "../../models/grants";
|
||||
import GetAuthPage from "../../views/authorize";
|
||||
import { ObjectID } from "mongodb";
|
||||
|
||||
// const AuthRoute = Stacker(GetUserMiddleware(true), async (req: Request, res: Response) => {
|
||||
// let { response_type, client_id, redirect_uri, scope, state, nored } = req.query;
|
||||
// const sendError = (type) => {
|
||||
// if (redirect_uri === "$local")
|
||||
// redirect_uri = "/code";
|
||||
// res.redirect(redirect_uri += `?error=${type}&state=${state}`);
|
||||
// }
|
||||
// /**
|
||||
// * error
|
||||
// REQUIRED. A single ASCII [USASCII] error code from the
|
||||
// following:
|
||||
// invalid_request
|
||||
// The request is missing a required parameter, includes an
|
||||
// invalid parameter value, includes a parameter more than
|
||||
// once, or is otherwise malformed.
|
||||
// unauthorized_client
|
||||
// The client is not authorized to request an authorization
|
||||
// code using this method.
|
||||
// access_denied
|
||||
// The resource owner or authorization server denied the
|
||||
// request.
|
||||
// */
|
||||
// try {
|
||||
|
||||
// if (response_type !== "code") {
|
||||
// return sendError("unsupported_response_type");
|
||||
// } else {
|
||||
// let client = await Client.findOne({ client_id: client_id })
|
||||
// if (!client) {
|
||||
// return sendError("unauthorized_client")
|
||||
// }
|
||||
|
||||
// if (redirect_uri && client.redirect_url !== redirect_uri) {
|
||||
// Logging.log(redirect_uri, client.redirect_url);
|
||||
// return res.send("Invalid redirect_uri. Please check the integrity of the site requesting and contact the administrator of the page, you want to authorize!");
|
||||
// }
|
||||
|
||||
// let permissions: IPermission[] = [];
|
||||
// if (scope) {
|
||||
// let perms = (<string>scope).split(";").filter(e => e !== "read_user").map(p => new ObjectID(p));
|
||||
// permissions = await Permission.find({ _id: { $in: perms } })
|
||||
|
||||
// if (permissions.length != perms.length) {
|
||||
// return sendError("invalid_scope");
|
||||
// }
|
||||
// }
|
||||
|
||||
// let code = ClientCode.new({
|
||||
// user: req.user._id,
|
||||
// client: client._id,
|
||||
// permissions: permissions.map(p => p._id),
|
||||
// validTill: moment().add(30, "minutes").toDate(),
|
||||
// code: randomBytes(16).toString("hex")
|
||||
// });
|
||||
// await ClientCode.save(code);
|
||||
|
||||
// let redir = client.redirect_url === "$local" ? "/code" : client.redirect_url;
|
||||
|
||||
// let ruri = redir + `?code=${code.code}&state=${state}`;
|
||||
// if (nored === "true") {
|
||||
// res.json({
|
||||
// redirect_uri: ruri
|
||||
// })
|
||||
// } else {
|
||||
// res.redirect(ruri);
|
||||
// }
|
||||
// }
|
||||
// } catch (err) {
|
||||
// Logging.error(err);
|
||||
// sendError("server_error")
|
||||
// }
|
||||
// })
|
||||
|
||||
const GetAuthRoute = (view = false) =>
|
||||
Stacker(GetUserMiddleware(false), async (req: Request, res: Response) => {
|
||||
let {
|
||||
response_type,
|
||||
client_id,
|
||||
redirect_uri,
|
||||
scope = "",
|
||||
state,
|
||||
nored,
|
||||
} = req.query as { [key: string]: string };
|
||||
const sendError = (type) => {
|
||||
if (redirect_uri === "$local") redirect_uri = "/code";
|
||||
res.redirect(
|
||||
(redirect_uri += `?error=${type}${state ? "&state=" + state : ""}`)
|
||||
);
|
||||
};
|
||||
|
||||
const scopes = scope.split(";").filter((e: string) => e !== "");
|
||||
|
||||
Logging.debug("Scopes:", scope);
|
||||
|
||||
try {
|
||||
if (response_type !== "code") {
|
||||
return sendError("unsupported_response_type");
|
||||
} else {
|
||||
let client = await Client.findOne({ client_id: client_id });
|
||||
if (!client) {
|
||||
return sendError("unauthorized_client");
|
||||
}
|
||||
|
||||
if (redirect_uri && client.redirect_url !== redirect_uri) {
|
||||
Logging.log(redirect_uri, client.redirect_url);
|
||||
return res.send(
|
||||
"Invalid redirect_uri. Please check the integrity of the site requesting and contact the administrator of the page, you want to authorize!"
|
||||
);
|
||||
}
|
||||
|
||||
let permissions: IPermission[] = [];
|
||||
let proms: PromiseLike<void>[] = [];
|
||||
if (scopes) {
|
||||
for (let perm of scopes.filter((e) => e !== "read_user")) {
|
||||
let oid = undefined;
|
||||
try {
|
||||
oid = new ObjectID(perm);
|
||||
} catch (err) {
|
||||
Logging.error(err);
|
||||
continue;
|
||||
}
|
||||
proms.push(
|
||||
Permission.findById(oid).then((p) => {
|
||||
if (!p) return Promise.reject(new Error());
|
||||
permissions.push(p);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let err = undefined;
|
||||
await Promise.all(proms).catch((e) => {
|
||||
err = e;
|
||||
});
|
||||
|
||||
if (err) {
|
||||
Logging.error(err);
|
||||
return sendError("invalid_scope");
|
||||
}
|
||||
|
||||
let grant: IGrant | undefined = await Grant.findOne({
|
||||
client: client._id,
|
||||
user: req.user._id,
|
||||
});
|
||||
|
||||
Logging.debug("Grant", grant, permissions);
|
||||
|
||||
let missing_permissions: IPermission[] = [];
|
||||
|
||||
if (grant) {
|
||||
missing_permissions = grant.permissions
|
||||
.map((perm) => permissions.find((p) => p._id.equals(perm)))
|
||||
.filter((e) => !!e);
|
||||
} else {
|
||||
missing_permissions = permissions;
|
||||
}
|
||||
|
||||
let client_granted_perm = missing_permissions.filter(
|
||||
(e) => e.grant_type == "client"
|
||||
);
|
||||
if (client_granted_perm.length > 0) {
|
||||
return sendError("no_permission");
|
||||
}
|
||||
|
||||
if (!grant && missing_permissions.length > 0) {
|
||||
await new Promise<void>((yes, no) =>
|
||||
GetUserMiddleware(false, true)(
|
||||
req,
|
||||
res,
|
||||
(err?: Error | string) => (err ? no(err) : yes())
|
||||
)
|
||||
); // Maybe unresolved when redirect is happening
|
||||
|
||||
if (view) {
|
||||
res.send(
|
||||
GetAuthPage(
|
||||
req.__,
|
||||
client.name,
|
||||
permissions.map((perm) => {
|
||||
return {
|
||||
name: perm.name,
|
||||
description: perm.description,
|
||||
logo: client.logo,
|
||||
};
|
||||
})
|
||||
)
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
if ((req.body.allow = "true")) {
|
||||
if (!grant)
|
||||
grant = Grant.new({
|
||||
client: client._id,
|
||||
user: req.user._id,
|
||||
permissions: [],
|
||||
});
|
||||
|
||||
grant.permissions.push(
|
||||
...missing_permissions.map((e) => e._id)
|
||||
);
|
||||
await Grant.save(grant);
|
||||
} else {
|
||||
return sendError("access_denied");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let code = ClientCode.new({
|
||||
user: req.user._id,
|
||||
client: client._id,
|
||||
permissions: permissions.map((p) => p._id),
|
||||
validTill: moment().add(30, "minutes").toDate(),
|
||||
code: randomBytes(16).toString("hex"),
|
||||
});
|
||||
await ClientCode.save(code);
|
||||
|
||||
let redir =
|
||||
client.redirect_url === "$local" ? "/code" : client.redirect_url;
|
||||
let ruri =
|
||||
redir + `?code=${code.code}${state ? "&state=" + state : ""}`;
|
||||
if (nored === "true") {
|
||||
res.json({
|
||||
redirect_uri: ruri,
|
||||
});
|
||||
} else {
|
||||
res.redirect(ruri);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
Logging.error(err);
|
||||
sendError("server_error");
|
||||
}
|
||||
});
|
||||
|
||||
export default GetAuthRoute;
|
63
Backend/src/api/oauth/index.ts
Normal file
63
Backend/src/api/oauth/index.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import { Router } from "express";
|
||||
import GetAuthRoute from "./auth";
|
||||
import JWTRoute from "./jwt";
|
||||
import Public from "./public";
|
||||
import RefreshTokenRoute from "./refresh";
|
||||
|
||||
const OAuthRoue: Router = Router();
|
||||
/**
|
||||
* @api {post} /oauth/auth
|
||||
* @apiName OAuthAuth
|
||||
*
|
||||
* @apiGroup oauth
|
||||
* @apiPermission user Special required
|
||||
*
|
||||
* @apiParam {String} response_type must be "code" others are not supported
|
||||
* @apiParam {String} client_id ClientID
|
||||
* @apiParam {String} redirect_uri The URI to redirect with code
|
||||
* @apiParam {String} scope Scope that contains the requested permissions (comma seperated list of permissions)
|
||||
* @apiParam {String} state State, that will be passed to redirect_uri for client
|
||||
* @apiParam {String} nored Deactivates the Redirect response from server and instead returns the redirect URI in JSON response
|
||||
*/
|
||||
OAuthRoue.post("/auth", GetAuthRoute(false));
|
||||
|
||||
/**
|
||||
* @api {get} /oauth/jwt
|
||||
* @apiName OAuthJwt
|
||||
*
|
||||
* @apiGroup oauth
|
||||
* @apiPermission none
|
||||
*
|
||||
* @apiParam {String} refreshtoken
|
||||
*
|
||||
* @apiSuccess {String} token The JWT that allowes the application to access the recources granted for refresh token
|
||||
*/
|
||||
OAuthRoue.get("/jwt", JWTRoute);
|
||||
|
||||
/**
|
||||
* @api {get} /oauth/public
|
||||
* @apiName OAuthPublic
|
||||
*
|
||||
* @apiGroup oauth
|
||||
* @apiPermission none
|
||||
*
|
||||
* @apiSuccess {String} public_key The applications public_key. Used to verify JWT.
|
||||
*/
|
||||
OAuthRoue.get("/public", Public);
|
||||
|
||||
/**
|
||||
* @api {get} /oauth/refresh
|
||||
* @apiName OAuthRefreshGet
|
||||
*
|
||||
* @apiGroup oauth
|
||||
*/
|
||||
OAuthRoue.get("/refresh", RefreshTokenRoute);
|
||||
|
||||
/**
|
||||
* @api {post} /oauth/refresh
|
||||
* @apiName OAuthRefreshPost
|
||||
*
|
||||
* @apiGroup oauth
|
||||
*/
|
||||
OAuthRoue.post("/refresh", RefreshTokenRoute);
|
||||
export default OAuthRoue;
|
43
Backend/src/api/oauth/jwt.ts
Normal file
43
Backend/src/api/oauth/jwt.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { Request, Response } from "express";
|
||||
import promiseMiddleware from "../../helper/promiseMiddleware";
|
||||
import RequestError, { HttpStatusCode } from "../../helper/request_error";
|
||||
import RefreshToken from "../../models/refresh_token";
|
||||
import User from "../../models/user";
|
||||
import Client from "../../models/client";
|
||||
import { getAccessTokenJWT } from "../../helper/jwt";
|
||||
|
||||
const JWTRoute = promiseMiddleware(async (req: Request, res: Response) => {
|
||||
let { refreshtoken } = req.query as { [key: string]: string };
|
||||
if (!refreshtoken)
|
||||
throw new RequestError(
|
||||
req.__("Refresh token not set"),
|
||||
HttpStatusCode.BAD_REQUEST
|
||||
);
|
||||
|
||||
let token = await RefreshToken.findOne({ token: refreshtoken });
|
||||
if (!token)
|
||||
throw new RequestError(
|
||||
req.__("Invalid token"),
|
||||
HttpStatusCode.BAD_REQUEST
|
||||
);
|
||||
|
||||
let user = await User.findById(token.user);
|
||||
if (!user) {
|
||||
token.valid = false;
|
||||
await RefreshToken.save(token);
|
||||
throw new RequestError(
|
||||
req.__("Invalid token"),
|
||||
HttpStatusCode.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
|
||||
let client = await Client.findById(token.client);
|
||||
|
||||
let jwt = await getAccessTokenJWT({
|
||||
user,
|
||||
permissions: token.permissions,
|
||||
client,
|
||||
});
|
||||
res.json({ token: jwt });
|
||||
});
|
||||
export default JWTRoute;
|
6
Backend/src/api/oauth/public.ts
Normal file
6
Backend/src/api/oauth/public.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { Request, Response } from "express";
|
||||
import { public_key } from "../../keys";
|
||||
|
||||
export default function Public(req: Request, res: Response) {
|
||||
res.json({ public_key: public_key });
|
||||
}
|
122
Backend/src/api/oauth/refresh.ts
Normal file
122
Backend/src/api/oauth/refresh.ts
Normal file
@ -0,0 +1,122 @@
|
||||
import { Request, Response } from "express";
|
||||
import RequestError, { HttpStatusCode } from "../../helper/request_error";
|
||||
import User from "../../models/user";
|
||||
import Client from "../../models/client";
|
||||
import {
|
||||
getAccessTokenJWT,
|
||||
getIDToken,
|
||||
AccessTokenJWTExp,
|
||||
} from "../../helper/jwt";
|
||||
import Stacker from "../middlewares/stacker";
|
||||
import { GetClientAuthMiddleware } from "../middlewares/client";
|
||||
import ClientCode from "../../models/client_code";
|
||||
import Mail from "../../models/mail";
|
||||
import { randomBytes } from "crypto";
|
||||
import moment = require("moment");
|
||||
// import { JWTExpDur } from "../../keys";
|
||||
import RefreshToken from "../../models/refresh_token";
|
||||
import { getEncryptionKey } from "../../helper/user_key";
|
||||
import { refreshTokenValidTime } from "../../config";
|
||||
|
||||
// TODO:
|
||||
/*
|
||||
For example, the authorization server could employ refresh token
|
||||
rotation in which a new refresh token is issued with every access
|
||||
token refresh response. The previous refresh token is invalidated but retained by the authorization server. If a refresh token is
|
||||
compromised and subsequently used by both the attacker and the
|
||||
legitimate client, one of them will present an invalidated refresh
|
||||
token, which will inform the authorization server of the breach.
|
||||
*/
|
||||
|
||||
const RefreshTokenRoute = Stacker(
|
||||
GetClientAuthMiddleware(false, false, true),
|
||||
async (req: Request, res: Response) => {
|
||||
let grant_type = req.query.grant_type || req.body.grant_type;
|
||||
if (!grant_type || grant_type === "authorization_code") {
|
||||
let code = req.query.code || req.body.code;
|
||||
let nonce = req.query.nonce || req.body.nonce;
|
||||
|
||||
let c = await ClientCode.findOne({ code: code });
|
||||
if (!c || moment(c.validTill).isBefore()) {
|
||||
throw new RequestError(
|
||||
req.__("Invalid code"),
|
||||
HttpStatusCode.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
|
||||
let client = await Client.findById(c.client);
|
||||
|
||||
let user = await User.findById(c.user);
|
||||
let mails = await Promise.all(user.mails.map((m) => Mail.findOne(m)));
|
||||
|
||||
let token = RefreshToken.new({
|
||||
user: c.user,
|
||||
client: c.client,
|
||||
permissions: c.permissions,
|
||||
token: randomBytes(16).toString("hex"),
|
||||
valid: true,
|
||||
validTill: moment().add(refreshTokenValidTime).toDate(),
|
||||
});
|
||||
await RefreshToken.save(token);
|
||||
await ClientCode.delete(c);
|
||||
|
||||
let mail = mails.find((e) => e.primary);
|
||||
if (!mail) mail = mails[0];
|
||||
|
||||
res.json({
|
||||
refresh_token: token.token,
|
||||
token: token.token,
|
||||
access_token: await getAccessTokenJWT({
|
||||
client: client,
|
||||
user: user,
|
||||
permissions: c.permissions,
|
||||
}),
|
||||
token_type: "bearer",
|
||||
expires_in: AccessTokenJWTExp.asSeconds(),
|
||||
profile: {
|
||||
uid: user.uid,
|
||||
email: mail ? mail.mail : "",
|
||||
name: user.name,
|
||||
enc_key: getEncryptionKey(user, client),
|
||||
},
|
||||
id_token: getIDToken(user, client.client_id, nonce),
|
||||
});
|
||||
} else if (grant_type === "refresh_token") {
|
||||
let refresh_token = req.query.refresh_token || req.body.refresh_token;
|
||||
if (!refresh_token)
|
||||
throw new RequestError(
|
||||
req.__("refresh_token not set"),
|
||||
HttpStatusCode.BAD_REQUEST
|
||||
);
|
||||
|
||||
let token = await RefreshToken.findOne({ token: refresh_token });
|
||||
if (!token || !token.valid || moment(token.validTill).isBefore())
|
||||
throw new RequestError(
|
||||
req.__("Invalid token"),
|
||||
HttpStatusCode.BAD_REQUEST
|
||||
);
|
||||
|
||||
token.validTill = moment().add(refreshTokenValidTime).toDate();
|
||||
await RefreshToken.save(token);
|
||||
|
||||
let user = await User.findById(token.user);
|
||||
let client = await Client.findById(token.client);
|
||||
let jwt = await getAccessTokenJWT({
|
||||
user,
|
||||
client,
|
||||
permissions: token.permissions,
|
||||
});
|
||||
res.json({
|
||||
access_token: jwt,
|
||||
expires_in: AccessTokenJWTExp.asSeconds(),
|
||||
});
|
||||
} else {
|
||||
throw new RequestError(
|
||||
"invalid grant_type",
|
||||
HttpStatusCode.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export default RefreshTokenRoute;
|
19
Backend/src/api/user/account.ts
Normal file
19
Backend/src/api/user/account.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { Request, Response } from "express";
|
||||
import Stacker from "../middlewares/stacker";
|
||||
import { GetUserMiddleware } from "../middlewares/user";
|
||||
import LoginToken, { CheckToken } from "../../models/login_token";
|
||||
import RequestError, { HttpStatusCode } from "../../helper/request_error";
|
||||
|
||||
export const GetAccount = Stacker(
|
||||
GetUserMiddleware(true, true),
|
||||
async (req: Request, res: Response) => {
|
||||
let user = {
|
||||
id: req.user.uid,
|
||||
name: req.user.name,
|
||||
username: req.user.username,
|
||||
birthday: req.user.birthday,
|
||||
gender: req.user.gender,
|
||||
};
|
||||
res.json({ user });
|
||||
}
|
||||
);
|
19
Backend/src/api/user/contact.ts
Normal file
19
Backend/src/api/user/contact.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { Request, Response } from "express";
|
||||
import Stacker from "../middlewares/stacker";
|
||||
import { GetUserMiddleware } from "../middlewares/user";
|
||||
import Mail from "../../models/mail";
|
||||
|
||||
export const GetContactInfos = Stacker(
|
||||
GetUserMiddleware(true, true),
|
||||
async (req: Request, res: Response) => {
|
||||
let mails = await Promise.all(
|
||||
req.user.mails.map((mail) => Mail.findById(mail))
|
||||
);
|
||||
|
||||
let contact = {
|
||||
mails: mails.filter((e) => !!e),
|
||||
phones: req.user.phones,
|
||||
};
|
||||
res.json({ contact });
|
||||
}
|
||||
);
|
132
Backend/src/api/user/index.ts
Normal file
132
Backend/src/api/user/index.ts
Normal file
@ -0,0 +1,132 @@
|
||||
import { Router } from "express";
|
||||
import { GetAccount } from "./account";
|
||||
import { GetContactInfos } from "./contact";
|
||||
import Login from "./login";
|
||||
import Register from "./register";
|
||||
import { DeleteToken, GetToken } from "./token";
|
||||
import TwoFactorRoute from "./twofactor";
|
||||
import OAuthRoute from "./oauth";
|
||||
|
||||
const UserRoute: Router = Router();
|
||||
|
||||
/**
|
||||
* @api {post} /user/register
|
||||
* @apiName UserRegister
|
||||
*
|
||||
* @apiGroup user
|
||||
* @apiPermission none
|
||||
*
|
||||
* @apiParam {String} mail EMail linked to this Account
|
||||
* @apiParam {String} username The new Username
|
||||
* @apiParam {String} password Password hashed and salted like specification
|
||||
* @apiParam {String} salt The Salt used for password hashing
|
||||
* @apiParam {String} regcode The regcode, that should be used
|
||||
* @apiParam {String} gender Gender can be: "male", "female", "other", "none"
|
||||
* @apiParam {String} name The real name of the User
|
||||
*
|
||||
* @apiSuccess {Boolean} success
|
||||
*
|
||||
* @apiErrorExample {Object} Error-Response:
|
||||
{
|
||||
error: [
|
||||
{
|
||||
message: "Some Error",
|
||||
field: "username"
|
||||
}
|
||||
],
|
||||
status: 400
|
||||
}
|
||||
*/
|
||||
UserRoute.post("/register", Register);
|
||||
|
||||
/**
|
||||
* @api {post} /user/login?type=:type
|
||||
* @apiName UserLogin
|
||||
*
|
||||
* @apiParam {String} type Type could be either "username" or "password"
|
||||
*
|
||||
* @apiGroup user
|
||||
* @apiPermission none
|
||||
*
|
||||
* @apiParam {String} username Username (either username or uid required)
|
||||
* @apiParam {String} uid (either username or uid required)
|
||||
* @apiParam {String} password Password hashed and salted like specification (only on type password)
|
||||
* @apiParam {Number} time in milliseconds used to hash password. This is used to make passwords "expire"
|
||||
*
|
||||
* @apiSuccess {String} uid On type = "username"
|
||||
* @apiSuccess {String} salt On type = "username"
|
||||
*
|
||||
* @apiSuccess {String} login On type = "password". Login Token
|
||||
* @apiSuccess {String} special On type = "password". Special Token
|
||||
* @apiSuccess {Object[]} tfa Will be set when TwoFactorAuthentication is required
|
||||
* @apiSuccess {String} tfa.id The ID of the TFA Method
|
||||
* @apiSuccess {String} tfa.name The name of the TFA Method
|
||||
* @apiSuccess {String} tfa.type The type of the TFA Method
|
||||
*/
|
||||
UserRoute.post("/login", Login);
|
||||
UserRoute.use("/twofactor", TwoFactorRoute);
|
||||
|
||||
/**
|
||||
* @api {get} /user/token
|
||||
* @apiName UserGetToken
|
||||
*
|
||||
* @apiGroup user
|
||||
* @apiPermission user
|
||||
*
|
||||
* @apiSuccess {Object[]} token
|
||||
* @apiSuccess {String} token.id The Token ID
|
||||
* @apiSuccess {String} token.special Identifies Special Token
|
||||
* @apiSuccess {String} token.ip IP the token was optained from
|
||||
* @apiSuccess {String} token.browser The Browser the token was optained from (User Agent)
|
||||
* @apiSuccess {Boolean} token.isthis Shows if it is token used by this session
|
||||
*/
|
||||
UserRoute.get("/token", GetToken);
|
||||
|
||||
/**
|
||||
* @api {delete} /user/token/:id
|
||||
* @apiParam {String} id The id of the token to be deleted
|
||||
*
|
||||
* @apiName UserDeleteToken
|
||||
*
|
||||
*
|
||||
* @apiGroup user
|
||||
* @apiPermission user
|
||||
*
|
||||
* @apiSuccess {Boolean} success
|
||||
*/
|
||||
UserRoute.delete("/token/:id", DeleteToken);
|
||||
|
||||
/**
|
||||
* @api {delete} /user/account
|
||||
* @apiName UserGetAccount
|
||||
*
|
||||
* @apiGroup user
|
||||
* @apiPermission user
|
||||
*
|
||||
* @apiSuccess {Boolean} success
|
||||
* @apiSuccess {Object[]} user
|
||||
* @apiSuccess {String} user.id User ID
|
||||
* @apiSuccess {String} user.name Full name of the user
|
||||
* @apiSuccess {String} user.username Username of user
|
||||
* @apiSuccess {Date} user.birthday Birthday
|
||||
* @apiSuccess {Number} user.gender Gender of user (none = 0, male = 1, female = 2, other = 3)
|
||||
*/
|
||||
UserRoute.get("/account", GetAccount);
|
||||
|
||||
/**
|
||||
* @api {delete} /user/account
|
||||
* @apiName UserGetAccount
|
||||
*
|
||||
* @apiGroup user
|
||||
* @apiPermission user
|
||||
*
|
||||
* @apiSuccess {Boolean} success
|
||||
* @apiSuccess {Object} contact
|
||||
* @apiSuccess {Object[]} user.mail EMail addresses
|
||||
* @apiSuccess {Object[]} user.phone Phone numbers
|
||||
*/
|
||||
UserRoute.get("/contact", GetContactInfos);
|
||||
|
||||
UserRoute.use("/oauth", OAuthRoute);
|
||||
|
||||
export default UserRoute;
|
134
Backend/src/api/user/login.ts
Normal file
134
Backend/src/api/user/login.ts
Normal file
@ -0,0 +1,134 @@
|
||||
import { Request, Response } from "express";
|
||||
import User, { IUser } from "../../models/user";
|
||||
import { randomBytes } from "crypto";
|
||||
import moment = require("moment");
|
||||
import LoginToken from "../../models/login_token";
|
||||
import promiseMiddleware from "../../helper/promiseMiddleware";
|
||||
import TwoFactor, { TFATypes, TFANames } from "../../models/twofactor";
|
||||
import * as crypto from "crypto";
|
||||
import Logging from "@hibas123/nodelogging";
|
||||
|
||||
const Login = promiseMiddleware(async (req: Request, res: Response) => {
|
||||
let type = req.query.type as string;
|
||||
if (type === "username") {
|
||||
let { username, uid } = req.query as { [key: string]: string };
|
||||
let user = await User.findOne(
|
||||
username ? { username: username.toLowerCase() } : { uid: uid }
|
||||
);
|
||||
if (!user) {
|
||||
res.json({ error: req.__("User not found") });
|
||||
} else {
|
||||
res.json({ salt: user.salt, uid: user.uid });
|
||||
}
|
||||
return;
|
||||
} else if (type === "password") {
|
||||
const sendToken = async (user: IUser, tfa?: any[]) => {
|
||||
let ip =
|
||||
req.headers["x-forwarded-for"] || req.connection.remoteAddress;
|
||||
let client = {
|
||||
ip: Array.isArray(ip) ? ip[0] : ip,
|
||||
browser: req.headers["user-agent"],
|
||||
};
|
||||
|
||||
let token_str = randomBytes(16).toString("hex");
|
||||
let tfa_exp = moment().add(5, "minutes").toDate();
|
||||
let token_exp = moment().add(6, "months").toDate();
|
||||
let token = LoginToken.new({
|
||||
token: token_str,
|
||||
valid: true,
|
||||
validTill: tfa ? tfa_exp : token_exp,
|
||||
user: user._id,
|
||||
validated: tfa ? false : true,
|
||||
...client,
|
||||
});
|
||||
await LoginToken.save(token);
|
||||
|
||||
let special_str = randomBytes(24).toString("hex");
|
||||
let special_exp = moment().add(30, "minutes").toDate();
|
||||
let special = LoginToken.new({
|
||||
token: special_str,
|
||||
valid: true,
|
||||
validTill: tfa ? tfa_exp : special_exp,
|
||||
special: true,
|
||||
user: user._id,
|
||||
validated: tfa ? false : true,
|
||||
...client,
|
||||
});
|
||||
await LoginToken.save(special);
|
||||
|
||||
res.json({
|
||||
login: { token: token_str, expires: token.validTill.toUTCString() },
|
||||
special: {
|
||||
token: special_str,
|
||||
expires: special.validTill.toUTCString(),
|
||||
},
|
||||
tfa,
|
||||
});
|
||||
};
|
||||
|
||||
let { username, password, uid, date } = req.body;
|
||||
|
||||
let user = await User.findOne(
|
||||
username ? { username: username.toLowerCase() } : { uid: uid }
|
||||
);
|
||||
if (!user) {
|
||||
res.json({ error: req.__("User not found") });
|
||||
} else {
|
||||
let upw = user.password;
|
||||
if (date) {
|
||||
if (
|
||||
!moment(date).isBetween(
|
||||
moment().subtract(1, "minute"),
|
||||
moment().add(1, "minute")
|
||||
)
|
||||
) {
|
||||
res.json({
|
||||
error: req.__(
|
||||
"Invalid timestamp. Please check your devices time!"
|
||||
),
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
upw = crypto
|
||||
.createHash("sha512")
|
||||
.update(upw + date.toString())
|
||||
.digest("hex");
|
||||
}
|
||||
}
|
||||
if (upw !== password) {
|
||||
res.json({ error: req.__("Password or username wrong") });
|
||||
} else {
|
||||
let twofactor = await TwoFactor.find({
|
||||
user: user._id,
|
||||
valid: true,
|
||||
});
|
||||
let expired = twofactor.filter((e) =>
|
||||
e.expires ? moment().isAfter(moment(e.expires)) : false
|
||||
);
|
||||
await Promise.all(
|
||||
expired.map((e) => {
|
||||
e.valid = false;
|
||||
return TwoFactor.save(e);
|
||||
})
|
||||
);
|
||||
twofactor = twofactor.filter((e) => e.valid);
|
||||
if (twofactor && twofactor.length > 0) {
|
||||
let tfa = twofactor.map((e) => {
|
||||
return {
|
||||
id: e._id,
|
||||
name: e.name || TFANames.get(e.type),
|
||||
type: e.type,
|
||||
};
|
||||
});
|
||||
await sendToken(user, tfa);
|
||||
} else {
|
||||
await sendToken(user);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
res.json({ error: req.__("Invalid type!") });
|
||||
}
|
||||
});
|
||||
|
||||
export default Login;
|
21
Backend/src/api/user/oauth/_helper.ts
Normal file
21
Backend/src/api/user/oauth/_helper.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import RequestError, { HttpStatusCode } from "../../../helper/request_error";
|
||||
import Client, { IClient } from "../../../models/client";
|
||||
|
||||
export async function getClientWithOrigin(client_id: string, origin: string) {
|
||||
const client = await Client.findOne({
|
||||
client_id,
|
||||
});
|
||||
|
||||
const clientNotFoundError = new RequestError(
|
||||
"Client not found!",
|
||||
HttpStatusCode.BAD_REQUEST
|
||||
);
|
||||
|
||||
if (!client) throw clientNotFoundError;
|
||||
|
||||
const clientUrl = new URL(client.redirect_url);
|
||||
|
||||
if (clientUrl.hostname !== origin) throw clientNotFoundError;
|
||||
|
||||
return client;
|
||||
}
|
12
Backend/src/api/user/oauth/index.ts
Normal file
12
Backend/src/api/user/oauth/index.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { Router } from "express";
|
||||
import { GetJWTByUser } from "./jwt";
|
||||
import { GetPermissionsForAuthRequest } from "./permissions";
|
||||
import { GetTokenByUser } from "./refresh_token";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get("/jwt", GetJWTByUser);
|
||||
router.get("/permissions", GetPermissionsForAuthRequest);
|
||||
router.get("/refresh_token", GetTokenByUser);
|
||||
|
||||
export default router;
|
25
Backend/src/api/user/oauth/jwt.ts
Normal file
25
Backend/src/api/user/oauth/jwt.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { Request, Response } from "express";
|
||||
import Stacker from "../../middlewares/stacker";
|
||||
import { GetUserMiddleware } from "../../middlewares/user";
|
||||
import { URL } from "url";
|
||||
import Client from "../../../models/client";
|
||||
import RequestError, { HttpStatusCode } from "../../../helper/request_error";
|
||||
import { getAccessTokenJWT } from "../../../helper/jwt";
|
||||
import { getClientWithOrigin } from "./_helper";
|
||||
|
||||
export const GetJWTByUser = Stacker(
|
||||
GetUserMiddleware(true, false),
|
||||
async (req: Request, res: Response) => {
|
||||
const { client_id, origin } = req.query as { [key: string]: string };
|
||||
|
||||
const client = await getClientWithOrigin(client_id, origin);
|
||||
|
||||
const jwt = await getAccessTokenJWT({
|
||||
user: req.user,
|
||||
client: client,
|
||||
permissions: [],
|
||||
});
|
||||
|
||||
res.json({ jwt });
|
||||
}
|
||||
);
|
38
Backend/src/api/user/oauth/permissions.ts
Normal file
38
Backend/src/api/user/oauth/permissions.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { Request, Response } from "express";
|
||||
import Stacker from "../../middlewares/stacker";
|
||||
import { GetUserMiddleware } from "../../middlewares/user";
|
||||
import { URL } from "url";
|
||||
import Client from "../../../models/client";
|
||||
import RequestError, { HttpStatusCode } from "../../../helper/request_error";
|
||||
import { randomBytes } from "crypto";
|
||||
import moment = require("moment");
|
||||
import RefreshToken from "../../../models/refresh_token";
|
||||
import { refreshTokenValidTime } from "../../../config";
|
||||
import { getClientWithOrigin } from "./_helper";
|
||||
import Permission from "../../../models/permissions";
|
||||
|
||||
export const GetPermissionsForAuthRequest = Stacker(
|
||||
GetUserMiddleware(true, false),
|
||||
async (req: Request, res: Response) => {
|
||||
const { client_id, origin, permissions } = req.query as {
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
const client = await getClientWithOrigin(client_id, origin);
|
||||
|
||||
const perm = permissions.split(",").filter((e) => !!e);
|
||||
|
||||
const resolved = await Promise.all(
|
||||
perm.map((p) => Permission.findById(p))
|
||||
);
|
||||
|
||||
if (resolved.some((e) => e.grant_type !== "user")) {
|
||||
throw new RequestError(
|
||||
"Invalid Permission requested",
|
||||
HttpStatusCode.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
|
||||
res.json({ permissions: resolved });
|
||||
}
|
||||
);
|
49
Backend/src/api/user/oauth/refresh_token.ts
Normal file
49
Backend/src/api/user/oauth/refresh_token.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import { Request, Response } from "express";
|
||||
import Stacker from "../../middlewares/stacker";
|
||||
import { GetUserMiddleware } from "../../middlewares/user";
|
||||
import { URL } from "url";
|
||||
import Client from "../../../models/client";
|
||||
import RequestError, { HttpStatusCode } from "../../../helper/request_error";
|
||||
import { randomBytes } from "crypto";
|
||||
import moment = require("moment");
|
||||
import RefreshToken from "../../../models/refresh_token";
|
||||
import { refreshTokenValidTime } from "../../../config";
|
||||
import { getClientWithOrigin } from "./_helper";
|
||||
import Permission from "../../../models/permissions";
|
||||
|
||||
export const GetTokenByUser = Stacker(
|
||||
GetUserMiddleware(true, false),
|
||||
async (req: Request, res: Response) => {
|
||||
const { client_id, origin, permissions } = req.query as {
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
const client = await getClientWithOrigin(client_id, origin);
|
||||
|
||||
const perm = permissions.split(",").filter((e) => !!e);
|
||||
|
||||
const resolved = await Promise.all(
|
||||
perm.map((p) => Permission.findById(p))
|
||||
);
|
||||
|
||||
if (resolved.some((e) => e.grant_type !== "user")) {
|
||||
throw new RequestError(
|
||||
"Invalid Permission requested",
|
||||
HttpStatusCode.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
|
||||
let token = RefreshToken.new({
|
||||
user: req.user._id,
|
||||
client: client._id,
|
||||
permissions: resolved.map((e) => e._id),
|
||||
token: randomBytes(16).toString("hex"),
|
||||
valid: true,
|
||||
validTill: moment().add(refreshTokenValidTime).toDate(),
|
||||
});
|
||||
|
||||
await RefreshToken.save(token);
|
||||
|
||||
res.json({ token });
|
||||
}
|
||||
);
|
155
Backend/src/api/user/register.ts
Normal file
155
Backend/src/api/user/register.ts
Normal file
@ -0,0 +1,155 @@
|
||||
import { Request, Response, Router } from "express";
|
||||
import Stacker from "../middlewares/stacker";
|
||||
import verify, { Types } from "../middlewares/verify";
|
||||
import promiseMiddleware from "../../helper/promiseMiddleware";
|
||||
import User, { Gender } from "../../models/user";
|
||||
import { HttpStatusCode } from "../../helper/request_error";
|
||||
import Mail from "../../models/mail";
|
||||
import RegCode from "../../models/regcodes";
|
||||
|
||||
const Register = Stacker(
|
||||
verify({
|
||||
mail: {
|
||||
type: Types.EMAIL,
|
||||
notempty: true,
|
||||
},
|
||||
username: {
|
||||
type: Types.STRING,
|
||||
notempty: true,
|
||||
},
|
||||
password: {
|
||||
type: Types.STRING,
|
||||
notempty: true,
|
||||
},
|
||||
salt: {
|
||||
type: Types.STRING,
|
||||
notempty: true,
|
||||
},
|
||||
regcode: {
|
||||
type: Types.STRING,
|
||||
notempty: true,
|
||||
},
|
||||
gender: {
|
||||
type: Types.STRING,
|
||||
notempty: true,
|
||||
},
|
||||
name: {
|
||||
type: Types.STRING,
|
||||
notempty: true,
|
||||
},
|
||||
// birthday: {
|
||||
// type: Types.DATE
|
||||
// }
|
||||
}),
|
||||
promiseMiddleware(async (req: Request, res: Response) => {
|
||||
let {
|
||||
username,
|
||||
password,
|
||||
salt,
|
||||
mail,
|
||||
gender,
|
||||
name,
|
||||
birthday,
|
||||
regcode,
|
||||
} = req.body;
|
||||
let u = await User.findOne({ username: username.toLowerCase() });
|
||||
if (u) {
|
||||
let err = {
|
||||
message: [
|
||||
{
|
||||
message: req.__("Username taken"),
|
||||
field: "username",
|
||||
},
|
||||
],
|
||||
status: HttpStatusCode.BAD_REQUEST,
|
||||
nolog: true,
|
||||
};
|
||||
throw err;
|
||||
}
|
||||
|
||||
let m = await Mail.findOne({ mail: mail });
|
||||
if (m) {
|
||||
let err = {
|
||||
message: [
|
||||
{
|
||||
message: req.__("Mail linked with other account"),
|
||||
field: "mail",
|
||||
},
|
||||
],
|
||||
status: HttpStatusCode.BAD_REQUEST,
|
||||
nolog: true,
|
||||
};
|
||||
throw err;
|
||||
}
|
||||
|
||||
let regc = await RegCode.findOne({ token: regcode });
|
||||
if (!regc) {
|
||||
let err = {
|
||||
message: [
|
||||
{
|
||||
message: req.__("Invalid registration code"),
|
||||
field: "regcode",
|
||||
},
|
||||
],
|
||||
status: HttpStatusCode.BAD_REQUEST,
|
||||
nolog: true,
|
||||
};
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (!regc.valid) {
|
||||
let err = {
|
||||
message: [
|
||||
{
|
||||
message: req.__("Registration code already used"),
|
||||
field: "regcode",
|
||||
},
|
||||
],
|
||||
status: HttpStatusCode.BAD_REQUEST,
|
||||
nolog: true,
|
||||
};
|
||||
throw err;
|
||||
}
|
||||
|
||||
let g = -1;
|
||||
switch (gender) {
|
||||
case "male":
|
||||
g = Gender.male;
|
||||
break;
|
||||
case "female":
|
||||
g = Gender.female;
|
||||
break;
|
||||
case "other":
|
||||
g = Gender.other;
|
||||
break;
|
||||
default:
|
||||
g = Gender.none;
|
||||
break;
|
||||
}
|
||||
|
||||
let user = User.new({
|
||||
username: username.toLowerCase(),
|
||||
password: password,
|
||||
salt: salt,
|
||||
gender: g,
|
||||
name: name,
|
||||
// birthday: birthday,
|
||||
admin: false,
|
||||
});
|
||||
|
||||
regc.valid = false;
|
||||
await RegCode.save(regc);
|
||||
|
||||
let ml = Mail.new({
|
||||
mail: mail,
|
||||
primary: true,
|
||||
});
|
||||
|
||||
await Mail.save(ml);
|
||||
|
||||
user.mails.push(ml._id);
|
||||
await User.save(user);
|
||||
res.json({ success: true });
|
||||
})
|
||||
);
|
||||
export default Register;
|
45
Backend/src/api/user/token.ts
Normal file
45
Backend/src/api/user/token.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { Request, Response } from "express";
|
||||
import Stacker from "../middlewares/stacker";
|
||||
import { GetUserMiddleware } from "../middlewares/user";
|
||||
import LoginToken, { CheckToken } from "../../models/login_token";
|
||||
import RequestError, { HttpStatusCode } from "../../helper/request_error";
|
||||
|
||||
export const GetToken = Stacker(
|
||||
GetUserMiddleware(true, true),
|
||||
async (req: Request, res: Response) => {
|
||||
let raw_token = await LoginToken.find({
|
||||
user: req.user._id,
|
||||
valid: true,
|
||||
});
|
||||
let token = await Promise.all(
|
||||
raw_token
|
||||
.map(async (token) => {
|
||||
await CheckToken(token);
|
||||
return {
|
||||
id: token._id,
|
||||
special: token.special,
|
||||
ip: token.ip,
|
||||
browser: token.browser,
|
||||
isthis: token._id.equals(
|
||||
token.special ? req.token.special._id : req.token.login._id
|
||||
),
|
||||
};
|
||||
})
|
||||
.filter((t) => t !== undefined)
|
||||
);
|
||||
res.json({ token });
|
||||
}
|
||||
);
|
||||
|
||||
export const DeleteToken = Stacker(
|
||||
GetUserMiddleware(true, true),
|
||||
async (req: Request, res: Response) => {
|
||||
let { id } = req.params;
|
||||
let token = await LoginToken.findById(id);
|
||||
if (!token || !token.user.equals(req.user._id))
|
||||
throw new RequestError("Invalid ID", HttpStatusCode.BAD_REQUEST);
|
||||
token.valid = false;
|
||||
await LoginToken.save(token);
|
||||
res.json({ success: true });
|
||||
}
|
||||
);
|
100
Backend/src/api/user/twofactor/backup/index.ts
Normal file
100
Backend/src/api/user/twofactor/backup/index.ts
Normal file
@ -0,0 +1,100 @@
|
||||
import { Router } from "express";
|
||||
import Stacker from "../../../middlewares/stacker";
|
||||
import { GetUserMiddleware } from "../../../middlewares/user";
|
||||
import TwoFactor, {
|
||||
TFATypes as TwoFATypes,
|
||||
IBackupCode,
|
||||
} from "../../../../models/twofactor";
|
||||
import RequestError, { HttpStatusCode } from "../../../../helper/request_error";
|
||||
import moment = require("moment");
|
||||
import { upgradeToken } from "../helper";
|
||||
import * as crypto from "crypto";
|
||||
import Logging from "@hibas123/nodelogging";
|
||||
|
||||
const BackupCodeRoute = Router();
|
||||
|
||||
// TODO: Further checks if this is good enough randomness
|
||||
function generateCode(length: number) {
|
||||
let bytes = crypto.randomBytes(length);
|
||||
let nrs = "";
|
||||
bytes.forEach((b, idx) => {
|
||||
let nr = Math.floor((b / 255) * 9.9999);
|
||||
if (nr > 9) nr = 9;
|
||||
nrs += String(nr);
|
||||
});
|
||||
return nrs;
|
||||
}
|
||||
|
||||
BackupCodeRoute.post(
|
||||
"/",
|
||||
Stacker(GetUserMiddleware(true, true), async (req, res) => {
|
||||
//Generating new
|
||||
let codes = Array(10).map(() => generateCode(8));
|
||||
console.log(codes);
|
||||
let twofactor = TwoFactor.new(<IBackupCode>{
|
||||
user: req.user._id,
|
||||
type: TwoFATypes.OTC,
|
||||
valid: true,
|
||||
data: codes,
|
||||
name: "",
|
||||
});
|
||||
await TwoFactor.save(twofactor);
|
||||
res.json({
|
||||
codes,
|
||||
id: twofactor._id,
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
BackupCodeRoute.put(
|
||||
"/",
|
||||
Stacker(
|
||||
GetUserMiddleware(true, false, undefined, false),
|
||||
async (req, res) => {
|
||||
let { login, special } = req.token;
|
||||
let { id, code }: { id: string; code: string } = req.body;
|
||||
|
||||
let twofactor: IBackupCode = await TwoFactor.findById(id);
|
||||
|
||||
if (
|
||||
!twofactor ||
|
||||
!twofactor.valid ||
|
||||
!twofactor.user.equals(req.user._id) ||
|
||||
twofactor.type !== TwoFATypes.OTC
|
||||
) {
|
||||
throw new RequestError(
|
||||
"Invalid Method!",
|
||||
HttpStatusCode.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
|
||||
if (twofactor.expires && moment().isAfter(twofactor.expires)) {
|
||||
twofactor.valid = false;
|
||||
await TwoFactor.save(twofactor);
|
||||
throw new RequestError(
|
||||
"Invalid Method!",
|
||||
HttpStatusCode.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
code = code.replace(/\s/g, "");
|
||||
let valid = twofactor.data.find((c) => c === code);
|
||||
|
||||
if (valid) {
|
||||
twofactor.data = twofactor.data.filter((c) => c !== code);
|
||||
await TwoFactor.save(twofactor);
|
||||
let [login_exp, special_exp] = await Promise.all([
|
||||
upgradeToken(login),
|
||||
upgradeToken(special),
|
||||
]);
|
||||
res.json({ success: true, login_exp, special_exp });
|
||||
} else {
|
||||
throw new RequestError(
|
||||
"Invalid or already used code!",
|
||||
HttpStatusCode.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
export default BackupCodeRoute;
|
16
Backend/src/api/user/twofactor/helper.ts
Normal file
16
Backend/src/api/user/twofactor/helper.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import LoginToken, { ILoginToken } from "../../../models/login_token";
|
||||
import moment = require("moment");
|
||||
|
||||
export async function upgradeToken(token: ILoginToken) {
|
||||
token.data = undefined;
|
||||
token.valid = true;
|
||||
token.validated = true;
|
||||
//TODO durations from config
|
||||
let expires = (token.special
|
||||
? moment().add(30, "minute")
|
||||
: moment().add(6, "months")
|
||||
).toDate();
|
||||
token.validTill = expires;
|
||||
await LoginToken.save(token);
|
||||
return expires;
|
||||
}
|
56
Backend/src/api/user/twofactor/index.ts
Normal file
56
Backend/src/api/user/twofactor/index.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { Router } from "express";
|
||||
import YubiKeyRoute from "./yubikey";
|
||||
import { GetUserMiddleware } from "../../middlewares/user";
|
||||
import Stacker from "../../middlewares/stacker";
|
||||
import TwoFactor from "../../../models/twofactor";
|
||||
import * as moment from "moment";
|
||||
import RequestError, { HttpStatusCode } from "../../../helper/request_error";
|
||||
import OTCRoute from "./otc";
|
||||
import BackupCodeRoute from "./backup";
|
||||
|
||||
const TwoFactorRouter = Router();
|
||||
|
||||
TwoFactorRouter.get(
|
||||
"/",
|
||||
Stacker(GetUserMiddleware(true, true), async (req, res) => {
|
||||
let twofactor = await TwoFactor.find({ user: req.user._id, valid: true });
|
||||
let expired = twofactor.filter((e) =>
|
||||
e.expires ? moment().isAfter(moment(e.expires)) : false
|
||||
);
|
||||
await Promise.all(
|
||||
expired.map((e) => {
|
||||
e.valid = false;
|
||||
return TwoFactor.save(e);
|
||||
})
|
||||
);
|
||||
twofactor = twofactor.filter((e) => e.valid);
|
||||
let tfa = twofactor.map((e) => {
|
||||
return {
|
||||
id: e._id,
|
||||
name: e.name,
|
||||
type: e.type,
|
||||
};
|
||||
});
|
||||
res.json({ methods: tfa });
|
||||
})
|
||||
);
|
||||
|
||||
TwoFactorRouter.delete(
|
||||
"/:id",
|
||||
Stacker(GetUserMiddleware(true, true), async (req, res) => {
|
||||
let { id } = req.params;
|
||||
let tfa = await TwoFactor.findById(id);
|
||||
if (!tfa || !tfa.user.equals(req.user._id)) {
|
||||
throw new RequestError("Invalid id", HttpStatusCode.BAD_REQUEST);
|
||||
}
|
||||
tfa.valid = false;
|
||||
await TwoFactor.save(tfa);
|
||||
res.json({ success: true });
|
||||
})
|
||||
);
|
||||
|
||||
TwoFactorRouter.use("/yubikey", YubiKeyRoute);
|
||||
TwoFactorRouter.use("/otc", OTCRoute);
|
||||
TwoFactorRouter.use("/backup", BackupCodeRoute);
|
||||
|
||||
export default TwoFactorRouter;
|
135
Backend/src/api/user/twofactor/otc/index.ts
Normal file
135
Backend/src/api/user/twofactor/otc/index.ts
Normal file
@ -0,0 +1,135 @@
|
||||
import { Router } from "express";
|
||||
import Stacker from "../../../middlewares/stacker";
|
||||
import { GetUserMiddleware } from "../../../middlewares/user";
|
||||
import TwoFactor, {
|
||||
TFATypes as TwoFATypes,
|
||||
IOTC,
|
||||
} from "../../../../models/twofactor";
|
||||
import RequestError, { HttpStatusCode } from "../../../../helper/request_error";
|
||||
import moment = require("moment");
|
||||
import { upgradeToken } from "../helper";
|
||||
import Logging from "@hibas123/nodelogging";
|
||||
|
||||
import * as speakeasy from "speakeasy";
|
||||
import * as qrcode from "qrcode";
|
||||
import config from "../../../../config";
|
||||
|
||||
const OTCRoute = Router();
|
||||
|
||||
OTCRoute.post(
|
||||
"/",
|
||||
Stacker(GetUserMiddleware(true, true), async (req, res) => {
|
||||
const { type } = req.query;
|
||||
if (type === "create") {
|
||||
//Generating new
|
||||
let secret = speakeasy.generateSecret({
|
||||
name: config.core.name,
|
||||
issuer: config.core.name,
|
||||
});
|
||||
let twofactor = TwoFactor.new(<IOTC>{
|
||||
user: req.user._id,
|
||||
type: TwoFATypes.OTC,
|
||||
valid: false,
|
||||
data: secret.base32,
|
||||
});
|
||||
let dataurl = await qrcode.toDataURL(secret.otpauth_url);
|
||||
await TwoFactor.save(twofactor);
|
||||
res.json({
|
||||
image: dataurl,
|
||||
id: twofactor._id,
|
||||
});
|
||||
} else if (type === "validate") {
|
||||
// Checking code and marking as valid
|
||||
const { code, id } = req.body;
|
||||
Logging.debug(req.body, id);
|
||||
let twofactor: IOTC = await TwoFactor.findById(id);
|
||||
const err = () => {
|
||||
throw new RequestError("Invalid ID!", HttpStatusCode.BAD_REQUEST);
|
||||
};
|
||||
if (
|
||||
!twofactor ||
|
||||
!twofactor.user.equals(req.user._id) ||
|
||||
twofactor.type !== TwoFATypes.OTC ||
|
||||
!twofactor.data ||
|
||||
twofactor.valid
|
||||
) {
|
||||
Logging.debug("Not found or wrong user", twofactor);
|
||||
err();
|
||||
}
|
||||
|
||||
if (twofactor.expires && moment().isAfter(moment(twofactor.expires))) {
|
||||
await TwoFactor.delete(twofactor);
|
||||
Logging.debug("Expired!", twofactor);
|
||||
err();
|
||||
}
|
||||
|
||||
let valid = speakeasy.totp.verify({
|
||||
secret: twofactor.data,
|
||||
encoding: "base32",
|
||||
token: code,
|
||||
});
|
||||
|
||||
if (valid) {
|
||||
twofactor.expires = undefined;
|
||||
twofactor.valid = true;
|
||||
await TwoFactor.save(twofactor);
|
||||
res.json({ success: true });
|
||||
} else {
|
||||
throw new RequestError("Invalid Code!", HttpStatusCode.BAD_REQUEST);
|
||||
}
|
||||
} else {
|
||||
throw new RequestError("Invalid type", HttpStatusCode.BAD_REQUEST);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
OTCRoute.put(
|
||||
"/",
|
||||
Stacker(
|
||||
GetUserMiddleware(true, false, undefined, false),
|
||||
async (req, res) => {
|
||||
let { login, special } = req.token;
|
||||
let { id, code } = req.body;
|
||||
let twofactor: IOTC = await TwoFactor.findById(id);
|
||||
|
||||
if (
|
||||
!twofactor ||
|
||||
!twofactor.valid ||
|
||||
!twofactor.user.equals(req.user._id) ||
|
||||
twofactor.type !== TwoFATypes.OTC
|
||||
) {
|
||||
throw new RequestError(
|
||||
"Invalid Method!",
|
||||
HttpStatusCode.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
|
||||
if (twofactor.expires && moment().isAfter(twofactor.expires)) {
|
||||
twofactor.valid = false;
|
||||
await TwoFactor.save(twofactor);
|
||||
throw new RequestError(
|
||||
"Invalid Method!",
|
||||
HttpStatusCode.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
|
||||
let valid = speakeasy.totp.verify({
|
||||
secret: twofactor.data,
|
||||
encoding: "base32",
|
||||
token: code,
|
||||
});
|
||||
|
||||
if (valid) {
|
||||
let [login_exp, special_exp] = await Promise.all([
|
||||
upgradeToken(login),
|
||||
upgradeToken(special),
|
||||
]);
|
||||
res.json({ success: true, login_exp, special_exp });
|
||||
} else {
|
||||
throw new RequestError("Invalid Code", HttpStatusCode.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
export default OTCRoute;
|
206
Backend/src/api/user/twofactor/yubikey/index.ts
Normal file
206
Backend/src/api/user/twofactor/yubikey/index.ts
Normal file
@ -0,0 +1,206 @@
|
||||
import { Router, Request } from "express";
|
||||
import Stacker from "../../../middlewares/stacker";
|
||||
import { UserMiddleware, GetUserMiddleware } from "../../../middlewares/user";
|
||||
import * as u2f from "u2f";
|
||||
import config from "../../../../config";
|
||||
import TwoFactor, {
|
||||
TFATypes as TwoFATypes,
|
||||
IYubiKey,
|
||||
} from "../../../../models/twofactor";
|
||||
import RequestError, { HttpStatusCode } from "../../../../helper/request_error";
|
||||
import moment = require("moment");
|
||||
import LoginToken from "../../../../models/login_token";
|
||||
import { upgradeToken } from "../helper";
|
||||
import Logging from "@hibas123/nodelogging";
|
||||
|
||||
const U2FRoute = Router();
|
||||
|
||||
/**
|
||||
* Registerinf a new YubiKey
|
||||
*/
|
||||
U2FRoute.post(
|
||||
"/",
|
||||
Stacker(GetUserMiddleware(true, true), async (req, res) => {
|
||||
const { type } = req.query;
|
||||
if (type === "challenge") {
|
||||
const registrationRequest = u2f.request(config.core.url);
|
||||
|
||||
let twofactor = TwoFactor.new(<IYubiKey>{
|
||||
user: req.user._id,
|
||||
type: TwoFATypes.U2F,
|
||||
valid: false,
|
||||
data: {
|
||||
registration: registrationRequest,
|
||||
},
|
||||
});
|
||||
await TwoFactor.save(twofactor);
|
||||
res.json({
|
||||
request: registrationRequest,
|
||||
id: twofactor._id,
|
||||
appid: config.core.url,
|
||||
});
|
||||
} else {
|
||||
const { response, id } = req.body;
|
||||
Logging.debug(req.body, id);
|
||||
let twofactor: IYubiKey = await TwoFactor.findById(id);
|
||||
const err = () => {
|
||||
throw new RequestError("Invalid ID!", HttpStatusCode.BAD_REQUEST);
|
||||
};
|
||||
if (
|
||||
!twofactor ||
|
||||
!twofactor.user.equals(req.user._id) ||
|
||||
twofactor.type !== TwoFATypes.U2F ||
|
||||
!twofactor.data.registration ||
|
||||
twofactor.valid
|
||||
) {
|
||||
Logging.debug("Not found or wrong user", twofactor);
|
||||
err();
|
||||
}
|
||||
|
||||
if (twofactor.expires && moment().isAfter(moment(twofactor.expires))) {
|
||||
await TwoFactor.delete(twofactor);
|
||||
Logging.debug("Expired!", twofactor);
|
||||
err();
|
||||
}
|
||||
|
||||
const result = u2f.checkRegistration(
|
||||
twofactor.data.registration,
|
||||
response
|
||||
);
|
||||
|
||||
if (result.successful) {
|
||||
twofactor.data = {
|
||||
keyHandle: result.keyHandle,
|
||||
publicKey: result.publicKey,
|
||||
};
|
||||
twofactor.expires = undefined;
|
||||
twofactor.valid = true;
|
||||
await TwoFactor.save(twofactor);
|
||||
res.json({ success: true });
|
||||
} else {
|
||||
throw new RequestError(
|
||||
result.errorMessage,
|
||||
HttpStatusCode.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
U2FRoute.get(
|
||||
"/",
|
||||
Stacker(
|
||||
GetUserMiddleware(true, false, undefined, false),
|
||||
async (req, res) => {
|
||||
let { login, special } = req.token;
|
||||
let twofactor: IYubiKey = await TwoFactor.findOne({
|
||||
user: req.user._id,
|
||||
type: TwoFATypes.U2F,
|
||||
valid: true,
|
||||
});
|
||||
|
||||
if (!twofactor) {
|
||||
throw new RequestError(
|
||||
"Invalid Method!",
|
||||
HttpStatusCode.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
|
||||
if (twofactor.expires) {
|
||||
if (moment().isAfter(twofactor.expires)) {
|
||||
twofactor.valid = false;
|
||||
await TwoFactor.save(twofactor);
|
||||
throw new RequestError(
|
||||
"Invalid Method!",
|
||||
HttpStatusCode.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let request = u2f.request(config.core.url, twofactor.data.keyHandle);
|
||||
login.data = {
|
||||
type: "ykr",
|
||||
request,
|
||||
};
|
||||
let r;
|
||||
if (special) {
|
||||
special.data = login.data;
|
||||
r = LoginToken.save(special);
|
||||
}
|
||||
|
||||
await Promise.all([r, LoginToken.save(login)]);
|
||||
res.json({ request });
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
U2FRoute.put(
|
||||
"/",
|
||||
Stacker(
|
||||
GetUserMiddleware(true, false, undefined, false),
|
||||
async (req, res) => {
|
||||
let { login, special } = req.token;
|
||||
let twofactor: IYubiKey = await TwoFactor.findOne({
|
||||
user: req.user._id,
|
||||
type: TwoFATypes.U2F,
|
||||
valid: true,
|
||||
});
|
||||
|
||||
let { response } = req.body;
|
||||
if (
|
||||
!twofactor ||
|
||||
!login.data ||
|
||||
login.data.type !== "ykr" ||
|
||||
(special && (!special.data || special.data.type !== "ykr"))
|
||||
) {
|
||||
throw new RequestError(
|
||||
"Invalid Method!",
|
||||
HttpStatusCode.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
|
||||
if (twofactor.expires && moment().isAfter(twofactor.expires)) {
|
||||
twofactor.valid = false;
|
||||
await TwoFactor.save(twofactor);
|
||||
throw new RequestError(
|
||||
"Invalid Method!",
|
||||
HttpStatusCode.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
|
||||
let login_exp;
|
||||
let special_exp;
|
||||
let result = u2f.checkSignature(
|
||||
login.data.request,
|
||||
response,
|
||||
twofactor.data.publicKey
|
||||
);
|
||||
if (result.successful) {
|
||||
if (special) {
|
||||
let result = u2f.checkSignature(
|
||||
special.data.request,
|
||||
response,
|
||||
twofactor.data.publicKey
|
||||
);
|
||||
if (result.successful) {
|
||||
special_exp = await upgradeToken(special);
|
||||
} else {
|
||||
throw new RequestError(
|
||||
result.errorMessage,
|
||||
HttpStatusCode.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
}
|
||||
login_exp = await upgradeToken(login);
|
||||
} else {
|
||||
throw new RequestError(
|
||||
result.errorMessage,
|
||||
HttpStatusCode.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
res.json({ success: true, login_exp, special_exp });
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
export default U2FRoute;
|
Reference in New Issue
Block a user