User Authentication Flow Works
This commit is contained in:
@@ -1,5 +0,0 @@
|
||||
node_modules
|
||||
# Keep environment variables out of version control
|
||||
.env
|
||||
|
||||
/generated/prisma
|
||||
@@ -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,
|
||||
}); */
|
||||
});
|
||||
@@ -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"),
|
||||
},
|
||||
});
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import {
|
||||
prisma,
|
||||
refreshTokenDuration,
|
||||
sessionDuration,
|
||||
accessTokenDuration,
|
||||
accessTokenPrivateKey,
|
||||
refreshTokenPrivateKey,
|
||||
} from "../constants";
|
||||
|
||||
+23
-28
@@ -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 },
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user