Implementing basic auth_grant

This commit is contained in:
Fabian Stamm 2020-03-17 16:27:57 +01:00
parent 92cc97c396
commit 44d02b0110
12 changed files with 304 additions and 100 deletions

21
.vscode/launch.json vendored
View File

@ -3,13 +3,14 @@
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [{
"type": "node",
"request": "launch",
"name": "Launch Program",
"program": "${workspaceFolder}/lib/index.js",
"outFiles": [
"${workspaceFolder}/**/*.js"
]
}]
}
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch Program",
"program": "${workspaceFolder}/lib/index.js",
"outFiles": ["${workspaceFolder}/**/*.js"],
"preLaunchTask": "build"
}
]
}

22
.vscode/tasks.json vendored Normal file
View File

@ -0,0 +1,22 @@
{
// Unter https://go.microsoft.com/fwlink/?LinkId=733558
// finden Sie Informationen zum Format von "tasks.json"
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "build-ts",
"group": "build",
"problemMatcher": ["$tsc"],
"presentation": {
"echo": true,
"reveal": "never",
"focus": false,
"panel": "shared",
"showReuseMessage": true,
"clear": false
},
"label": "build"
}
]
}

View File

@ -9,5 +9,7 @@
"No special token": "No special token",
"You are not logged in or your login is expired(No special token)": "You are not logged in or your login is expired(No special token)",
"Special token invalid": "Special token invalid",
"You are not logged in or your login is expired(Special token invalid)": "You are not logged in or your login is expired(Special token invalid)"
"You are not logged in or your login is expired(Special token invalid)": "You are not logged in or your login is expired(Special token invalid)",
"No login token": "No login token",
"Login token invalid": "Login token invalid"
}

View File

@ -9,6 +9,7 @@
"install": "run-s install-views install-views_repo",
"build": "run-s build-ts build-doc build-views build-views_repo",
"watch": "concurrently \"npm:watch-*\"",
"dev": "npm run watch",
"build-doc": "apidoc -i src/ -p apidoc/",
"build-ts": "tsc",
"watch-ts": "tsc -w",

View File

@ -1,6 +1,6 @@
import { Request, Response, Router } from "express"
import Stacker from "../middlewares/stacker";
import { GetClientAuthMiddleware } from "../middlewares/client";
import { GetClientAuthMiddleware, GetClientApiAuthMiddleware } from "../middlewares/client";
import { GetUserMiddleware } from "../middlewares/user";
import { createJWT } from "../../keys";
import Client from "../../models/client";
@ -44,4 +44,13 @@ ClientRouter.get("/user", Stacker(GetClientAuthMiddleware(false), GetUserMiddlew
res.redirect(redirect_uri + "?jwt=" + jwt + (state ? `&state=${state}` : ""));
}));
export default ClientRouter;
ClientRouter.get("/account", Stacker(GetClientApiAuthMiddleware(), async (req: Request, res) => {
res.json({
user: {
username: req.user.username,
name: req.user.name,
}
})
}));
export default ClientRouter;

View File

