Browse Source

First version of OpenAuth remake

improving-auth
Fabian Stamm 4 years ago
commit
ac69e73344
  1. 8
      .drone.yml
  2. 9
      .editorconfig
  3. 9
      .gitignore
  4. 1
      README.md
  5. 11
      example.config.ini
  6. 36
      locales/de.json
  7. 7
      locales/en.json
  8. 4592
      package-lock.json
  9. 54
      package.json
  10. 12
      src/api/admin.ts
  11. 88
      src/api/admin/client.ts
  12. 45
      src/api/admin/permission.ts
  13. 34
      src/api/admin/regcode.ts
  14. 42
      src/api/admin/user.ts
  15. 18
      src/api/api.ts
  16. 0
      src/api/client.ts
  17. 14
      src/api/client/user.ts
  18. 8
      src/api/internal.ts
  19. 29
      src/api/internal/oauth.ts
  20. 25
      src/api/internal/password.ts
  21. 76
      src/api/middlewares/client.ts
  22. 24
      src/api/middlewares/stacker.ts
  23. 83
      src/api/middlewares/user.ts
  24. 125
      src/api/middlewares/verify.ts
  25. 13
      src/api/oauth.ts
  26. 81
      src/api/oauth/auth.ts
  27. 27
      src/api/oauth/jwt.ts
  28. 6
      src/api/oauth/public.ts
  29. 79
      src/api/oauth/refresh.ts
  30. 8
      src/api/user.ts
  31. 81
      src/api/user/login.ts
  32. 142
      src/api/user/register.ts
  33. 46
      src/config.ts
  34. 3
      src/database.ts
  35. 11
      src/express.d.ts
  36. 20
      src/helper/jwt.ts
  37. 5
      src/helper/promiseMiddleware.ts
  38. 388
      src/helper/request_error.ts
  39. 72
      src/index.ts
  40. 56
      src/jwt.ts
  41. 75
      src/keys.ts
  42. 36
      src/models/client.ts
  43. 27
      src/models/client_code.ts
  44. 26
      src/models/login_token.ts
  45. 24
      src/models/mail.ts
  46. 24
      src/models/permissions.ts
  47. 30
      src/models/refresh_token.ts
  48. 55
      src/models/regcodes.ts
  49. 69
      src/models/user.ts
  50. 64
      src/testdata.ts
  51. 21
      src/views/admin.ts
  52. 26
      src/views/authorize.ts
  53. 39
      src/views/login.ts
  54. 21
      src/views/register.ts
  55. 104
      src/views/views.ts
  56. 103
      src/web.ts
  57. 23
      tsconfig.json
  58. 160
      views/build.js
  59. 0
      views/dummy.js
  60. 3240
      views/package-lock.json
  61. 27
      views/package.json
  62. 20
      views/shared/cookie.js
  63. 13
      views/shared/event.js
  64. 14
      views/shared/formdata.js
  65. 13
      views/shared/inputs.js
  66. 113
      views/shared/inputs.scss
  67. 2
      views/shared/mat_bs.scss
  68. 16
      views/shared/request.js
  69. 1
      views/shared/sha512.js
  70. 7
      views/shared/style.scss
  71. 244
      views/src/admin/admin.hbs
  72. 158
      views/src/admin/admin.js
  73. 103
      views/src/admin/admin.scss
  74. 51
      views/src/authorize/authorize.hbs
  75. 15
      views/src/authorize/authorize.js
  76. 72
      views/src/authorize/authorize.scss
  77. 47
      views/src/login/login.hbs
  78. 140
      views/src/login/login.js
  79. 129
      views/src/login/login.scss
  80. 7
      views/src/main/main.hbs
  81. 1
      views/src/main/main.js
  82. 0
      views/src/main/main.scss
  83. 115
      views/src/register/register.hbs
  84. 149
      views/src/register/register.js
  85. 95
      views/src/register/register.scss
  86. 14
      views/src/user/user.hbs
  87. 1
      views/src/user/user.js
  88. 0
      views/src/user/user.scss
  89. 2233
      views/yarn.lock

