Skip to content

Commit

Permalink
Merge pull request wso2#1 from DonOmalVindula/nextjs-qsg
Browse files Browse the repository at this point in the history
Update docs to use server components in user profile
  • Loading branch information
sagara-gunathunga authored Dec 4, 2024
2 parents bc79608 + cdcd279 commit 3b3be4d
Show file tree
Hide file tree
Showing 5 changed files with 225 additions and 7 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
template: templates/complete-guide.html
heading: Display logged-in user details
read_time: 2 min
read_time: 4 min
---

At this point, we’ve successfully implemented login and logout capabilities using the Asgardeo provider for Auth.js. The next step is to explore how to access and display logged-in user details within the app utilizing the callbacks provided by auth.js library. To retrieve user information from the ID token provided by Asgardeo, the simplest approach is to use the JWT (JSON Web Token) returned during authentication. In auth.js, you can leverage the JWT callback function to access and manipulate this token. The JWT callback is triggered whenever a JWT is created or updated (e.g., at sign-in), making it a great place to include the user's information
Expand Down Expand Up @@ -165,9 +165,180 @@ Then, you can update `page.tsx` as given below to display the above user attribu

```
!!! Tip
If you don’t get any value for given_name and family_name, it might be because you have not added these values when creating the user in Asgardeo. You can add these values either using the **Asgardeo console** or logging into the **My Account** of that particular user.
## Displaying user details in the server side
Using the above information from the `session` object. Let's create a `ServerProfile` server component to display the user details. To do this, create a file `/src/app/server-profile/page.tsx` as follows.
```javascript title="/src/app/server-profile/page.tsx"
import { auth } from "@/auth";
import { SignOutButton } from "@/components/sign-out-button";
import { redirect } from "next/navigation";

interface UserDetails {
emails: string[];
name: {
givenName: string;
familyName: string;
};
}

const fetchUserDetails = async (accessToken: string): Promise<UserDetails> => {
try {
const response = await fetch(process.env.NEXT_PUBLIC_AUTH_ASGARDEO_ME_ENDPOINT as string, {
method: "GET",
headers: {
Accept: "application/scim+json",
"Content-Type": "application/scim+json",
Authorization: `Bearer ${accessToken}`,
},
});

if (!response.ok) {
throw new Error("Failed to fetch protected data");
}

return response.json();
} catch (error) {
console.error("Error fetching protected data:", error);
throw error;
}
};

const ServerProfile = async () => {
const session = await auth();

if (!session || !session.user || !session.user.access_token) {
return;
}

let userDetails: UserDetails;

try {
userDetails = await fetchUserDetails(session.user.access_token);
} catch {
return (
<div className="h-screen w-full flex items-center justify-center">
<h1>Failed to fetch user details</h1>
</div>
);
}

const goToIndex = async () => {
"use server";
redirect("/");
};

return (
<div className="h-screen w-full flex flex-col items-center justify-center">
<h1 className="mb-5">Profile Page</h1>
<p>Email: {userDetails.emails?.[0]}</p>
<p>First Name: {userDetails.name?.givenName}</p>
<p>Last Name: {userDetails.name?.familyName}</p>
<form action={goToIndex}>
<button
type="submit"
className="rounded-full border border-solid flex items-center justify-center text-sm h-10 px-4 mt-3"
>
Go to index page
</button>
</form>
<div className="mt-5">
<SignOutButton />
</div>
</div>
);
};

export default ServerProfile;
```
This component is fully server-side rendered and will fetch the user details from the Asgardeo server. The `fetchUserDetails` function is used to fetch the user details from the Asgardeo server using the access token. The `ServerProfile` component will display the user details if the user is logged in. If the user is not logged in, the component will display an error message.
When a user is logged in and if your visit **http://localhost:3000/server-profile**, the following content should be visible:
![Profile screen (server component)]({{base_path}}/complete-guides/nextjs/assets/img/image23.png){: width="800" style="display: block; margin: 0;"}
## Displaying user details in the client side
In previous steps we used session data and retrieved current user information using the session object in the `auth()` function provided by the Auth.js library. What if we wanted to do the same in the client-side? As we can have both client and server components in Next.js, it is important to have both as we want to secure both components using authentication with Next.js and Asgardeo.
The approach is very similar to server-side components. To demonstrate this, let’s create a user profile component in our application. To get session information in the client-side, you can use the `useSession()` hook offered by Auth.js. Now using this hook, let's create a file `/src/app/client-profile/page.tsx` as follows.
```javascript title="/src/app/client-profile/page.tsx"
"use client";

import { SignOutButton } from "@/components/sign-out-button";
import { useSession } from "next-auth/react";

export default function Profile() {
const { data: session } = useSession()

if (!session) {
return (
<div className="h-screen w-full flex items-center justify-center">
<h1>You need to sign in to view this page</h1>
</div>
);
}

return (
<div className="h-screen w-full flex flex-col items-center justify-center">
<h1 className="mb-5">Profile Page</h1>
<p>Email : {session?.user?.email}</p>
<p>First Name : {session?.user?.given_name}</p>
<p>Last Name : {session?.user?.family_name}</p>
<div className="mt-5">
<SignOutButton />
</div>
</div>
);
}

