From 43edcb58267f3e15a72e29fdf156d4a39352d7f9 Mon Sep 17 00:00:00 2001 From: Michael Fatemi Date: Sat, 10 Apr 2021 15:07:06 -0400 Subject: [PATCH] add authentication --- package-lock.json | 130 +++++++++++++++++++++++++++++++++++++++++- package.json | 4 ++ src/auth.ts | 35 ++++++++++++ src/auth_ion.ts | 97 +++++++++++++++++++++++++++++++ src/data.ts | 14 +++++ src/getRedirectUrl.ts | 3 + 6 files changed, 282 insertions(+), 1 deletion(-) create mode 100644 src/auth.ts create mode 100644 src/auth_ion.ts create mode 100644 src/getRedirectUrl.ts diff --git a/package-lock.json b/package-lock.json index c41d5f2..d69a11d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,6 +4,68 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@hapi/boom": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@hapi/boom/-/boom-9.1.2.tgz", + "integrity": "sha512-uJEJtiNHzKw80JpngDGBCGAmWjBtzxDCz17A9NO2zCi8LLBlb5Frpq4pXwyN+2JQMod4pKz5BALwyneCgDg89Q==", + "requires": { + "@hapi/hoek": "9.x.x" + } + }, + "@hapi/bourne": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@hapi/bourne/-/bourne-2.0.0.tgz", + "integrity": "sha512-WEezM1FWztfbzqIUbsDzFRVMxSoLy3HugVcux6KDDtTqzPsLE8NDRHfXvev66aH1i2oOKKar3/XDjbvh/OUBdg==" + }, + "@hapi/hoek": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.1.1.tgz", + "integrity": "sha512-CAEbWH7OIur6jEOzaai83jq3FmKmv4PmX1JYfs9IrYcGEVI/lyL1EXJGCj7eFVJ0bg5QR8LMxBlEtA+xKiLpFw==" + }, + "@hapi/topo": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.0.0.tgz", + "integrity": "sha512-tFJlT47db0kMqVm3H4nQYgn6Pwg10GTZHb1pwmSiv1K4ks6drQOtfEF5ZnPjkvC+y4/bUPHK+bc87QvLcL+WMw==", + "requires": { + "@hapi/hoek": "^9.0.0" + } + }, + "@hapi/wreck": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/@hapi/wreck/-/wreck-17.1.0.tgz", + "integrity": "sha512-nx6sFyfqOpJ+EFrHX+XWwJAxs3ju4iHdbB/bwR8yTNZOiYmuhA8eCe7lYPtYmb4j7vyK/SlbaQsmTtUrMvPEBw==", + "requires": { + "@hapi/boom": "9.x.x", + "@hapi/bourne": "2.x.x", + "@hapi/hoek": "9.x.x" + } + }, + "@sideway/address": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.1.tgz", + "integrity": "sha512-+I5aaQr3m0OAmMr7RQ3fR9zx55sejEYR2BFJaxL+zT3VM2611X0SHvPWIbAUBZVTn/YzYKbV8gJ2oT/QELknfQ==", + "requires": { + "@hapi/hoek": "^9.0.0" + } + }, + "@sideway/formula": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.0.tgz", + "integrity": "sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==" + }, + "@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==" + }, + "@types/axios": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@types/axios/-/axios-0.14.0.tgz", + "integrity": "sha1-7CMA++fX3d1+udOr+HmZlkyvzkY=", + "requires": { + "axios": "*" + } + }, "@types/body-parser": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz", @@ -76,6 +138,16 @@ "@types/node": "*" } }, + "@types/simple-oauth2": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@types/simple-oauth2/-/simple-oauth2-4.1.0.tgz", + "integrity": "sha512-FkAXBdBDFU6i2Rj+N/Sr5bZO8zrMnFnSvyzWtEUXgj7jdDZ0bcgxJ134ctwZbmNvSUzS4Qvt+NvszSbw8Zq+qg==" + }, + "@types/uuid": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.0.tgz", + "integrity": "sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ==" + }, "accepts": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", @@ -90,6 +162,14 @@ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" }, + "axios": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", + "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", + "requires": { + "follow-redirects": "^1.10.0" + } + }, "body-parser": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", @@ -233,6 +313,11 @@ "unpipe": "~1.0.0" } }, + "follow-redirects": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.3.tgz", + "integrity": "sha512-DUgl6+HDzB0iEptNQEXLx/KhTmDb8tZUHSeLqpnjpknR70H0nC2t9N73BK6fN4hOvJ84pKlIQVQ4k5FFlBedKA==" + }, "forwarded": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", @@ -273,6 +358,18 @@ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" }, + "joi": { + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.4.0.tgz", + "integrity": "sha512-F4WiW2xaV6wc1jxete70Rw4V/VuMd6IN+a5ilZsxG4uYtUXWu2kq9W5P2dz30e7Gmw8RCbY/u/uk+dMPma9tAg==", + "requires": { + "@hapi/hoek": "^9.0.0", + "@hapi/topo": "^5.0.0", + "@sideway/address": "^4.1.0", + "@sideway/formula": "^3.0.0", + "@sideway/pinpoint": "^2.0.0" + } + }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -422,6 +519,32 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" }, + "simple-oauth2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/simple-oauth2/-/simple-oauth2-4.2.0.tgz", + "integrity": "sha512-AV62tGdq9JfLd/uveKpeNtQl+VVm89a35QKlwGuvisYIjCoz2ZmTGRGuSIGiYr+QUhSKJ5kYN1jq2BBa/ac/GQ==", + "requires": { + "@hapi/hoek": "^9.0.4", + "@hapi/wreck": "^17.0.0", + "debug": "^4.1.1", + "joi": "^17.3.0" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, "statuses": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", @@ -456,10 +579,15 @@ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" } } -} \ No newline at end of file +} diff --git a/package.json b/package.json index 9bff123..550ba6f 100644 --- a/package.json +++ b/package.json @@ -19,13 +19,17 @@ }, "homepage": "https://github.com/myfatemi04/Carpool-Backend#readme", "dependencies": { + "@types/axios": "^0.14.0", "@types/cors": "^2.8.10", "@types/express": "^4.17.11", "@types/node": "^14.14.37", + "@types/simple-oauth2": "^4.1.0", "@types/uuid": "^8.3.0", + "axios": "^0.21.1", "cors": "^2.8.5", "dotenv": "^8.2.0", "express": "^4.17.1", + "simple-oauth2": "^4.2.0", "typescript": "^4.2.4", "uuid": "^8.3.2" } diff --git a/src/auth.ts b/src/auth.ts new file mode 100644 index 0000000..831a662 --- /dev/null +++ b/src/auth.ts @@ -0,0 +1,35 @@ +import * as simpleoauth2 from 'simple-oauth2'; +import { v4 } from 'uuid'; +import { getAccountIDFromIonCode } from './auth_ion'; + +const sessions: { + // Maps to user ID + [sessionID: string]: string; +} = {}; + +export function getUserIDFromSessionToken(sessionToken: string): string | null { + if (sessionToken in sessions) { + return sessions[sessionToken]; + } else { + return null; + } +} + +export async function createSessionFromCodeAndProvider( + code: string, + provider: 'ion' +): Promise { + if (provider === 'ion') { + const accountID = await getAccountIDFromIonCode(code); + return createSession(accountID); + } +} + +// Returns the newly-created session ID +export function createSession(userID: string): string { + const id = v4(); + + sessions[id] = userID; + + return id; +} diff --git a/src/auth_ion.ts b/src/auth_ion.ts new file mode 100644 index 0000000..54b3764 --- /dev/null +++ b/src/auth_ion.ts @@ -0,0 +1,97 @@ +import axios from 'axios'; +import { AuthorizationCode } from 'simple-oauth2'; +import { getUserByEmail, registerUserFromIonProfile } from './data'; +import getRedirectUrl from './getRedirectUrl'; + +export const authorizationCode = new AuthorizationCode({ + client: { + id: process.env.ION_CLIENT_ID, + secret: process.env.ION_CLIENT_SECRET, + }, + auth: { + tokenHost: 'https://ion.tjhsst.edu/oauth/', + authorizePath: 'https://ion.tjhsst.edu/oauth/authorize', + tokenPath: 'https://ion.tjhsst.edu/oauth/token', + }, +}); + +export const redirectUrl = getRedirectUrl('ion'); + +export const authorizationUrl = authorizationCode.authorizeURL({ + scope: 'read', + redirect_uri: redirectUrl, +}); + +export interface IonProfile { + id: string; + ion_username: string; // eg "2022mfatemi" + sex: string; // Capitalized + title: null; + display_name: string; + short_name: string; + // Given name + first_name: string; + // Middle name + middle_name?: string; + // Family name + last_name: string; + nickname?: string; + // Internal TJ email (@tjhsst.edu) + tj_email: string; + // Other external personal emails + emails: string[]; + grade: { + number: number; + name: string; // freshman, sophomore, junior, senior + }; + graduation_year?: number; + user_type: 'student' | 'teacher' | ''; + phones: string[]; // eg "Mobile Phone: XXXXXXXXXX", including the text "Mobile Phone" + websites: string[]; + counselor?: { + id: string; + // Url to API endpoint for profile info + url: string; + user_type: 'counselor'; + username: string; + full_name: string; + first_name: string; + last_name: string; + }; + address: string | null; + /** DO NOT USE */ + picture: string; // URL of the profile photo of the user. Seems to give empty photos right now. + is_eighth_admin: boolean; + is_announcements_admin: boolean; + is_teacher: boolean; + is_student: boolean; + // 8th period absences count + absences: number; +} + +export async function getIonProfile(code: string): Promise { + let accessToken = await authorizationCode.getToken({ + code, + redirect_uri: redirectUrl, + scope: 'read', + }); + + const profileUrl = + 'https://ion.tjhsst.edu/api/profile?format=json&access_token=' + + accessToken.token.access_token; + + const response = await axios.get(profileUrl); + + return response.data; +} + +export async function getAccountIDFromIonCode(code: string) { + const profile = await getIonProfile(code); + const user = await getUserByEmail(profile.tj_email); + + if (user == null) { + return await registerUserFromIonProfile(profile); + } else { + return user.id; + } +} diff --git a/src/data.ts b/src/data.ts index 3238d08..d841353 100644 --- a/src/data.ts +++ b/src/data.ts @@ -1,3 +1,5 @@ +import { IonProfile } from './auth_ion'; + /** * Records users by id */ @@ -64,3 +66,15 @@ export function getPoolByID(poolID: string): Carpool.Pool | undefined { export function getGroupByID(groupID: string): Carpool.Group | undefined { return groups[groupID]; } + +export async function getUserByEmail( + email: string +): Promise { + return undefined; +} + +export async function registerUserFromIonProfile( + profile: IonProfile +): Promise { + return 'new_user_0'; +} diff --git a/src/getRedirectUrl.ts b/src/getRedirectUrl.ts new file mode 100644 index 0000000..ddbf9fe --- /dev/null +++ b/src/getRedirectUrl.ts @@ -0,0 +1,3 @@ +export default function getRedirectUrl(provider: string) { + return `http://localhost:3000/auth/${provider}/callback`; +}