8
.drone.yml

@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
pipeline:
core:
image: node
commands:
- node --version && npm --version
- npm install
- cd views && npm install && cd ..
- npm run build

9
.editorconfig

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
root = true
[*]
indent_style = space
indent_size = 3
end_of_line = crlf
charset = utf-8
trim_trailing_whitespace = false
insert_final_newline = false

9
.gitignore vendored

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
node_modules/
views/out/
lib/
keys/
*.old
logs/
*.sqlite
yarn-error\.log
config.ini

1
README.md

@ -0,0 +1 @@ @@ -0,0 +1 @@
OpenAuthService

11
example.config.ini

@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
[core]
name = OpenAuthService
[web]
port = 3000
[mail]
server = mail.stamm.me
username = test
password = test
port = 595

36
locales/de.json

@ -0,0 +1,36 @@ @@ -0,0 +1,36 @@
{
"User not found": "Benutzer nicht gefunden",
"Password or username wrong": "Passwort oder Benutzername falsch",
"Authorize %s": "Authorize %s",
"Login": "Einloggen",
"You are not logged in or your login is expired": "Du bist nicht länger angemeldet oder deine Anmeldung ist abgelaufen.",
"Username or Email": "Benutzername oder Email",
"Password": "Passwort",
"Next": "Weiter",
"Register": "Registrieren",
"Mail": "Mail",
"Repeat Password": "Passwort wiederholen",
"Username": "Benutzername",
"Name": "Name",
"Registration code": "Registrierungs Schlüssel",
"You need to select one of the options": "Du musst eine der Optionen auswälen",
"Male": "Mann",
"Female": "Frau",
"Other": "Anderes",
"Registration code required": "Registrierungs Schlüssel benötigt",
"Username required": "Benutzername benötigt",
"Name required": "Name benötigt",
"Mail required": "Mail benötigt",
"The passwords do not match": "Die Passwörter stimmen nicht überein",
"Password is required": "Password benötigt",
"Invalid registration code": "Ungültiger Registrierungs Schlüssel",
"Username taken": "Benutzername nicht verfügbar",
"Mail linked with other account": "Mail ist bereits mit einem anderen Account verbunden",
"Registration code already used": "Registrierungs Schlüssel wurde bereits verwendet",
"Administration": "Administration",
"Field {{field}} is not defined": "Feld {{field}} ist nicht deklariert",
"Field {{field}} has wrong type. It should be from type {{type}}": "Feld {{field}} hat den falschen Type. Es sollte vom Typ {{type}} sein",
"Client has no permission for acces password auth": "Dieser Client hat keine Berechtigung password auth zu benutzen",
"Invalid token": "Ungültiges Token",
"By clicking on ALLOW, you allow this app to access the requested recources.": "Wenn sie ALLOW drücken, berechtigen sie die Applikation die beantragten Resourcen zu benutzen."
}

7
locales/en.json

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
{
"Login": "Login",
"Username or Email": "Username or Email",
"Password": "Password",
"Next": "Next",
"Invalid code": "Invalid code"
}

4592
package-lock.json generated

File diff suppressed because it is too large Load Diff

54
package.json

