feat(auth): Added 2FA/OTP manage endpoints
- CodeWise: Changed everything to use undefined when not present - CodeWise: checks for nonpresent value using `isNullish` - enableOtp: enable Otp, return topt url. Does nothing when already enabled - disableOtp: disable 2FA Totp for the user - statusOtp: get the 2FA status for the user. return the Totp Url if enabled - loginDemo: split into two files - loginDemo: supports for 2FA - loginDemo: better response box
This commit is contained in:
parent
29a5d38530
commit
a7c753f38b
17 changed files with 341 additions and 175 deletions
|
|
@ -6,6 +6,7 @@ set -x
|
|||
|
||||
mkdir -p /volumes/static/auth/
|
||||
cp -r /extra/login_demo.html /volumes/static/auth/index.html
|
||||
cp -r /extra/login_demo.js /volumes/static/auth/login_demo.js
|
||||
|
||||
# run the CMD [ ... ] from the dockerfile
|
||||
exec "$@"
|
||||
|
|
|
|||
|
|
@ -1,84 +1,38 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<title> Demo Page For Login :) </title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1> Welcome <span id="t-username"> </span> </h1>
|
||||
<head>
|
||||
<title>Demo Page For Login :)</title>
|
||||
</head>
|
||||
|
||||
<input id="i-username" type="text" placeholder="Username"> </input>
|
||||
<input id="i-password" type="text" placeholder="Password"> </input>
|
||||
<br />
|
||||
<br />
|
||||
<body>
|
||||
<h1>
|
||||
Welcome <span id="t-username"></span>
|
||||
</h1>
|
||||
<input id="i-username" type="text" placeholder="Username">
|
||||
</input>
|
||||
<input id="i-password" type="text" placeholder="Password">
|
||||
</input>
|
||||
<br />
|
||||
<input id="i-otp" type="text" placeholder="OTP">
|
||||
</input>
|
||||
<button id="b-otpSend">OTP - Send</button>
|
||||
<br />
|
||||
<br />
|
||||
<button id="b-login">Login</button>
|
||||
<br />
|
||||
<button id="b-logout">Logout</button>
|
||||
<br />
|
||||
<button id="b-signin">Signin</button>
|
||||
<br />
|
||||
<button id="b-whoami">Whoami</button>
|
||||
<div>
|
||||
<button id="b-otpStatus">OTP - Status</button>
|
||||
<button id="b-otpEnable">OTP - Enable</button>
|
||||
<button id="b-otpDisable">OTP - Disable</button>
|
||||
</div>
|
||||
<pre id="d-response"></pre>
|
||||
<script src="./login_demo.js"> </script>
|
||||
</body>
|
||||
|
||||
<button id="b-login"> Login </button>
|
||||
<br />
|
||||
<button id="b-logout"> Logout </button>
|
||||
<br />
|
||||
<button id="b-signin"> Signin </button>
|
||||
<br />
|
||||
<button id="b-whoami"> Whoami </button>
|
||||
|
||||
<div id="d-response">
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const headers = {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
const tUsername = document.querySelector("#t-username")
|
||||
|
||||
const iUsername = document.querySelector("#i-username");
|
||||
const iPassword = document.querySelector("#i-password");
|
||||
|
||||
const bLogin = document.querySelector("#b-login");
|
||||
const bLogout = document.querySelector("#b-logout");
|
||||
const bSignin = document.querySelector("#b-signin");
|
||||
const bWhoami = document.querySelector("#b-whoami");
|
||||
|
||||
const dResponse = document.querySelector("#d-response");
|
||||
|
||||
bWhoami.addEventListener("click", async () => {
|
||||
let username = "";
|
||||
try {
|
||||
let res = await fetch("/api/auth/whoami");
|
||||
const json = await res.json();
|
||||
if (json?.kind === "success")
|
||||
username = json?.payload?.name;
|
||||
else
|
||||
username = `<not logged in: ${json.msg}>`
|
||||
} catch {
|
||||
username = `<not logged in: threw>`
|
||||
}
|
||||
tUsername.innerText = username;
|
||||
}, 1000);
|
||||
|
||||
bLogin.addEventListener("click", async () => {
|
||||
const name = iUsername.value;
|
||||
const password = iPassword.value;
|
||||
|
||||
let res = await fetch("/api/auth/login", {method: "POST", body: JSON.stringify({name, password}), headers});
|
||||
let j = await res.json();
|
||||
if (j?.payload?.token)
|
||||
document.cookie = `token=${j?.payload?.token}`;
|
||||
dResponse.innerText = JSON.stringify(j, space=4);
|
||||
})
|
||||
bLogout.addEventListener("click", async () => {
|
||||
let res = await fetch("/api/auth/logout", { method: "POST" });
|
||||
dResponse.innerText = `done - status:${res.status}`;
|
||||
})
|
||||
bSignin.addEventListener("click", async () => {
|
||||
const name = iUsername.value;
|
||||
const password = iPassword.value;
|
||||
|
||||
let res = await fetch("/api/auth/signin", {method: "POST", body: JSON.stringify({name, password}), headers});
|
||||
let j = await res.json();
|
||||
if (j?.payload?.token)
|
||||
document.cookie = `token=${j?.payload?.token};`;
|
||||
dResponse.innerText = JSON.stringify(j, space=4);
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
107
src/auth/extra/login_demo.js
Normal file
107
src/auth/extra/login_demo.js
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
const headers = {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
|
||||
const tUsername = document.querySelector("#t-username")
|
||||
|
||||
const iUsername = document.querySelector("#i-username");
|
||||
const iPassword = document.querySelector("#i-password");
|
||||
const iOtp = document.querySelector("#i-otp");
|
||||
|
||||
const bOtpSend = document.querySelector("#b-otpSend");
|
||||
const bLogin = document.querySelector("#b-login");
|
||||
const bLogout = document.querySelector("#b-logout");
|
||||
const bSignin = document.querySelector("#b-signin");
|
||||
const bWhoami = document.querySelector("#b-whoami");
|
||||
|
||||
const bOtpStatus = document.querySelector("#b-otpStatus");
|
||||
const bOtpEnable = document.querySelector("#b-otpEnable");
|
||||
const bOtpDisable = document.querySelector("#b-otpDisable");
|
||||
|
||||
const dResponse = document.querySelector("#d-response");
|
||||
|
||||
function setResponse(obj) {
|
||||
let obj_str = JSON.stringify(obj, null, 4);
|
||||
dResponse.innerText = obj_str;
|
||||
}
|
||||
let otpToken = null;
|
||||
|
||||
bOtpSend.addEventListener("click", async () => {
|
||||
let res = await fetch("/api/auth/otp", { method: "POST", body: JSON.stringify({ code: iOtp.value, token: otpToken }), headers });
|
||||
const json = await res.json();
|
||||
|
||||
setResponse(json);
|
||||
if (json.kind === "success") {
|
||||
if (json?.payload?.token)
|
||||
document.cookie = `token=${json?.payload?.token}`;
|
||||
}
|
||||
});
|
||||
|
||||
bOtpStatus.addEventListener("click", async () => {
|
||||
let res = await fetch("/api/auth/statusOtp");
|
||||
const json = await res.json();
|
||||
|
||||
setResponse(json);
|
||||
});
|
||||
|
||||
bOtpEnable.addEventListener("click", async () => {
|
||||
let res = await fetch("/api/auth/enableOtp", { method: "PUT" });
|
||||
const json = await res.json();
|
||||
|
||||
setResponse(json);
|
||||
});
|
||||
|
||||
bOtpDisable.addEventListener("click", async () => {
|
||||
let res = await fetch("/api/auth/disableOtp", { method: "PUT" });
|
||||
const json = await res.json();
|
||||
|
||||
setResponse(json);
|
||||
});
|
||||
|
||||
bWhoami.addEventListener("click", async () => {
|
||||
let username = "";
|
||||
try {
|
||||
let res = await fetch("/api/auth/whoami");
|
||||
const json = await res.json();
|
||||
setResponse(json);
|
||||
if (json?.kind === "success")
|
||||
username = json?.payload?.name;
|
||||
else
|
||||
username = `<not logged in:${json.msg}>`
|
||||
} catch {
|
||||
username = `<not logged in: threw>`
|
||||
}
|
||||
tUsername.innerText = username;
|
||||
});
|
||||
|
||||
bLogin.addEventListener("click", async () => {
|
||||
const name = iUsername.value;
|
||||
const password = iPassword.value;
|
||||
|
||||
let res = await fetch("/api/auth/login", { method: "POST", body: JSON.stringify({ name, password }), headers });
|
||||
let json = await res.json();
|
||||
if (json?.kind === "otpRequired") {
|
||||
otpToken = json?.payload?.token;
|
||||
} else if (json?.kind === "success") {
|
||||
if (json?.payload?.token)
|
||||
document.cookie = `token=${json?.payload?.token}`;
|
||||
}
|
||||
setResponse(json);
|
||||
})
|
||||
|
||||
bLogout.addEventListener("click", async () => {
|
||||
let res = await fetch("/api/auth/logout", { method: "POST" });
|
||||
setResponse(await res.json());
|
||||
})
|
||||
|
||||
bSignin.addEventListener("click", async () => {
|
||||
const name = iUsername.value;
|
||||
const password = iPassword.value;
|
||||
|
||||
let res = await fetch("/api/auth/signin", { method: "POST", body: JSON.stringify({ name, password }), headers });
|
||||
let json = await res.json();
|
||||
if (json?.payload?.token)
|
||||
document.cookie = `token=${json?.payload?.token};`;
|
||||
setResponse(json);
|
||||
})
|
||||
27
src/auth/src/routes/disableOtp.ts
Normal file
27
src/auth/src/routes/disableOtp.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { FastifyPluginAsync } from "fastify";
|
||||
|
||||
import { Static, Type } from "@sinclair/typebox";
|
||||
import { makeResponse, typeResponse, isNullish } from "@shared/utils"
|
||||
|
||||
|
||||
export const WhoAmIRes = Type.Union([
|
||||
typeResponse("success", "disableOtp.success"),
|
||||
typeResponse("failure", "disableOtp.failure.generic")
|
||||
]);
|
||||
|
||||
export type WhoAmIRes = Static<typeof WhoAmIRes>;
|
||||
|
||||
const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
|
||||
fastify.put(
|
||||
"/api/auth/disableOtp",
|
||||
{ schema: { response: { "2xx": WhoAmIRes } }, config: { requireAuth: true } },
|
||||
async function(req, _res) {
|
||||
if (isNullish(req.authUser))
|
||||
return makeResponse("failure", "disableOtp.failure.generic");
|
||||
this.db.deleteUserOtpSecret(req.authUser.id);
|
||||
return makeResponse("success", "disableOtp.success");
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export default route;
|
||||
31
src/auth/src/routes/enableOtp.ts
Normal file
31
src/auth/src/routes/enableOtp.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { FastifyPluginAsync } from "fastify";
|
||||
|
||||
import { Static, Type } from "@sinclair/typebox";
|
||||
import { isNullish, makeResponse, typeResponse } from "@shared/utils"
|
||||
import { Otp } from "@shared/auth";
|
||||
|
||||
|
||||
export const WhoAmIRes = Type.Union([
|
||||
typeResponse("success", "enableOtp.success", { url: Type.String({ description: "The otp url to feed into a 2fa app" }) }),
|
||||
typeResponse("failure", ["enableOtp.failure.noUser", "enableOtp.failure.noSecret"])
|
||||
]);
|
||||
|
||||
export type WhoAmIRes = Static<typeof WhoAmIRes>;
|
||||
|
||||
const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
|
||||
fastify.put(
|
||||
"/api/auth/enableOtp",
|
||||
{ schema: { response: { "2xx": WhoAmIRes } }, config: { requireAuth: true } },
|
||||
async function(req, _res) {
|
||||
if (isNullish(req.authUser))
|
||||
return makeResponse("failure", "enableOtp.failure.noUser");
|
||||
let otpSecret = this.db.ensureUserOtpSecret(req.authUser!.id);
|
||||
if (isNullish(otpSecret))
|
||||
return makeResponse("failure", "enableOtp.failure.noSecret");
|
||||
let otp = new Otp({ secret: otpSecret });
|
||||
return makeResponse("success", "enableOtp.success", { url: otp.totpURL });
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export default route;
|
||||
|
|
@ -1,9 +1,8 @@
|
|||
import { FastifyPluginAsync } from "fastify";
|
||||
|
||||
import { Static, Type } from "@sinclair/typebox";
|
||||
import { user as userDb } from "@shared/database";
|
||||
import type { } from "@shared/auth";
|
||||
import { typeResponse, makeResponse } from "@shared/utils"
|
||||
import { typeResponse, makeResponse, isNullish } from "@shared/utils"
|
||||
import { verifyUserPassword } from "@shared/database/mixin/user";
|
||||
|
||||
export const LoginReq = Type.Object({
|
||||
name: Type.String(),
|
||||
|
|
@ -21,27 +20,26 @@ export const LoginRes = Type.Union([
|
|||
|
||||
export type LoginRes = Static<typeof LoginRes>;
|
||||
|
||||
const route: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
|
||||
const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
|
||||
fastify.post<{ Body: LoginReq; Response: LoginRes }>(
|
||||
"/api/auth/login",
|
||||
{ schema: { body: LoginReq, response: { "2xx": LoginRes } }, },
|
||||
async function(req, res) {
|
||||
async function(req, _res) {
|
||||
try {
|
||||
let { name, password } = req.body;
|
||||
let user = this.db.getUserFromName(name);
|
||||
|
||||
// does the user exist
|
||||
// does it have a password setup ?
|
||||
if (user === null || user.password === null)
|
||||
if (isNullish(user?.password))
|
||||
return makeResponse("failed", "login.failed.invalid");
|
||||
|
||||
// does the password he provided match the one we have
|
||||
if (!(await userDb.verifyUserPassword(user, password)))
|
||||
if (!(await verifyUserPassword(user, password)))
|
||||
return makeResponse("failed", "login.failed.invalid");
|
||||
|
||||
// does the user has 2FA up ?
|
||||
if (user.otp !== undefined) {
|
||||
console.log(user);
|
||||
if (!isNullish(user.otp)) {
|
||||
// yes -> we ask them to fill it,
|
||||
// send them somehting to verify that they indeed passed throught the user+password phase
|
||||
return makeResponse("otpRequired", "login.otpRequired", { token: this.signJwt("otp", user.name) });
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { FastifyPluginAsync } from "fastify";
|
||||
|
||||
const route: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
|
||||
const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
|
||||
fastify.post(
|
||||
"/api/auth/logout",
|
||||
async function(req, res) {
|
||||
return res.clearCookie("token").send("bye :(")
|
||||
async function(_req, res) {
|
||||
return res.clearCookie("token").send("{}")
|
||||
},
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { FastifyPluginAsync } from "fastify";
|
|||
|
||||
import { Static, Type } from "@sinclair/typebox";
|
||||
import { JwtType, Otp } from "@shared/auth";
|
||||
import { typeResponse, makeResponse } from "@shared/utils";
|
||||
import { typeResponse, makeResponse, isNullish } from "@shared/utils";
|
||||
|
||||
const OtpReq = Type.Object({
|
||||
token: Type.String({ description: "The token given at the login phase" }),
|
||||
|
|
@ -12,7 +12,7 @@ const OtpReq = Type.Object({
|
|||
type OtpReq = Static<typeof OtpReq>;
|
||||
|
||||
const OtpRes = Type.Union([
|
||||
typeResponse("failed", ["otp.failed.generic", "otp.failed.invalid", "otp.failed.timeout"]),
|
||||
typeResponse("failed", ["otp.failed.generic", "otp.failed.invalid", "otp.failed.timeout", "otp.failed.noSecret"]),
|
||||
typeResponse("success", "otp.success", { token: Type.String({ description: "the JWT Token" }) }),
|
||||
]);
|
||||
|
||||
|
|
@ -20,11 +20,11 @@ type OtpRes = Static<typeof OtpRes>;
|
|||
|
||||
const OTP_TOKEN_TIMEOUT_SEC = 120;
|
||||
|
||||
const route: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
|
||||
const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
|
||||
fastify.post<{ Body: OtpReq }>(
|
||||
"/api/auth/otp",
|
||||
{ schema: { body: OtpReq, response: { "2xx": OtpRes } } },
|
||||
async function(req, res) {
|
||||
async function(req, _res) {
|
||||
try {
|
||||
const { token, code } = req.body;
|
||||
// lets try to decode+verify the jwt
|
||||
|
|
@ -41,10 +41,10 @@ const route: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
|
|||
|
||||
// get the Otp sercret from the db
|
||||
let otpSecret = this.db.getUserFromName(dJwt.who)?.otp;
|
||||
if (otpSecret === null)
|
||||
if (isNullish(otpSecret))
|
||||
// oops, either no user, or user without otpSecret
|
||||
// fuck off
|
||||
return makeResponse("failed", "otp.failed.invalid");
|
||||
return makeResponse("failed", "otp.failed.noSecret");
|
||||
|
||||
// good lets now verify the token you gave us is the correct one...
|
||||
let otpHandle = new Otp({ secret: otpSecret });
|
||||
|
|
@ -65,6 +65,7 @@ const route: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
|
|||
} catch {
|
||||
return makeResponse("failed", "otp.failed.generic");
|
||||
}
|
||||
return makeResponse("failed", "otp.failed.generic");
|
||||
},
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { FastifyPluginAsync } from "fastify";
|
||||
|
||||
import { Static, Type } from "@sinclair/typebox";
|
||||
import { typeResponse, makeResponse } from "@shared/utils";
|
||||
import { typeResponse, makeResponse, isNullish } from "@shared/utils";
|
||||
|
||||
const USERNAME_CHECK: RegExp = /^[a-zA-Z\_0-9]+$/;
|
||||
|
||||
|
|
@ -49,10 +49,10 @@ const route: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
|
|||
return makeResponse("failed", "signin.failed.password.toolong");
|
||||
// password is good too !
|
||||
|
||||
if (this.db.getUserFromName(name) !== null)
|
||||
if (this.db.getUserFromName(name) !== undefined)
|
||||
return makeResponse("failed", "signin.failed.username.existing");
|
||||
let u = await this.db.createUser(name, password);
|
||||
if (u === null)
|
||||
if (isNullish(u))
|
||||
return makeResponse("failed", "signin.failed.generic");
|
||||
|
||||
// every check has been passed, they are now logged in, using this token to say who they are...
|
||||
|
|
|
|||
32
src/auth/src/routes/statusOtp.ts
Normal file
32
src/auth/src/routes/statusOtp.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { FastifyPluginAsync } from "fastify";
|
||||
|
||||
import { Static, Type } from "@sinclair/typebox";
|
||||
import { isNullish, makeResponse, typeResponse } from "@shared/utils"
|
||||
import { Otp } from "@shared/auth";
|
||||
|
||||
|
||||
export const StatusOtpRes = Type.Union([
|
||||
typeResponse("success", "statusOtp.success.enabled", { url: Type.String({ description: "The otp url to feed into a 2fa app" }) }),
|
||||
typeResponse("success", "statusOtp.success.disabled"),
|
||||
typeResponse("failure", "statusOtp.failure.generic")
|
||||
]);
|
||||
|
||||
export type StatusOtpRes = Static<typeof StatusOtpRes>;
|
||||
|
||||
const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
|
||||
fastify.get(
|
||||
"/api/auth/statusOtp",
|
||||
{ schema: { response: { "2xx": StatusOtpRes } }, config: { requireAuth: true } },
|
||||
async function(req, _res) {
|
||||
if (isNullish(req.authUser))
|
||||
return makeResponse("failure", "statusOtp.failure.generic");
|
||||
let otpSecret = this.db.getUserOtpSecret(req.authUser.id);
|
||||
if (isNullish(otpSecret))
|
||||
return makeResponse("success", "statusOtp.success.disabled");
|
||||
let otp = new Otp({ secret: otpSecret })
|
||||
return makeResponse("success", "statusOtp.success.enabled", { url: otp.totpURL });
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export default route;
|
||||
|
|
@ -1,19 +1,24 @@
|
|||
import { FastifyPluginAsync } from "fastify";
|
||||
|
||||
import { Static, Type } from "@sinclair/typebox";
|
||||
import { makeResponse, typeResponse } from "@shared/utils"
|
||||
import { isNullish, makeResponse, typeResponse } from "@shared/utils"
|
||||
|
||||
|
||||
export const WhoAmIRes = typeResponse("success", "whoami.success", { name: Type.String() });
|
||||
export const WhoAmIRes = Type.Union([
|
||||
typeResponse("success", "whoami.success", { name: Type.String() }),
|
||||
typeResponse("failure", "whoami.failure.generic")
|
||||
]);
|
||||
|
||||
export type WhoAmIRes = Static<typeof WhoAmIRes>;
|
||||
|
||||
const route: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
|
||||
const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
|
||||
fastify.get(
|
||||
"/api/auth/whoami",
|
||||
{ schema: { response: { "2xx": WhoAmIRes } }, config: { requireAuth: true } },
|
||||
async function(req, res) {
|
||||
return makeResponse("success", "whoami.success", { name: req.authUser?.name })
|
||||
async function(req, _res) {
|
||||
if (isNullish(req.authUser))
|
||||
return makeResponse("success", "whoami.failure.generic")
|
||||
return makeResponse("success", "whoami.success", { name: req.authUser.name })
|
||||
},
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue