0

I am currently using the credentials provider to verify a user on an LDAP. I am also using Hasura as a backend which requires certain claims in the jwt. So a custom encode and decode function is required. This is where i run into my issues. If i used the default encoding for Next-Auth everything works and I am able to log into my app. When i create my own encode and decode function i cannot get past the login screen. I am also using the NextJs middleware to ensure a valid session is present.

Here is my[...nextauth].ts file

export default NextAuth({
  providers: [
    CredentialsProvider({
      id: 'credentials',
      name: 'credentials',
      credentials: {
        username: { label: 'Username', type: 'text', placeholder: '' },
        password: { label: 'Password', type: 'password' },
      },
      async authorize(credentials, req) {
        console.log(
          `Authorize Function called with creds - ${JSON.stringify(
            credentials
          )}`
        )
        //Developement Authentication
        if (process.env.RUNNING_ENV == 'DEVELOPMENT')
          return {
            id: process.env.DEV_USER as string,
            group: process.env.DEV_GROUP as string,
          }
        // const { username, password } = credentials
        const username = credentials?.username
        const password = credentials?.password
        if (!username || !password) {
          throw new Error('Enter Username and Password')
        }
        try {
          const response = await authenticate(username, password)
          //console.log(`Response: ${JSON.stringify(response)}`)
          return { username, group: 'cyberlab' }
        } catch (err) {
          //console.log(`Error authenticating: ${err}`)
        }
        return null
      },
    }),
  ],
  secret: process.env.NEXTAUTH_SECRET,
  pages: {
    signIn: '/login',
  },
  //Specifies JSON web tokens will be used.
  jwt: {
    secret: process.env.JWT_SECRET,
    async encode({ token, secret, maxAge }) {
      console.log(`Before Encode function token: ${JSON.stringify(token)}`)
      const jwtClaims = {
        sub: token.username,
        name: token.username,
        iat: Date.now() / 1000,
        exp: Math.floor(Date.now() / 1000) + 3600,
        hasura: {
          'x-hasura-allowed-roles': [
            'CRU',
            'Forensic',
            'EvSpecialist',
            'Preview',
            'dany',
          ],
          'x-hasura-default-role': 'dany',
          'x-hasura-user-id': token.username,
        },
      }
      const encodedToken = jwt.sign(jwtClaims, secret, {
        algorithm: 'HS512',
      })
      console.log(`Encoded Token: ${JSON.stringify(encodedToken)}`)
      return encodedToken
    },
    async decode({ token, secret }) {
      const decodedToken = jwt.verify(token, secret, {
        algorithms: ['HS512'],
      })
      //console.log(`Decoded Token: ${JSON.stringify(decodedToken)}`)
      return decodedToken
    },
  },
  callbacks: {
    async jwt({ token, user }) {
      console.log(`Callback token: ${JSON.stringify(token)}`)
      console.log(`Callback user: ${JSON.stringify(user)}`)
      // * Hasura required claims will be added here
      // TODO check to see if hasura claims exist, if not assign them else just pass the token.
      if (user) {
        console.log(`Adding User in callback`)
        token.username = user.id
        token.group = user.group
        // token.hasura = {
        //   'x-hasura-allowed-roles': [
        //     'cru',
        //     'forensic',
        //     'evSpecialist',
        //     'preview',
        //     'dany',
        //   ],
        //   'x-hasura-default-role': 'dany',
        //   'x-hasura-user-id': token.username,
        // }
      }
      return token
    },
    async session({ session, token, user }) {
      // session.type = token.type
      session.name = token.username
      session.user = user
      session.group = token.group
      //console.log(`Session in Callback: ${JSON.stringify(session)}`)
      //console.log(`Session type in callback: ${session.type}`)
      //console.log(`Session User in callback: ${session.name}`)
      return session
    },
  },
  session: {
    //Sets the session to use JSON Web Token
    strategy: 'jwt',
    //Sets the max idle time before token expires in seconds - Currently 1hr
    maxAge: 3600,
  },
})

Here is my terminal output