@ -0,0 +1,54 @@ @@ -0,0 +1,54 @@
{
"name": "open_auth_service",
"version": "1.0.0",
"main": "lib/index.js",
"author": "Fabian Stamm <dev@fabianstamm.de>",
"license": "MIT",
"scripts": {
"start": "node lib/index.js",
"build": "tsc && cd views && npm run build && cd ..",
"watch-ts": "tsc -w",
"watch-views": "cd views && npm run watch",
"watch-node": "nodemon --ignore ./views lib/index.js",
"watch": "concurrently \"npm:watch-*\""
},
"devDependencies": {
"@types/body-parser": "^1.17.0",
"@types/compression": "^0.0.36",
"@types/cookie-parser": "^1.4.1",
"@types/dotenv": "^4.0.3",
"@types/express": "^4.16.0",
"@types/handlebars": "^4.0.39",
"@types/i18n": "^0.8.3",
"@types/ini": "^1.3.29",
"@types/jsonwebtoken": "^8.3.0",
"@types/mongodb": "^3.1.14",
"@types/node": "^10.12.2",
"@types/node-rsa": "^0.4.3",
"@types/uuid": "^3.4.4",
"concurrently": "^4.0.1",
"nodemon": "^1.18.6",
"typescript": "^3.1.6"
},
"dependencies": {
"@hibas123/nodelogging": "^1.3.21",
"@hibas123/nodeloggingserver_client": "^1.1.2",
"@hibas123/safe_mongo": "^1.3.3",
"body-parser": "^1.18.3",
"compression": "^1.7.3",
"cookie-parser": "^1.4.3",
"cors": "^2.8.5",
"dotenv": "^6.1.0",
"express": "^4.16.4",
"handlebars": "^4.0.12",
"i18n": "^0.8.3",
"ini": "^1.3.5",
"jsonwebtoken": "^8.3.0",
"moment": "^2.22.2",
"mongodb": "^3.1.9",
"node-rsa": "^1.0.1",
"reflect-metadata": "^0.1.12",
"tedious": "^2.6.4",
"uuid": "^3.3.2"
}
}

12
src/api/admin.ts

@ -0,0 +1,12 @@ @@ -0,0 +1,12 @@
import { Request, Router } from "express";
import ClientRoute from "./admin/client";
import UserRoute from "./admin/user";
import RegCodeRoute from "./admin/regcode";
import PermissionRoute from "./admin/permission";
const AdminRoute: Router = Router();
AdminRoute.use("/client", ClientRoute);
AdminRoute.use("/regcode", RegCodeRoute)
AdminRoute.use("/user", UserRoute)
AdminRoute.use("/permission", PermissionRoute);
export default AdminRoute;

88
src/api/admin/client.ts

@ -0,0 +1,88 @@ @@ -0,0 +1,88 @@
import { Router, Request } from "express";
import { GetUserMiddleware } from "../middlewares/user";
import RequestError, { HttpStatusCode } from "../../helper/request_error";
import promiseMiddleware from "../../helper/promiseMiddleware";
import Client from "../../models/client";
import User from "../../models/user";
import verify, { Types } from "../middlewares/verify";
import { randomBytes } from "crypto";
const ClientRouter: Router = Router();
ClientRouter.use(GetUserMiddleware(true, true), (req: Request, res, next) => {
if (!req.isAdmin) res.sendStatus(HttpStatusCode.FORBIDDEN)
else next()
});
ClientRouter.route("/")
.get(promiseMiddleware(async (req, res) => {
let clients = await Client.find({});
//ToDo check if user is required!
res.json(clients);
}))
.delete(promiseMiddleware(async (req, res) => {
let { id } = req.query;
await Client.delete(id);
res.json({ success: true });
}))
.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
}
}, 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);
}))
.put(verify({
id: {
type: Types.STRING,
query: true
},
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
}
}, true), promiseMiddleware(async (req, res) => {
let { id } = req.query;
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;

45
src/api/admin/permission.ts

@ -0,0 +1,45 @@ @@ -0,0 +1,45 @@
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";
const PermissionRoute: Router = Router();
PermissionRoute.use(GetUserMiddleware(true, true), (req: Request, res, next) => {
if (!req.isAdmin) res.sendStatus(HttpStatusCode.FORBIDDEN)
else next()
});
PermissionRoute.route("/")
.get(promiseMiddleware(async (req, res) => {
let permission = await Permission.find({});
res.json(permission);
}))
.post(verify({
clientId: {
type: Types.NUMBER
},
name: {
type: Types.STRING
},
description: {
type: Types.STRING
}
}, true), promiseMiddleware(async (req, res) => {
let client = await Client.findById(req.body.clientId);
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
});
await Permission.save(permission);
res.json(permission);
}))
export default PermissionRoute;

34
src/api/admin/regcode.ts