@ -7,38 +7,103 @@ 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 { ObjectID } from "bson";
import Grant, { IGrant } from "../../models/grants";
import GetAuthPage from "../../views/authorize";
const AuthRoute = Stacker(GetUserMiddleware(true), async (req: Request, res: Response) => {
// 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;
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 {
const scopes = scope.split(";");
Logging.debug("Scopes:", scope);
try {
if (response_type !== "code") {
return sendError("unsupported_response_type");
} else {
let client = await Client.findOne({ client_id: client_id })
let client = await Client.findOne({ client_id: client_id });
if (!client) {
return sendError("unauthorized_client")
return sendError("unauthorized_client");
}
if (redirect_uri && client.redirect_url !== redirect_uri) {
@ -47,12 +112,74 @@ const AuthRoute = Stacker(GetUserMiddleware(true), async (req: Request, res: Res
}
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 } })
let proms: PromiseLike<void>[] = [];
if (scopes) {
for (let perm of scopes.filter(e => e !== "read_user")) {
proms.push(
Permission.findById(perm).then(p => {
if (!p) return Promise.reject(new Error());
permissions.push(p);
})
);
}
}
if (permissions.length != perms.length) {
return sendError("invalid_scope");
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) => 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");
}
}
}
@ -66,7 +193,6 @@ const AuthRoute = Stacker(GetUserMiddleware(true), async (req: Request, res: Res
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({
@ -80,5 +206,6 @@ const AuthRoute = Stacker(GetUserMiddleware(true), async (req: Request, res: Res
Logging.error(err);
sendError("server_error")
}
})
export default AuthRoute;
});
export default GetAuthRoute;

View File

@ -1,5 +1,5 @@
import { Router } from "express";
import AuthRoute from "./auth";
import GetAuthRoute from "./auth";
import JWTRoute from "./jwt";
import Public from "./public";
import RefreshTokenRoute from "./refresh";
@ -19,7 +19,7 @@ const OAuthRoue: Router = Router();
* @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", AuthRoute);
OAuthRoue.post("/auth", GetAuthRoute(false));
/**
* @api {get} /oauth/jwt
@ -60,4 +60,4 @@ OAuthRoue.get("/refresh", RefreshTokenRoute);
* @apiGroup oauth
*/
OAuthRoue.post("/refresh", RefreshTokenRoute);
export default OAuthRoue;
export default OAuthRoue;

23
src/models/grants.ts Normal file
View File

@ -0,0 +1,23 @@
import DB from "../database";
import { ModelDataBase } from "@hibas123/safe_mongo/lib/model";
import { ObjectID } from "mongodb";
export interface IGrant extends ModelDataBase {
user: ObjectID;
client: ObjectID;
permissions: ObjectID[];
}
const Grant = DB.addModel<IGrant>({
name: "grant",
versions: [{
migration: () => { },
schema: {
user: { type: ObjectID },
client: { type: ObjectID },
permissions: { type: ObjectID, array: true }
}
}]
})
export default Grant;

View File

@ -1,12 +1,12 @@
import DB from "../database";
import { ModelDataBase } from "@hibas123/safe_mongo/lib/model";
import { ObjectID } from "mongodb";
import { v4 } from "uuid";
export interface IPermission extends ModelDataBase {
name: string;
description: string;
client: ObjectID;
grant_type: "user" | "client";
}
const Permission = DB.addModel<IPermission>({
@ -18,7 +18,15 @@ const Permission = DB.addModel<IPermission>({
description: { type: String },
client: { type: ObjectID }
}
}, {
migration: (old) => { old.grant_type = "user" },
schema: {
name: { type: String },
description: { type: String },
client: { type: ObjectID },
grant_type: { type: String, default: "user" }
}
}]
})
export default Permission;
export default Permission;

View File

@ -71,8 +71,11 @@ export default async function TestData() {
name: "TestPerm",
description: "Permission just for testing purposes",
client: c._id
})
Permission.save(perm);
});
await (await (Permission as any)._collection).insertOne(perm);
// Permission.save(perm);
}
let r = await RegCode.findOne({ token: "test" });
@ -138,4 +141,4 @@ export default async function TestData() {
// })
// Logging.debug("OTC Code is:", code);
// }, 1000)
}
}

View File

@ -53,56 +53,62 @@ ViewRouter.get(
}
);
ViewRouter.get(
"/auth",
Stacker(GetUserMiddleware(false, true), async (req, res) => {
let {
scope,
redirect_uri,
state,
client_id
}: { [key: string]: string } = req.query;
const sendError = type => {
res.redirect((redirect_uri += `?error=${type}&state=${state}`));
};
let client = await Client.findOne({ client_id: client_id });
if (!client) {
return sendError("unauthorized_client");
}
let permissions: IPermission[] = [];
let proms: PromiseLike<void>[] = [];
if (scope) {
for (let perm of scope.split(";").filter(e => e !== "read_user")) {
proms.push(
Permission.findById(perm).then(p => {
if (!p) return Promise.reject(new Error());
permissions.push(p);
})
);
}
}
let err = false;
await Promise.all(proms).catch(e => {
err = true;
});
Logging.debug(err);
if (err) {
return sendError("invalid_scope");
}
let scopes = await Promise.all(
permissions.map(async perm => {
let client = await Client.findById(perm.client);
return {
name: perm.name,
description: perm.description,
logo: client.logo
};
})
);
res.send(GetAuthPage(req.__, client.name, scopes));
})
);
import GetAuthRoute from "../api/oauth/auth";
ViewRouter.get("/auth", GetAuthRoute(true))
// ViewRouter.get(
// "/auth",
// Stacker(GetUserMiddleware(false, true), async (req, res) => {
// let {
// scope,
// redirect_uri,
// state,
// client_id
// }: { [key: string]: string } = req.query;
// const sendError = type => {
// res.redirect((redirect_uri += `?error=${type}&state=${state}`));
// };
// let client = await Client.findOne({ client_id: client_id });
// if (!client) {
// return sendError("unauthorized_client");
// }
// let permissions: IPermission[] = [];
// let proms: PromiseLike<void>[] = [];
// if (scope) {
// for (let perm of scope.split(";").filter(e => e !== "read_user")) {
// proms.push(
// Permission.findById(perm).then(p => {
// if (!p) return Promise.reject(new Error());
// permissions.push(p);
// })
// );
// }
// }
// let err = false;
// await Promise.all(proms).catch(e => {
// err = true;
// });
// Logging.debug(err);
// if (err) {
// return sendError("invalid_scope");
// }
// let scopes = await Promise.all(
// permissions.map(async perm => {
// let client = await Client.findById(perm.client);
// return {
// name: perm.name,
// description: perm.description,
// logo: client.logo
// };
// })
// );
// res.send(GetAuthPage(req.__, client.name, scopes));
// })
// );
if (config.core.dev) {
const logo =

View File

@ -44,8 +44,10 @@
</div>
</div>
<form method="post" action="/api/oauth/auth?" id="hidden_form" style="display: none;"></form>
<form method="post" action="/api/oauth/auth?" id="hidden_form" style="display: none;">
<input name="accept" value="true" />
</form>
</body>
</html>
</html>