Callback token: {"sub":"JDoe"}
Callback user: {"id":"JDoe","group":"DEVELOPMENT"}
Adding User in callback
Before Encode function token: {"sub":"JDoe","username":"JDoe","group":"DEVELOPMENT"}
Encoded Token: "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJKRG9lIiwibmFtZSI6IkpEb2UiLCJpYXQiOjE2NDYzMTg2NTMuODA4LCJleHAiOjE2NDYzMjIyNTMsImhhc3VyYSI6eyJ4LWhhc3VyYS1hbGxvd2VkLXJvbGVzIjpbIkNSVSIsIkZvcmVuc2ljIiwiRXZTcGVjaWFsaXN0IiwiUHJldmlldyIsImRhbnkiXSwieC1oYXN1cmEtZGVmYXVsdC1yb2xlIjoiZGFueSIsIngtaGFzdXJhLXVzZXItaWQiOiJKRG9lIn19.lMkiBj6eIKT0CH-6sullN3qO9pDZimKLNfsUSR6G8WUrdtK_DD1kmtmu_nmwpE-RWSkEwSQC-u3g-ocRtrSinQ"
Callback token: {"sub":"JDoe","name":"JDoe","iat":1646318653.808,"exp":1646322253,"hasura":{"x-hasura-allowed-roles":["CRU","Forensic","EvSpecialist","Preview","dany"],"x-hasura-default-role":"dany","x-hasura-user-id":"JDoe"}}
Callback user: undefined
Before Encode function token: {"sub":"JDoe","name":"JDoe","iat":1646318653.808,"exp":1646322253,"hasura":{"x-hasura-allowed-roles":["CRU","Forensic","EvSpecialist","Preview","dany"],"x-hasura-default-role":"dany","x-hasura-user-id":"JDoe"}}
Encoded Token: "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2NDYzMTg2NTMuODcyLCJleHAiOjE2NDYzMjIyNTMsImhhc3VyYSI6eyJ4LWhhc3VyYS1hbGxvd2VkLXJvbGVzIjpbIkNSVSIsIkZvcmVuc2ljIiwiRXZTcGVjaWFsaXN0IiwiUHJldmlldyIsImRhbnkiXSwieC1oYXN1cmEtZGVmYXVsdC1yb2xlIjoiZGFueSJ9fQ.lPao8tCFq7z0Pb8tIO7sm0L91fkwajA-Uuu_OgG6rIgo4sC3z6Zd07q1XaKNQ0P3-xt2c1bF0up6tVab3djG-g"
Callback token: {"sub":"JDoe","name":"JDoe","iat":1646318653.808,"exp":1646322253,"hasura":{"x-hasura-allowed-roles":["CRU","Forensic","EvSpecialist","Preview","dany"],"x-hasura-default-role":"dany","x-hasura-user-id":"JDoe"}}
Callback user: undefined
Before Encode function token: {"sub":"JDoe","name":"JDoe","iat":1646318653.808,"exp":1646322253,"hasura":{"x-hasura-allowed-roles":["CRU","Forensic","EvSpecialist","Preview","dany"],"x-hasura-default-role":"dany","x-hasura-user-id":"JDoe"}}
Encoded Token: "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2NDYzMTg2NTUuMjk3LCJleHAiOjE2NDYzMjIyNTUsImhhc3VyYSI6eyJ4LWhhc3VyYS1hbGxvd2VkLXJvbGVzIjpbIkNSVSIsIkZvcmVuc2ljIiwiRXZTcGVjaWFsaXN0IiwiUHJldmlldyIsImRhbnkiXSwieC1oYXN1cmEtZGVmYXVsdC1yb2xlIjoiZGFueSJ9fQ.wnaes7CejSGCjHTzwNKTsNV3pfhy4iYqyRmfaNhpELQvJDciYMfTyFc2O4byq8cLAP2brUpfDwyQZIFZNMAGPA"
Callback token: {"iat":1646318655.297,"exp":1646322255,"hasura":{"x-hasura-allowed-roles":["CRU","Forensic","EvSpecialist","Preview","dany"],"x-hasura-default-role":"dany"}}
Callback user: undefined
Before Encode function token: {"iat":1646318655.297,"exp":1646322255,"hasura":{"x-hasura-allowed-roles":["CRU","Forensic","EvSpecialist","Preview","dany"],"x-hasura-default-role":"dany"}}
Encoded Token: "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2NDYzMTg2NTUuNzQ3LCJleHAiOjE2NDYzMjIyNTUsImhhc3VyYSI6eyJ4LWhhc3VyYS1hbGxvd2VkLXJvbGVzIjpbIkNSVSIsIkZvcmVuc2ljIiwiRXZTcGVjaWFsaXN0IiwiUHJldmlldyIsImRhbnkiXSwieC1oYXN1cmEtZGVmYXVsdC1yb2xlIjoiZGFueSJ9fQ.6saQlySc7rgHikj85iejpz6nRm9fEtEdupz_j1hcwZ0TcZiQlLvY1-M-9xkju2F-0MlWmQwIj-bfEU7BmuEc5w"
Callback token: {"iat":1646318655.297,"exp":1646322255,"hasura":{"x-hasura-allowed-roles":["CRU","Forensic","EvSpecialist","Preview","dany"],"x-hasura-default-role":"dany"}}
Callback user: undefined
Before Encode function token: {"iat":1646318655.297,"exp":1646322255,"hasura":{"x-hasura-allowed-roles":["CRU","Forensic","EvSpecialist","Preview","dany"],"x-hasura-default-role":"dany"}}
Encoded Token: "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2NDYzMTg2NTUuOTgsImV4cCI6MTY0NjMyMjI1NSwiaGFzdXJhIjp7IngtaGFzdXJhLWFsbG93ZWQtcm9sZXMiOlsiQ1JVIiwiRm9yZW5zaWMiLCJFdlNwZWNpYWxpc3QiLCJQcmV2aWV3IiwiZGFueSJdLCJ4LWhhc3VyYS1kZWZhdWx0LXJvbGUiOiJkYW55In19.0tqw2fg_GjQGY55sHCKxscipHaGj9IHrdgNKBETsOzMPn5Rai3CFx1TMJP9ZOcyAzbSBmrXN31uWKCrda37X_g"

