diff --git a/app/.env.local.sample b/app/.env.local.sample index 7cfbb2f4..504f1ef2 100644 --- a/app/.env.local.sample +++ b/app/.env.local.sample @@ -1,3 +1,4 @@ +##### BEGIN: Environment variables for firebase config. ##### # Environment variables for firebase-admin GOOGLE_CREDS_PATH=required, path to your service account json file that you downloaded from firebase console. @@ -19,3 +20,29 @@ NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID= # for cookies signing COOKIE_SECRET_CURRENT= COOKIE_SECRET_PREVIOUS= + +##### END: Environment variables for firebase config. ##### + +#### BEGIN: Environment variables for auth0 config #### + +# A long, secret value used to encrypt the session cookie +AUTH0_SECRET='a_long_random_string' +# The base url of your application +AUTH0_BASE_URL='http://localhost:3000' +# The url of your Auth0 tenant domain +AUTH0_ISSUER_BASE_URL='https://sample_app.us.auth0.com' +# Your Auth0 application's Client ID +AUTH0_CLIENT_ID='your client id' +# Your Auth0 application's Client Secret +AUTH0_CLIENT_SECRET='your client secret' +# API route for auth0 login (do not not change, since we have used custom routes for auth0) +NEXT_PUBLIC_AUTH0_LOGIN='/api/auth0/login' +# API route for auth0 profile (do not not change, since we have used custom routes for auth0) +NEXT_PUBLIC_AUTH0_PROFILE='/api/auth0/me' +# API route for auth0 callback (do not not change, since we have used custom routes for auth0) +AUTH0_CALLBACK='/api/auth0/callback' +# URL to redirect after logout (set a value to override default). Must be present in list of logout urls in auth0 dashboard. +AUTH0_POST_LOGOUT_REDIRECT='/' +# For more environment variables visit https://auth0.github.io/nextjs-auth0/modules/config.html + +#### END: Environment variables for auth0 config #### diff --git a/app/components/auth/auth0/README.md b/app/components/auth/auth0/README.md new file mode 100644 index 00000000..320e0bc3 --- /dev/null +++ b/app/components/auth/auth0/README.md @@ -0,0 +1,95 @@ +RC4Community uses `@auth0/nextjs-auth0` to integrate authentication with Auth0. + +## Setting up +1. Set up environment variables +``` +# A long, secret value used to encrypt the session cookie +AUTH0_SECRET='a_long_random_string' +# The base url of your application +AUTH0_BASE_URL='http://localhost:3000' +# The url of your Auth0 tenant domain +AUTH0_ISSUER_BASE_URL='https://sample_app.us.auth0.com' +# Your Auth0 application's Client ID +AUTH0_CLIENT_ID='your client id' +# Your Auth0 application's Client Secret +AUTH0_CLIENT_SECRET='your client secret' +# API route for auth0 login (do not not change, since we have used custom routes for auth0) +NEXT_PUBLIC_AUTH0_LOGIN='/api/auth0/login' +# API route for auth0 profile (do not not change, since we have used custom routes for auth0) +NEXT_PUBLIC_AUTH0_PROFILE='/api/auth0/me' +# API route for auth0 callback (do not not change, since we have used custom routes for auth0) +AUTH0_CALLBACK='/api/auth0/callback' +# URL to redirect after logout (set a value to override default). Must be present in list of logout urls in auth0 dashboard. +AUTH0_POST_LOGOUT_REDIRECT='/' +``` +For more environment variables visit [https://auth0.github.io/nextjs-auth0/modules/config.html](https://auth0.github.io/nextjs-auth0/modules/config.html) + +2. You must add callback url and logout urls accordingly in application settings in auth0 dashboard. +3. Wrap your app with `` component. +``` +import '/styles/globals.css'; +import 'bootstrap/dist/css/bootstrap.min.css'; +import Layout from '../components/layout'; +import SSRProvider from 'react-bootstrap/SSRProvider'; +import { UserProvider } from '@auth0/nextjs-auth0'; + +function MyApp({ Component, pageProps }) { + return ( + + + + + + + + ); +} + +export default MyApp; +``` +4. Get login url from `getAuth0LoginURL()` and logout url from `getAuth0LogoutURL()`. Use `useUser()` hook to get user details. +``` +import { useUser, withPageAuthRequired } from '@auth0/nextjs-auth0'; +import { getAuth0LoginURL, getAuth0LogoutURL } from '/app/components/auth/auth0'; +export default () => { + const { user, error, isLoading } = useUser(); + if (isLoading) return
Loading...
; + if (error) return
{error.message}
; + if (user) { + return ( +
+ Welcome {user.name}! Logout +
+ ); + } + return Login; +}; +``` + +5. To protect a page and api route, use `withPageAuthRequired` and `withApiAuthRequired` +``` +import { withPageAuthRequired } from '@auth0/nextjs-auth0'; + +.... + +export const getServerSideProps = withPageAuthRequired({ + async getServerSideProps(){ + return { + props: { + customProp: "customPropValue" + } + } + } +}); + +``` +``` +import { withApiAuthRequired, getSession } from '@auth0/nextjs-auth0'; + +export default withApiAuthRequired(function ProtectedRoute(req, res) { + const session = getSession(req, res); + ... +}); +``` + +To read more about using `@auth0/nextjs-auth0` visit https://github.com/auth0/nextjs-auth0#readme diff --git a/app/components/auth/auth0/index.js b/app/components/auth/auth0/index.js new file mode 100644 index 00000000..e5815de7 --- /dev/null +++ b/app/components/auth/auth0/index.js @@ -0,0 +1,9 @@ +import Auth0AuthMenuButtonModule from './ui/Auth0AuthMenuButton'; +import Auth0UserInfoModule from './ui/Auth0UserInfo'; +import functions from './lib/functions'; + +export const Auth0AuthMenuButton = Auth0AuthMenuButtonModule; +export const Auth0AuthUserInfo = Auth0UserInfoModule; + +export const getAuth0LoginURL = functions.getAuth0LoginURL; +export const getAuth0LogoutURL = functions.getAuth0LogoutURL; diff --git a/app/components/auth/auth0/lib/functions.js b/app/components/auth/auth0/lib/functions.js new file mode 100644 index 00000000..61c5abed --- /dev/null +++ b/app/components/auth/auth0/lib/functions.js @@ -0,0 +1,47 @@ +import urlJoin from 'url-join'; +const prepareUrl = (url,returnTo=null,redirectToThisPage=false) => { + let returnUrl = urlJoin(process.env.AUTH0_BASE_URL || '/', url); + if(typeof window !== 'undefined'){ + if(returnTo){ + return returnUrl + '?returnTo='+encodeURIComponent(returnTo); + } else if(redirectToThisPage){ + return returnUrl + '?returnTo='+encodeURIComponent(window.location.href); + } + } + return returnUrl; +} + +/** + * + * @param {String} returnTo + * URL to redirect to after logout. + * @param {boolean} redirectToThisPage + * Whether to redirect to the url where the function is called. + * @returns logoutUrl + */ +export const getAuth0LoginURL = ({ + returnTo = null, + redirectToThisPage = false +} = {}) => { + return prepareUrl('/api/auth0/login',returnTo,redirectToThisPage) +}; + +/** + * + * @param {String} returnTo + * URL to redirect to after logout. The url passed must be added in logout urls list auth0 dashboard. + * @param {boolean} redirectToThisPage + * Whether to redirect to the url where the function is called. The url must be added in logout urls list in auth0 dashboard. + * @returns logoutUrl + */ +export const getAuth0LogoutURL = ({ + returnTo = null, + redirectToThisPage = false +} = {}) => { + return prepareUrl('/api/auth0/logout',returnTo,redirectToThisPage) +}; + +export default { + getAuth0LoginURL, + getAuth0LogoutURL +}; diff --git a/app/components/auth/auth0/styles/Auth0AuthMenuButton.module.css b/app/components/auth/auth0/styles/Auth0AuthMenuButton.module.css new file mode 100644 index 00000000..049c784c --- /dev/null +++ b/app/components/auth/auth0/styles/Auth0AuthMenuButton.module.css @@ -0,0 +1,45 @@ +.authDialogWrapper { + position: relative; +} +.authContainer { + display: block; + position: absolute; + right: 8px; + top: 62px; + width: 354px; + max-height: -webkit-calc(100vh - 62px - 100px); + max-height: calc(100vh - 62px - 100px); + overflow-y: auto; + overflow-x: hidden; + border-radius: 8px; + margin-left: 12px; + z-index: 991; + line-height: normal; + background: #fff; + border: 1px solid #ccc; + border-color: rgba(0,0,0,.2); + color: #000; + -webkit-box-shadow: 0 2px 10px rgb(0 0 0 / 20%); + box-shadow: 0 2px 10px rgb(0 0 0 / 20%); + -webkit-user-select: text; + user-select: text; +} + +.avatar { + background: var(--bs-gray-300); + border-radius: 50%; + width: 42px; + height: 42px; + display: flex; + justify-content: center; + align-items: center; +} + +.avatarButton { + background: none; + border: none; +} + +.avatarButton:focus { + outline: none; +} diff --git a/app/components/auth/auth0/ui/Auth0AuthMenuButton.js b/app/components/auth/auth0/ui/Auth0AuthMenuButton.js new file mode 100644 index 00000000..fb2eb3de --- /dev/null +++ b/app/components/auth/auth0/ui/Auth0AuthMenuButton.js @@ -0,0 +1,46 @@ +import { useState } from "react"; +import { NoUserAvatar } from "../../NoUserAvatar"; +import styles from "../styles/Auth0AuthMenuButton.module.css"; +import { useUser } from "@auth0/nextjs-auth0"; +import Auth0UserInfo from "./Auth0UserInfo"; +import { getAuth0LoginURL } from "../lib/functions"; + +export default function Auth0AuthMenuButton({}){ + const {user} = useUser(); + const [isOpen,setOpen] = useState(false); + return ( +
+
+ {user? + + : + + + + } +
+ { user && isOpen && +
+ +
+ } +
+ ) +} diff --git a/app/components/auth/auth0/ui/Auth0UserInfo.js b/app/components/auth/auth0/ui/Auth0UserInfo.js new file mode 100644 index 00000000..0235e609 --- /dev/null +++ b/app/components/auth/auth0/ui/Auth0UserInfo.js @@ -0,0 +1,43 @@ +import { useUser } from "@auth0/nextjs-auth0"; +import { Button } from "react-bootstrap"; +import { NoUserAvatar } from "../../NoUserAvatar"; +import { getAuth0LogoutURL } from "../lib/functions"; + +export default function Auth0UserInfo(){ + const {user} = useUser(); + if(!user) + return
; + return ( + <> +
+
+ { + user.picture ? + {user.name} + : + + } +
+
+ {user.name} +
+
+ {user.email} +
+
+
+ + + +
+ + ) +} diff --git a/app/components/layout.js b/app/components/layout.js index 9ab1ae6f..32f5e3a6 100644 --- a/app/components/layout.js +++ b/app/components/layout.js @@ -1,5 +1,4 @@ import '../styles/Layout.module.css' -import { withFirebaseAuthUser } from './auth/firebase'; import Footer from './footer' import Menubar from './menubar' @@ -15,4 +14,4 @@ function Layout(props) { ) } -export default withFirebaseAuthUser()(Layout); +export default Layout; diff --git a/app/components/menubar.js b/app/components/menubar.js index 5411c6b5..7357f39d 100644 --- a/app/components/menubar.js +++ b/app/components/menubar.js @@ -1,7 +1,7 @@ import { useState } from 'react'; import { Navbar, Nav, NavDropdown, Container } from 'react-bootstrap'; import styles from '../styles/Menubar.module.css'; -import { FirebaseAuthMenuButton } from './auth/firebase'; +import { Auth0AuthMenuButton } from './auth/auth0'; import BrandLogo from "./brandlogo"; import RocketChatLinkButton from './rocketchatlinkbutton'; @@ -71,7 +71,7 @@ export default function Menubar(props) {
- +
diff --git a/app/package.json b/app/package.json index b86fe8a7..2ff8804d 100644 --- a/app/package.json +++ b/app/package.json @@ -9,6 +9,7 @@ "lint": "next lint" }, "dependencies": { + "@auth0/nextjs-auth0": "^1.7.0", "bootstrap": "^5.1.3", "firebase": "^9.6.3", "next": "12.0.7", @@ -20,7 +21,8 @@ "react-i18next": "^11.15.3", "react-icons": "^4.3.1", "react-slick": "^0.28.1", - "slick-carousel": "^1.8.1" + "slick-carousel": "^1.8.1", + "url-join": "^4.0.1" }, "devDependencies": { "eslint": "8.6.0", diff --git a/app/pages/_app.js b/app/pages/_app.js index aeb786e0..24a684a9 100644 --- a/app/pages/_app.js +++ b/app/pages/_app.js @@ -2,16 +2,16 @@ import '/styles/globals.css'; import 'bootstrap/dist/css/bootstrap.min.css'; import Layout from '../components/layout'; import SSRProvider from 'react-bootstrap/SSRProvider'; -import { initAuth } from '../components/auth/firebase'; - -initAuth(); +import { UserProvider } from '@auth0/nextjs-auth0'; function MyApp({ Component, pageProps }) { return ( - - - + + + + + ); } diff --git a/app/pages/api/auth0/[...auth0].js b/app/pages/api/auth0/[...auth0].js new file mode 100644 index 00000000..921a506b --- /dev/null +++ b/app/pages/api/auth0/[...auth0].js @@ -0,0 +1,17 @@ +import { handleAuth, handleLogout } from '@auth0/nextjs-auth0'; + +export default handleAuth({ + async logout(req, res) { + // You don't strictly need to sanitise `req.query.returnTo` because it has to be in Auth0's "Allowed Logout URLs" + // But if you ever added a local logout option you should sanitise it, like we do with the login `returnTo` + // eg https://github.com/auth0/nextjs-auth0/blob/beta/src/handlers/login.ts#L70-L72 + const returnTo = req.query.returnTo; + try { + await handleLogout(req, res, { + returnTo + }); + } catch (error) { + res.status(error.status || 400).end(error.message); + } + } +}); diff --git a/app/pages/examples/auth0-protected-page.js b/app/pages/examples/auth0-protected-page.js new file mode 100644 index 00000000..c2b4dac7 --- /dev/null +++ b/app/pages/examples/auth0-protected-page.js @@ -0,0 +1,27 @@ +import { useUser, withPageAuthRequired } from '@auth0/nextjs-auth0'; +import { getAuth0LoginURL, getAuth0LogoutURL } from '../../components/auth/auth0'; + +export default ({customProp}) => { + const { user, error, isLoading } = useUser(); + console.log(customProp); + if (isLoading) return
Loading...
; + if (error) return
{error.message}
; + if (user) { + return ( +
+ Welcome {user.name}! Logout +
+ ); + } + return Login; +}; + +export const getServerSideProps = withPageAuthRequired({ + async getServerSideProps(){ + return { + props: { + customProp: "customPropValue" + } + } + } +}); diff --git a/app/pages/examples/auth0.js b/app/pages/examples/auth0.js new file mode 100644 index 00000000..b4fb0391 --- /dev/null +++ b/app/pages/examples/auth0.js @@ -0,0 +1,5 @@ +import Auth0 from "./auth0-protected-page"; + +export default () => { + return +}; diff --git a/app/pages/index.js b/app/pages/index.js index 690c81dd..70783eb8 100644 --- a/app/pages/index.js +++ b/app/pages/index.js @@ -8,7 +8,6 @@ import Searchbox from '../components/searchbox'; import Growthcounters from '../components/growthcounters'; import { Container, Col } from 'react-bootstrap'; import { fetchAPI } from '../lib/api'; -import { withFirebaseAuthUser } from '../components/auth/firebase'; function Home(props) { return ( @@ -71,7 +70,7 @@ function Home(props) { ); } -export default withFirebaseAuthUser()(Home); +export default Home; export async function getStaticProps({ params }) { const carousels = await fetchAPI('/carousels');