diff --git a/.vscode/launch.json b/.vscode/launch.json index 445707e..cc4f503 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -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" - ] - }] -} \ No newline at end of file + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Launch Program", + "program": "${workspaceFolder}/lib/index.js", + "outFiles": ["${workspaceFolder}/**/*.js"], + "preLaunchTask": "build" + } + ] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..1033e39 --- /dev/null +++ b/.vscode/tasks.json @@ -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" + } + ] +} diff --git a/locales/en.json b/locales/en.json index a7bf8e9..407cc36 100644 --- a/locales/en.json +++ b/locales/en.json @@ -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" } \ No newline at end of file diff --git a/package.json b/package.json index 13ce4bf..deaa6c9 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/api/client/index.ts b/src/api/client/index.ts index ec8ba75..0eace9d 100644 --- a/src/api/client/index.ts +++ b/src/api/client/index.ts @@ -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; \ No newline at end of file +ClientRouter.get("/account", Stacker(GetClientApiAuthMiddleware(), async (req: Request, res) => { + res.json({ + user: { + username: req.user.username, + name: req.user.name, + } + }) +})); + +export default ClientRouter; diff --git a/src/api/oauth/auth.ts b/src/api/oauth/auth.ts index ec513c3..e5dce2c 100644 --- a/src/api/oauth/auth.ts +++ b/src/api/oauth/auth.ts @@ -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 = (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 = (scope).split(";").filter(e => e !== "read_user").map(p => new ObjectID(p)); - permissions = await Permission.find({ _id: { $in: perms } }) + let proms: PromiseLike[] = []; + 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((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; \ No newline at end of file +}); + +export default GetAuthRoute; diff --git a/src/api/oauth/index.ts b/src/api/oauth/index.ts index 220f025..16ae123 100644 --- a/src/api/oauth/index.ts +++ b/src/api/oauth/index.ts @@ -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; \ No newline at end of file +export default OAuthRoue; diff --git a/src/models/grants.ts b/src/models/grants.ts new file mode 100644 index 0000000..5112c37 --- /dev/null +++ b/src/models/grants.ts @@ -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({ + name: "grant", + versions: [{ + migration: () => { }, + schema: { + user: { type: ObjectID }, + client: { type: ObjectID }, + permissions: { type: ObjectID, array: true } + } + }] +}) + +export default Grant; diff --git a/src/models/permissions.ts b/src/models/permissions.ts index 932c987..e408eec 100644 --- a/src/models/permissions.ts +++ b/src/models/permissions.ts @@ -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({ @@ -18,7 +18,15 @@ const Permission = DB.addModel({ 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; \ No newline at end of file +export default Permission; diff --git a/src/testdata.ts b/src/testdata.ts index d3e5141..bd16877 100644 --- a/src/testdata.ts +++ b/src/testdata.ts @@ -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) -} \ No newline at end of file +} diff --git a/src/views/views.ts b/src/views/views.ts index 11f0cfa..f808679 100644 --- a/src/views/views.ts +++ b/src/views/views.ts @@ -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[] = []; - 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[] = []; +// 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 = diff --git a/views/src/authorize/authorize.hbs b/views/src/authorize/authorize.hbs index d20b94f..e211c7a 100644 --- a/views/src/authorize/authorize.hbs +++ b/views/src/authorize/authorize.hbs @@ -44,8 +44,10 @@ - + - \ No newline at end of file +