Here is what my middleware currently looks like.

import { NextApiRequest } from 'next'
import { getToken } from 'next-auth/jwt'
import { NextResponse, NextRequest } from 'next/server'

//Groups and Pages Associated with groups
// const userGroups = [
//   'CRU',
//   'Forensic',
//   'Evidence Specialist',
//   'Preview',
//   'Dany',
//   'DEVELOPMENT',
//   'TEST',
// ]

//Group access pages.
const CRU_PAGES: string[] = ['']
const FORENSIC_PAGES: string[] = ['']
const EVSPEVIALIST_PAGES: string[] = ['']
const PREVIEW_PAGES: string[] = ['']
const DANY_PAGES: string[] = ['']
const DEV_PAGES: string[] = ['/testpages', '/testpage', '/cases']
const TEST_PAGES: string[] = ['/testpage']
//Default Access Routes
const DEFAULT_PAGES: string[] = [
  '/',
  '/unauthorized',
  '/api',
  '/favicon.ico',
  '/login',
]

//The following function will protect the routes or redirect to unauthorized if not allowed to access a page.
function checkAuthPath(pathname: string, group: string, req: NextApiRequest) {
  var authorizedPages: string[] = ['']
  //Based on group assign authorized pages to the authorizedPages array.
  switch (group) {
    case 'CRU':
      authorizedPages = CRU_PAGES
      break
    case 'Forensic':
      authorizedPages = FORENSIC_PAGES
      break
    case 'Evidence Specialist':
      authorizedPages = EVSPEVIALIST_PAGES
      break
    case 'Preview':
      authorizedPages = PREVIEW_PAGES
      break
    case 'Dany':
      authorizedPages = DANY_PAGES
      break
    case 'DEVELOPMENT':
      authorizedPages = DEV_PAGES
      break
    default:
      authorizedPages = TEST_PAGES
  }
  authorizedPages = authorizedPages.concat(DEFAULT_PAGES)

  //Determine if request path is in the authorized paths Array. If not redirect to unauthorized.
  if (authorizedPages.includes('/' + pathname.split('/')[3])) {
    //console.log(`pathname in checkauth function: ${pathname}`)
    //console.log('Authorized')
    return NextResponse.next()
  } else {
    //console.log('Unauthorized')
    return NextResponse.redirect(new URL('/unauthorized', req.url))
  }
}

export async function middleware(req: NextApiRequest) {
  const token = await getToken({ req, secret: process.env.JWT_SECRET })
  // console.log(`Middleware Token: ${JSON.stringify(token)}`)
  const pathname = req.url

  if (typeof pathname === 'string') {
    if (pathname.includes('/api/auth') || token) {
      if (token) {
        //console.log('Token Found')
        //console.log(`pathname: ${pathname}`)
        const group = token.group as string
        return checkAuthPath(pathname, group, req)
      } else return NextResponse.next()
    }
    if (
      (!token && pathname !== 'http://localhost:3000/login') ||
      pathname.includes('/api/auth')
    ) {
      //&& !pathname.includes('/api/auth/signin'
      //console.log('Token Not Found')
      return NextResponse.redirect(new URL('/login', req.url))
    }
  } else {
    throw new Error('Pathname Undefined')
  }
}

As you can see by the last output i am missing then user and group from the JWT. Also why does it loop so many times?

Law
  • 51
  • 1
  • Did you solve it? If yes, please tell me how? If not, maybe my question can help you: https://github.com/nextauthjs/next-auth/discussions/2711 – Jan Apr 22 '22 at 16:00

1 Answers1

0

Solved by including the custom encoding and decoding in the middleware too.

Law
  • 51
  • 1