Skip to content

Commit

Permalink
Documentation + lint fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
AydanPirani committed Aug 15, 2023
1 parent 2da38bb commit 97bddbb
Show file tree
Hide file tree
Showing 2 changed files with 66 additions and 37 deletions.
29 changes: 23 additions & 6 deletions src/services/auth/auth-lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@ type VerifyCallback = (err: Error | null, user?: Profile | false, info?: object)
type VerifyFunction = (accessToken: string, refreshToken: string, profile: Profile, done: VerifyCallback) => void;


// Function that returns a RequestHandler based on the strategies and options passed in
export const authenticateFunction: AuthenticateFunction = (strategies: string | string[], options: AuthenticateOptions) => {
return passport.authenticate(strategies, options, undefined) as RequestHandler;
};


// Do-Nothing function to be used in OAuth strategies
export const verifyFunction: VerifyFunction = (_1: string, _2: string, user: Profile, callback: VerifyCallback) => {
return callback(null, user);
};
Expand All @@ -30,13 +32,15 @@ export async function getJwtPayload(provider: string, data: ProfileData): Promis
const userId: string = provider + data.id;
const email: string = data.email;

// Create payload object
const payload: JwtPayload = {
id: userId,
email: email,
provider: provider,
roles: [],
};

// Get roles, and assign those to payload.roles if they exist
await getRoles(userId).then((userRoles: Role[]) => {
if (userRoles.length) {
payload.roles = userRoles;
Expand Down Expand Up @@ -65,16 +69,18 @@ export function generateJwtToken(payload?: JwtPayload): string {
throw new Error("No JWT token passed in!");
}

// Ensure that the secret actually exists
const secret: string | undefined = process.env.JWT_SECRET;

if (!secret) {
throw new Error("No secret provided for signing!");
}

// Appends an expiry field to the JWT token
const options: SignOptions = {
expiresIn: "7d",
};

// Generate a token, and return it
const token: string = jsonwebtoken.sign(payload, secret, options);
return token;
}
Expand All @@ -85,30 +91,35 @@ export function decodeJwtToken(token?: string): JwtPayload {
throw new Error("no token provided!");
}

// Ensure that we have a secret to parse token
const secret: string | undefined = process.env.JWT_SECRET;
if (!secret) {
throw new Error("no secret to parse JWT token!");
}

// Verify already ensures that the token isn't expired. If it is, it returns an error
return jsonwebtoken.verify(token, secret) as JwtPayload;
}


export async function initializeRoles(id: string, provider: Provider, email: string): Promise<Role[]> {
const roles: Role[] = [];

// Check if this is a staff email
if (provider == Provider.GOOGLE && email.endsWith("@hackillinois.org")) {
roles.push(Role.STAFF);
// If email in the system admin list, add the admin role
if (Constants.SYSTEM_ADMIN_LIST.includes(email.replace("@hackillinois.org", ""))) {
roles.push(Role.ADMIN);
}
}

// Add the basic USER role in the provider
if (provider == Provider.GITHUB) {
roles.push(Role.USER);
}


// Create a new rolesEntry for the database, and insert it into the collection
const newUser: RolesSchema = {_id: new ObjectId(), id: id, provider: provider, roles: roles};
const collection: Collection = await DatabaseHelper.getCollection("auth", "roles");
await collection.insertOne(newUser);
Expand All @@ -122,7 +133,10 @@ export async function getRoles(id: string): Promise<Role[]> {
let roles: Role[] = [];

try {
// Get the roles for the user from the collection
const userRoles: RolesSchema | null = await collection.findOne({ id: id }) as RolesSchema | null;

// If roles are non-empty, modify the return list
if (userRoles != null) {
roles = userRoles.roles as Role[];
}
Expand All @@ -134,20 +148,23 @@ export async function getRoles(id: string): Promise<Role[]> {


export async function updateRoles(userId: string, role: Role, operation: RoleOperation): Promise<void> {
var filter: Partial<RolesSchema> | undefined;
let filter: Partial<RolesSchema> | undefined;

// Get filter, representing operation to perform on mongoDB
switch (operation) {
case RoleOperation.ADD: filter = {"$addToSet": {"roles": role}}; break;
case RoleOperation.REMOVE: filter = {"$pull": {"roles": role}}; break
default: return Promise.reject("no valid operation passed in");
case RoleOperation.ADD: filter = {"$addToSet": {"roles": role}}; break;
case RoleOperation.REMOVE: filter = {"$pull": {"roles": role}}; break;
default: return Promise.reject("no valid operation passed in");
}

// Appoly filter to roles collection, based on the operation
const collection: Collection = await DatabaseHelper.getCollection("auth", "roles");
await collection.updateOne({id: userId}, filter);
return Promise.resolve();
}


// Check if a user is an ADMIN or a STAFF
export function hasElevatedPerms(payload: JwtPayload): boolean {
const roles: Role[] = payload.roles;
return roles.includes(Role.ADMIN) || roles.includes(Role.STAFF);
Expand Down
74 changes: 43 additions & 31 deletions src/services/auth/auth-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,10 @@ authRouter.get("/test/", (_: Request, res: Response) => {
});


authRouter.get("/github/", (req: Request, res: Response, next: NextFunction) => {
const provider: string = "github";
SelectAuthProvider(provider)(req, res, next);
});
authRouter.get("/github/", SelectAuthProvider("github"));

authRouter.get("/google/", (req: Request, res: Response, next: NextFunction) => {
const provider: string = "google";
SelectAuthProvider(provider)(req, res, next);
});

authRouter.get("/google/", SelectAuthProvider("google"));


authRouter.get("/:PROVIDER/callback/", (req: Request, res: Response, next: NextFunction) => {
Expand All @@ -57,26 +52,29 @@ authRouter.get("/:PROVIDER/callback/", (req: Request, res: Response, next: NextF
res.status(Constants.UNAUTHORIZED_REQUEST).send();
}

// Data manipulation to store types of parsable inputs
const user: GithubProfile | GoogleProfile = req.user as GithubProfile | GoogleProfile;
const data: ProfileData = user._json as ProfileData;
data.id = data.id ?? user.id;
let payload: JwtPayload | undefined = undefined;

// Load in the payload with the actual values stored in the database
await getJwtPayload(user.provider, data).then( (parsedPayload: JwtPayload) => {
payload = parsedPayload;
}).catch( (error: Error) => {
res.status(Constants.BAD_REQUEST).send(error);
});

// Generate the token, and return it
const token: string = generateJwtToken(payload);
res.status(Constants.SUCCESS).send({ token: token });

});


authRouter.get("/roles/:USERID", async (req: Request, res: Response) => {
const targetUser: string | undefined = req.params.USERID;

// Check if we have a user to get roles for - if not, get roles for current user
if (!targetUser) {
res.redirect("/auth/roles/");
return;
Expand All @@ -85,15 +83,19 @@ authRouter.get("/roles/:USERID", async (req: Request, res: Response) => {
try {
const payload: JwtPayload = decodeJwtToken(req.headers.authorization);

// Cases: Target user already logged in, auth user is admin
if (payload.id == targetUser) {
res.status(Constants.SUCCESS).send({id: payload.id, roles: payload.roles});
} else if (hasElevatedPerms(payload)) {
var roles: Role[] = [];
await getRoles(targetUser).then((targetRoles: Role[]) => {roles = targetRoles}).catch((error: Error) => {throw error});
let roles: Role[] = [];
await getRoles(targetUser).then((targetRoles: Role[]) => {
roles = targetRoles;
}).catch((error: Error) => {
throw error;
});
console.log(roles);
res.status(Constants.SUCCESS).send({id: targetUser, roles: roles});
}
else {
} else {
res.status(Constants.FORBIDDEN).send("not authorized to perform this operation!");
}
} catch (error) {
Expand All @@ -103,10 +105,10 @@ authRouter.get("/roles/:USERID", async (req: Request, res: Response) => {


authRouter.put("/roles/:OPERATION/", async (req: Request, res: Response) => {
const operation: string = req.params.OPERATION ?? "";

const op: RoleOperation | undefined = (<any>RoleOperation)[operation];
// Parse to get operation type
const op: RoleOperation | undefined = RoleOperation[req.params.operation as keyof typeof RoleOperation];

// No operation - fail out
if (!op) {
res.status(Constants.BAD_REQUEST).send({error: "operation not specified!"});
return;
Expand All @@ -115,81 +117,91 @@ authRouter.put("/roles/:OPERATION/", async (req: Request, res: Response) => {
try {
const payload: JwtPayload = decodeJwtToken(req.headers.authorization);

// Not authenticated with modify roles perms
if (!hasElevatedPerms(payload)) {
res.status(Constants.FORBIDDEN).send({error: "not permitted to modify roles!"});
}

// Check if role to add/remove actually exists
const data: ModifyRoleRequest = req.body as ModifyRoleRequest;
const role: Role | undefined = (<any>Role)[data.role];
const role: Role | undefined = Role[data.role as keyof typeof Role];
if (!role) {
res.status(Constants.BAD_REQUEST).send({error: "invalid role passed in!"});
return;
}

await updateRoles(data.id, role, op).catch((error) => {
// Try to update roles, if possible
await updateRoles(data.id, role, op).catch((error: string) => {
console.log(error);
res.status(Constants.INTERNAL_ERROR).send({error: error});
});

// Get new roles for the current user, and return them
await getRoles(data.id).then((roles: Role[]) => {
res.status(Constants.SUCCESS).send({id: data.id, roles: roles});
}).catch((error) => {
}).catch((error: string) => {
console.log(error);
res.status(Constants.INTERNAL_ERROR).send({error: error});
});

} catch (error) {
res.status(Constants.FORBIDDEN).send(error);
}
})
});


authRouter.get("/list/roles/", (req: Request, res: Response) => {
try {
const payload: JwtPayload = decodeJwtToken(req.headers.authorization);

// Check if current user should be able to access all roles
if (!hasElevatedPerms(payload)) {
res.status(Constants.FORBIDDEN).send({error: "not authorized to perform this operation!"});
return;
}

const roles: string[] = Object.keys(Role).filter((item) => {
// Filter enum to get all possible string keys
const roles: string[] = Object.keys(Role).filter((item: string) => {
return isNaN(Number(item));
});

res.status(Constants.SUCCESS).send({roles: roles});
} catch (error) {
res.status(Constants.FORBIDDEN).send({error: error});
res.status(Constants.FORBIDDEN).send({error: error as string});
}
})
});


authRouter.get("/roles/", (req: Request, res: Response) => {
try {
const payload: JwtPayload = decodeJwtToken(req.headers.authorization);
console.log(payload);
res.status(Constants.SUCCESS).send({id: payload.id, roles: payload.roles});
} catch (error) {
res.status(Constants.FORBIDDEN).send({error: error});
res.status(Constants.FORBIDDEN).send({error: error as string});
}
});


authRouter.get("/token/refresh", async (req: Request, res: Response) => {
try {
// Get old data from token
const oldPayload: JwtPayload = decodeJwtToken(req.headers.authorization);
const data: ProfileData = {
id: oldPayload.id,
email: oldPayload.email
}
var newPayload: JwtPayload | undefined;
await getJwtPayload(oldPayload.provider, data).then((payload: JwtPayload) => {newPayload = payload});
email: oldPayload.email,
};

// Generate a new payload for the token
let newPayload: JwtPayload | undefined;
await getJwtPayload(oldPayload.provider, data).then((payload: JwtPayload) => {
newPayload = payload;
});

// Create and return a new token with the payload
const newToken: string = generateJwtToken(newPayload);
res.status(Constants.SUCCESS).send({token: newToken});

} catch (error) {
res.status(Constants.FORBIDDEN).send({error: error})
res.status(Constants.FORBIDDEN).send({error: error as string});
}
});

Expand Down

0 comments on commit 97bddbb

Please sign in to comment.