Restructure access token model to store user ID

Signed-off-by: Aelita4 <kontakt@mikorosa.pl>
This commit is contained in:
Aelita4 2024-01-26 08:52:40 +01:00
parent d3ac213590
commit eafea0c1f5
Signed by: Aelita4
GPG Key ID: E44490C2025906C1
7 changed files with 61 additions and 18 deletions

View File

@ -1,6 +1,7 @@
import { AccessTokens } from './mongodb'; import { AccessTokens } from './mongodb';
import type AccessToken from '../../types/AccessToken'; import type AccessToken from '../../types/AccessToken';
import { createHash } from 'crypto'; import { createHash } from 'crypto';
import { ObjectId } from 'mongodb';
export const createAccessToken = async (accessToken: AccessToken) => { export const createAccessToken = async (accessToken: AccessToken) => {
const newAccessToken = await (await AccessTokens()).insertOne(accessToken); const newAccessToken = await (await AccessTokens()).insertOne(accessToken);
@ -12,7 +13,7 @@ export const getAccessToken = async (accessToken: string) => {
const type = accessToken.split(".")[0]; const type = accessToken.split(".")[0];
const createdAt = new Date(Number(Buffer.from(accessToken.split(".")[1], 'base64url').toString())); const createdAt = new Date(Number(Buffer.from(accessToken.split(".")[1], 'base64url').toString()));
const username = Buffer.from(accessToken.split(".")[2], 'base64url').toString(); const user = new ObjectId(Buffer.from(accessToken.split(".")[2], 'base64url').toString());
const random = accessToken.split(".")[3]; const random = accessToken.split(".")[3];
const entropy = createHash("sha256").update(random).digest("hex"); const entropy = createHash("sha256").update(random).digest("hex");
@ -20,7 +21,7 @@ export const getAccessToken = async (accessToken: string) => {
return accessTokens.findOne({ $and: [ return accessTokens.findOne({ $and: [
{ type }, { type },
{ createdAt }, { createdAt },
{ username }, { user },
{ entropy } { entropy }
] }) as Promise<AccessToken | null>; ] }) as Promise<AccessToken | null>;
} }
@ -28,7 +29,7 @@ export const getAccessToken = async (accessToken: string) => {
export const getAllAccessTokens = async () => { export const getAllAccessTokens = async () => {
const master: AccessToken = { const master: AccessToken = {
type: "X", type: "X",
username: "0", user: null,
entropy: "b70972be3921e04a8d0c442f64c2c19a4ac01e886e8e5649916f9b88870fa6dd", entropy: "b70972be3921e04a8d0c442f64c2c19a4ac01e886e8e5649916f9b88870fa6dd",
createdAt: new Date(1698851542427), createdAt: new Date(1698851542427),
expiresAt: null, expiresAt: null,

View File

@ -2,6 +2,8 @@ import { Users } from '../db/mongodb';
import type User from '../../types/User'; import type User from '../../types/User';
import type Resources from '../../types/Resources'; import type Resources from '../../types/Resources';
import type Building from '../../types/Building'; import type Building from '../../types/Building';
import type AccessToken from '../../types/AccessToken';
import { ObjectId } from 'mongodb';
export const getAllUsers = async () => { export const getAllUsers = async () => {
const users = await Users(); const users = await Users();
@ -13,6 +15,13 @@ export const createUser = async (user: User) => {
return newUser; return newUser;
} }
export const getUserById = async (id: ObjectId) => {
const users = await Users();
return users.findOne({
_id: id
}) as Promise<User | null>;
}
export const getUserByNickOrEmail = async (searchString: string) => { export const getUserByNickOrEmail = async (searchString: string) => {
const users = await Users(); const users = await Users();
return users.findOne({ return users.findOne({
@ -23,6 +32,13 @@ export const getUserByNickOrEmail = async (searchString: string) => {
}) as Promise<User | null>; }) as Promise<User | null>;
} }
export const getUserByAccessToken = async(accessToken: string | AccessToken): Promise<User | null> => {
if(typeof accessToken === "string") {
const userId = new ObjectId(Buffer.from(accessToken.split(".")[2], 'base64url').toString());
return getUserById(userId);
} else return getUserById(accessToken.user as ObjectId)
}
export const getUserResources = async (username: string): Promise<Resources> => { export const getUserResources = async (username: string): Promise<Resources> => {
const users = await Users(); const users = await Users();
const user = await users.findOne({ username }); const user = await users.findOne({ username });

View File

@ -1,5 +1,7 @@
import type { ObjectId } from "mongodb";
import type AccessToken from "../../types/AccessToken"; import type AccessToken from "../../types/AccessToken";
import { getAccessToken } from "../db/accessTokens"; import { getAccessToken } from "../db/accessTokens";
import { getUserById } from "../db/users";
export default async function validateAccessToken(request: Request): Promise<Response | AccessToken> { export default async function validateAccessToken(request: Request): Promise<Response | AccessToken> {
let accessToken = request.url.split("?")[1]?.split("&").filter((x) => x.split("=")[0] === "token")[0].split("=")[1]; let accessToken = request.url.split("?")[1]?.split("&").filter((x) => x.split("=")[0] === "token")[0].split("=")[1];
@ -31,11 +33,20 @@ export default async function validateAccessToken(request: Request): Promise<Res
}), { status: 401 } }), { status: 401 }
); );
const user = await getUserById(response.user as ObjectId);
if(!user) return new Response (
JSON.stringify({
code: 404,
message: "Not found",
data: "Access token does not match any user"
}), { status: 404 }
);
if(response.createdAt.getTime() > Date.now()) return new Response( if(response.createdAt.getTime() > Date.now()) return new Response(
JSON.stringify({ JSON.stringify({
code: 403, code: 403,
message: "Forbidden", message: "Forbidden",
data: "Access token is invalid for user " + response.username + ", are you travelling in time?" data: "Access token is invalid for user " + user.username + ", are you travelling in time?"
}), { status: 403 } }), { status: 403 }
); );
@ -43,7 +54,7 @@ export default async function validateAccessToken(request: Request): Promise<Res
JSON.stringify({ JSON.stringify({
code: 403, code: 403,
message: "Forbidden", message: "Forbidden",
data: "Access token is invalid for user " + response.username + ", token expired" data: "Access token is invalid for user " + user.username + ", token expired"
}), { status: 403 } }), { status: 403 }
); );

View File

@ -1,6 +1,7 @@
--- ---
import Layout from "../../layouts/Layout.astro"; import Layout from "../../layouts/Layout.astro";
import { getAllAccessTokens } from "../../lib/db/accessTokens"; import { getAllAccessTokens } from "../../lib/db/accessTokens";
import { getUserByAccessToken } from "../../lib/db/users";
const tokens = await getAllAccessTokens(); const tokens = await getAllAccessTokens();
@ -26,13 +27,13 @@ const type = {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{tokens.map(token => <tr> {tokens.map(async token => <tr>
<td>{token._id}</td> <td>{token._id}</td>
<td>{type[token.type] ?? `Other`}</td> <td>{type[token.type] ?? `Other`}</td>
<td>{token.username}</td> <td>{(await getUserByAccessToken(token))?.username}</td>
<td>{token.entropy}</td> <td>{token.entropy}</td>
<td>{token.createdAt.toISOString().slice(0, 19).replace(/-/g, "/").replace("T", " ")}</td> <td>{token.createdAt.toISOString().slice(0, 19).replace(/-/g, "/").replace("T", " ")}</td>
<td>{token.expiresAt?.toISOString().slice(0, 19).replace(/-/g, "/").replace("T", " ") ?? "null"}</td> <td>{token.expiresAt?.toISOString().slice(0, 19).replace(/-/g, "/").replace("T", " ") ?? "Never"}</td>
<td>{token.createdFrom}</td> <td>{token.createdFrom}</td>
<td>{token.expiresAt !== null && Date.now() > token.expiresAt.getTime() ? <span style="color: red;">Expired {Math.floor((Date.now() - token.expiresAt.getTime()) / 1000 / 60)} minutes ago</span> : Date.now() < token.createdAt.getTime() ? <span style="color: cyan;">TimeTravelException (check in {Math.floor((token.createdAt.getTime() - Date.now()) / 1000 / 60)} minutes)</span> : <span style="color: lime;">Valid for{token.expiresAt === null ? "ever" : ` ${Math.floor((token.expiresAt.getTime() - Date.now()) / 1000 / 60)} minutes`}</span>}</td> <td>{token.expiresAt !== null && Date.now() > token.expiresAt.getTime() ? <span style="color: red;">Expired {Math.floor((Date.now() - token.expiresAt.getTime()) / 1000 / 60)} minutes ago</span> : Date.now() < token.createdAt.getTime() ? <span style="color: cyan;">TimeTravelException (check in {Math.floor((token.createdAt.getTime() - Date.now()) / 1000 / 60)} minutes)</span> : <span style="color: lime;">Valid for{token.expiresAt === null ? "ever" : ` ${Math.floor((token.expiresAt.getTime() - Date.now()) / 1000 / 60)} minutes`}</span>}</td>
</tr>)} </tr>)}

View File

@ -2,6 +2,8 @@ import { randomBytes, createHash } from "crypto";
import type { APIRoute } from "astro"; import type { APIRoute } from "astro";
import type AccessToken from "../../../types/AccessToken"; import type AccessToken from "../../../types/AccessToken";
import { createAccessToken } from "../../../lib/db/accessTokens"; import { createAccessToken } from "../../../lib/db/accessTokens";
import { getUserByNickOrEmail } from "../../../lib/db/users";
import type { ObjectId } from "mongodb";
export const POST: APIRoute = async({ request }) => { export const POST: APIRoute = async({ request }) => {
const data = await request.json().catch(() => {return new Response( const data = await request.json().catch(() => {return new Response(
@ -9,7 +11,7 @@ export const POST: APIRoute = async({ request }) => {
code: 400, code: 400,
message: "Bad Request", message: "Bad Request",
error: "Invalid JSON" error: "Invalid JSON"
}) }), { status: 400 }
)}); )});
if(!data.username) return new Response( if(!data.username) return new Response(
@ -17,7 +19,7 @@ export const POST: APIRoute = async({ request }) => {
code: 400, code: 400,
message: "Bad Request", message: "Bad Request",
error: "Username is required" error: "Username is required"
}) }), { status: 400 }
) )
const header = request.headers.get("Authorization"); const header = request.headers.get("Authorization");
@ -28,7 +30,7 @@ export const POST: APIRoute = async({ request }) => {
code: 401, code: 401,
message: "Unauthorized", message: "Unauthorized",
error: "Access Token is required" error: "Access Token is required"
}) }), { status: 401 }
) )
if(token !== import.meta.env.MASTER_ACCESSTOKEN) return new Response( if(token !== import.meta.env.MASTER_ACCESSTOKEN) return new Response(
@ -36,21 +38,30 @@ export const POST: APIRoute = async({ request }) => {
code: 401, code: 401,
message: "Unauthorized", message: "Unauthorized",
error: "Invalid Access Token" error: "Invalid Access Token"
}) }), { status: 401 }
) )
else { else {
const userFromDb = await getUserByNickOrEmail(data.username);
if(!userFromDb) return new Response(
JSON.stringify({
code: 404,
message: "Not found",
error: `User ${data.username} not found`
}), { status: 404 }
)
const now = new Date(); const now = new Date();
const timestamp = Buffer.from(String(Date.now())).toString('base64url'); const timestamp = Buffer.from(String(Date.now())).toString('base64url');
const username = Buffer.from(data.username).toString('base64url'); const user = Buffer.from(userFromDb._id?.toString() ?? "").toString('base64url');
const random = randomBytes(16).toString("base64url"); const random = randomBytes(16).toString("base64url");
const randomHashed = createHash("sha256").update(random).digest("hex"); const randomHashed = createHash("sha256").update(random).digest("hex");
const expiresIn = (data.duration ?? 86400) * 1000; const expiresIn = (data.duration ?? 86400) * 1000;
const tokenString = `A.${timestamp}.${username}.${random}`; const tokenString = `A.${timestamp}.${user}.${random}`;
const accessToken: AccessToken = { const accessToken: AccessToken = {
type: "A", type: "A",
username: data.username, user: userFromDb._id as ObjectId,
entropy: randomHashed.toString(), entropy: randomHashed.toString(),
createdAt: now, createdAt: now,
expiresAt: new Date(now.getTime() + expiresIn), expiresAt: new Date(now.getTime() + expiresIn),

View File

@ -1,16 +1,19 @@
import type { APIRoute } from "astro"; import type { APIRoute } from "astro";
import validateAccessToken from "../../../lib/utils/validateAccessToken"; import validateAccessToken from "../../../lib/utils/validateAccessToken";
import { getAccessToken } from "../../../lib/db/accessTokens"; import { getUserByAccessToken } from "../../../lib/db/users";
import type User from "../../../types/User";
export const GET: APIRoute = async({ request }) => { export const GET: APIRoute = async({ request }) => {
const response = await validateAccessToken(request); const response = await validateAccessToken(request);
if(response instanceof Response) return response; if(response instanceof Response) return response;
const user = (await getUserByAccessToken(response)) as User;
return new Response( return new Response(
JSON.stringify({ JSON.stringify({
code: 200, code: 200,
message: "OK", message: "OK",
data: "Access token valid for user " + response.username data: "Access token valid for user " + user.username
}) })
); );
} }

View File

@ -3,7 +3,7 @@ import type { ObjectId } from "mongodb";
export default interface AccessToken { export default interface AccessToken {
_id?: ObjectId; _id?: ObjectId;
type: "A" | "X"; type: "A" | "X";
username: string; user: ObjectId | null;
entropy: string; entropy: string;
createdAt: Date; createdAt: Date;
expiresAt: Date | null; expiresAt: Date | null;