is
2022/08/22
 
22 Aug, 2022 | 3 min read

WSO2 Identity Server User Authentication in a Next.js Application

  • Dimuthu Kasun
  • Associate Software Engineer - WSO2

Next.js is an open source React web development framework built on top of Node.js. 

The Next.js framework is known for:

  • SEO friendliness
  • Quick time to market
  • Easy development of applications with server-side rendering, static-site generation,client-side rendering, and incremental static-regeneration.

With the help of the NextAuth.js library, I will explain how to authenticate a user in a Next.js application with WSO2 Identity Server. NextAuth is an open source authentication solution for Next.js applications. More information about NextAuth.js is available here.

    I'll use my existing Next.js application to show how NextAuth.js integrates with WSO2 Identity Server and user authentication flow. 

    You can find the Next.js template I used to create the above sample application here.

    This application only has one page, which serves as the landing page, and NextAuth.js is not integrated. I'll use this as an example, and you can use the Next.js application sample from above to follow the instructions in this article.

    1. Create a service provider in the WSO2 Identity Server management console.

    In order to start an authentication flow with WSO2 Identity Server using the authorization_code grant type, we must first create a service provider in the WSO2 Identity Server management console.

    • Download the most recent version of WSO2 Identity Server.
    • Extract the "ZIP Archive" after downloading it. Go to <extracted_path>/bin/ directory and start the identity server.

     ./wso2server.sh 

  • Access the management console application at  https://localhost:9443/carbon  in the browser. You can log in to the application using the super admin credentials.

username : admin
password : admin

  • Create a new service provider.
  • Enter a name for the service provider, and click Register.
  • You will then be redirected to the new service provider page.
  • Expand the “Inbound Authentication Configuration”. Then expand the “OAuth/OpenID Connect Configuration” and click Configure to add the OAuth/OpenID Connect protocol configuration to the service provider.
  • Add the below URL as the Callback Url, and this is required for the NextAuth.js as the callback. More information about this callback URL can be found here.