@ -0,0 +1,34 @@ @@ -0,0 +1,34 @@
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.use(GetUserMiddleware(true, true), (req: Request, res, next) => {
if (!req.isAdmin) res.sendStatus(HttpStatusCode.FORBIDDEN)
else next()
});
RegCodeRoute.route("/")
.get(promiseMiddleware(async (req, res) => {
let regcodes = await RegCode.find({});
res.json(regcodes);
}))
.delete(promiseMiddleware(async (req, res) => {
let { id } = req.query;
await RegCode.delete(id);
res.json({ success: true });
}))
.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;

42
src/api/admin/user.ts

@ -0,0 +1,42 @@ @@ -0,0 +1,42 @@
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("/")
.get(promiseMiddleware(async (req, res) => {
let users = await User.find({});
res.json(users);
}))
.delete(promiseMiddleware(async (req, res) => {
let { id } = req.query;
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 });
})).put(promiseMiddleware(async (req, res) => {
let { id } = req.query;
let user = await User.findById(id);
user.admin = !user.admin;
await User.save(user);
res.json({ success: true })
}))
export default UserRoute;

18
src/api/api.ts

@ -0,0 +1,18 @@ @@ -0,0 +1,18 @@
import * as express from "express"
import AdminRoute from "./admin";
import UserRoute from "./user";
import InternalRoute from "./internal";
import Login from "./user/login";
import { AuthGetUser } from "./client/user";
const ApiRouter: express.IRouter<void> = express.Router();
ApiRouter.use("/admin", AdminRoute);
ApiRouter.use("/user", UserRoute);
ApiRouter.use("/internal", InternalRoute);
ApiRouter.use("/user", AuthGetUser);
// Legacy reasons (deprecated)
ApiRouter.post("/login", Login);
export default ApiRouter;

0
src/api/client.ts

14
src/api/client/user.ts

@ -0,0 +1,14 @@ @@ -0,0 +1,14 @@
import { Request, Response } from "express"
import Stacker from "../middlewares/stacker";
import { GetClientAuthMiddleware } from "../middlewares/client";
import { GetUserMiddleware } from "../middlewares/user";
import { createJWT } from "../../keys";
export const AuthGetUser = Stacker(GetClientAuthMiddleware(false), GetUserMiddleware(true, false), async (req: Request, res: Response) => {
let jwt = await createJWT({
client: req.client.client_id,
uid: req.user.uid,
username: req.user.username
}, 30); //after 30 seconds this token is invalid
res.redirect(req.query.redirect_uri + "?jwt=" + jwt)
});

8
src/api/internal.ts

@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
import { Router } from "express";
import { OAuthInternalApp } from "./internal/oauth";
import PasswordAuth from "./internal/password";
const InternalRoute: Router = Router();
InternalRoute.get("/oauth", OAuthInternalApp);
InternalRoute.post("/password", PasswordAuth)
export default InternalRoute;

29
src/api/internal/oauth.ts

@ -0,0 +1,29 @@ @@ -0,0 +1,29 @@
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
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();
});

25
src/api/internal/password.ts

@ -0,0 +1,25 @@ @@ -0,0 +1,25 @@
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;

76
src/api/middlewares/client.ts

@ -0,0 +1,76 @@ @@ -0,0 +1,76 @@
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 && 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 || req.headers.authorization;
if (!token)
throw invalid_err;
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;
}
}
}

24
src/api/middlewares/stacker.ts

