add simple app authorization page

This commit is contained in:
Mystikfluu 2023-02-10 16:05:58 +01:00
parent abc03b4cde
commit 83941add4d
10 changed files with 233 additions and 8 deletions

21
css/authorize.css Normal file
View File

@ -0,0 +1,21 @@
button {
font-size: 18px;
margin: 10px;
}
.center {
text-align: center;
margin: auto;
width: 50%;
border: 3px solid green;
padding: 10px;
}
body {
background-color: var(--bg-color);
color: var(--text-color);
}
.hidden {
display: none;
}

26
package-lock.json generated
View File

@ -17,6 +17,7 @@
"express": "^4.18.2", "express": "^4.18.2",
"express-fileupload": "^1.3.1", "express-fileupload": "^1.3.1",
"express-useragent": "^1.0.15", "express-useragent": "^1.0.15",
"hcaptcha": "^0.1.1",
"html-minifier-terser": "^7.1.0", "html-minifier-terser": "^7.1.0",
"lru-cache": "^7.14.1", "lru-cache": "^7.14.1",
"mysql2": "^3.1.0", "mysql2": "^3.1.0",
@ -26,6 +27,9 @@
"uglify-js": "^3.17.4", "uglify-js": "^3.17.4",
"unsafe_encrypt": "^1.0.4", "unsafe_encrypt": "^1.0.4",
"ws": "^8.12.0" "ws": "^8.12.0"
},
"devDependencies": {
"@hcaptcha/types": "^1.0.3"
} }
}, },
"node_modules/@colors/colors": { "node_modules/@colors/colors": {
@ -80,6 +84,12 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/@hcaptcha/types": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@hcaptcha/types/-/types-1.0.3.tgz",
"integrity": "sha512-1mbU6eSGawRrqeahRrOzZo/SVLI6oZ5/azuBpSyVrRRR96CnS3fOVDWfzxpngfxKD0/I9Rwu6c/3ITqD8rXeTQ==",
"dev": true
},
"node_modules/@jridgewell/gen-mapping": { "node_modules/@jridgewell/gen-mapping": {
"version": "0.3.2", "version": "0.3.2",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz",
@ -1086,6 +1096,11 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/hcaptcha": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/hcaptcha/-/hcaptcha-0.1.1.tgz",
"integrity": "sha512-iMrDmH2VpIEKOrcKWidVjI89FdDKTEdZ7PfPWkP27sTazIIkob8YfdY2ezaufAnWBiUUcvzsn0qF+dyXtBH2Vw=="
},
"node_modules/hpack.js": { "node_modules/hpack.js": {
"version": "2.1.6", "version": "2.1.6",
"resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz",
@ -2498,6 +2513,12 @@
"yargs": "^16.2.0" "yargs": "^16.2.0"
} }
}, },
"@hcaptcha/types": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@hcaptcha/types/-/types-1.0.3.tgz",
"integrity": "sha512-1mbU6eSGawRrqeahRrOzZo/SVLI6oZ5/azuBpSyVrRRR96CnS3fOVDWfzxpngfxKD0/I9Rwu6c/3ITqD8rXeTQ==",
"dev": true
},
"@jridgewell/gen-mapping": { "@jridgewell/gen-mapping": {
"version": "0.3.2", "version": "0.3.2",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz",
@ -3278,6 +3299,11 @@
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A=="
}, },
"hcaptcha": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/hcaptcha/-/hcaptcha-0.1.1.tgz",
"integrity": "sha512-iMrDmH2VpIEKOrcKWidVjI89FdDKTEdZ7PfPWkP27sTazIIkob8YfdY2ezaufAnWBiUUcvzsn0qF+dyXtBH2Vw=="
},
"hpack.js": { "hpack.js": {
"version": "2.1.6", "version": "2.1.6",
"resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz",

View File

@ -8,6 +8,7 @@
"express": "^4.18.2", "express": "^4.18.2",
"express-fileupload": "^1.3.1", "express-fileupload": "^1.3.1",
"express-useragent": "^1.0.15", "express-useragent": "^1.0.15",
"hcaptcha": "^0.1.1",
"html-minifier-terser": "^7.1.0", "html-minifier-terser": "^7.1.0",
"lru-cache": "^7.14.1", "lru-cache": "^7.14.1",
"mysql2": "^3.1.0", "mysql2": "^3.1.0",
@ -36,5 +37,8 @@
"bugs": { "bugs": {
"url": "https://github.com/002Hub/IPost/issues" "url": "https://github.com/002Hub/IPost/issues"
}, },
"homepage": "https://github.com/002Hub/IPost#readme" "homepage": "https://github.com/002Hub/IPost#readme",
"devDependencies": {
"@hcaptcha/types": "^1.0.3"
}
} }

View File