https://{host}/api/auth/callback/wso2is
Eg: https://localhost:3000/api/auth/callback/wso2is

  • Click Add at the bottom of the page.
  • You will be redirected to the previous application page. You can find the client_id and client_secret in the “OAuth/OpenID Connect Configuration” section in Inbound Authentication Configurations.

    2. Create New User

    This step can be skipped, and you can return to it once you've completed the others. Once we've finished all the steps, we can create a new user to authenticate. Since we can use our super admin credentials to authenticate after the integration, this can also be thought of as an additional step.

    • In the Users and Roles section of the left side pane, click Add.
    • Click the Add New User button.
    • Fill in the required fields and then click Finish.

    3. Allow CORS Requests

    As our application is running on  https://localhost:3000 , WSO2 Identity Server will automatically block the API requests (SCIM/Me API requests). To allow CORS requests from our Next.js application, add our application origin  https://localhost:3000  to allowed_origins in  <identity_server_extracted_path>/repository/conf/deployment.toml  file.

    Add the following configuration to the  deployment.toml  file:

    [cors]
    allow_generic_http_requests = true
    allow_any_origin = false
    allowed_origins = [
    "https://localhost:3000"
    ]
    allow_subdomains = false
    supported_methods = [
    "GET",
    "POST",
    "HEAD",
    "OPTIONS"
    ]
    support_any_header = true
    supported_headers = []
    exposed_headers = []
    supports_credentials = true
    max_age = 3600
    tag_requests = false

    • Restart WSO2 Identity Server after saving the file.

    4. Integrate the Next.js application with NextAuth.js

    Now that the sample application has been set up, we need to integrate the NextAuth.js library, which we're using for authentication, into our application.

    • Install next-auth dependency. Go to the project directory and run one of the commands below.

     yarn add next-auth or npm i next-auth 

    • Append the following properties to the  .env.local  file.
    • ClientID and ClientSecret can be found in the OAuth/OpenID Connect Configuration section of the application page in the management console that we created in the first step.
    • The internal_login scope is required to invoke the SCIM/Me endpoint which we will use in this application.
    • Since we didn't create any new tenants, the super tenant ( carbon.super ) will be the default.

    WSO2IS_CLIENT_ID=<client_id_of_the_appliaction>
    WSO2IS_CLIENT_SECRET=<client_secret_of_the_appliaction>
    WSO2IS_SCOPES=openid internal_login
    WSO2IS_HOST=<https://host:port>
    WSO2IS_TENANT_NAME=carbon.super

    • Create a file [...nextauth].tsx in /pages/api/auth directory. This file contains all global NextAuth.js configurations. All the requests that come to /api/auth/* (sign in, sign out requests) will be handled by NextAuth.js

    import NextAuth from "next-auth"

    export default NextAuth({

    providers: [

    {
    id: "wso2is",
    name: "WSO2IS",
    clientId: process.env.WSO2IS_CLIENT_ID,
    clientSecret: process.env.WSO2IS_CLIENT_SECRET,
    type: "oauth",
    wellKnown: process.env.WSO2IS_HOST + "/t/" + process.env.WSO2IS_TENANT_NAME + "/oauth2/token/.well-known/openid-configuration",
    authorization: { params: { scope: process.env.WSO2IS_SCOPES } },
    profile(profile) {
    return {
    id: profile.sub
    }
    },
    },
    ],
    secret: process.env.SECRET,
    callbacks: {
    async jwt({ token, user, account, profile, isNewUser }) {
    if (account) {
    token.accessToken = account.access_token
    token.idToken = account.id_token
    }
    return token
    },
    async session({ session, token, user }) {
    session.accessToken = token.accessToken
    session.idToken = token.idToken
    return session
    }
    },
    debug: true,
    })

    Providers: This is where we'll configure all of the identity providers we'll be using in our application. Of course, this is also the place where we need to configure WSO2 Identity Server.

    More information about custom providers can be found here.

    Callbacks: We can use callbacks to manage what needs to happen when a specific action is performed (sign in, sign out). We can use session and jwt callbacks to retrieve access_token and id_token from the Account model. The Account model can be used to get information about OAuth accounts associated with a user.

    As you can see, in the above code snippet, what we have done is attach accessToken and idToken values with a session callback, which is retrieved from the account object in the jwt callback.

    You can find out more information about callbacks and models by using the links below:Callbacks | NextAuth.js

    Models | NextAuth.js

    Now that we've successfully added NextAuth.js to our Next.js app, we can use WSO2 Identity Server to verify a user's identity.

    5. Implement sign in, sign out, and API invocation

    • Implement “Login with Identity Server” button functionality

    Open/components/header/index.tsx file and add Login button onClick event functionality as below.

    import { signIn } from "next-auth/react"

    <Button
    onClick={(e) => {
    e.preventDefault()
    signIn("wso2is", { callbackUrl: "/home" })
    }}
    primary>
    Login with Identity Server
    </Button>

    Here, we can initiate the sign-in flow by simply calling the signIn method in NextAuth.js.

    When you call the signIn () method without any arguments, you will be redirected to the NextAuth.js sign-in page, which will list all of the providers you have configured. If you want to skip that and get redirected to authenticate with a specific provider immediately, call the signIn() method with the provider's id. Here, the ID will be wso2is .

    If you enter the callbackUrl argument, NextAuth will automatically go to that path when the signIn flow is done. I have added the /home path as callbackUrl . The next step would be to implement our home page.

    • Implement Home page

    Create the file home.tsx in the /pages directory. As you may know, Next.js has a file-system-based router built on the concept of pages. So the route for our home page will be /home .

    import { useSession, signIn, getSession, signOut } from 'next-auth/react'
    import { useState, useEffect } from 'react'
    import jwt from "jwt-decode"
    import { tw } from 'twind'
    import dynamic from 'next/dynamic'
    import Button from '@/../components/button';

    export default function Page({ session, tenant, host }) {
    const [content, setContent] = useState(null)
    const [id_token, setIDToken] = useState()
    const JsonViewer = dynamic(
    () => import("../components/JsonViewer"),
    { ssr: false }
    )

    useEffect(() => {
    if (!session) {
    signIn("wso2is", { callbackUrl: "/home" })
    } else {
    const { accessToken, idToken } = session
    setIDToken(idToken)

    const res = fetch(host + "/t/" + tenant + "/scim2/Me", {
    method: 'get',
    headers: new Headers({
    "authorization": "Bearer " + accessToken
    })
    }).then(r => r.json().then(data => ({ status: r.status, body: data })))
    .then(res => {
    setContent(res)
    }).catch(err => {
    signOut({ callbackUrl: "/" })
    })
    }

    }, [])
    if (session) {
    if (content) {
    return (
    <main style={{ width: "800px", marginLeft: "auto", marginRight: "auto" }}>
    <p className={tw(`mt-2 text-5xl lg:text-3xl font-bold tracking-tight text-gray-900`)} style={{ marginTop: '70px', marginBottom: "50px", textAlign: 'center' }}>
    Decoded Id Token
    </p>
    <JsonViewer
    src={jwt(id_token)}
    name={null}
    enableClipboard={false}
    displayObjectSize={false}
    displayDataTypes={false}
    iconStyle="square"
    theme="monokai"
    />

    <p className={tw(`mt-2 text-5xl lg:text-3xl font-bold tracking-tight text-gray-900`)} style={{ marginTop: '70px', marginBottom: "50px", textAlign: 'center' }}>
    SCIM 2.0 /Me Endpoint Response
    </p>
    <JsonViewer
    src={content}
    name={null}
    enableClipboard={false}
    displayObjectSize={false}
    displayDataTypes={false}
    iconStyle="square"
    theme="monokai"
    />
    <div style={{ textAlign: 'center' }}>
    <Button
    onClick={(e) => {
    e.preventDefault()
    signOut({ callbackUrl: "/" })
    }}
    style={{ marginTop: "30px", marginBottom: "40px" }}
    primary>Logout</Button>
    </div>

    </main>
    )
    } else {
    return (
    <h1 className={tw(`mt-2 text-5xl lg:text-3xl font-bold tracking-tight text-gray-900`)} style={{ marginTop: '100px', textAlign: 'center' }}>
    Loading...
    </h1>
    )
    }

    } else {
    return (
    <h1 className={tw(`mt-2 text-5xl lg:text-3xl font-bold tracking-tight text-gray-900`)} style={{ marginTop: '100px', textAlign: 'center' }}>
    Access Denied! Redirecting to Login...
    </h1>
    )
    }
    }

    // Export the `session` prop to use sessions with Server Side Rendering
    export async function getServerSideProps(context) {
    return {
    props: {
    session: await getSession(context),
    tenant: process.env.WSO2IS_TENANT_NAME,
    host: process.env.WSO2IS_HOST
    }
    }
    }

    getServerSideProps: This is the asynchronous function that we can export to enable server-side rendering in the Next.js application. Next.js will pre-render this page on each request using the data returned by getServerSideProps. Here, I have returned session, host, and tenant name values as props.

    • Why do we use the session that is returned by getServerSideProps?- If you refresh the home page in a browser or use the usesession () hook to retrieve a session, an error will appear. With the session returned by  getServerSideProps , it will always assign a value to the session on every request before rendering the page.
    • Why do we use the values of org and host instead of directly accessing process.env.WSO2IS_TENANT_NAME and process.env.WSO2IS_HOST from the client-side?- If we need to access variables on the client-side in  Next.js , we need to configure them in the  next.config.js  file. By adding the variables to next.config.js, this will expose the secrets to the client-side, and anyone can see these secrets. As the tenant name and host can vary from environment to environment, it would be better to store all the variables in one place. I chose to store them in  env.*  .The next problem is that we can’t access the variables in the  env.*  file from the client-side in Next.js using  process.env.WSO2IS_TENANT_NAME  or  process.env.WSO2IS_HOST . So, we need to access them from the server-side and pass them to the client-side.
    • Implement SignOut functionality.

    As you can see on the home page above (/pages/home.tsx), the following is the log out button onClick event functionality.

    import { signOut } from "next-auth/react"

    <Button
    onClick={(e) => {
    e.preventDefault()
    signOut({ callbackUrl: "/" })
    }}
    primary>
    Logout
    </Button>

    Here, we can initiate signOut flow by simply calling the signOut method in NextAuth.js.When calling the signOut() method with no arguments, the session will be deleted, it won’t be redirected to any page, and it will stay in the same path(`/home`). Specifying the callbackUrl, you will be redirected to the entered callback URL after the session is deleted.

    • Invoke SCIM2 /Me Endpoint

    We can invoke SCIM2 API endpoints in WSO2 Identity Server using the accessToken returned after a successful login.

    AccessToken is stored in the Account model object and with the callbacks that I have implemented in [...nextauth].tsx. We can access them via session as show below.

    export default function Page({ session,org }) {

    const { accessToken, idToken } = session

    }

    export async function getServerSideProps(context) {
    return {
    props: {
    session: await getSession(context),
    org:process.env.WSO2_TENANT_NAME
    }
    }
    }

    Now, we can invoke SCIM2 /Me endpoint with this accessToken.

    const res = fetch(host+"/t/"+org+"/scim2/Me", { method:
    'get',
    headers: new Headers({
    "authorization": "Bearer " + accessToken
    })
    })
    .then(r => r.json().then(data => ({
    status: r.status, body: data })))
    .then(res => {
    console.log("API Response"+JSON.stringify(res))
    })

    6. Run the Application

    So far, we have completed:

    • Creating a service provider in the WSO2 Identity Server management console
    • Creating a new user in the Identity Server
    • Configuring Identity Server to allow CORS requests from Next.js applications
    • NextAuth.js Integration with our Next.js application
    • Configure WSO2 Identity Server as a provider in NextAuth.js
    • SignIn flow
    • SignOut flow
    • API Invocation

    The next step is to run the application.

     yarn dev or npm run dev 

    As you can see in the package.json file,  "dev":"NODE_TLS_REJECT_UNAUTHORIZED='0' next"  is the command that will run with any of the above commands.

    • Why is  NODE_TLS_REJECT_UNAUTHORIZED='0'  needed for running the application?This is because the downloadable version of the WSO2 Identity server comes with a self-signed certificate and it causes an error in the NextAuth.js authentication flow. So, I have used this command as a small hack. This is not recommended for production and you should use CA-signed certificates in WSO2 Identity Server for the production environment.
    • You open the application by accessing  https://localhost:3000  in the browser. You can log in with the user that we created earlier. You can also log in with the super admin credentials. Click Continue.
    • Give consent to access the user’s information from our NextJS application.
    • You will then be able to see the homepage that we implemented earlier with the decoded id token information and SCIM/Me API response.
    • If you try to log out, it will be successful and you will be redirected to the landing page of the application.

    I hope this article helped you understand how to use the NextAuth.js library to authenticate users with WSO2 Identity Server in a Next.js application, as well as how to invoke the WSO2 Identity Server APIs using an access token that has the necessary permissions.

    Here you can find a sample Next.js app that has all of the features described in this article.

      If this sounds interesting, we encourage you to try out the early adopter version of Asgardeo, an identity as a service (IDaaS) solution that enables developers without security expertise to easily embed customer identity and access management (CIAM) features into their apps within minutes.

      You can also follow us on Twitter or join the IAM4Devs community. Alternatively, if you’re looking for an enterprise-grade, API-driven, open source solution that can manage millions of user identities without spiraling costs, please check out WSO2 Identity Server.

English