Add Keycloak authorization in NextJs application (react-keycloak/ssr) - javascript

What is the correct way to add keycloak authentication to page component which is server side rendered (using getServerSideProps). I want to ensure that getServerSideProps() method wont be executed if customer is not allowed to view page.
I tried to check if user is authorized inside getServerSideProps() -> it always returns that keycloak.login is not a function.
export const getServerSideProps: GetServerSideProps = async (context:
GetServerSidePropsContext & AppContext) => {
let keycloak = getKeycloakInstance(keycloakConfig,
SSRCookies(parseCookies(context.ctx?.req)))
if (!keycloak?.authenticated) {
keycloak.login();
}
...
Second approach is to add keycloak inside component -> again when I try to call keycloak.login(), same error apears like on first approch (keycloak.login is not a function), but when I test it with console.log(keycloak), then that keycloak object is there with all methods inside it.
import {useKeycloak} from "#react-keycloak/ssr";
import {KeycloakInstance} from "keycloak-js";
...
const ProductDetailPage: React.FC<{
errorCode: number | null;
product: Product; }> = ({ errorCode, product }): ReactElement => {
const { keycloak } = useKeycloak<KeycloakInstance>();
if (!keycloak?.authenticated) {
keycloak.login();
}
...
Conclusion:
When I move keycloak.login() inside useEffect() in second approach, it works and customer is redirected to login but the problem is that page is still shown for a second before redirect. I want to ensure that getServerSideProps() method wont call api and fetch all data for component if customer is not allowed to access that page and that customer is immediatelly redirected to login if tries to access non-public page.

Related

TypeError: X is not a function when calling custom hook

I'm trying to write a simple login page. I want to pass the token generated from the login component to my auth component which will set context, and then send the user back to the home page after login. I'm running into an issue trying to call the useAuthContext hook inside the handleLogin element. Here's I think the relevant code:
const authState = useAuthState();
const handleLogin = accessToken => {
console.log(accessToken);
authState({ accessToken });
history.push("/");
}
handleLogin gets called with the onSuccess handler in the login component.
I get an error that authState is not a function.
I've looked at code samples that seem to work just like this when needing to call a hook inside an arrow function, so not sure what I'm doing wrong.
Edit: Problem is staring at me in the face.
authState is not a value under my Context Provider. Problem solved.

Is there a way to do a login redirect immediately when the SPA is accessed?

I have created an SPA. Refer to the docs.
So, accessing the SPA and pressing the [SignIn] button will take you to the login screen. And if the authentication is successful, you will be redirected to SPA again.This is a common process.
However, I would like to see the login page from the moment I access the SPA.
In other words, we want to display the login page from the beginning without pressing the [SignIN] button and without firing the onclick event.
In the sample, the following process is executed after the button is clicked.
function signIn() {
myMSALObj.loginRedirect(loginRequest);
}
Is it possible to execute this function at the moment the URL is opened and display the login screen to the user?
I found out that there is something called "onload" and tried to use it, but it did not work.
Thank you.
It seems you want your entire application protected by Azure AD authentication. If that's the case, then what you can do is wrap your entire application inside <MsalAuthenticationTemplate>.
This component will ensure that a user must be signed-in before accessing the application.
Here's a code sample taken from this link:
import React from "react";
import { MsalAuthenticationTemplate } from "#azure/msal-react";
import { InteractionType } from "#azure/msal-browser";
function ErrorComponent({error}) {
return <p>An Error Occurred: {error}</p>;
}
function LoadingComponent() {
return <p>Authentication in progress...</p>;
}
export function Example() {
const authRequest = {
scopes: ["openid", "profile"]
};
return (
// authenticationRequest, errorComponent and loadingComponent props are optional
<MsalAuthenticationTemplate
interactionType={InteractionType.Popup}
authenticationRequest={authRequest}
errorComponent={ErrorComponent}
loadingComponent={LoadingComponent}
>
<p>At least one account is signed in!</p>
</MsalAuthenticationTemplate>
)
};

How to fetch the API details from a component folder using Next.js?

I have a reusable component that I have created on the components folder where I have all the details from the user that logs in to the system which is a header section.
I am trying to use getInitialProps using fetch with isomorphic-unfetch.
static async getInitialProps(ctx) {
const UserdetailsRespone = await fetch("my API url");
const UserdetailsJson = await UserdetailsRespone.json();
return { UserDetails: UserdetailsJson.root[0] };
}
In the render method when I log this.props.UserDetails I get undefined.
It is the same API fetch as the one I am doing in the pages folder where I am able to fetch an API response. But I am not able to fetch the response in the components folder.
Can someone help me to solve it?
Thanks in Advance.
getInitialProps runs server side and client side. So you will have to be careful how you use it. I usually use typeof Window === 'undefined' to check whether getInitialProps is being called server side or not.
If you have a child component that you need to make a call everytime it is mounted, why not stick with componentDidMount?
async componentDidMount() {
const userDetailsResponse = await fetch("my API url");
const userDetailsJson = await userDetailsResponse.json();
this.setState({userDetails: userDetailsJson})
}
You can access the properties from state instead of props then.
If you're using getInitialProps in a child component, it won't work. It only works in the default export of every page.
From official docs
Edit:
As mentioned by Uzair Ashraf, using fetch is the way to go for fetching data in components. You can take a look at swr too.

Next/React-Apollo: React props not hooked up to apollo cache when query comes from getInitialProps

I'm using nextjs and react-apollo (with hooks). I am trying to update the user object in the apollo cache after a mutation (I don't want to refetch). What is happening is that the user seems to be getting updated in the cache just fine but the user object that the component uses is not getting updated. Here is the relevant code:
The page:
// pages/index.js
...
const Page = ({ user }) => {
return <MyPage user={user} />;
};
Page.getInitialProps = async (context) => {
const { apolloClient } = context;
const user = await apolloClient.query({ query: GetUser }).then(({ data: { user } }) => user);
return { user };
};
export default Page;
And the component:
// components/MyPage.jsx
...
export default ({ user }) => {
const [toggleActive] = useMutation(ToggleActive, {
variables: { id: user.id },
update: proxy => {
const currentData = proxy.readQuery({ query: GetUser });
if (!currentData || !currentData.user) {
return;
}
console.log('user active in update:', currentData.user.isActive);
proxy.writeQuery({
query: GetUser,
data: {
...currentData,
user: {
...currentData.user,
isActive: !currentData.user.isActive
}
}
});
}
});
console.log('user active status:', user.isActive);
return <button onClick={toggleActive}>Toggle active</button>;
};
When I continuously press the button, the console log in the update function shows the user active status as flipping back and forth, so it seems that the apollo cache is getting updated properly. However, the console log in the component always shows the same status value.
I don't see this problem happening with any other apollo cache updates that I'm doing where the data object that the component uses is acquired in the component using the useQuery hook (i.e. not from a query in getInitialProps).
Note that my ssr setup for apollo is very similar to the official nextjs example: https://github.com/zeit/next.js/tree/canary/examples/with-apollo
The issue is that you're calling the client's query method. This method simply makes a request to the server and returns a Promise that resolves to the response. So getInitialProps is called before the page is rendered, query is called, the Promise resolves and you pass the resulting user object down to your page component as a prop. An update to your cache will not trigger getInitialProps to be ran again (although I believe navigating away and navigating back should), so the user prop will never change after the initial render.
If you want to subscribe to changes in your cache, instead of using the query method and getInitialProps, you should use the useQuery hook. You could also use the Query component or the graphql HOC to the same effect, although both of these are now deprecated in favor of the new hooks API.
export default () => {
const { data: { user } = {} } = useQuery(GetUser)
const [toggleActive] = useMutation(ToggleActive, { ... })
...
})
The getDataFromTree method (combined with setting the initial cache state) used in the boilerplate code ensures that any queries fetched for your page with the useQuery hook are ran before the page render, added to your cache and used for the actual server-side rendering.
useQuery utilizes the client's watchQuery method to create an observable which updates on changes to the cache. As a result, after the component is initially rendered server-side, any changes to the cache on the client-side will trigger a rerender of the component.