@ -18,7 +18,7 @@ export const setup = function (router, con, server) {
console.log("error parsing header",err) console.log("error parsing header",err)
} }
} }
if(req.body.auth != undefined) { if(req.body.auth !== undefined && req.originalUrl!=="/redeemauthcode") {
if(typeof req.body.auth === "string") { if(typeof req.body.auth === "string") {
try{ try{
req.body.auth = JSON.parse(req.body.auth) req.body.auth = JSON.parse(req.body.auth)

125
routes/authorize.js Normal file
View File

@ -0,0 +1,125 @@
import {randomBytes} from "crypto"
import {SHA256} from "../extra_modules/SHA.js";
import {unsign} from "../extra_modules/unsign.js";
export const setup = function (router, con, server) {
const temp_code_to_token = {}
router.post("/authorize",async (req,res) => {
if (!unsign(req.cookies.AUTH_COOKIE, req, res)){
return
}
let data = await server.hcaptcha.verify(req.body["h-captcha-response"])
if(data.success) {
console.log("captcha success")
let appid = req.body.application_id
if(typeof appid === "string") {
appid = Number(appid)
}
if(typeof appid === "number") {
const token = randomBytes(150).toString("base64")
let tokencode;
while(tokencode===undefined || temp_code_to_token[tokencode]!==undefined) {
tokencode = randomBytes(15).toString("base64")
}
temp_code_to_token[tokencode]={
"userid":res.locals.userid,
"appid":appid,
"token":token
}
setTimeout(() => {
let data = temp_code_to_token[tokencode]
if(data !== undefined && data.token===token && data.appid === appid && data.userid === userid) {
temp_code_to_token[tokencode]=undefined
}
}, 300000); //wait for 5 minutes
const sql = "SELECT application_auth_url FROM ipost.application where application_id=?"
con.query(sql,[appid],(err,result) => {
if(err || result.length !== 1) {
console.err(err)
res.redirect(`/authorize?id=${req.body.application_id}`)
return
}
res.redirect(`${result[0].application_auth_url}?code=${tokencode}`)
})
return
}
}
res.redirect(`/authorize?id=${req.body.application_id}`)
})
router.post("/redeemauthcode", (req,res) => {
if(temp_code_to_token[req.body.authcode]===undefined) {
res.status(400)
res.json({"status":400,"message":"invalid code given"})
return
}
if(typeof req.body.auth === "string") {
try{
req.body.auth = JSON.parse(req.body.auth)
} catch(err) {
console.log("error parsing",err)
}
}
if(
typeof req.body.auth !== "object" ||
typeof req.body.auth.secret !== "string" ||
typeof req.body.auth.appid !== "number" ||
req.body.auth.secret.length !== 200 ||
Buffer.from(req.body.auth.secret,"base64").length !== 150 ||
req.body.auth.appid !== temp_code_to_token[req.body.authcode].appid
) {
//console.log(1,req.body.auth,temp_code_to_token[req.body.authcode].appid)
res.status(420).send("invalid authentication object")
return;
}
const appid = req.body.auth.appid
const checksecret = SHA256(req.body.auth.secret,appid,10000)
const checksql = "SELECT application_id from ipost.application where application_secret=? and application_id=?"
const checkvalues = [checksecret,appid]
con.query(checksql,checkvalues,(error,result_object) => {
if(error || result_object[0]===undefined || result_object[0].application_id!==appid) {
res.status(400)
res.json({"status":400,"message":"invalid code given"})
return
}
let data = temp_code_to_token[req.body.authcode]
temp_code_to_token[req.body.authcode] = undefined
const sql = "INSERT INTO `ipost`.`auth_tokens`(`auth_token`,`auth_token_u_id`,`auth_token_isfrom_application_id`) VALUES(?,?,?);"
const values = [SHA256(data.token,appid,10000),data.userid,data.appid] //token,id,appid
con.query(sql,values,(err,result) => {
if(err) {
res.json({"status":500,"message":"error redeeming code"})
console.err(err)
} else {
res.json({"status":200,"message":"successfully redeemed code","token":data.token})
}
})
})
})
}

View File

@ -11,6 +11,7 @@ import { setup as userroutessetup } from "./api/userRoutes.js";
import { setup as servefilessetup} from "./serve_static_files.js" import { setup as servefilessetup} from "./serve_static_files.js"
import { setup as userfilessetup} from "./userfiles.js" import { setup as userfilessetup} from "./userfiles.js"
import { setup as userauthsetup} from "./user_auth.js" import { setup as userauthsetup} from "./user_auth.js"
import { setup as applicationsetup} from "./authorize.js"
export const setup = function (router, con, server) { export const setup = function (router, con, server) {
const setuproute = handler => handler(router,con,server) const setuproute = handler => handler(router,con,server)
@ -34,4 +35,6 @@ export const setup = function (router, con, server) {
setuproute(userfilessetup) //needs getPID and getDMPID setuproute(userfilessetup) //needs getPID and getDMPID
setuproute(userauthsetup) //login & register setuproute(userauthsetup) //login & register
setuproute(applicationsetup)
} }

View File

@ -80,7 +80,8 @@ export const setup = function (router, con, server) {
newrelic: load_var("./extra_modules/newrelic_monitor.html"), newrelic: load_var("./extra_modules/newrelic_monitor.html"),
getPID: server.global_page_variables.getPID, getPID: server.global_page_variables.getPID,
getDMPID: server.global_page_variables.getDMPID, getDMPID: server.global_page_variables.getDMPID,
unauthorized_description: "Chat now by creating an account on IPost" unauthorized_description: "Chat now by creating an account on IPost",
hcaptcha_sitekey: server.hcaptcha.sitekey
} }

View File

@ -13,6 +13,7 @@ import {unsign} from "./extra_modules/unsign.js";
import { readFileSync, appendFile } from "fs"; import { readFileSync, appendFile } from "fs";
import { format } from "util"; import { format } from "util";
import { setup as SETUP_ROUTES} from "./routes/setup_all_routes.js" import { setup as SETUP_ROUTES} from "./routes/setup_all_routes.js"
import { verify as verifyHCaptcha_int } from "hcaptcha"
import { ensureExists } from "./extra_modules/ensureExists.js" import { ensureExists } from "./extra_modules/ensureExists.js"
@ -58,6 +59,13 @@ function log_info(level, ...info) {
} }
console.log = log_info; console.log = log_info;
const hcaptcha_secret = config.hcaptcha_secret
// wrapper for the HCaptcha verify function
function verifyHCaptcha(token) {
return verifyHCaptcha_int(hcaptcha_secret,token,undefined,config.hcaptcha_sitekey)
}
const WebSocket = ws.WebSocketServer; const WebSocket = ws.WebSocketServer;
const router = Router(); const router = Router();
@ -259,7 +267,7 @@ app.use(bodyParser.default.json({ limit: "100mb" }));
app.use(bodyParser.default.urlencoded({ limit: "100mb", extended: true })); app.use(bodyParser.default.urlencoded({ limit: "100mb", extended: true }));
app.use(cookieParser(cookiesecret)); app.use(cookieParser(cookiesecret));
app.use(compression()) app.use(compression())
var blocked_headers = [ let blocked_headers = [
'HTTP_VIA', 'HTTP_VIA',
'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED_FOR',
'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED_FOR',
@ -334,7 +342,11 @@ var commonfunctions = {
ensureExists, ensureExists,
"dirname": __dirname, "dirname": __dirname,
config, config,
DID_I_FINALLY_ADD_HTTPS DID_I_FINALLY_ADD_HTTPS,
hcaptcha: {
"verify":verifyHCaptcha,
"sitekey":config.hcaptcha_sitekey
}
}; };
SETUP_ROUTES(router,con,commonfunctions) SETUP_ROUTES(router,con,commonfunctions)

View File

@ -155,12 +155,14 @@
"level": 5 "level": 5
}, },
"ssl": { "ssl": {
"privateKey": "/etc/letsencrypt/live/ipost.rocks/privkey.pem", "privateKey": "./etc/letsencrypt/live/ipost.rocks/privkey.pem",
"certificate" : "/etc/letsencrypt/live/ipost.rocks/cert.pem" "certificate" : "./etc/letsencrypt/live/ipost.rocks/fullchain.pem"
}, },
"ports": { "ports": {
"http": 9999, "http": 9999,
"https": 9998 "https": 9998
}, },
"disallow_proxies_by_headers": true "disallow_proxies_by_headers": true,
"hcaptcha_secret": "0x0000000000000000000000000000000000000000",
"hcaptcha_sitekey": "10000000-ffff-ffff-ffff-000000000001"
} }

31
views/authorize.html Normal file
View File

@ -0,0 +1,31 @@
<!DOCTYPE html>
<html>
<head>
<% if(user.username === undefined) { %>
<meta name="description" content="<%-unauthorized_description%>">
<% } else { %>
<meta name="description" content="IPost Extension Authorization Page">
<% } %>
<title>Authorize App</title>
<style>
<%- globalcss %>
<%- loadfile("./css/authorize.css") %>
</style>
<% if(user.username === undefined) { %>
<script> document.location.href = '/no_login?r='+encodeURIComponent(document.location.pathname) </script>
<% } else { %>
<script src="https://js.hcaptcha.com/1/api.js" async defer></script>
<% } %>
</head>
<body>
<div class="center">
<h1>Authorize App</h1>
<p>Please authorize the app to access your information:</p>
<form action="/authorize" method="post">
<input type="number" value=<%- query.id %> class="hidden" name="application_id" id="application_id">
<div class="h-captcha" data-sitekey="<%- hcaptcha_sitekey %>"></div>
<input type="submit" value="Authorize">
</form>
</div>
</body>
</html>