From a9361306f9c3e17f849c5146f25c94aefc37ba5f Mon Sep 17 00:00:00 2001 From: photon0205 Date: Wed, 25 Oct 2023 00:00:00 +0530 Subject: [PATCH 1/3] feat: add gitlab oauth --- src/backend/.gitignore | 1 + src/backend/auth/github.ts | 53 ++++++++++++++++++++++----- src/backend/db.ts | 12 +++--- src/backend/dependencies.ts | 3 +- src/backend/server.ts | 22 +++++++---- src/backend/utils/get-user.ts | 34 +++++++++++++++++ src/backend/utils/github-user.ts | 9 ----- src/frontend/src/components/Login.vue | 23 +++++++++--- src/frontend/src/main.ts | 2 +- src/frontend/src/router/index.ts | 2 +- src/frontend/src/utils/authorize.ts | 18 +++++++-- src/frontend/src/utils/github-url.ts | 10 ----- src/frontend/src/utils/oauth-urls.ts | 29 +++++++++++++++ 13 files changed, 163 insertions(+), 55 deletions(-) create mode 100644 src/backend/utils/get-user.ts delete mode 100644 src/backend/utils/github-user.ts delete mode 100644 src/frontend/src/utils/github-url.ts create mode 100644 src/frontend/src/utils/oauth-urls.ts diff --git a/src/backend/.gitignore b/src/backend/.gitignore index 4c49bd7..c91f384 100644 --- a/src/backend/.gitignore +++ b/src/backend/.gitignore @@ -1 +1,2 @@ .env +deno.lock \ No newline at end of file diff --git a/src/backend/auth/github.ts b/src/backend/auth/github.ts index 6d1fa64..465436f 100644 --- a/src/backend/auth/github.ts +++ b/src/backend/auth/github.ts @@ -4,36 +4,68 @@ import { checkUser } from "../db.ts"; const key = await crypto.subtle.generateKey( { name: "HMAC", hash: "SHA-512" }, true, - ["sign", "verify"], + ["sign", "verify"] ); async function githubAuth(ctx: Context, id: string, secret: string) { + await authenticateAndCreateJWT(ctx, id, secret, "github"); +} + +async function gitlabAuth(ctx: Context, id: string, secret: string) { + await authenticateAndCreateJWT(ctx, id, secret, "gitlab"); +} + +async function authenticateAndCreateJWT( + ctx: Context, + id: string, + secret: string, + provider: string +) { if (!ctx.request.hasBody) { ctx.throw(415); } const code = await ctx.request.body().value; console.log(code); - const rootUrl = new URL("https://github.com/login/oauth/access_token"); + const oauthUrl = + provider === "github" + ? "https://github.com/login/oauth/access_token" + : provider === "gitlab" + ? "https://gitlab.com/oauth/token" + : null; + + if (oauthUrl === null) { + ctx.response.body = "Unsupported provider"; + return; + } if (code !== null) { + const rootUrl = new URL(oauthUrl); rootUrl.search = new URLSearchParams({ client_id: id, client_secret: secret, code, }).toString(); + const resp = await fetch(rootUrl.toString(), { method: "POST", headers: { - "Accept": "application/json", + Accept: "application/json", }, }); + const body = await resp.json(); - const { status, githubId } = await checkUser(body.access_token); + const { status, userId } = await checkUser(body.access_token, provider); + ctx.response.headers.set("Access-Control-Allow-Origin", "*"); + if (status.matchedCount == 1) { - const id_jwt = await create({ alg: "HS512", typ: "JWT" }, { - githubId: githubId, - }, key); + const id_jwt = await create( + { alg: "HS512", typ: "JWT" }, + { + [`${provider}Id`]: userId, + }, + key + ); ctx.response.body = id_jwt; } else { ctx.response.body = "not authorized"; @@ -43,18 +75,19 @@ async function githubAuth(ctx: Context, id: string, secret: string) { } } -async function githubId(ctx: Context) { +async function handleJwtAuthentication(ctx: Context) { ctx.response.headers.set("Access-Control-Allow-Origin", "*"); if (!ctx.request.hasBody) { ctx.throw(415); } const jwt_token = await ctx.request.body().value; + const provider = jwt_token.startsWith("github") ? "github" : "gitlab"; try { const payload = await verify(jwt_token, key); - ctx.response.body = payload.githubId!; + ctx.response.body = payload[`${provider}Id`]!; } catch (error) { ctx.response.body = "not verified"; } } -export { githubAuth, githubId }; +export { githubAuth, gitlabAuth, handleJwtAuthentication }; diff --git a/src/backend/db.ts b/src/backend/db.ts index 9997aa3..2d44837 100644 --- a/src/backend/db.ts +++ b/src/backend/db.ts @@ -1,4 +1,4 @@ -import getGithubUser from "./utils/github-user.ts"; +import getProviderUser from "./utils/get-user.ts"; import { Context } from "./dependencies.ts"; const DATA_API_KEY = Deno.env.get("MONGO_API_KEY")!; @@ -18,7 +18,7 @@ const options = { body: "", }; -async function checkUser(accessToken: string) { +async function checkUser(accessToken: string, provider: string) { const auth_query = { collection: "user_auth", database: DATABASE, @@ -26,11 +26,11 @@ async function checkUser(accessToken: string) { filter: {}, update: {}, }; - const githubId = await getGithubUser(accessToken); - auth_query.filter = { "githubId": githubId }; + const userId = await getProviderUser(accessToken, provider); + auth_query.filter = { [`${provider}Id`]: userId }; auth_query.update = { $set: { - "githubId": githubId, + [`${provider}Id`]: userId, "authToken": accessToken, }, }; @@ -38,7 +38,7 @@ async function checkUser(accessToken: string) { options.body = JSON.stringify(auth_query); const status_resp = await fetch(update_url.toString(), options); const status = await status_resp.json(); - return { status, githubId }; + return { status, userId }; } async function getMaps(ctx: Context) { diff --git a/src/backend/dependencies.ts b/src/backend/dependencies.ts index 12c24d5..1a76d1b 100644 --- a/src/backend/dependencies.ts +++ b/src/backend/dependencies.ts @@ -5,5 +5,6 @@ import { } from "https://deno.land/x/oak@v12.5.0/mod.ts"; import { Session } from "https://deno.land/x/oak_sessions@v4.1.9/mod.ts"; import { create, verify } from "https://deno.land/x/djwt@v2.9.1/mod.ts"; +import { oakCors } from "https://deno.land/x/cors@v1.2.2/mod.ts"; -export { Application, Context, create, Router, Session, verify }; +export { Application, Context, create, Router, Session, verify, oakCors }; diff --git a/src/backend/server.ts b/src/backend/server.ts index f2137f6..110f61f 100644 --- a/src/backend/server.ts +++ b/src/backend/server.ts @@ -1,23 +1,31 @@ -import { Application, Router, Session } from "./dependencies.ts"; -import { githubAuth, githubId } from "./auth/github.ts"; +import { Application, Router, Session, oakCors } from "./dependencies.ts"; import { addMaps, deleteMaps, getMaps } from "./db.ts"; +import { githubAuth, gitlabAuth, handleJwtAuthentication } from "./auth/github.ts"; const router = new Router(); const app = new Application(); -const PORT = 7000; +const PORT = 8000; -const id: string = Deno.env.get("GITHUB_OAUTH_CLIENT_ID")!; -const secret: string = Deno.env.get("GITHUB_OAUTH_CLIENT_SECRET")!; +const githubClientId: string = Deno.env.get("GITHUB_OAUTH_CLIENT_ID")!; +const githubClientSecret: string = Deno.env.get("GITHUB_OAUTH_CLIENT_SECRET")!; +const gitlabClientId: string = Deno.env.get("GITLAB_OAUTH_CLIENT_ID")!; +const gitlabClientSecret: string = Deno.env.get("GITLAB_OAUTH_CLIENT_SECRET")!; app.use(Session.initMiddleware()); router - .post("/auth/github", (ctx) => githubAuth(ctx, id, secret)) - .post("/auth/jwt", (ctx) => githubId(ctx)) + .post("/auth/github", (ctx) => + githubAuth(ctx, githubClientId, githubClientSecret) + ) + .post("/auth/gitlab", (ctx) => + gitlabAuth(ctx, gitlabClientId, gitlabClientSecret) + ) + .post("/auth/jwt", (ctx) => handleJwtAuthentication(ctx)) .get("/map", (ctx) => getMaps(ctx)) .post("/map", (ctx) => addMaps(ctx)) .post("/mapdel", (ctx) => deleteMaps(ctx)); +app.use(oakCors()); app.use(router.routes()); app.use(router.allowedMethods()); app.listen({ port: PORT }); diff --git a/src/backend/utils/get-user.ts b/src/backend/utils/get-user.ts new file mode 100644 index 0000000..bdc3f02 --- /dev/null +++ b/src/backend/utils/get-user.ts @@ -0,0 +1,34 @@ +export default async function getProviderUser(accessToken: string, provider: string) { + let apiUrl = ""; + let authorizationHeader = ""; + + if (provider === "github") { + apiUrl = "https://api.github.com/user"; + authorizationHeader = `Bearer ${accessToken}`; + } else if (provider === "gitlab") { + apiUrl = "https://gitlab.com/api/v4/user"; + authorizationHeader = `Bearer ${accessToken}`; + } else { + throw new Error("Unsupported provider"); + } + + const user_resp = await fetch(apiUrl, { + headers: { + Authorization: authorizationHeader, + }, + }); + + if (user_resp.status !== 200) { + throw new Error(`Failed to fetch user data from ${provider}`); + } + + const user = await user_resp.json(); + + if (provider === "github") { + return user.login; + } else if (provider === "gitlab") { + return user.username; + } else { + throw new Error("Unsupported provider"); + } +} diff --git a/src/backend/utils/github-user.ts b/src/backend/utils/github-user.ts deleted file mode 100644 index e7831aa..0000000 --- a/src/backend/utils/github-user.ts +++ /dev/null @@ -1,9 +0,0 @@ -export default async function getGithubUser(accessToken: string) { - const user_resp = await fetch("https://api.github.com/user", { - headers: { - Authorization: `Bearer ${accessToken}`, - }, - }); - const user = await user_resp.json(); - return user.login; -} diff --git a/src/frontend/src/components/Login.vue b/src/frontend/src/components/Login.vue index a28fd32..71a24ac 100644 --- a/src/frontend/src/components/Login.vue +++ b/src/frontend/src/components/Login.vue @@ -1,21 +1,32 @@ + diff --git a/src/frontend/src/main.ts b/src/frontend/src/main.ts index e13cdea..bf15e10 100644 --- a/src/frontend/src/main.ts +++ b/src/frontend/src/main.ts @@ -1,7 +1,7 @@ import { createApp, Vue } from "vue"; import "./style.css"; import App from "./App.vue"; -import router from "./router/index.ts"; +import router from "./router/index"; const app = createApp(App); app.use(router); diff --git a/src/frontend/src/router/index.ts b/src/frontend/src/router/index.ts index d77e73f..087b012 100644 --- a/src/frontend/src/router/index.ts +++ b/src/frontend/src/router/index.ts @@ -7,7 +7,7 @@ import { import Home from "../components/Home.vue"; import Login from "../components/Login.vue"; import NotFound from "../components/404.vue"; -import { check_jwt } from "../utils/authorize.ts"; +import { check_jwt } from "../utils/authorize"; const routes = [ { diff --git a/src/frontend/src/utils/authorize.ts b/src/frontend/src/utils/authorize.ts index c38c1dc..f8fa5ea 100644 --- a/src/frontend/src/utils/authorize.ts +++ b/src/frontend/src/utils/authorize.ts @@ -1,9 +1,19 @@ -import router from "../router/index.ts"; +import router from "../router/index"; const backend = import.meta.env.VITE_APP_BACKEND; -async function authorize(code: string) { - const rootUrl = new URL(`${backend}/auth/github`); +async function authorize(code: string, provider: string) { + let backendUrl: string; + if (provider === "github") { + backendUrl = `${backend}/auth/github`; + } else if (provider === "gitlab") { + backendUrl = `${backend}/auth/gitlab`; + } else { + console.error("Unsupported authentication provider"); + return; + } + + const rootUrl = new URL(backendUrl); if (code !== undefined) { const resp = await fetch(rootUrl.toString(), { method: "POST", @@ -14,7 +24,7 @@ async function authorize(code: string) { }); console.log(resp); const status = await resp.text(); - if (status == "not authorized") { + if (status === "not authorized") { alert("Invalid Credentials"); router.push("/login"); } else { diff --git a/src/frontend/src/utils/github-url.ts b/src/frontend/src/utils/github-url.ts deleted file mode 100644 index 55415ad..0000000 --- a/src/frontend/src/utils/github-url.ts +++ /dev/null @@ -1,10 +0,0 @@ -export function githubUrl() { - const rootUrl = "https://github.com/login/oauth/authorize"; - const options = { - client_id: import.meta.env.VITE_APP_GITHUB_OAUTH_CLIENT_ID, - redirect_uri: import.meta.env.VITE_APP_GITHUB_OAUTH_REDIRECT_URL, - scope: "user:email", - }; - const qs: URLSearchParams = new URLSearchParams(options); - return `${rootUrl}?${qs.toString()}`; -} diff --git a/src/frontend/src/utils/oauth-urls.ts b/src/frontend/src/utils/oauth-urls.ts new file mode 100644 index 0000000..2522b01 --- /dev/null +++ b/src/frontend/src/utils/oauth-urls.ts @@ -0,0 +1,29 @@ +function oauthUrl(provider: string) { + let rootUrl: string, clientId: string, redirectUri: string, scope: string; + const responseType = "code"; + if (provider === "github") { + rootUrl = "https://github.com/login/oauth/authorize"; + clientId = import.meta.env.VITE_APP_GITHUB_OAUTH_CLIENT_ID; + redirectUri = import.meta.env.VITE_APP_GITHUB_OAUTH_REDIRECT_URL; + scope = "user:email"; + } else if (provider === "gitlab") { + rootUrl = "https://gitlab.com/oauth/authorize"; + clientId = import.meta.env.VITE_APP_GITLAB_OAUTH_CLIENT_ID; + redirectUri = import.meta.env.VITE_APP_GITLAB_OAUTH_REDIRECT_URL; + scope = "read_user"; + } else { + console.error("Unsupported provider"); + return ""; + } + + const options = { + client_id: clientId, + redirect_uri: redirectUri, + scope: scope, + response_type: responseType + }; + const qs = new URLSearchParams(options); + return `${rootUrl}?${qs.toString()}`; +} + +export { oauthUrl }; From c6259b1f95334a48285d120b3a9a248585d9a039 Mon Sep 17 00:00:00 2001 From: Abhijna Raghavendra Date: Thu, 29 Feb 2024 10:17:20 +0530 Subject: [PATCH 2/3] feat: implement login modal on frontend --- src/frontend/public/df-logo.png | Bin 0 -> 2181 bytes src/frontend/public/github-logo.png | Bin 0 -> 6393 bytes src/frontend/public/gitlab-logo.png | Bin 0 -> 4692 bytes src/frontend/src/components/Login.vue | 39 ++++++- src/frontend/src/components/loginmodal.vue | 123 +++++++++++++++++++++ 5 files changed, 156 insertions(+), 6 deletions(-) create mode 100644 src/frontend/public/df-logo.png create mode 100644 src/frontend/public/github-logo.png create mode 100644 src/frontend/public/gitlab-logo.png create mode 100644 src/frontend/src/components/loginmodal.vue diff --git a/src/frontend/public/df-logo.png b/src/frontend/public/df-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..b43603a909459a32c53fa07672739d0d32707587 GIT binary patch literal 2181 zcmW+&dpMNq79R}8CC0c8qZ!54_Q|ow*__%^AA=?#s*%Z)UE4J^YDAQ%iBraH$fRN1 zkMQUtjHFyT6;0!kacLTn)MSp3y~oC$66dx5c-MM{w9U0q zDAX3}eo7$l3b5KR8o=)Ou67wXt#kW><4`Ca!_B3FD&m`gph{ezw>zp~kT44XIuiDU zQK+U|owbwdC=|w=N`a3gsw_;3sdZrGSZbfwC&`FmBQHbt_WurkGv$SjLgSOd^C43ifXuItX}8wE_D z3=xq1K&df+GB8Xj(4~vcMF(bh=qc!!%)@iei6@{c zRdo9nYKlZui-H-N2*C_yc;gU)3Cz$z2u3i&2LK$*&_@VZnBfnA9?URA2)Zx>G$&}o z3{!-Fff+#nXuu2$gn)(_#{p1<8AOEe#Hk{FHsN*ii((4C3oA2YzJJ|ZcoCblv;S;g zUA!*UO(KeYu5rbaBs`zU*Jy!5tuRai)eY%?(5_ArHVuig+4nPNY9`ObZajWFpwlzc z^f6+6Xs+r_cD%#ewOoR1u6L$|F)sh`M1OMOZtS0Ow{V(5?DHrhf|ot|PNj2r8Z{+Z zm?^`D%K1xyZnXRc-pk1Ct;RA!e?`L&m(KQlpSHB7O8Q+>U-^3e^7M=3&CypoMjgk=g9sxf_4P{VLP_&JO?k{riY&Q&&B`K(U1NxZbgtzD zkFTDR_aAUp6hDphT`J1_>JaEw>F|*EXRKs+RY-MSeLG{)9_U)KM&I=zQvcTJg_N-R zk-2W;kwCYG=i3Z|5cT%L0ZmA`b*A;htgMZ<6`8MXCCRbf`PsFa@7&$W$c>)RROiZ+`1~o|=ud3HBjr1H=gKeGS*d8^&~*72$yR!A zP-L}Uey4+;1es*qiAnk;q>F9-;v1c3VW->j8#PfJDildc{DuR+B%Mg5t?hD19{ zBqV3Ssi$R1I|})O8E0=i(#72~cyJ{a*Nw65TTHfTATIN{XDCKkqJq{t%qA@l_Q0l%zY>3$y!a`x2r) z@jUgID}dS9J@}C3@Wd;=q&L_iQw5d zT9p@je|=m7alLgj2GMT{NwxgGI@_*c;XNdDd7~}V1BnSzb?9Z^pCIYYuJy0<5Xe8r zYuzCOxq5F%WVl^;vB{WYNxSTOlQcD!>D@%H;M!6+i_NZoxoHU$6pX^7^URtpU|I?F zfUgG(taNzlg&tYR<%v%CCU^AvU`|M%rF=S6iFa4#GW?%CB};@85T2BDJo zr>Y+5rR|ESD$f3z(;9~>*5>bCRF9j;b)BHQv{-vgZF*Mve5f!e6#>e?oa=o_; zcc>t)IM_=oKk8EehI{~qa`$lp*w%FA88qIeg8PcX3CIV?9*wUC2;_>a=}C^PESy-R zW)N1eBZIazjZ}3+pg;k}fl98w7bk#BS5kl)H1K!l1nj0O(I5v<87QVkS8Ad0Kku*P z5~?lxhq7LtsHy?nt|u4BljJs=`zR}7k~u(Hg>qE&3zC}`Jpom52= zDD(k?OMy~7H~~&{r3vgc0A!6I=nZ9(s#3sL2KF+1+6JsnK5NrK?tj~S)lKK~^ueYb zW}L7iv9exw9NmY;BeMx{b-!ZBbiolvG=Ar2qkCK}GQIT(X+!Nrawc1|`SvmJ*MOpW L`cWF(e`Wn2b9DUL literal 0 HcmV?d00001 diff --git a/src/frontend/public/github-logo.png b/src/frontend/public/github-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..6cb3b705d018006a2bd4200ea94c9d5fb98b6f76 GIT binary patch literal 6393 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 Hu0mjfZ$b4` literal 0 HcmV?d00001 diff --git a/src/frontend/public/gitlab-logo.png b/src/frontend/public/gitlab-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..cd269a8e59287d862b72e4a656bb8cb93a2c3c3e GIT binary patch literal 4692 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 literal 0 HcmV?d00001 diff --git a/src/frontend/src/components/Login.vue b/src/frontend/src/components/Login.vue index 11bc9b3..c3da471 100644 --- a/src/frontend/src/components/Login.vue +++ b/src/frontend/src/components/Login.vue @@ -2,11 +2,14 @@