How can I use Keycloak in Next.js? - reactjs

I'm trying to authenticate in Keycloak in Next.js using #react-keycloak/nextjs but once I pass the login it keeps sending me back to the Keycloak server and returning to the main page until it gives me an error because my token key is expired.
This is my _app.js:
import { ApolloProvider } from '#apollo/react-hooks'
import CssBaseline from '#material-ui/core/CssBaseline'
import { ThemeProvider } from '#material-ui/core/styles'
import { Persistors, SSRKeycloakProvider } from '#react-keycloak/nextjs'
import Layout from 'components/layout/Layout'
import { useApollo } from 'lib/apolloClient'
import KeycloakLoading from 'components/KeycloakLoading'
import {
keycloakCfg,
keycloakProviderInitConfig,
onKeycloakLogout,
onKeycloakTokens,
} from 'lib/keycloak'
import Head from 'next/head'
import PropTypes from 'prop-types'
import React from 'react'
import theme from 'styles/theme'
import 'leaflet/dist/leaflet.css'
import 'react-leaflet-markercluster/dist/styles.min.css'
import 'styles/styles.css'
import 'styles/animations.css'
import '../styles/globals.css'
// export function reportWebVitals(metric) {
// // These metrics can be sent to any analytics service
// console.log(metric)
// }
export default function MyApp(props) {
const { Component, pageProps, cookies } = props
const apolloClient = useApollo(pageProps.initialApolloState)
React.useEffect(() => {
// Remove the server-side injected CSS.
const jssStyles = document.querySelector('#jss-server-side')
if (jssStyles) {
jssStyles.parentElement.removeChild(jssStyles)
}
}, [])
return (
<React.Fragment>
<Head>
<title>My page</title>
<meta
name="viewport"
content="minimum-scale=1, initial-scale=1, width=device-width"
/>
</Head>
<ThemeProvider theme={theme}>
<CssBaseline />
<SSRKeycloakProvider
keycloakConfig={keycloakCfg}
persistor={Persistors.Cookies(cookies)}
initConfig={keycloakProviderInitConfig}
onTokens={onKeycloakTokens}
onAuthLogout={onKeycloakLogout}
LoadingComponent={
<React.Fragment>
<KeycloakLoading />
</React.Fragment>
}
>
<ApolloProvider client={apolloClient}>
<Layout>
<Component {...pageProps} />
</Layout>
</ApolloProvider>
</SSRKeycloakProvider>
</ThemeProvider>
</React.Fragment>
)
}
MyApp.propTypes = {
Component: PropTypes.elementType.isRequired,
pageProps: PropTypes.object.isRequired,
cookies: PropTypes.any,
}
And this is my Keycloak configuration
export const keycloakCfg = {
realm: APP_CONSTANTS.KEYCLOAK_REALM,
url: `${APP_CONSTANTS.KEYCLOAK_HOST}/auth`,
clientId: APP_CONSTANTS.KEYCLOAK_CLIENT_ID,
}
export const keycloakProviderInitConfig = {
onLoad: 'login-required',
}
If anyone could point out what is wrong or alternatively tell me another way to use Keycloak in next.

React-keycloak/nextjs is deprecated in favor of react-keycloak/ssr. I followed the examples here: https://www.npmjs.com/package/#react-keycloak/ssr and was able to fully authenticate without issue.
I'm not sure what is causing your looping issues but it doesn't look like you are using the required cookie parser or passing them into the props via getInitialProps so that may be a good place to start.

An alternative would be:
Next-Auth has implemented a keycloak provider which would make your life much easier protecting both Frontend and Backend. Just specify keycloak options/configs in _app.js and you all set.
import {signOut, getSession, useSession } from 'next-auth/react';
export default function Home() {
let session = useSession();
console.log(session);
return (
<div >
<button onClick={() => {
console.log(session);
signOut();
console.log(session);
}}> Sign Out </button>
<h1>H1 test</h1>
</div>
)
}
export const getServerSideProps = async (ctx) => {
const backendSession = await getSession(ctx)
console.log(backendSession);
return {
props: {
}
}
}

Related

Next JS with react-intl creates an ancestry issue

