0

I'm trying to authenticate to Xero's API. I get a 'code' which is then exchanged for an access_token. I'm still new to NextJS and React so I'm likely not thinking about this correctly.

The code I have results in the right data being returned, however I don't know how to use it effectively in the rest of the app. I couldn't figure out how to use NextAuth in a custom provider so tried to roll my own.

  • The user clicks the button 'Connect to Xero' - this is a href to initiate the process and takes the user to Xero to login in the browser. User authenticates. Xero calls the callback
  • the callback at /api/callback responds
  • I extract the 'code' and then make the subsequent request to Xero to swap it for an access token.

This is where I get stuck - because the initial action is a href redirect, I'm not sure how to get the end API result back into my code as state/something usable. In effect Xero is calling the api/callback page and that's where the user is left.

I've tried to put useState hooks into the api/callback however that breaks the rule of hooks.

Any pointers greatly appreciated.

Code;

pages/index.js

import React from 'react'
import Layout from '../components/Layout'
import TopNav from '../components/TopNav'
import Link from 'next/link';   

export default function Main(props) { 
  
  const test = props.URL

  return (
    <>
      <Layout>
        <TopNav name="Main page"/>  
        <p>this is the main page</p>
        
        <Link href={test} passHref={true}>
          <button className=' w-40 border rounded-md py-3 px-3 flex items-center justify-center text-sm font-medium sm:flex-1'>
            Connect to Xero
          </button>
        </Link>
      </Layout>    
    </>
  )
}

export async function getStaticProps() {
  const XeroAuthURL = "https://login.xero.com/identity/connect/authorize?response_type=code&client_id="
  const client_ID = process.env.XERO_CLIENT_ID
  const redirect_uri = process.env.XERO_REDIRECT_URI
  const scope = "offline_access openid profile email accounting.settings"

  const URL = `${XeroAuthURL}${client_ID}&redirect_uri=${redirect_uri}&scope=${scope}` 
  
  return {
    props: {
      URL: URL
    },
  };
}

/api/callback.js

import axios from "axios"
const qs = require('qs');

export default async function callback(req, res) {
    
    try {
        //callback from Xero will deliver the code, scope + state (if given)
        //https://developer.xero.com/documentation/guides/oauth2/auth-flow/#2-users-are-redirected-back-to-you-with-a-code
        console.log(`REQ = ${JSON.stringify(req.query)}`)
            
        
       //exchange code for tokens - https://developer.xero.com/documentation/guides/oauth2/auth-flow/#3-exchange-the-code
        var data = qs.stringify({
            'code': req.query.code,
            'grant_type': 'authorization_code',
            'redirect_uri': 'http://localhost:3000/api/callback' 
          });
            
        var config = {
            method: 'post',
            url: 'https://identity.xero.com/connect/token',
            headers: { 
                'Content-Type': 'application/x-www-form-urlencoded', 
                'Authorization': 'Basic **put your authorisation result here**'
            },
            data : data
        };

        try {
            const response = await axios(config)
            //response has the data I want to put into State
            console.log(JSON.stringify(response.data));
              //save data off here somehow???
              //tried redirecting but unsure if can pass the result
              res.redirect(307, '/')

        } catch (error) {
            console.error(error)
            res.status(error.status || 500).end(error.message)
        }
            
        
    } catch (error) {
      console.error(error)
      res.status(error.status || 500).end(error.message)
    }
  }
gorlaz
  • 468
  • 6
  • 20
  • Have you considered using cookies to pass whatever data you need to your app? – juliomalves Feb 13 '22 at 17:20
  • 1
    I hadn't actually - thats a great idea thanks. Given this is only until i add some permanent storage, a cookie will be perfect for the moment. – gorlaz Feb 14 '22 at 03:34

1 Answers1

0

Added a not-secure cookie that I can use while testing. Do not use this in production as the cookie is not httpOnly and not secure.

import axios from "axios"
import Cookies from 'cookies'
const qs = require('qs');

export default async function callback(req, res) {
    
    const cookies = new Cookies(req,res)
    try {
    
        var data = qs.stringify({
            'code': req.query.code,
            'grant_type': 'authorization_code',
            'redirect_uri': 'http://localhost:3000/api/callback' 
          });
            
        var config = {
            method: 'post',
            url: 'https://identity.xero.com/connect/token',
            headers: { 
                'Content-Type': 'application/x-www-form-urlencoded', 
                'Authorization': 'Basic **YOUR AUTH CODE HERE**'
            },
            data : data
        };

        try {
            var response = await axios(config)
            response.data.expires_at = Date.now() + response.data.expires_in*1000
              //save data off
              //TO DO - THIS IS REALLY BAD - ONLY USE THIS TEMPORARILY UNTIL HAVE GOT PERMSTORAGE SETUP
              cookies.set('myCookieName', JSON.stringify(response.data), {
                secure: false,
                httpOnly: false
              }
            )
              res.redirect(307, '/')
              //return ({ data: response.data })

        } catch (error) {
            console.error(error)
            res.status(error.status || 500).end(error.message)
        }
            
        
    } catch (error) {
      console.error(error)
      res.status(error.status || 500).end(error.message)
    }
  }

Then in the index;

import React from 'react'
import Layout from '../components/Layout'
import TopNav from '../components/TopNav'
import Link from 'next/link';   
import { getCookie } from 'cookies-next';

export default function Main(props) { 
  
  //check for cookie
  //TO DO THIS IS REALLY BAD; CHANGE WHEN GET PERM STORAGE ORGANISED
  const cookie = getCookie('myCookieName');

  const URL = props.URL
  
  return (
    <>
      <Layout>
        <TopNav name="Main page"/>  
        <p>this is the main page</p>
        
        <Link href={URL} passHref={true}>
          <button className=' w-40 border rounded-md py-3 px-3 flex items-center justify-center text-sm font-medium sm:flex-1'>
            Connect to Xero
          </button>
        </Link>
        <p>{cookie ? cookie : 'waiting for cookie...'}</p>
      </Layout>    
    </>
  )
}



export async function getStaticProps() {
  const XeroAuthURL = "https://login.xero.com/identity/connect/authorize?response_type=code&client_id="
  const client_ID = process.env.XERO_CLIENT_ID
  const redirect_uri = process.env.XERO_REDIRECT_URI
  const scope = "offline_access openid profile email accounting.settings"

  //console.log(`URL - ${XeroAuthURL}${client_ID}&redirect_uri=${redirect_uri}&scope=${scope}`)
  const URL = `${XeroAuthURL}${client_ID}&redirect_uri=${redirect_uri}&scope=${scope}` 
  
  return {
    props: {
      URL: URL,
    },
  };
}
gorlaz
  • 468
  • 6
  • 20