User Authentication Flow Works

This commit is contained in:
2026-01-25 15:03:17 -06:00
parent 1bf0acdf39
commit e76caa68f1
22 changed files with 275 additions and 248 deletions
-5
View File
@@ -1,5 +0,0 @@
node_modules
# Keep environment variables out of version control
.env
/generated/prisma
+36
View File
@@ -0,0 +1,36 @@
import { Hono } from "hono/tiny";
import { createRoute } from "../../modules/api-utils/createRoute";
import * as msal from "@azure/msal-node";
import { msalClient } from "../../constants";
import { users } from "../../managers/users";
/* /v1/authRedirect */
export default createRoute("get", ["/"], async (c) => {
c.status(200);
console.log("Query", c.req.query());
const tokenRequest: msal.AuthorizationCodeRequest = {
code: c.req.query().code as string,
scopes: ["user.read"],
redirectUri: "http://localhost:3000/v1/auth/redirect",
};
const authResult = await msalClient.acquireTokenByCode(tokenRequest);
await users.authenticate(authResult);
// This closes the window because duh
return c.html(`
<script>
window.close();
</script>
`);
/* return c.json({
status: 200,
message: "Auth Redirect Endpoint",
data: authResult,
successful: true,
}); */
});
-12
View File
@@ -1,12 +0,0 @@
// This file was generated by Prisma, and assumes you run Prisma commands using `bun --bun run prisma [command]`.
import { defineConfig, env } from "prisma/config";
export default defineConfig({
schema: "prisma/schema.prisma",
migrations: {
path: "prisma/migrations",
},
datasource: {
url: env("DATABASE_URL"),
},
});
+1
View File
@@ -47,6 +47,7 @@ app.notFound((c) => {
});
v1.route("/teapot", teapot);
v1.route("/auth/redirect", await import("./auth/redirect").then(m => m.default));
app.route("/v1", v1);
+25 -13
View File
@@ -1,34 +1,46 @@
import { readFileSync } from "fs";
import { PrismaPg } from '@prisma/adapter-pg'
import { PrismaClient } from '../generated/prisma/client'
const connectionString = `${process.env.DATABASE_URL}`
const adapter = new PrismaPg({ connectionString })
import { PrismaPg } from "@prisma/adapter-pg";
import { Prisma, PrismaClient } from "../generated/prisma/client";
import * as msal from "@azure/msal-node";
const connectionString = `${process.env.DATABASE_URL}`;
const adapter = new PrismaPg({ connectionString });
interface EnvKey {
PORT: number;
};
PORT: number;
}
// ENV CONSTANTS
export const PORT = process.env.PORT;
export const prisma = new PrismaClient({ adapter })
export const prisma = new PrismaClient({ adapter });
export const sessionDuration = 30 * 24 * 60 * 60000;
export const accessTokenDuration = "10min";
export const refreshTokenDuration = "30d";
export const accessTokenPrivateKey = readFileSync(
`${import.meta.dir}/../.accessToken.key`
`${import.meta.dir}/../.accessToken.key`,
).toString();
export const refreshTokenPrivateKey = readFileSync(
`${import.meta.dir}/../.refreshToken.key`
`${import.meta.dir}/../.refreshToken.key`,
).toString();
export const permissionsPrivateKey = readFileSync(
`${import.meta.dir}/../.permissions.key`
`${import.meta.dir}/../.permissions.key`,
);
export const apiKeyTokenPrivateKey = readFileSync(
`${import.meta.dir}/../.apiKeyToken.key`
);
`${import.meta.dir}/../.apiKeyToken.key`,
);
// Microsoft Auth Constants
const msalConfig: msal.Configuration = {
auth: {
clientId: process.env.MICROSOFT_CLIENT_ID!,
authority: `https://login.microsoftonline.com/${process.env.MICROSOFT_TENANT_ID!}`,
clientSecret: process.env.MICROSOFT_CLIENT_SECRET!,
},
};
// MSAL Client Instance
export const msalClient = new msal.ConfidentialClientApplication(msalConfig);
+5 -16
View File
@@ -10,8 +10,6 @@ import { events } from "../modules/globalEvents";
import { RoleController } from "./RoleController";
import { roles } from "../managers/roles";
export default class UserController {
public id: string;
public name: string | null;
@@ -116,8 +114,8 @@ export default class UserController {
await Promise.all(
this._roles.map(async (v) =>
collection.set(v.id, await roles.fetch(v.id))
)
collection.set(v.id, await roles.fetch(v.id)),
),
);
return collection;
@@ -140,15 +138,6 @@ export default class UserController {
sessions: {
select: { id: true },
},
apiKeys: {
select: { id: true },
},
projects: {
select: { id: true },
},
services: {
select: { id: true },
},
},
});
@@ -158,7 +147,7 @@ export default class UserController {
(v) =>
`resource.${v}.[${(resources![v] as { id: string }[])
.map((o) => o.id)
.join(",")}].user.${this.id}.implicit`
.join(",")}].user.${this.id}.implicit`,
);
console.log(implicitPermissions);
@@ -190,8 +179,8 @@ export default class UserController {
roles: opts?.safeReturn
? undefined
: this._roles.size > 0
? this._roles.map((v) => v.moniker)
: undefined,
? this._roles.map((v) => v.moniker)
: undefined,
login: opts?.safeReturn ? undefined : this.login,
email: opts?.safeReturn ? undefined : this.email,
image: this.image,
+9 -9
View File
@@ -38,7 +38,7 @@ export const roles = {
if (checkMoniker)
throw new RoleError(
"Moniker is already taken.",
"Another role with this moniker already exists in the databse."
"Another role with this moniker already exists in the databse.",
);
const id = cuid();
@@ -76,7 +76,7 @@ export const roles = {
* @param identifier - Options for fetching a role.
* @returns {RoleController} - Role Controller
*/
async fetch(identifier:string, opt?: { requestingUser?: UserController }) {
async fetch(identifier: string, opt?: { requestingUser?: UserController }) {
const roleData = await prisma.role.findFirst({
where: { OR: [{ id: identifier }, { moniker: identifier }] },
include: {
@@ -98,11 +98,11 @@ export const roles = {
if (
opt?.requestingUser &&
!(await opt.requestingUser.hasPermission(
this._buildPermissionNode(roleData.id, "read")
this._buildPermissionNode(roleData.id, "read"),
))
)
throw new InsufficientPermission(
"You do not have permission to access this role."
"You do not have permission to access this role.",
);
const controller = new RoleController(roleData);
@@ -123,20 +123,20 @@ export const roles = {
include: { users: { include: { roles: true } } },
});
roles. map((v:any) => collection.set(v.id, new RoleController(v)));
roles.map((v: any) => collection.set(v.id, new RoleController(v)));
if (opt?.requestingUser) {
const permittedRoles = await Promise.all(
collection.map(async (v) =>
(await opt.requestingUser?.hasPermission(
this._buildPermissionNode(v.id, "read")
this._buildPermissionNode(v.id, "read"),
))
? v.id
: null
)
: null,
),
);
collection = collection.filter((v) =>
permittedRoles.filter((x) => x !== null).includes(v.id)
permittedRoles.filter((x) => x !== null).includes(v.id),
);
}
-2
View File
@@ -1,8 +1,6 @@
import {
prisma,
refreshTokenDuration,
sessionDuration,
accessTokenDuration,
accessTokenPrivateKey,
refreshTokenPrivateKey,
} from "../constants";
+23 -28
View File
@@ -1,7 +1,12 @@
import { ms } from "zod/locales";
import { User } from "../../generated/prisma/client";
import { prisma } from "../constants";
import { SessionTokensObject } from "../controllers/SessionController";
import UserController from "../controllers/UserController";
import { fetchMicrosoftUser } from "../modules/fetchMicrosoftUser";
import { events } from "../modules/globalEvents";
import { sessions } from "./sessions";
import * as msal from "@azure/msal-node";
export const users = {
/**
@@ -13,25 +18,22 @@ export const users = {
* @summary It creates a user if one doesn't exist and will supply a session id
*
* @async
* @param ghCode - The code supplied in the callback url of a GitHub oAuth transaction
* @param authRequest - The code supplied in the callback url of the Microsoft oAuth transaction
*/
/* async authenticate(ghCode: string): Promise<SessionTokensObject> {
const token = await ghApp.oauth.createToken({ code: ghCode }).catch((e) => {
throw new AuthenticationError("Invalid OAuth code...");
});
const userOK = await ghApp.oauth.getUserOctokit({
token: token.authentication.token,
});
const ghUser = await userOK.request("GET /user");
async authenticate(
authRequest: msal.AuthenticationResult,
): Promise<SessionTokensObject> {
let id = authRequest.uniqueId as string;
let user =
(await this.fetchUser({ userId: ghUser.data.id })) ??
(await this.createUser(token.authentication.token));
(await this.fetchUser({ userId: id })) ??
(await this.createUser(authRequest.accessToken));
const tokens = await sessions.create({ user });
events.emit("user:authenticated", { user, tokens });
return tokens;
}, */
},
/**
* Check to see if the user exists
@@ -59,8 +61,8 @@ export const users = {
id: string;
email: string;
login: string;
userId: number;
}>
userId: string;
}>,
) {
if (Object.keys(identifier).length == 0) return null;
const userData = await prisma.user.findFirst({
@@ -79,28 +81,21 @@ export const users = {
/**
* Create a new user
*
* This method will poll GitHub and get all the information on the user to then create the
* This method will poll Microsoft and get all the information on the user to then create the
* record in our database. On top of that it also pushes it into the user cache.
*
* @param token - The Github token provided by the auth method
* @param token - The Microsoft token provided by the auth method
* @returns {Promise<UserController>} The new user controller for the user
*/
async createUser(token: string): Promise<UserController> {
const ghUser = await (
await ghApp.oauth.getUserOctokit({ token })
).request("GET /user");
const emails = await (
await ghApp.oauth.getUserOctokit({ token })
).request("GET /user/emails");
const msData = await fetchMicrosoftUser(token);
const newUser = await prisma.user.create({
data: {
userId: ghUser.data.id,
email: emails.data[0].email,
image: ghUser.data.avatar_url,
name: ghUser.data.name,
login: ghUser.data.login,
userId: msData.id,
email: msData.mail,
name: `${msData.givenName} ${msData.surname}`,
login: msData.userPrincipalName,
token,
},
include: { roles: true },
+33
View File
@@ -0,0 +1,33 @@
import { MicrosoftGraphUser } from "../types/MSAuthTypes";
/**
* Fetch Microsoft User
*
* This function fetches user data from Microsoft Graph API using the provided access token.
* It makes a GET request to the `/me` endpoint and returns the user data in JSON format.
*
* @param accessToken - This is the access token provided by Microsoft.
* @returns - Raw API Data from Microsoft.
*/
export const fetchMicrosoftUser = async (
accessToken: string,
): Promise<MicrosoftGraphUser> => {
const graphEndpoint = "https://graph.microsoft.com/v1.0/me";
const response = await fetch(graphEndpoint, {
method: "GET",
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
if (!response.ok) {
throw new Error(
`Graph request failed: ${response.status} ${response.statusText}`,
);
}
const data = (await response.json()) as MicrosoftGraphUser;
return data;
};
+14
View File
@@ -0,0 +1,14 @@
export interface MicrosoftGraphUser {
"@odata.context": string;
businessPhones: string[];
displayName: string;
givenName: string;
jobTitle: string | null;
mail: string;
mobilePhone: string | null;
officeLocation: string | null;
preferredLanguage: string | null;
surname: string;
userPrincipalName: string;
id: string;
}