From 74d8beb3384105baf6c0d6909a61153c0a8d83ba Mon Sep 17 00:00:00 2001 From: pedropapa Date: Sun, 8 Oct 2023 16:25:00 +1100 Subject: [PATCH 1/6] feat: move types to common folder --- src/{ => common}/types/oauth.types.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/{ => common}/types/oauth.types.ts (100%) diff --git a/src/types/oauth.types.ts b/src/common/types/oauth.types.ts similarity index 100% rename from src/types/oauth.types.ts rename to src/common/types/oauth.types.ts From 21c9dcd5d8b098f1c24e43bd7e8d2d6d847805f2 Mon Sep 17 00:00:00 2001 From: pedropapa Date: Sun, 8 Oct 2023 16:26:43 +1100 Subject: [PATCH 2/6] feat: abstracting oauthclient class --- src/common/oauth-client.ts | 57 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 src/common/oauth-client.ts diff --git a/src/common/oauth-client.ts b/src/common/oauth-client.ts new file mode 100644 index 0000000..d7884d2 --- /dev/null +++ b/src/common/oauth-client.ts @@ -0,0 +1,57 @@ +import type { OAuthToken, OAuthUserInfo } from "./types/oauth.types"; + +export class OAuthClient { + private domain: string; + private clientId: string; + private clientSecret: string; + private redirectUri: string; + + constructor( + domain: string, + clientId: string, + clientSecret: string, + redirectUri: string + ) { + this.domain = domain; + this.clientId = clientId; + this.clientSecret = clientSecret; + this.redirectUri = redirectUri; + } + + getUserInfo = async (accessToken: string): Promise => { + const request = await fetch(`https://${this.domain}/userinfo`, { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + + if (request.status !== 200) { + throw new Error("Error fetching auth token"); + } + + return await request.json(); + }; + + getTokenInfo = async (code: string): Promise => { + const formData = new URLSearchParams(); + formData.append("grant_type", "authorization_code"); + formData.append("client_id", this.clientId); + formData.append("client_secret", this.clientSecret); + formData.append("code", code); + formData.append("redirect_uri", this.redirectUri); + + const request = await fetch(`https://${this.domain}/oauth/token`, { + method: "POST", + body: formData, + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + }); + + if (request.status !== 200) { + throw new Error("Error fetching auth token"); + } + + return await request.json(); + }; +} From fb147881af8a2a178e36a339c6c222e292bec8d9 Mon Sep 17 00:00:00 2001 From: pedropapa Date: Sun, 8 Oct 2023 16:26:52 +1100 Subject: [PATCH 3/6] feat: auth0client class --- src/common/oauth/providers/auth0-client.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/common/oauth/providers/auth0-client.ts diff --git a/src/common/oauth/providers/auth0-client.ts b/src/common/oauth/providers/auth0-client.ts new file mode 100644 index 0000000..e701920 --- /dev/null +++ b/src/common/oauth/providers/auth0-client.ts @@ -0,0 +1,12 @@ +import { OAuthClient } from "../../oauth-client"; + +export class Auth0Client extends OAuthClient { + constructor() { + super( + import.meta.env.PUBLIC_AUTH0_DOMAIN, + import.meta.env.PUBLIC_AUTH0_CLIENT_ID, + import.meta.env.AUTH0_SECRET, + import.meta.env.PUBLIC_AUTH0_LOGOUT_URI + ); + } +} From 20d4b2c15694cfe0c165687bc2a404b856efff67 Mon Sep 17 00:00:00 2001 From: pedropapa Date: Sun, 8 Oct 2023 16:27:21 +1100 Subject: [PATCH 4/6] refactor: turning logout page into endpoint --- src/components/LogoutCallbackHandler.tsx | 19 -------------- src/pages/logout.astro | 22 ---------------- src/pages/logout.ts | 32 ++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 41 deletions(-) delete mode 100644 src/components/LogoutCallbackHandler.tsx delete mode 100644 src/pages/logout.astro create mode 100644 src/pages/logout.ts diff --git a/src/components/LogoutCallbackHandler.tsx b/src/components/LogoutCallbackHandler.tsx deleted file mode 100644 index 0415a2d..0000000 --- a/src/components/LogoutCallbackHandler.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React, { useEffect } from "react"; -import { useAuthToken } from "../hooks/useAuthToken"; -import { useUser } from "../hooks/useUser"; - -export const LogoutCallbackHandler = () => { - const { removeAuthTokens } = useAuthToken(); - const { removeUserInfo } = useUser(); - - useEffect(() => { - removeAuthTokens(); - removeUserInfo(); - - window.location.href = '/'; - }, []) - - return ( - <> - ) -} \ No newline at end of file diff --git a/src/pages/logout.astro b/src/pages/logout.astro deleted file mode 100644 index 5297164..0000000 --- a/src/pages/logout.astro +++ /dev/null @@ -1,22 +0,0 @@ ---- -import Layout from '../layouts/Layout.astro'; -import Container from "../components/Container.astro" -import { LogoutCallbackHandler } from "../components/LogoutCallbackHandler" - -export const prerender = true; ---- - -
- -
-
-

- Logging you out... -

-
-
-
-
-
- - \ No newline at end of file diff --git a/src/pages/logout.ts b/src/pages/logout.ts new file mode 100644 index 0000000..16efff1 --- /dev/null +++ b/src/pages/logout.ts @@ -0,0 +1,32 @@ +import { type APIRoute } from "astro"; + +export const GET: APIRoute = async ({ redirect, cookies }) => { + try { + cookies.delete("authToken", { + path: "/", + }); + cookies.delete("authTokenExpiresIn", { + path: "/", + }); + cookies.delete("authTokenType", { + path: "/", + }); + cookies.delete("refreshToken", { + path: "/", + }); + cookies.delete("userInfo", { + path: "/", + }); + + return redirect("/"); + } catch (error) { + return new Response( + JSON.stringify({ + error, + }), + { + status: 400, + } + ); + } +}; From 582d8b5a56cd8efc792d89d3d7792f6b5543a63d Mon Sep 17 00:00:00 2001 From: pedropapa Date: Sun, 8 Oct 2023 16:27:38 +1100 Subject: [PATCH 5/6] refactor: turning login page into endpoint --- src/components/LoginCallbackHandler.tsx | 54 ------------------------- src/pages/login_callback.astro | 43 -------------------- src/pages/login_callback.ts | 51 +++++++++++++++++++++++ 3 files changed, 51 insertions(+), 97 deletions(-) delete mode 100644 src/components/LoginCallbackHandler.tsx delete mode 100644 src/pages/login_callback.astro create mode 100644 src/pages/login_callback.ts diff --git a/src/components/LoginCallbackHandler.tsx b/src/components/LoginCallbackHandler.tsx deleted file mode 100644 index 43da540..0000000 --- a/src/components/LoginCallbackHandler.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import React, { useEffect } from "react"; -import { useAuthToken } from "../hooks/useAuthToken"; -import PropTypes from 'prop-types'; -import { useUser } from "../hooks/useUser"; -import type { OAuthToken, OAuthUserInfo } from "../types/oauth.types"; - -type ComponentProps = { - auth: { - token: OAuthToken; - user: OAuthUserInfo; - }, - state: string | null; -} - -export const LoginCallbackHandler: React.FunctionComponent = ({ auth, state }: ComponentProps) => { - const { setAuthToken, setRefreshToken, setAuthTokenExpiresIn, setAuthTokenType } = useAuthToken(); - const { storeUserInfo } = useUser(); - - const { token, user } = auth; - - useEffect(() => { - const date = new Date(); - - setAuthToken(token.access_token); - setRefreshToken(token.refresh_token); - setAuthTokenExpiresIn(date.setTime(date.getTime() + (token.expires_in * 1000)).toString()); - setAuthTokenType(token.token_type); - storeUserInfo(user); - - if (state) { - window.location.href = atob(state); - } else { - window.location.href = '/'; - } - }, []); - - return ( - <> - ) -} - -LoginCallbackHandler.propTypes = { - token: PropTypes.objectOf( - PropTypes.shape({ - access_token: PropTypes.string.isRequired, - refresh_token: PropTypes.string.isRequired, - id_token: PropTypes.string.isRequired, - scope: PropTypes.string.isRequired, - expires_in: PropTypes.number.isRequired, - token_type: PropTypes.string.isRequired, - }) - ).isRequired, - state: PropTypes.string -} \ No newline at end of file diff --git a/src/pages/login_callback.astro b/src/pages/login_callback.astro deleted file mode 100644 index 21db4cf..0000000 --- a/src/pages/login_callback.astro +++ /dev/null @@ -1,43 +0,0 @@ ---- -import Layout from '../layouts/Layout.astro'; -import Container from "../components/Container.astro" -import { LoginCallbackHandler } from "../components/LoginCallbackHandler" - -const code = Astro.url.searchParams.get('code'); -const state = Astro.url.searchParams.get('state'); - -if (!code) { - return Astro.redirect("/"); -} - -const authenticateRequest = await fetch(`${Astro.url.protocol}//${Astro.url.host}/authenticate`, { - headers: { - "Accept": "application/json" - }, - method: 'POST', - body: JSON.stringify({ - code, - }) -}); - -if (authenticateRequest.status !== 200) { - return Astro.redirect("/"); -} - -const authenticate = await authenticateRequest.json(); ---- - -
- -
-
-

- Authenticating you... -

-
-
-
-
-
- - \ No newline at end of file diff --git a/src/pages/login_callback.ts b/src/pages/login_callback.ts new file mode 100644 index 0000000..426ba05 --- /dev/null +++ b/src/pages/login_callback.ts @@ -0,0 +1,51 @@ +import { type APIRoute } from "astro"; +import { Auth0Client } from "../common/oauth/providers/auth0-client"; + +const oauthClient = new Auth0Client(); + +export const GET: APIRoute = async ({ redirect, url, cookies }) => { + const code = url.searchParams.get("code"); + const state = url.searchParams.get("state"); + + if (!code) { + return new Response( + JSON.stringify({ + error: "No code provided", + }), + { + status: 400, + } + ); + } + + try { + const token = await oauthClient.getTokenInfo(code); + const user = await oauthClient.getUserInfo(token.access_token); + + const date = new Date(); + const tokenExpiresAt = date.setSeconds( + date.getSeconds() + token.expires_in + ); + + cookies.set("authToken", token.access_token); + cookies.set("authTokenExpiresIn", tokenExpiresAt.toString()); + cookies.set("authTokenType", token.token_type); + cookies.set("refreshToken", token.refresh_token); + cookies.set("userInfo", JSON.stringify(user)); + + if (state) { + return redirect(atob(state)); + } else { + return redirect("/"); + } + } catch (error) { + return new Response( + JSON.stringify({ + error, + }), + { + status: 400, + } + ); + } +}; From 4293e354f04d71254a6565c639b72257ea68c3e1 Mon Sep 17 00:00:00 2001 From: pedropapa Date: Sun, 8 Oct 2023 16:28:19 +1100 Subject: [PATCH 6/6] feat: removing deprecated logic/code --- src/hooks/useAuthToken.tsx | 32 +++++---------- src/hooks/useUser.tsx | 8 ++-- src/pages/authenticate.ts | 82 -------------------------------------- 3 files changed, 14 insertions(+), 108 deletions(-) delete mode 100644 src/pages/authenticate.ts diff --git a/src/hooks/useAuthToken.tsx b/src/hooks/useAuthToken.tsx index 1c1e60a..0426700 100644 --- a/src/hooks/useAuthToken.tsx +++ b/src/hooks/useAuthToken.tsx @@ -2,33 +2,21 @@ import { useEffect, useState } from "react"; import { useCookies } from "react-cookie"; export const useAuthToken = () => { - const [cookies, setCookie, removeCookie] = useCookies(['authToken', 'authTokenExpiresIn', 'authTokenType', 'refreshToken']); - const [authToken, updateAuthToken] = useState(null); - const [refreshToken, updateRefreshToken] = useState(null); - const [authTokenExpiresIn, updateAuthTokenExpiresIn] = useState(null); - const [authTokenType, updateTokenType] = useState(null); + const [cookies] = useCookies(['authToken', 'authTokenExpiresIn', 'authTokenType', 'refreshToken']); + const [authToken, setAuthToken] = useState(null); + const [refreshToken, setRefreshToken] = useState(null); + const [authTokenExpiresIn, setAuthTokenExpiresIn] = useState(null); + const [authTokenType, setTokenType] = useState(null); - const setAuthToken = (token: string) => setCookie('authToken', token); - const setRefreshToken = (type: string) => setCookie('refreshToken', type); - const setAuthTokenExpiresIn = (expiresIn: string) => setCookie('authTokenExpiresIn', expiresIn); - const setAuthTokenType = (type: string) => setCookie('authTokenType', type); - - const removeAuthTokens = () => { - removeCookie('authToken', {path:'/'}); - removeCookie('authTokenExpiresIn', {path:'/'}); - removeCookie('authTokenType', {path:'/'}); - removeCookie('refreshToken', {path:'/'}); - }; - useEffect(() => { const { authToken, authTokenExpiresIn, authTokenType, refreshToken } = cookies; - updateAuthToken(authToken); - updateRefreshToken(refreshToken); - updateAuthTokenExpiresIn(authTokenExpiresIn); - updateTokenType(authTokenType); + setAuthToken(authToken); + setRefreshToken(refreshToken); + setAuthTokenExpiresIn(authTokenExpiresIn); + setTokenType(authTokenType); }, [cookies]); return { - authToken, setAuthToken, authTokenExpiresIn, setAuthTokenExpiresIn, authTokenType, setAuthTokenType, removeAuthTokens, refreshToken, setRefreshToken + authToken, authTokenExpiresIn, authTokenType, refreshToken }; }; diff --git a/src/hooks/useUser.tsx b/src/hooks/useUser.tsx index ae2f2e2..e1ed639 100644 --- a/src/hooks/useUser.tsx +++ b/src/hooks/useUser.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from "react"; import { useAuthToken } from "./useAuthToken"; import { useCookies } from "react-cookie"; -import type { OAuthUserInfo } from "../types/oauth.types"; +import type { OAuthUserInfo } from "../common/types/oauth.types"; type UserInfo = { nickname: string; @@ -12,17 +12,17 @@ type UserInfo = { } export const useUser = () => { - const [cookies, setCookie, removeCookie] = useCookies(['userInfo']); + const [cookies, setCookie] = useCookies(['userInfo']); const { authToken, authTokenExpiresIn } = useAuthToken(); const [ isUserAuthenticated, setIsAuthenticated ] = useState(false); const [ userInfo, setUserInfo ] = useState(null); const storeUserInfo = (userInfo: OAuthUserInfo) => setCookie('userInfo', userInfo); - const removeUserInfo = () => removeCookie('userInfo'); useEffect(() => { const isTokenExpired = new Date() > new Date(authTokenExpiresIn); + setIsAuthenticated(authToken && !isTokenExpired); }, [authToken, authTokenExpiresIn]); @@ -35,6 +35,6 @@ export const useUser = () => { }, [cookies]); return { - isUserAuthenticated, userInfo, storeUserInfo, removeUserInfo, + isUserAuthenticated, userInfo, storeUserInfo, }; }; diff --git a/src/pages/authenticate.ts b/src/pages/authenticate.ts deleted file mode 100644 index c1be704..0000000 --- a/src/pages/authenticate.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { type APIRoute } from "astro"; -import type { OAuthToken, OAuthUserInfo } from "../types/oauth.types"; - -export const POST: APIRoute = async ({ request }) => { - const { code } = await request.json(); - - if (!code) { - return new Response( - JSON.stringify({ - error: "No code provided", - }), - { - status: 400, - } - ); - } - - try { - const token = await getTokenInfo(code); - const user = await getUserInfo(token.access_token); - - return new Response( - JSON.stringify({ - token, - user, - }) - ); - } catch (error) { - console.error(error); - return new Response( - JSON.stringify({ - error, - }), - { - status: 400, - } - ); - } -}; - -const getUserInfo = async (accessToken: string): Promise => { - const request = await fetch( - `https://${import.meta.env.PUBLIC_AUTH0_DOMAIN}/userinfo`, - { - headers: { - Authorization: `Bearer ${accessToken}`, - }, - } - ); - - if (request.status !== 200) { - throw new Error("Error fetching auth token"); - } - - return await request.json(); -}; - -const getTokenInfo = async (code: string): Promise => { - const formData = new URLSearchParams(); - formData.append("grant_type", "authorization_code"); - formData.append("client_id", import.meta.env.PUBLIC_AUTH0_CLIENT_ID); - formData.append("client_secret", import.meta.env.AUTH0_SECRET); - formData.append("code", code); - formData.append("redirect_uri", import.meta.env.PUBLIC_AUTH0_REDIRECT_URI); - - const request = await fetch( - `https://${import.meta.env.PUBLIC_AUTH0_DOMAIN}/oauth/token`, - { - method: "POST", - body: formData, - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - } - ); - - if (request.status !== 200) { - throw new Error("Error fetching auth token"); - } - - return await request.json(); -};