@ -0,0 +1,24 @@ @@ -0,0 +1,24 @@
import { Request, Response, NextFunction, RequestHandler } from "express";
import promiseMiddleware from "../../helper/promiseMiddleware";
function call(handler: RequestHandler, req: Request, res: Response) {
return new Promise((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: RequestHandler[]) => {
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;

83
src/api/middlewares/user.ts

@ -0,0 +1,83 @@ @@ -0,0 +1,83 @@
import { NextFunction, Request, Response } from "express";
import LoginToken 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 Checks if requests wants an json or html for returning errors
* @param redirect_uri Sets the uri to redirect, if json is not set and user not logged in
*/
export function GetUserMiddleware(json = false, special_token: boolean = false, redirect_uri?: string) {
return promiseMiddleware(async function (req: Request, res: Response, next?: NextFunction) {
const invalid = () => {
throw new Invalid();
}
try {
let { login, special } = req.cookies
if (!login) invalid()
let token = await LoginToken.findOne({ token: login, valid: true })
if (!token) invalid()
let user = await User.findById(token.user);
if (!user) {
token.valid = false;
await LoginToken.save(token);
invalid();
}
if (token.validTill.getTime() < new Date().getTime()) { //Token expired
token.valid = false;
await LoginToken.save(token);
invalid()
}
if (special) {
Logging.debug("Special found")
let st = await LoginToken.findOne({ token: special, special: true, valid: true })
if (st && st.valid && st.user.toHexString() === token.user.toHexString()) {
if (st.validTill.getTime() < new Date().getTime()) { //Token expired
Logging.debug("Special expired")
st.valid = false;
await LoginToken.save(st);
} else {
Logging.debug("Special valid")
req.special = true;
}
}
}
if (special_token && !req.special) invalid();
req.user = user
req.isAdmin = user.admin;
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=" + new Buffer(redirect_uri ? redirect_uri : req.originalUrl).toString("base64"))
} else {
throw new RequestError(req.__("You are not logged in or your login is expired"), HttpStatusCode.UNAUTHORIZED)
}
} else {
if (next) next(e);
else throw e;
}
return false;
}
});
}
export const UserMiddleware = GetUserMiddleware();

125
src/api/middlewares/verify.ts

@ -0,0 +1,125 @@ @@ -0,0 +1,125 @@
import { Request, Response, NextFunction } from "express"
import { Logging } from "@hibas123/nodelogging";
import { isBoolean, isString, isNumber, isObject, isDate, isArray, isSymbol } 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 (isNumber(data)) return;
break;
case Types.EMAIL:
if (isEmail(data)) return;
break;
case Types.BOOLEAN:
if (isBoolean(data)) return;
break;
case Types.OBJECT:
if (isObject(data)) return;
break;
case Types.ARRAY:
if (isArray(data)) return;
break;
case Types.DATE:
if (isDate(data)) return;
break;
case Types.ENUM:
if (isString(data)) {
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()
}
}

13
src/api/oauth.ts

@ -0,0 +1,13 @@ @@ -0,0 +1,13 @@
import { Router } from "express";
import AuthRoute from "./oauth/auth";
import JWTRoute from "./oauth/jwt";
import Public from "./oauth/public";
import RefreshTokenRoute from "./oauth/refresh";
const OAuthRoue: Router = Router();
OAuthRoue.post("/auth", AuthRoute);
OAuthRoue.get("/jwt", JWTRoute)
OAuthRoue.get("/public", Public)
OAuthRoue.get("/refresh", RefreshTokenRoute);
OAuthRoue.post("/refresh", RefreshTokenRoute);
export default OAuthRoue;

81
src/api/oauth/auth.ts