I'm trying to setup NEXT Js with react-intl.
I've added to my _app.tsx the setup for react-intl as follows:
import { IntlProvider } from 'react-intl';
import English from "../../lang/compiled-locales/en-US.json"
import French from "../../lang/compiled-locales/fr.json"
import configs from '../../configs';
// https://itnext.io/next-js-react-intl-add-internationalisation-to-your-nextjs-app-4f933104b1f7
const MyApp = ({ Component, pageProps }: AppProps) => {
const { locale } = useRouter()
const messages = useMemo(() => {
switch (locale) {
case "fr":
return French
default:
return English
}
}, [locale])
return <IntlProvider
locale={locale || configs.languages.default}
messages={messages}>
<Component {...pageProps} />
</IntlProvider>
}
export default MyApp
When I ran it, it worked like a charm, but when I do a page reload, I get the following error:
Server Error
Error: [React Intl] Could not find required `intl` object. <IntlProvider> needs to exist in the component ancestry.
This error happened while generating the page. Any console logs will be displayed in the terminal window.
I'm guessing it's something to do with SSR but no idea how to fix. Thank you in advance for your help.
Here is the sample usage in my pages/index.tsx
import type { NextPage } from 'next'
import Head from 'next/head'
import { FormattedMessage } from 'react-intl'
import Header from '../components/Header'
const Home: NextPage = () => {
return <div className="">
<Head>
<title><FormattedMessage defaultMessage="FlipFlip - Print your videos for real!" id="landing.seo.title" /></title>
</Head>
<Header />
</div>
}
export default Home

useEffect() keeps re-rendering, how to stop it from re-rendering

import React from "react";
import { ThemeProvider } from "styled-components";
import { theme } from "./theme-default";
import { HashRouter as Router, Route, Switch } from "react-router-dom";
import { GlobalStyle } from "./themes";
import { Security } from '#okta/okta-react';
import { OktaAuth } from "#okta/okta-auth-js";
import { FeeSitePageHeader } from "./app/fee--site-page-header";
import { FeeSitePageFooter } from "./app/fee-site-page-footer";
import Dashboard from "./app/Dashboard/Dashboard";
import Logout from "./app/Logout/Logout";
import Login from "./app/Login/Login";
function App() {
const config = {
issuer: 'https://dev-95779092.okta.com/',
clientId: '***',
redirectUri: window.location.origin + '/?redirect_url=/login/callback'
};
const authClient = new OktaAuth(config);
function restoreOriginalUri() {
console.log(restoreOriginalUri);
};
return (
<ThemeProvider theme={theme}>
<GlobalStyle />
<Router >
<FeeSitePageHeader />
<Security oktaAuth={authClient} restoreOriginalUri={restoreOriginalUri}>
<Switch>
<Route path="/" exact>
<Dashboard />
</Route>
</Switch>
</Security>
<FeeSitePageFooter />
</Router>
</ThemeProvider>
);
}
export default App;
This is the dashboard component.
import { useOktaAuth } from '#okta/okta-react';
import React from 'react';
import * as Styled from "./Dashboard.styled";
function Dashboard(){
const { authState, oktaAuth } = useOktaAuth();
React.useEffect(() => {
oktaAuth.signInWithRedirect();
if (window.location.search?.includes('redirect_url=/login/callback')) {
console.log("Check if the browser contains redirect");
}
}, [])
return(
<>
<Styled.CardsWrapper>Apple</Styled.CardsWrapper>
<Styled.CardsWrapper>Apple</Styled.CardsWrapper>
<Styled.CardsWrapper>Apple</Styled.CardsWrapper>
<Styled.CardsWrapper>Apple</Styled.CardsWrapper>
<Styled.CardsWrapper>Apple</Styled.CardsWrapper>
<Styled.CardsWrapper>Apple</Styled.CardsWrapper>
</>
);
}
export default Dashboard;
When the application starts, call goes to OKTA, authenticate logic does its part and url changes to "http://localhost:8080/#/dashboard?redirect_url=/login/callback". After this I want useEffects to get called, Problem is Dashboard keeps re-rendering itself how to stop re-rendering here?
See the docs when you are using the functional component you shouldn't recreate the octaAuth every time.
https://github.com/okta/okta-react#creating-react-router-routes-with-function-based-components
Then you call signInWithRedirect in useEffect every time when component mounts. It should be wrapped with authState.isAuthenticated. Example in docs https://github.com/okta/okta-react#show-login-and-logout-buttons-function-based
When you change the URL you "reload" the page hence - useEffect runs again.
Pass the parameter inside the useEffect hook
React.useEffect(() => {
oktaAuth.signInWithRedirect();
if (window.location.search?.includes('redirect_url=/login/callback')) {
console.log("Check if the browser contains redirect");
}
}, [authState])

