suhaotian / xior

A lite request lib based on fetch with plugin support and similar API to axios.
https://www.npmjs.com/package/xior
MIT License
114 stars 1 forks source link

Add https.agent to xior.create() and interceptors #13

Closed monecchi closed 1 month ago

monecchi commented 2 months ago

Hi there. I'm giving xior a try on Next.js 14, but the issue is I need to send client certificates (certificate.crt, certificate.key) in all requests along with access_token acquired during authentication.

I've messed around with axios on Next.js API Routes where I have access to node and I got something along the following lines working.

I'm setting up the certificates with axios as bellow:

/lib/banking-api.ts

import https from 'https'
import fs from 'fs'
import axios from 'axios'
import { getServerSession } from 'next-auth'
import { authOptions } from '@/auth'

const baseURL = process.env.BANKING_API_BASE_URL

const certFile = fs.readFileSync('certificates/api-cert.crt')
const keyFile = fs.readFileSync('certificates/api-cert.key')

export const httpsAgent = new https.Agent({
  cert: certFile,
  key: keyFile,
  //passphrase: ''
})

Here I finish axios configuration such as passing token to headers config...

export const httpClient = axios.create({
  baseURL: baseURL,
  headers: {
    'x-account': process.env.BANKING_API_ACCOUNT,
    withCredentials: true
  },
  httpsAgent: httpsAgent
})

httpClient.interceptors.request.use(
  async config => {
    const session = await getServerSession(authOptions)
    const token = session?.user?.access_token
    config.headers.Authorization = token ? `Bearer ${token}` : ''
    return config
  },
  error => {
    return Promise.reject(error)
  }
)

const api = httpClient

export default api

Within API routes I've tried to use Next.js 14 fetch() but it seems not to support the https.agent configuration so I couldn't set up the certificates. I've tried something along the lines bellow:

import { httpsAgent } from '@/lib/banking-api'

const result = await fetch('https://api.banking.com', {
    (...)
    agent: httpsAgent
})

Bellow is an API route that successfuly works as expected, but given the way I've set up axios as httpClient under /lib/banking-api.ts, a few issues arrise as I explain ahead.

/api/banking/balance/route.ts

'use server'
import type { NextApiHandler } from 'next'
import { NextApiRequest, NextApiResponse } from 'next'
import { NextResponse } from 'next/server'
import { httpClient } from '@/lib/banking-api'

const dateSimple = () => new Date().toISOString().slice(0, 10) // 2024-04-19

export const GET: NextApiHandler = async (request: NextApiRequest, response: NextApiResponse) => {
  try {
    // calling external api service with custom axios config
    const response = await httpClient.get('https://api.banking.com/v2/balance', {
      params: {
        date: dateSimple()
      }
    })

    const data = response.data

    return NextResponse.json(data, { status: 200 })
  } catch (error) {
    return NextResponse.json(error, { status: 500 })
  }
})

On client components or pages explicity setting 'use client' directive, if I make a request to https://localhost:3000/api/banking/balance using Next.js fetch() I successfully get a response data from the API route above. I then use React.useState() to manage the data for the component or page...

However, If I try to import the custom axios httpClient and use it to make requests within client components, 'fs' and 'https' modules won't load giving "module not found" error as long as they're used server side only.

On server components or pages if I make a request to https://localhost:3000/api/banking/balance using Next.js fetch() I get a 401 unauthorized response as long as fetch() is not sending the client certificates in the request.

suhaotian commented 2 months ago

I got you!

Try to proxy the API on the server side, 2 steps:

Step 1. Create file pages/api/[...path].ts:

don't forget to install http-proxy

This will enable you visit your next.js server url like 'http://localhost:3000/api/hello' then proxy to your API on the server side.

import fs from 'fs';
import { IncomingMessage, ServerResponse } from 'http';
import httpProxy from 'http-proxy';
import https from 'https';

const baseURL = process.env.BANKING_API_BASE_URL;
const certFile = fs.readFileSync('certificates/api-cert.crt');
const keyFile = fs.readFileSync('certificates/api-cert.key');

export const httpsAgent = new https.Agent({
  cert: certFile,
  key: keyFile,
  //passphrase: ''
});

const proxy = httpProxy.createProxyServer();
export default function api(req: IncomingMessage, res: ServerResponse<IncomingMessage>) {
  return new Promise((resolve, reject) => {
    proxy.web(
      req,
      res,
      {
        target: baseURL,
        changeOrigin: true,
        headers: {
          "x-account": process.env.BANKING_API_ACCOUNT || "",
        },
      },
      (err) => {
        if (err) {
          return reject(err);
        } else {
          resolve(1);
        }
      }
    );
  });}

export const config = {
  api: {
    bodyParser: false,
  },
};

Steps 2. Use axios or xior to request:

import axios from 'xior';
import { getServerSession } from 'next-auth'
import { authOptions } from '@/auth'

const isServer = typeof document === 'undefined';

export const httpClient = axios.create({
  baseURL: isServer ? 'http://localhost:3000' : '',  // This url depends on your next.js server address
});

const dateSimple = () => new Date().toISOString().slice(0, 10) // 2024-04-19

httpClient.interceptors.request.use(
  async (config) => {
    let token = '';
    if (isServer) {
      const session = await getServerSession(authOptions)
      token = session?.user?.access_token
    } else {
       // TODO: get token for client?
     }
    if (token) {
        config.headers.Authorization = `Bearer ${token}`;
     }
      config.params['date'] = dateSimple();
       return config
  },
  (error) => {
    return Promise.reject(error)
  }
)

const api = httpClient 

export default api

Usage:


httpClient.get('/v2/balance')

Tips: you can add syntax highlight to your code like:```ts console.log('hi'); ```

console.log('hi') 
monecchi commented 2 months ago

@suhaotian Thank you so much for guiding me in detail! I'll give it try and come back here to share any further achievements. I've applied the highlight syntax for a better reading, thanks for the tip!

I've came across tons of questions without any answer from other people trying to deal with that kind of two-way (mTLS) Client Certificate communication relying solely on Next.js 13 / 14, but I'm happy I might have found the way!