@ -0,0 +1,81 @@ @@ -0,0 +1,81 @@
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 { Sequelize } from "sequelize-typescript";
import ClientCode from "../../models/client_code";
import moment = require("moment");
import { randomBytes } from "crypto";
import { ObjectID } from "bson";
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) => {
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(";").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 ruri = client.redirect_url + `?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")
}
})
export default AuthRoute;

27
src/api/oauth/jwt.ts

@ -0,0 +1,27 @@ @@ -0,0 +1,27 @@
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 Permission from "../../models/permissions";
import Client from "../../models/client";
import getOAuthJWT from "../../helper/jwt";
const JWTRoute = promiseMiddleware(async (req: Request, res: Response) => {
let { refreshtoken } = req.query;
if (!refreshtoken) throw new RequestError(req.__("Refresh token not set"), HttpStatusCode.BAD_REQUEST);
let token = await RefreshToken.findOne({ where: { token: refreshtoken }, include: [User, Permission, Client] });
if (!token) throw new RequestError(req.__("Invalid token"), HttpStatusCode.BAD_REQUEST);
let user = await User.findById(token.user);
if (!user) {
//TODO handle error!
}
let client = await Client.findById(token.client);
let jwt = await getOAuthJWT({ user, permissions: token.permissions, client });
res.json({ token: jwt });
})
export default JWTRoute;

6
src/api/oauth/public.ts

@ -0,0 +1,6 @@ @@ -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 })
}

79
src/api/oauth/refresh.ts

@ -0,0 +1,79 @@ @@ -0,0 +1,79 @@
import { Request, Response } from "express";
import promiseMiddleware from "../../helper/promiseMiddleware";
import RequestError, { HttpStatusCode } from "../../helper/request_error";
import User from "../../models/user";
import Permission from "../../models/permissions";
import Client from "../../models/client";
import getOAuthJWT 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 { Promise } from "bluebird";
const RefreshTokenRoute = Stacker(GetClientAuthMiddleware(false, false, true), async (req: Request, res: Response) => {
let code = req.query.code || req.body.code;
let redirect_uri = req.query.redirect_uri || req.body.redirect_uri;
let grant_type = req.query.grant_type || req.body.grant_type;
if (!grant_type || grant_type === "authorization_code") {
let c = await ClientCode.findOne({ where: { code: code }, include: [{ model: User, include: [Mail] }, Client, Permission] })
if (!c) {
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(6, "months").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 getOAuthJWT({
client: client,
user: user,
permissions: c.permissions
}),
token_type: "bearer",
expires_in: JWTExpDur.asSeconds(),
profile: {
uid: user.uid,
email: mail ? mail.mail : "",
name: user.name,
}
});
} 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) throw new RequestError(req.__("Invalid token"), HttpStatusCode.BAD_REQUEST);
let user = await User.findById(token.user);
let client = await Client.findById(token.client)
let jwt = await getOAuthJWT({ user, client, permissions: token.permissions });
res.json({ access_token: jwt, expires_in: JWTExpDur.asSeconds() });
} else {
throw new RequestError("invalid grant_type", HttpStatusCode.BAD_REQUEST);
}
})
export default RefreshTokenRoute;

8
src/api/user.ts

@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
import { Request, Router } from "express";
import Register from "./user/register";
import Login from "./user/login";
const UserRoute: Router = Router();
UserRoute.post("/register", Register);
UserRoute.post("/login", Login)
export default UserRoute;

81
src/api/user/login.ts

@ -0,0 +1,81 @@ @@ -0,0 +1,81 @@
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 RequestError, { HttpStatusCode } from "../../helper/request_error";
import promiseMiddleware from "../../helper/promiseMiddleware";
const Login = promiseMiddleware(async (req: Request, res: Response) => {
let type = req.query.type;
if (type === "username") {
let { username, uid } = req.query;
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;
}
const sendToken = async (user: IUser) => {
let token_str = randomBytes(16).toString("hex");
let token_exp = moment().add(6, "months").toDate()
let token = LoginToken.new({
token: token_str,
valid: true,
validTill: token_exp,
user: user._id
});
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: special_exp,
special: true,
user: user._id
});
await LoginToken.save(special);
res.json({
login: { token: token_str, expires: token_exp.toUTCString() },
special: { token: special_str, expires: special_exp.toUTCString() }
});
}
if (type === "password" || type === "twofactor") {
let { username, password, uid } = req.body;
let user = await User.findOne(username ? { username: username.toLowerCase() } : { uid: uid })
if (!user) {
res.json({ error: req.__("User not found") })
} else {
if (user.password !== password) {
res.json({ error: req.__("Password or username wrong") })
} else {
if (type === "twofactor") {
} else {
if (user.twofactor && user.twofactor.length > 0) {
let types = user.twofactor.map(f => {
return { type: f.type };
})
res.json({
types: types
});
} else {
await sendToken(user);
}
}
}
}
} else {
throw new RequestError("Invalid type!", HttpStatusCode.BAD_REQUEST);
}
});
export default Login;

142
src/api/user/register.ts

@ -0,0 +1,142 @@ @@ -0,0 +1,142 @@
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
})
user.mails.push(ml._id);
await User.save(user)
res.json({ success: true });
}))
export default Register;

46
src/config.ts

@ -0,0 +1,46 @@ @@ -0,0 +1,46 @@
export interface DatabaseConfig {
host: string
database: string
dialect: "sqlite" | "mysql" | "postgres" | "mssql"
username: string
password: string
storage: string
benchmark: "true" | "false" | undefined
}
export interface WebConfig {
port: string
secure: "true" | "false" | undefined
}
export interface CoreConfig {
name: string
}
export interface Config {
core: CoreConfig
database: DatabaseConfig
web: WebConfig
dev: boolean
logging: {
server: string;
appid: string;
token: string;
} | undefined
}
import * as ini from "ini";
import { readFileSync } from "fs";
import * as dotenv from "dotenv";
import { Logging } from "@hibas123/nodelogging";
dotenv.config();
const config: Config = ini.parse(readFileSync("./config.ini").toString())
if (process.env.DEV === "true") {
config.dev = true;
Logging.warning("DEV mode active. This can cause major performance issues, data loss and vulnerabilities! ")
}
export default config;

3
src/database.ts

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
import SafeMongo from "@hibas123/safe_mongo";
const DB = new SafeMongo("mongodb://localhost", "openauth");
export default DB;

11
src/express.d.ts vendored

@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
import { IUser } from "./models/user";
import { IClient } from "./models/client";
declare module "express" {
interface Request {
user: IUser;
client: IClient;
isAdmin: boolean;
special: boolean;
}
}

20
src/helper/jwt.ts

@ -0,0 +1,20 @@ @@ -0,0 +1,20 @@
import { IUser } from "../models/user";
import { ObjectID } from "bson";
import { createJWT } from "../keys";
import { IClient } from "../models/client";
export interface OAuthJWT {
user: string;
username: string;
permissions: string[];
application: string
}
export default function getOAuthJWT(token: { user: IUser, permissions: ObjectID[], client: IClient }) {
return createJWT(<OAuthJWT>{
user: token.user.uid,
username: token.user.username,
permissions: token.permissions.map(p => p.toHexString()),
application: token.client.client_id
})
}

5
src/helper/promiseMiddleware.ts

@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
import { Request, Response, NextFunction } from "express"
export default (fn: (req: Request, res: Response, next: NextFunction) => Promise<any>) => (req: Request, res: Response, next: NextFunction) => {
Promise.resolve(fn(req, res, next)).catch(next)
}

388
src/helper/request_error.ts

@ -0,0 +1,388 @@ @@ -0,0 +1,388 @@
/**
* Hypertext Transfer Protocol (HTTP) response status codes.
* @see {@link https://en.wikipedia.org/wiki/List_of_HTTP_status_codes}
*/
export enum HttpStatusCode {
/**
* The server has received the request headers and the client should proceed to send the request body
* (in the case of a request for which a body needs to be sent; for example, a POST request).
* Sending a large request body to a server after a request has been rejected for inappropriate headers would be inefficient.
* To have a server check the request's headers, a client must send Expect: 100-continue as a header in its initial request
* and receive a 100 Continue status code in response before sending the body. The response 417 Expectation Failed indicates the request should not be continued.
*/
CONTINUE = 100,
/**
* The requester has asked the server to switch protocols and the server has agreed to do so.
*/
SWITCHING_PROTOCOLS = 101,
/**
* A WebDAV request may contain many sub-requests involving file operations, requiring a long time to complete the request.
* This code indicates that the server has received and is processing the request, but no response is available yet.
* This prevents the client from timing out and assuming the request was lost.
*/
PROCESSING = 102,
/**
* Standard response for successful HTTP requests.
* The actual response will depend on the request method used.
* In a GET request, the response will contain an entity corresponding to the requested resource.
* In a POST request, the response will contain an entity describing or containing the result of the action.
*/
OK = 200,