Next.js Head - You have included the Google Maps JavaScript API multiple times on this page

In order to use the react-places-autocomplete lib, I implemented the gmaps script as stated in the doc but I get a "You have included the Google Maps JavaScript API multiple times on this page." error when I go to any page with 4-5 copy of the script tag.
If I remove the GooglePlacesScript component, no instance of the script is added.
If I put the GooglePlacesScript component at a page or component level, I still get the error wherever I go on the website somehow.
Any idea why Next is duplicating the script?
GooglePlacesScript component:
import React from "react";
import getConfig from "next/config";
import Head from "next/head";
const { serverRuntimeConfig, publicRuntimeConfig } = getConfig();
const GOOGLE_MAPS_API_KEY =
serverRuntimeConfig.GOOGLE_MAPS_API_KEY ||
publicRuntimeConfig.GOOGLE_MAPS_API_KEY;
const GooglePlacesScript = () => {
return (
<Head>
<script
type="text/javascript"
src={
"https://maps.googleapis.com/maps/api/js?key=" +
GOOGLE_MAPS_API_KEY +
"&libraries=places"
}
></script>
</Head>
);
};
export default GooglePlacesScript;
_app.js:
import { ThemeProvider } from "styled-components";
import { theme } from "#styles";
import { BaseCSS } from "styled-bootstrap-grid";
import "#fortawesome/fontawesome-svg-core/styles.css";
import "pure-react-carousel/dist/react-carousel.es.css";
import "react-input-range/lib/css/index.css";
import "#styles/_app.css";
import "swiper/swiper-bundle.min.css";
import CartManager from "#components/Cart/CartManager";
import CartContextProvider from "contexts/CartContext";
import AuthContextProvider from "contexts/AuthContext";
import AuthManager from "#components/Auth/AuthManager";
import FavoritesManager from "#components/Favorites/FavoritesManager";
import FavoritesContextProvider from "contexts/FavoritesContext";
import GooglePlacesScript from "#components/GooglePlacesScript";
function MyApp({ Component, pageProps }) {
return (
<ThemeProvider theme={theme}>
<BaseCSS />
<AuthContextProvider>
<AuthManager>
<CartContextProvider>
<CartManager>
<FavoritesContextProvider>
<FavoritesManager>
<GooglePlacesScript />
<Component {...pageProps} />
</FavoritesManager>
</FavoritesContextProvider>
</CartManager>
</CartContextProvider>
</AuthManager>
</AuthContextProvider>
</ThemeProvider>
);
}
export default MyApp;
Solution Edit: Updated GooglePlacesScript code using next/script (Next v11+):
import React from "react";
import getConfig from "next/config";
import Script from "next/script";
const { serverRuntimeConfig, publicRuntimeConfig } = getConfig();
const GOOGLE_MAPS_API_KEY =
serverRuntimeConfig.GOOGLE_MAPS_API_KEY ||
publicRuntimeConfig.GOOGLE_MAPS_API_KEY;
const source = `https://maps.googleapis.com/maps/api/js?key=${GOOGLE_MAPS_API_KEY}&libraries=places`;
const GooglePlacesScript = () => {
return (
<Script type="text/javascript" src={source} strategy="beforeInteractive" />
);
};
export default GooglePlacesScript;
Found Solution: Updated GooglePlacesScript code using next/script (Next v11+):
import React from "react";
import getConfig from "next/config";
import Script from "next/script";
const { serverRuntimeConfig, publicRuntimeConfig } = getConfig();
const GOOGLE_MAPS_API_KEY =
serverRuntimeConfig.GOOGLE_MAPS_API_KEY ||
publicRuntimeConfig.GOOGLE_MAPS_API_KEY;
const source = `https://maps.googleapis.com/maps/api/js?key=${GOOGLE_MAPS_API_KEY}&libraries=places`;
const GooglePlacesScript = () => {
return (
<Script type="text/javascript" src={source} strategy="beforeInteractive" />
);
};
export default GooglePlacesScript;
I would suggest using #googlemaps/react-wrapper.
import { Wrapper } from "#googlemaps/react-wrapper";
const MyApp = () => (
<Wrapper apiKey={"YOUR_API_KEY"}>
<MyMapComponent />
</Wrapper>
);