How can I use getInitialProps only during the NextJS site build?

When using NextJS to build a static site, I would like the getInitialProps method to fire only during the build step and not on the client.
In the build step, NextJS runs the getInitialProps method before each component's rendered HTML is used to generate the page's static HTML. On the client, NextJS also runs this method before the page component is rendered in order to return the necessary props for the component. Thus, large requests can delay the client's first paint as this is a blocking request.
// example usage of API call in getInitialProps
import fetch from 'isomorphic-unfetch'
function Page({ stars }) {
return <div>Next stars: {stars}</div>
}
Page.getInitialProps = async ({ req }) => {
const res = await fetch('https://api.github.com/repos/zeit/next.js')
const json = await res.json()
return { stars: json.stargazers_count }
}
export default Page
I'm unwilling to move my slow API request to componentDidMount in order to avoid the blocking request because I want to use the data returned during the build step to populate the static HTML, and this particular request doesn't need to be dynamic or updated after the build.
Is there a way I can make getInitialProps run only when next export builds and not as the client loads the page?
Is this good practice?
I found the workaround with NextJs 9.0.3 (other versions may also work, I didn't test that)
// XXXPage is your page
XXXPage.getInitialProps = async (req) => {
if (process.browser) {
return __NEXT_DATA__.props.pageProps;
}
// original logic
}
For version 9.3 or newer, it's recommended that you use getStaticProps for providing static build props.
export async function getStaticProps(context) {
return {
props: {}, // will be passed to the page component as props
}
}
Old answer
There are two ways is one way that I've found to prevent code in getInitialProps from running on a page component load.
1. Use a regular anchor tag without next/link to that page.
getInitialProps only runs when the page is linked from a next/link component. If a regular JSX anchor click me is used instead, the component's getInitialProps will not be invoked. Direct page loads to NextJS static site pages will not invoke getInitialProps.
Note that using a standard anchor instead of the next/link component will cause a full page refresh.
Because this is a poor solution, I've submitted a feature request.
2. Use req in the context argument to conditionally make the API call in getInitialProps.
I believe what #evgenifotia wanted to convey is that req is undefined in a site that's been exported.
// example usage of API call in getInitialProps
import fetch from 'isomorphic-unfetch'
function Page({ stars }) {
return <div>Next stars: {stars}</div>
}
Page.getInitialProps = async (ctx) => {
const { req } = ctx // context object: { req, res, pathname, query, asPath }
if (req) { // will only run during the build (next export)
const res = await fetch('https://api.github.com/repos/zeit/next.js')
const json = await res.json()
return { stars: json.stargazers_count }
}
return {}
}
export default Page
For more information about getInitialProps, see the documentation. One example there confirms that req is expected to only be defined on the server (or during the exporting build):
const userAgent = req ? req.headers['user-agent'] : navigator.userAgent`
This second option may work for some scenarios, but not mine where returning an empty result from getInitialProps will affect the component's this.props.
Note:
Shallow routing is not the answer. According to the documentation (see under "Notes" section):
Shallow routing works only for same page URL changes.
A more detailed and updated version as of the accepted answer:
const isInBroswer = typeof window !== 'undefined';
if (isInBroswer) {
const appCustomPropsString =
document.getElementById('__NEXT_DATA__')?.innerHTML;
if (!appCustomPropsString) {
throw new Error(`__NEXT_DATA__ script was not found`);
}
const appCustomProps = JSON.parse(appCustomPropsString).props;
return appCustomProps;
}

Categories

Resources