```
Since we are accessing the hooks provided by the Auth.js, it is important to wrap the whole application using the `<SessionProvider/>` provider. This can be achieved by wrapping the `/src/app/layout.tsx` file as it is the entry point of the application.
```javascript title="/src/app/profile/page.tsx" hl_lines="14-16"
import { SessionProvider } from "next-auth/react";

...
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<SessionProvider>
{children}
</SessionProvider>
</body>
</html>
);
}

```
!!! note
This a good time to remove the `<SessionProvider/>` we added to the `/src/app/page.tsx` in previous steps as this is no longer required.
When a user is logged in and if your visit http://localhost:3000/client-profile, the following content should be visible:
![Profile screen (client component)]({{base_path}}/complete-guides/nextjs/assets/img/image21.png){: width="800" style="display: block; margin: 0;"}
When a user is not logged in, it should look as follows:
![Profile screen (Not logged in)]({{base_path}}/complete-guides/nextjs/assets/img/image22.png){: width="800" style="display: block; margin: 0;"}
In this step, we further improved our Next.js app to display the user attributes. As the next step, we will try to secure routes within the app.
Original file line number Diff line number Diff line change
@@ -1,14 +1,61 @@
---
template: templates/complete-guide.html
heading: Securing Routes within the app
read_time: 2 min
read_time: 4 min
---
Assume we have a `<Profile/>` component that should only be accessible when a user is logged in. Because if a valid user is not in the session, there is no point of showing an empty profile page. Therefore we need to secure the route: http://localhost:3000/profile itself. This can vary depending on the router that you are using. In this example, we will be using the Next router to demonstrate how to secure a route using auth.js.

The `<Profile/>` components (server-side and client-side) we developed in the previous step should only be accessible when a user is already logged in. Because if a valid user is not in the session, there is no point of showing an empty profile page. Therefore we need to secure the routes: **http://localhost:3000/client-profile** and **http://localhost:3000/server-profile**. In this example, we will be demonstrating how to secure a route using Auth.js in both server-side and client-side.

## Create a Higher-Order Component (HOC) - withProtectedRoute
## Securing the Server-Side Components

A higher-order component in React is a function that takes a component and returns a new component. The HOC `withProtectedRoute` will check if a user is authenticated and either render the component or redirect the user to the login page.
### Update middleware to secure the server-side route

Recall that we created a `middleware.ts` file in the root of the `src` directory when configuring Auth.js? We will now update this file with the following configuration to securet the `/server-profile` route as follows.

```javascript title="src/middleware.ts" hl_lines="4"
export { auth as middleware } from "@/auth"

export const config = {
matcher: ["/server-profile"]
};
```

By defining a `matcher` configuration, we can control which routes trigger the middleware. For example, with a matcher set to ["/server-profile"], the middleware only runs for requests to **/server-profile**. This approach centralizes authentication logic while keeping other routes unaffected.

### Handle redirection using callbacks

Using the `authorized` callback in Auth.js, we can ensure that only authenticated users can access protected routes.

```javascript title="src/auth.ts"
...
export const { handlers, signIn, signOut, auth } = NextAuth({
...
callbacks: {
...
authorized: async ({ request, auth }) => {
// Logged in users are authenticated, otherwise redirect to index page
console.log({ request, auth });

if (!auth) {
return Response.redirect(new URL("/", request.nextUrl))
}

return !!auth
},
}
})
```

This callback checks the auth parameter, which holds the user's authentication data. If the user is not authenticated (auth is null or undefined), they are redirected to the login page (/) using `Response.redirect()`. For authenticated users, the function returns true, allowing access.

Now verify that you cannot access http://localhost:3000/server-profile URL when you are not logged in. You will be redirected to http://localhost:3000 if you do not have a valid user logged in.


## Securing the Client-Side Components

### Create a Higher-Order Component (HOC) - withProtectedRoute

A higher-order component in React is a function that takes a component and returns a new component (with additional functionality). The HOC `withProtectedRoute` will check if a user is authenticated and either render the component or redirect the user to the login page.

This can be achieved by using the status object in the `useSession()` hook provided by Auth.js. The status object can have three values depending on the authenticated state.

Expand Down Expand Up @@ -97,7 +144,7 @@ export default withProtectedRoute(Profile);

```
Now verify that you cannot access http://localhost:3000/profile URL when you are not logged in. You will be redirected to http://localhost:3000 if you do not have a valid user logged in.
Now verify that you cannot access http://localhost:3000/client-profile URL when you are not logged in. You will be redirected to http://localhost:3000 if you do not have a valid user logged in.
In this step, we looked into how to secure component routes within a Next.js app. Next, we will try to access a protected API from our Next.js app, which is a common requirement.
Expand Down

0 comments on commit 3b3be4d

Please sign in to comment.