How to provide context from contextApi for a specific set of routes in nextjs and preserve state with routing through linking?

I am using contextApi with nextjs and I'm having some trouble when providing a context just for certain routes. I am able to make the context available for just a few routes, but when I transition from one to the other through linking, I end up losing the state of my application.
I have three files inside my pages folder:
index.tsx,
Dashboard/index.tsx and
SignIn/index.tsx.
If I import the provider inside the files Dashboard/index.tsx and SignIn/index.tsx and go from one page to the other by pressing a Link component from next/link, the whole state is set back to the initial state.
The content of the Dashboard/index.tsx file
import React from 'react';
import Dashboard from '../../app/views/Dashboard';
import { AuthProvider } from '../../contexts/auth';
const Index: React.FC = () => (
<AuthProvider>
<Dashboard />
</AuthProvider>
);
export default Index;
This is the contend of the SignIn/index.tsx file:
import React from 'react';
import SignIn from '../../app/views/SignIn';
import { AuthProvider } from '../../contexts/auth';
const Index: React.FC = () => (
<AuthProvider>
<SignIn />
</AuthProvider>
);
export default Index;
The views folder is where I create the components that will be rendered.
The content of the file views/SignIn/index.tsx is:
import React, { useContext } from 'react';
import Link from 'next/link';
import { AuthContext } from '../../../contexts/auth';
const SignIn: React.FC = () => {
const { signed, signIn } = useContext(AuthContext);
async function handleSignIn() {
signIn();
}
return (
<div>
<Link href="Dashboard">Go back to Dashboard</Link>
<button onClick={handleSignIn}>Click me</button>
</div>
);
};
export default SignIn;
And the content of the file views/Dashboard/index.tsx is:
import React, { useContext } from 'react';
import Link from 'next/link';
import { AuthContext } from '../../../contexts/auth';
const Dashboard: React.FC = () => {
const { signed, signIn } = useContext(AuthContext);
async function handleSignIn() {
signIn();
}
return (
<div>
<Link href="SignIn">Go back to sign in page</Link>
<button onClick={handleSignIn}>Click me</button>
</div>
);
};
export default Dashboard;
I am able to access the context inside both /Dashboard and /SignIn, but when I press the link, the state comes back to the initial one. I figured out that the whole provider is rerenderized and therefore the new state becomes the initial state, but I wasn't able to go around this issue in a "best practices manner".
If I put the provider inside _app.tsx, I can maintain the state when transitioning between pages, but I end up providing this state to the / route as well, which I am trying to avoid.
I was able to go around this by doing the following, but it really does not seem to be the best solution for me.
I removed the Providers from Pages/SignIn/index.tsx and Pages/Dashboard/index.tsx and used the following snippet for the _app.tsx file:
import React from 'react';
import { AppProps } from 'next/app';
import { useRouter } from 'next/router';
import { AuthProvider } from '../contexts/auth';
const App: React.FC<AppProps> = ({ Component, pageProps }) => {
const router = useRouter();
const AuthProviderRoutes = ['/SignIn', '/Dashboard'];
return (
<>
{AuthProviderRoutes.includes(router.pathname) ? (
<AuthProvider>
<Component {...pageProps} />
</AuthProvider>
) : <Component {...pageProps} />}
</>
);
};
export default App;
Does anyone have a better solution?

Next.js 9+ FOUC (Flash or Unstyled Content) with Styled Components

Have spent a couple days on this issue sourcing solutions and ideas from most of SA and Reddit however to no avail..
Upon load both in production and local every load presents the whole html without any styling before then being injected into DOM..
Currently this is my projects key files:
_document.js
import { ServerStyleSheet } from "styled-components";
import Document, { Main, NextScript } from "next/document";
export default class MyDocument extends Document {
static async getInitialProps(ctx) {
const sheet = new ServerStyleSheet();
const originalRenderPage = ctx.renderPage;
try {
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: (App) => (props) =>
sheet.collectStyles(<App {...props} />),
});
const initialProps = await Document.getInitialProps(ctx);
return {
...initialProps,
styles: (
<>
{initialProps.styles}
{sheet.getStyleElement()}
</>
),
};
} finally {
sheet.seal();
}
}
render() {
return (
<html lang="en">
<Head>
<title>namesjames</title>
<link rel="icon" href="/favicon.ico" />
<script src="/static/chrome-fix.js" />
<link href="/above-the-fold.css" />
</Head>
<body>
<script src="/noflash.js" />
<Main />
<NextScript />
<script> </script>
</body>
</html>
);
}
}
_app.js
/* eslint-disable class-methods-use-this */
import App from "next/app";
import React from "react";
import { ThemeProvider } from "styled-components";
import { ParallaxProvider } from 'react-scroll-parallax';
import Header from "../components/Header";
import theme from "../theme";
import GlobalStyles from "../GlobalStyles";
import DarkModeToggle from '../components/toggle/toggleMode';
import Footer from '../components/Footer'
import LazyLoad from 'react-lazy-load';
import Router from 'next/router';
import styled from 'styled-components'
// import '../style.css'
const Loaded = styled.div`
opacity: ${(props) => props.loaded ? "1" : "0"};
`
export default class MyApp extends App {
state = { isLoading: false, loaded: false }
componentDidMount() {
// Logging to prove _app.js only mounts once,
// but initializing router events here will also accomplishes
// goal of setting state on route change
console.log('MOUNT');
this.setState({loaded: true})
Router.events.on('routeChangeStart', () => {
this.setState({ isLoading: true });
console.log('loading is true, routechangeStart')
});
Router.events.on('routeChangeComplete', () => {
this.setState({ isLoading: false });
console.log('loading is false, routeChangeComplete')
});
Router.events.on('routeChangeError', () => {
this.setState({ isLoading: false });
console.log('loading is false, routeChangeError')
});
}
render(): JSX.Element {
const { isLoading } = this.state;
const { Component, pageProps, router, loaded } = this.props;
return (
<Loaded loaded={this.state.loaded}>
<ThemeProvider theme={theme}>
<GlobalStyles />
<ParallaxProvider>
<Header />
{isLoading && 'STRING OR LOADING COMPONENT HERE...'}
<Component {...pageProps} key={router.route} />
<LazyLoad offsetVertical={500}>
<Footer />
</LazyLoad>
</ParallaxProvider>
<DarkModeToggle />
</ThemeProvider>
</Loaded>
);
}
}
index.js
import { color } from "styled-system";
import { OffWhite } from "../util/tokens";
import Hero from "../components/Hero";
import Banner from '../components/Banner'
import TitleText from '../components/TitleText'
import HomeTitleCopyScene from '../components/HomeTitleCopyScene'
import TwoCards from '../components/TwoCards'
import LazyLoad from 'react-lazy-load';
function Home(): JSX.Element {
return (
<div className="container">
<Banner />
<HomeTitleCopyScene />
<LazyLoad offsetVertical={1000}>
<TwoCards />
</LazyLoad>
</div>
);
}
export default Home;
As some might see I have tried multiple implementations and am just a bit confused as to what it could be at this stage..
Any help appreciated and I can provide more information upon request.. Many thanks
I found two solutions which helped -->
Was to hard style opacity:0 into the JSX and then upon styling injecting into DOM applying opacity: 1 !important onto any of the components displayed..
<section className="cards-block" style={{opacity:0}}>
Whilst this was effective this morning I discovered at some point during my development I had incorrectly imported Head from next/head and used this in my _document.js rather than using the correct Head from next/documents..
// import Head from "next/head"; --> incorrect
import { ServerStyleSheet } from "styled-components";
import Document, { Head, Main, NextScript } from "next/document";
Ergo -> a correctly rendering and injected element with no FOUC
Hope this helps someone out there
I've found a workaround for my small portfolio project:
just included this inline css to a <head> of my custom _document.js:
{<style dangerouslySetInnerHTML={{__html: `
html {background: #333}
body #__next div {visibility: hidden}
body.loaded #__next div {visibility: visible}
`}}></style>}
The "loaded" class is added to the body in _app.js:
if (process.browser) {
document.body.classList.add("loaded")
}
I'm hot sure if it's a good solution, any advice would be appreciated :)

Resources