Cvmcosta / ltijs

Turn your application into a fully integratable LTI 1.3 tool provider.
https://cvmcosta.github.io/ltijs/
Apache License 2.0
303 stars 70 forks source link

Login request gets stuck at "Retrieving key from jwt_set" (`got` library) #99

Open vandergav opened 3 years ago

vandergav commented 3 years ago

Hi Carlos,

Could you help me with setting up ltijs for production?

I have an external tool web app with these ltijs configurations: image image

And in Moodle: image image

After getting dev for ltijs to work locally, I am now trying to get ltijs to work for production but it seems like there is an issue with my login. Here are the logs just before the error (gets stuck at Retrieving key from jwt_set before timing out): image

I have tried deleting the platforms and also played around with the secure and samesite cookie settings for lti.setup but still faced the same issue.

As always, thanks so much for the help in advance :)

To add: i tried to the urls for keys and they seem to work: https://staging.tite.rdc.nie.edu.sg/keys https://moodle.tite.rdc.nie.edu.sg/mod/lti/certs.php

Cvmcosta commented 3 years ago

Hello! May i ask how you are deploying these servers? The request for keysets is a simple HTTP GET, are you sure the Moodle instance is accessible from within the LTI server?

vandergav commented 3 years ago

Hi Carlos, thank you for your reply. There is a proxy in front of my server so I'm not sure if that affects getting the JWT keyset from within the LTI server. Here's my Nginx configuration for reference.

server {
        listen 80;
    root /var/www/tite_staging/public/www;
        server_name staging.tite.rdc.nie.edu.sg;
    location /socket.io/ {
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-NginX-Proxy true;
        proxy_ssl_session_reuse off;
            proxy_cache_bypass $http_upgrade;
            proxy_redirect off;

            proxy_set_header Upgrade $http_upgrade;
                proxy_set_header Connection "upgrade";
                proxy_http_version 1.1;
                proxy_set_header Host $host;
                proxy_pass http://127.0.0.1:8086/socket.io/;
    }

        location / {
                proxy_set_header X-Real-IP $remote_addr;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                proxy_set_header X-NginX-Proxy true;
                proxy_ssl_session_reuse off;
                proxy_cache_bypass $http_upgrade;
                proxy_redirect off;

                proxy_set_header Upgrade $http_upgrade;
                proxy_set_header Connection "upgrade";
                proxy_http_version 1.1;
                proxy_set_header Host $host;
                proxy_pass http://127.0.0.1:8086/;
        }

        error_page 500 502 503 504 /errorPages/50x.html;
        location /errorPages/ {
                root  /var/www/tite_staging/public/www;
        }
        location /50x_files/ {
                root  /var/www/tite_staging/public/www/errorPages;
        }
}

If all else fails, I will try the other methods for authConfig: image May I check for JWK_KEY, do I use the keys from https://staging.tite.rdc.nie.edu.sg/keys or https://moodle.tite.rdc.nie.edu.sg/mod/lti/certs.php. Also wondering what the difference is between these 2 endpoints. Thanks so much again for your help.

Cvmcosta commented 3 years ago

The /keys endpoint on your LTI server contains the public keys used by the Platforms to validate messages signed by your Tool. the /certs.php endpoint on moodle contains the public keys used by your Tool to validate messages signed by Moodle.

You can try using JWK_KEY with a key from Moodle's certs.php endpoint if you know the kid of the key used to sign messages to your tool.

As a test you can try to make an http request "manually" from within your tool, after the setup, to see if the certs.php URL is reachable. You can install the 'got' package or any other you feel comfortable with and do a const res = await got.get('https://moodle.tite.rdc.nie.edu.sg/mod/lti/certs.php') or something like it.

vandergav commented 3 years ago

Hi Carlos, thank you for your reply and suggestions. I have tried making a http request, with https.get(..., after the setup but seems like the /certs.php endpoint can't be hit for some reason.

I then tried the RSA_KEY method and it worked so I'll take this as a (temporary) workaround for the "Retrieving key from jwt_set" issue for now.

However, I am now facing an issue when using the lti.NamesAndRoles.getMembers method. Based on the logs, the request seems to get stuck at awaiting return from the platform image

I then looked into Utils/Auth.js:

 /**
     * @description Gets a new access token from the platform.
     * @param {String} scopes - Request scopes
     * @param {Platform} platform - Platform object of the platform you want to access.
     */
  static async getAccessToken (scopes, platform, ENCRYPTIONKEY, Database) {
    const platformUrl = await platform.platformUrl()
    const clientId = await platform.platformClientId()
    const confjwt = {
      sub: clientId,
      iss: clientId,
      aud: await platform.platformAuthorizationServer(),
      jti: encodeURIComponent([...Array(25)].map(_ => (Math.random() * 36 | 0).toString(36)).join``)
    }

    const token = jwt.sign(confjwt, await platform.platformPrivateKey(), { algorithm: 'RS256', expiresIn: 60, keyid: await platform.platformKid() })

    const message = {
      grant_type: 'client_credentials',
      client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
      client_assertion: token,
      scope: scopes
    }

    provAuthDebug('Awaiting return from the platform')
    const access = await got.post(await platform.platformAccessTokenEndpoint(), { form: message }).json()
    provAuthDebug('Successfully generated new access_token')

    await Database.Replace(ENCRYPTIONKEY, 'accesstoken', { platformUrl: platformUrl, clientId: clientId, scopes: scopes }, { token: access }, { platformUrl: platformUrl, clientId: clientId, scopes: scopes })
    return access
  }
} 

and Platform.js:

  /**
     * @description Sets/Gets the platform access token endpoint used to authenticate messages to the platform.
     * @param {string} [accesstokenEndpoint] - Platform access token endpoint.
     */
  async platformAccessTokenEndpoint (accesstokenEndpoint) {
    if (!accesstokenEndpoint) return this.#accesstokenEndpoint
    await this.#Database.Modify(false, 'platform', { platformUrl: this.#platformUrl, clientId: this.#clientId }, { accesstokenEndpoint: accesstokenEndpoint })
    this.#accesstokenEndpoint = accesstokenEndpoint
    return accesstokenEndpoint
  }

I guess it might have to do with the accesstokenEndpoint, @param {string} accesstokenEndpoint - Access token endpoint for the platform.

I will continue looking into this and update again if theres anything

Cvmcosta commented 3 years ago

Yes, this seems to be the same issue. Moodl's endpoints not being accessible from within your Tool

vandergav commented 3 years ago

Hi Carlos, may I know if the @param {string} accesstokenEndpoint - Access token endpoint for the platform refers to the accesstokenEndpoint: 'https://moodle.tite.rdc.nie.edu.sg/mod/lti/token.php' specified in lti.registerPlatform for setup?

Anyway I have tried to access the keyset after setup using:

axios.get('https://moodle.tite.rdc.nie.edu.sg/mod/lti/certs.php')
.then(response => {
  console.log(response.data);
})
.catch(error => {
  console.log(error);
});

and got back the keyset. So now i'm not sure what the issue is. Thanks

I think I should mention that my tool and moodle site are running on the same server. Maybe it has something to do with this...

Cvmcosta commented 3 years ago

Hello! Yes, the accesstokenEndpoint is the token.php endpoint on moodle.

So you are now able to access the keyset? Are you able to access the https://moodle.tite.rdc.nie.edu.sg/mod/lti/token.php endpoint from within your server?

vandergav commented 3 years ago

Hi, yes I was able to access the keyset but only by using axios. I tried with the got library and the request timeout-ed while waiting for the response. Here are the 2 ways I did the HTTP calls after setup().

axios returned the key

axios.get('https://moodle.tite.rdc.nie.edu.sg/mod/lti/certs.php')
.then(response => {
  console.log(response.data);
})
.catch(error => {
  console.log(error);
});

got did not

(async () => {
    try {
        console.log('trying got http request')
        const res = await got.get('https://moodle.tite.rdc.nie.edu.sg/mod/lti/certs.php')
        console.log(res);
    } catch (error) {
        console.log(error);
    }
})();

I'm using "ltijs": "^5.7.3", "ltijs-sequelize": "^2.4.2", "got": "^11.8.2",

Thanks

To add, everything works fine on my local. It is only on my server where I am having issues with the got library presumably.

Adding the parts of the library that use got, Utils/Auth.js: const res = await got.get(keysEndpoint).json() const access = await got.post(await platform.platformAccessTokenEndpoint(), { form: message }).json()

Found this from the got repository. Thinking if it might be a proxy issue and if so, would need to tweak my nginx config file. https://github.com/sindresorhus/got/issues/1678

Cvmcosta commented 3 years ago

Hello, i am unable to reproduce the error. Could you please try the fix presented in the issue you mentioned and let me know if it works?

vandergav commented 3 years ago

Hi Carlos,

Hope you've been well.

I have been trying to solve the issue and so far have narrowed it down to the got library. Somehow the got library doesn't seem to be sending out the requests from my server to hit the proxy in front of it. So, as a workaround I am trying to change the parts of ltijs that use got to axios in my node_modules folder and rebuilding my app.

So far I have only changed some files of ltijs used by my app:

Auth.js image image

NamesAndRoles.js image

However it seems like now it is failing at generating a new access_token when I call the getMembers() method of the NamesAndRoles class,

My App image

Auth.js const access = await axios.post(await platform.platformAccessTokenEndpoint(), { ... returns a data: { error: 'invalid_request' } and status 400 Bad Request:

image

Not sure if this is an issue with my axios post request but if you have any pointers for me that'll be great (I have checked that await platform.platformAccessTokenEndpoint() for Auth.js returns a valid url, https://moodle.tite.rdc.nie.edu.sg/mod/lti/token.php)

Looking at the logs just before the error, image

NamesAndRoles.js image

Platform.js image

Auth.js (axios error happens here) image

Failed at const access = await axios.post(await platform.platformAccessTokenEndpoint(), { ...

Full logs:

  provider:main Receiving request at path: /login +17m
  provider:main Receiving a login request from: https://moodle.tite.rdc.nie.edu.sg +0ms
  provider:main Redirecting to platform authentication endpoint +6ms
  provider:main Target Link URI:  https://staging.tite.rdc.nie.edu.sg +0ms
  provider:main Login request:  +1ms
  provider:main {
  provider:main   response_type: 'id_token',
  provider:main   response_mode: 'form_post',
  provider:main   id_token_signed_response_alg: 'RS256',
  provider:main   scope: 'openid',
  provider:main   client_id: 'EVXFAyRoAkhO2Do',
  provider:main   redirect_uri: 'https://staging.tite.rdc.nie.edu.sg',
  provider:main   login_hint: '2',
  provider:main   nonce: 'yv1w4dege1sep0x5wfr694nf1',
  provider:main   prompt: 'none',
  provider:main   state: '9074badcaaaf9a5ca5a7f167f940a82f010c63c6fe37215b31',
  provider:main   lti_message_hint: '2',
  provider:main   lti_deployment_id: '2'
  provider:main } +0ms
  provider:main Receiving request at path: / +144ms
  provider:main Path does not match reserved endpoints +0ms
  provider:main Cookies received:  +0ms
  provider:main [Object: null prototype] {
  provider:main   'ltiaHR0cHM6Ly9tb29kbGUudGl0ZS5yZGMubmllLmVkdS5zZ0VWWEZBeVJvQWtoTzJEbzI%3D': '2',
  provider:main   state9074badcaaaf9a5ca5a7f167f940a82f010c63c6fe37215b31: 'https://moodle.tite.rdc.nie.edu.sg'
  provider:main } +0ms
  provider:main Received idtoken for validation +0ms
  provider:auth Response state: 9074badcaaaf9a5ca5a7f167f940a82f010c63c6fe37215b31 +17m
  provider:auth Attempting to validate iss claim +17m
  provider:auth Request Iss claim: https://moodle.tite.rdc.nie.edu.sg +0ms
  provider:auth Response Iss claim: https://moodle.tite.rdc.nie.edu.sg +0ms
  provider:auth Attempting to retrieve registered platform +0ms
  provider:auth Retrieving key from jwk_set +3ms
  provider:auth Converting JWK key to PEM key +54ms
  provider:auth Attempting to verify JWT with the given key +1ms
  provider:auth Token signature verified +1ms
  provider:auth Initiating OIDC aditional validation steps +0ms
  provider:auth Validating if aud (Audience) claim matches the value of the tool's clientId given by the platform +0ms
  provider:auth Aud claim: EVXFAyRoAkhO2Do +0ms
  provider:auth Checking alg claim. Alg: RS256 +0ms
  provider:auth Max age parameter:  10 +0ms
  provider:auth Checking iat claim to prevent old tokens from being passed. +0ms
  provider:auth Iat claim: 1623317100 +0ms
  provider:auth Exp claim: 1623317160 +1ms
  provider:auth Current_time: 1623317100.224 +0ms
  provider:auth Time passed: 0.2239999771118164 +0ms
  provider:auth Validating nonce +0ms
  provider:auth Nonce: yv1w4dege1sep0x5wfr694nf1 +0ms
  provider:auth Tool's clientId: EVXFAyRoAkhO2Do +0ms
  provider:auth Storing nonce +2ms
  provider:auth Initiating LTI 1.3 core claims validation +6ms
  provider:auth Checking Message type claim +0ms
  provider:auth Checking Target Link Uri claim +0ms
  provider:auth Checking Resource Link Id claim +0ms
  provider:auth Checking LTI Version claim +0ms
  provider:auth Checking Deployment Id claim +0ms
  provider:auth Checking Sub claim +0ms
  provider:auth Checking Roles claim +0ms
  provider:auth Successfully validated token! +71ms
  provider:main Generating ltik +85ms
  provider:main Redirecting to endpoint with ltik +0ms
socket connected
  provider:main Receiving request at path: /info +954ms
  provider:main Path does not match reserved endpoints +0ms
  provider:main Cookies received:  +0ms
  provider:main [Object: null prototype] {
  provider:main   'ltiaHR0cHM6Ly9tb29kbGUudGl0ZS5yZGMubmllLmVkdS5zZ0VWWEZBeVJvQWtoTzJEbzI%3D': '2'
  provider:main } +0ms
  provider:main Ltik found +0ms
  provider:main Ltik successfully verified +0ms
  provider:main Attempting to retrieve matching session cookie +0ms
  provider:auth Valid session found +968ms
  provider:main Passing request to next handler +6ms
  provider:main Receiving request at path: /members +64ms
  provider:main Path does not match reserved endpoints +1ms
  provider:main Cookies received:  +0ms
  provider:main [Object: null prototype] {
  provider:main   'ltiaHR0cHM6Ly9tb29kbGUudGl0ZS5yZGMubmllLmVkdS5zZ0VWWEZBeVJvQWtoTzJEbzI%3D': '2'
  provider:main } +0ms
  provider:main Ltik found +0ms
  provider:main Ltik successfully verified +0ms
  provider:main Attempting to retrieve matching session cookie +0ms
  provider:auth Valid session found +71ms
  provider:main Passing request to next handler +7ms
  provider:namesAndRolesService Attempting to retrieve memberships +17m
  provider:namesAndRolesService Target platform: https://moodle.tite.rdc.nie.edu.sg +0ms
  provider:namesAndRolesService Attempting to retrieve platform access_token for [https://moodle.tite.rdc.nie.edu.sg] +4ms
  provider:platform Valid access_token for https://moodle.tite.rdc.nie.edu.sg not found +17m
  provider:platform Attempting to generate new access_token for https://moodle.tite.rdc.nie.edu.sg +0ms
  provider:platform With scopes: https://purl.imsglobal.org/spec/lti-nrps/scope/contextmembership.readonly +0ms
  provider:auth Awaiting return from the platform +1s
platformAccessTokenEndpoint https://moodle.tite.rdc.nie.edu.sg/mod/lti/token.php
Error: Request failed with status code 400
    at createError (/var/www/tite_staging/node_modules/axios/lib/core/createError.js:16:15)
    at settle (/var/www/tite_staging/node_modules/axios/lib/core/settle.js:17:12)
    at IncomingMessage.handleStreamEnd (/var/www/tite_staging/node_modules/axios/lib/adapters/http.js:260:11)
    at IncomingMessage.emit (events.js:327:22)
    at IncomingMessage.EventEmitter.emit (domain.js:467:12)
    at endReadableNT (internal/streams/readable.js:1327:12)
    at processTicksAndRejections (internal/process/task_queues.js:80:21) {
  config: {
    url: 'https://moodle.tite.rdc.nie.edu.sg/mod/lti/token.php',
    method: 'post',
    data: '{"form":{"grant_type":"client_credentials","client_assertion_type":"urn:ietf:params:oauth:client-assertion-type:jwt-bearer","client_assertion":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRhYjkyODBmNTllMDg0MWUyZWE0NjY1NjViZWFhYTBkIn0.eyJzdWIiOiJFVlhGQXlSb0FraE8yRG8iLCJpc3MiOiJFVlhGQXlSb0FraE8yRG8iLCJhdWQiOiJodHRwczovL21vb2RsZS50aXRlLnJkYy5uaWUuZWR1LnNnL21vZC9sdGkvdG9rZW4ucGhwIiwianRpIjoibGIxdWtlajd0c25qdncxZjc3eWphOHV0YyIsImlhdCI6MTYyMzMxNzEwMSwiZXhwIjoxNjIzMzE3MTYxfQ.wo2ZFzc9j_GZDiNysr6ANn2ZE0NBkxIrPnXDrWXWlpzt3Lhipsj_jIeq4XSK34kKJzbTZvg02g7K-jramQaHk6IBwL-Xij6wcKKYeqkyY9UAP396WdBO_AK4lhARUIOEJJY8xzAYgp8A9Eq4ADuPquHA9NiQkRoOLrRQdBe-nqzf_lode7bFctfG-vdtzDBl3dV0wj1A6ZY_542RRyQRtqVqjIeGNPl7SJD62Gy3cysi82ftFxy2-72F-igl_jgJ7CsvBx9CLq7csqCqE-ckT0vLxbKa2a360w27oGC7o0O_bdstgsL1n0QptQHXXDtO4-S3txkAwoVw7fas-wg7dZ8z3705-f2g1KlCNSMb889h_rLwI7biIqKnqTGlakKn-A90GfshnHHQmLq88jqyXvvuGdYq6pkRzpDsQPmf8bdsV0V17DKNRjIPiAmswqzkzqOVdfjnivEmZEL0iaE2eKDqCKYNBgS7Bg-ulqPEPWeoQMEWTuVaFgnHqj_u3OnYWMua4rAy_p5m50rwHdersgdE7q-mnOJQk4itoJlhxzxw3fJh_rmn40jq0mvj7R5GEo1VwpeGeFJUvRRtHt5-Q8or_5jslkNwn5Jn2aRDTIK0_T8A150BHqTS3in8_uC3ShviOEOUPaRbEr9mnb_z0UKXvjefN4zK7BBeIPaZlyg","scope":"https://purl.imsglobal.org/spec/lti-nrps/scope/contextmembership.readonly"}}',
    headers: {
      Accept: 'application/json, text/plain, */*',
      'Content-Type': 'application/json;charset=utf-8',
      'User-Agent': 'axios/0.21.1',
      'Content-Length': 1245,
      host: 'moodle.tite.rdc.nie.edu.sg'
    },
    transformRequest: [ [Function: transformRequest] ],
    transformResponse: [ [Function: transformResponse] ],
    timeout: 0,
    adapter: [Function: httpAdapter],
    xsrfCookieName: 'XSRF-TOKEN',
    xsrfHeaderName: 'X-XSRF-TOKEN',
    maxContentLength: -1,
    maxBodyLength: -1,
    validateStatus: [Function: validateStatus]
  },
  request: <ref *1> ClientRequest {
    _events: [Object: null prototype] {
      socket: [Function (anonymous)],
      abort: [Function (anonymous)],
      aborted: [Function (anonymous)],
      connect: [Function (anonymous)],
      error: [Function (anonymous)],
      timeout: [Function (anonymous)],
      prefinish: [Function: requestOnPrefinish]
    },
    _eventsCount: 7,
    _maxListeners: undefined,
    outputData: [],
    outputSize: 0,
    writable: true,
    destroyed: false,
    _last: true,
    chunkedEncoding: false,
    shouldKeepAlive: false,
    _defaultKeepAlive: true,
    useChunkedEncodingByDefault: true,
    sendDate: false,
    _removedConnection: false,
    _removedContLen: false,
    _removedTE: false,
    _contentLength: null,
    _hasBody: true,
    _trailer: '',
    finished: true,
    _headerSent: true,
    socket: Socket {
      connecting: false,
      _hadError: false,
      _parent: null,
      _host: null,
      _readableState: [ReadableState],
      _events: [Object: null prototype],
      _eventsCount: 9,
      _maxListeners: undefined,
      _writableState: [WritableState],
      allowHalfOpen: false,
      _sockname: null,
      _pendingData: null,
      _pendingEncoding: '',
      server: null,
      _server: null,
      parser: null,
      _httpMessage: [Circular *1],
      write: [Function: writeAfterFIN],
      [Symbol(async_id_symbol)]: 2994,
      [Symbol(kHandle)]: null,
      [Symbol(kSetNoDelay)]: false,
      [Symbol(lastWriteQueueSize)]: 0,
      [Symbol(timeout)]: null,
      [Symbol(kBuffer)]: null,
      [Symbol(kBufferCb)]: null,
      [Symbol(kBufferGen)]: null,
      [Symbol(kCapture)]: false,
      [Symbol(kBytesRead)]: 389,
      [Symbol(kBytesWritten)]: 1505,
      [Symbol(RequestTimeout)]: undefined
    },
    _header: 'POST https://moodle.tite.rdc.nie.edu.sg/mod/lti/token.php HTTP/1.1\r\n' +
      'Accept: application/json, text/plain, */*\r\n' +
      'Content-Type: application/json;charset=utf-8\r\n' +
      'User-Agent: axios/0.21.1\r\n' +
      'Content-Length: 1245\r\n' +
      'host: moodle.tite.rdc.nie.edu.sg\r\n' +
      'Connection: close\r\n' +
      '\r\n',
    _keepAliveTimeout: 0,
    _onPendingData: [Function: noopPendingOutput],
    agent: Agent {
      _events: [Object: null prototype],
      _eventsCount: 2,
      _maxListeners: undefined,
      defaultPort: 80,
      protocol: 'http:',
      options: [Object],
      requests: {},
      sockets: [Object],
      freeSockets: {},
      keepAliveMsecs: 1000,
      keepAlive: false,
      maxSockets: Infinity,
      maxFreeSockets: 256,
      scheduling: 'fifo',
      maxTotalSockets: Infinity,
      totalSocketCount: 1,
      [Symbol(kCapture)]: false
    },
    socketPath: undefined,
    method: 'POST',
    maxHeaderSize: undefined,
    insecureHTTPParser: undefined,
    path: 'https://moodle.tite.rdc.nie.edu.sg/mod/lti/token.php',
    _ended: true,
    res: IncomingMessage {
      _readableState: [ReadableState],
      _events: [Object: null prototype],
      _eventsCount: 3,
      _maxListeners: undefined,
      socket: [Socket],
      httpVersionMajor: 1,
      httpVersionMinor: 1,
      httpVersion: '1.1',
      complete: true,
      headers: [Object],
      rawHeaders: [Array],
      trailers: {},
      rawTrailers: [],
      aborted: false,
      upgrade: false,
      url: '',
      method: null,
      statusCode: 400,
      statusMessage: 'Bad Request',
      client: [Socket],
      _consuming: true,
      _dumped: false,
      req: [Circular *1],
      responseUrl: 'http://10.55.99.61:3128/https://moodle.tite.rdc.nie.edu.sg/mod/lti/token.php',
      redirects: [],
      [Symbol(kCapture)]: false,
      [Symbol(RequestTimeout)]: undefined
    },
    aborted: false,
    timeoutCb: null,
    upgradeOrConnect: false,
    parser: null,
    maxHeadersCount: null,
    reusedSocket: false,
    host: '10.55.99.61',
    protocol: 'http:',
    _redirectable: Writable {
      _writableState: [WritableState],
      _events: [Object: null prototype],
      _eventsCount: 2,
      _maxListeners: undefined,
      _options: [Object],
      _ended: true,
      _ending: true,
      _redirectCount: 0,
      _redirects: [],
      _requestBodyLength: 1245,
      _requestBodyBuffers: [],
      _onNativeResponse: [Function (anonymous)],
      _currentRequest: [Circular *1],
      _currentUrl: 'http://10.55.99.61:3128/https://moodle.tite.rdc.nie.edu.sg/mod/lti/token.php',
      [Symbol(kCapture)]: false
    },
    [Symbol(kCapture)]: false,
    [Symbol(kNeedDrain)]: false,
    [Symbol(corked)]: 0,
    [Symbol(kOutHeaders)]: [Object: null prototype] {
      accept: [Array],
      'content-type': [Array],
      'user-agent': [Array],
      'content-length': [Array],
      host: [Array]
    }
  },
  response: {
    status: 400,
    statusText: 'Bad Request',
    headers: {
      server: 'nginx/1.16.1',
      date: 'Thu, 10 Jun 2021 09:25:01 GMT',
      'content-type': 'text/html; charset=utf-8',
      'x-powered-by': 'PHP/7.3.28',
      'strict-transport-security': 'max-age=16000000; includeSubDomains; preload;',
      'x-cache': 'MISS from rchinfra01',
      'x-cache-lookup': 'MISS from rchinfra01:3128',
      via: '1.1 rchinfra01 (squid/3.5.20)',
      connection: 'close'
    },
    config: {
      url: 'https://moodle.tite.rdc.nie.edu.sg/mod/lti/token.php',
      method: 'post',
      data: '{"form":{"grant_type":"client_credentials","client_assertion_type":"urn:ietf:params:oauth:client-assertion-type:jwt-bearer","client_assertion":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRhYjkyODBmNTllMDg0MWUyZWE0NjY1NjViZWFhYTBkIn0.eyJzdWIiOiJFVlhGQXlSb0FraE8yRG8iLCJpc3MiOiJFVlhGQXlSb0FraE8yRG8iLCJhdWQiOiJodHRwczovL21vb2RsZS50aXRlLnJkYy5uaWUuZWR1LnNnL21vZC9sdGkvdG9rZW4ucGhwIiwianRpIjoibGIxdWtlajd0c25qdncxZjc3eWphOHV0YyIsImlhdCI6MTYyMzMxNzEwMSwiZXhwIjoxNjIzMzE3MTYxfQ.wo2ZFzc9j_GZDiNysr6ANn2ZE0NBkxIrPnXDrWXWlpzt3Lhipsj_jIeq4XSK34kKJzbTZvg02g7K-jramQaHk6IBwL-Xij6wcKKYeqkyY9UAP396WdBO_AK4lhARUIOEJJY8xzAYgp8A9Eq4ADuPquHA9NiQkRoOLrRQdBe-nqzf_lode7bFctfG-vdtzDBl3dV0wj1A6ZY_542RRyQRtqVqjIeGNPl7SJD62Gy3cysi82ftFxy2-72F-igl_jgJ7CsvBx9CLq7csqCqE-ckT0vLxbKa2a360w27oGC7o0O_bdstgsL1n0QptQHXXDtO4-S3txkAwoVw7fas-wg7dZ8z3705-f2g1KlCNSMb889h_rLwI7biIqKnqTGlakKn-A90GfshnHHQmLq88jqyXvvuGdYq6pkRzpDsQPmf8bdsV0V17DKNRjIPiAmswqzkzqOVdfjnivEmZEL0iaE2eKDqCKYNBgS7Bg-ulqPEPWeoQMEWTuVaFgnHqj_u3OnYWMua4rAy_p5m50rwHdersgdE7q-mnOJQk4itoJlhxzxw3fJh_rmn40jq0mvj7R5GEo1VwpeGeFJUvRRtHt5-Q8or_5jslkNwn5Jn2aRDTIK0_T8A150BHqTS3in8_uC3ShviOEOUPaRbEr9mnb_z0UKXvjefN4zK7BBeIPaZlyg","scope":"https://purl.imsglobal.org/spec/lti-nrps/scope/contextmembership.readonly"}}',
      headers: [Object],
      transformRequest: [Array],
      transformResponse: [Array],
      timeout: 0,
      adapter: [Function: httpAdapter],
      xsrfCookieName: 'XSRF-TOKEN',
      xsrfHeaderName: 'X-XSRF-TOKEN',
      maxContentLength: -1,
      maxBodyLength: -1,
      validateStatus: [Function: validateStatus]
    },
    request: <ref *1> ClientRequest {
      _events: [Object: null prototype],
      _eventsCount: 7,
      _maxListeners: undefined,
      outputData: [],
      outputSize: 0,
      writable: true,
      destroyed: false,
      _last: true,
      chunkedEncoding: false,
      shouldKeepAlive: false,
      _defaultKeepAlive: true,
      useChunkedEncodingByDefault: true,
      sendDate: false,
      _removedConnection: false,
      _removedContLen: false,
      _removedTE: false,
      _contentLength: null,
      _hasBody: true,
      _trailer: '',
      finished: true,
      _headerSent: true,
      socket: [Socket],
      _header: 'POST https://moodle.tite.rdc.nie.edu.sg/mod/lti/token.php HTTP/1.1\r\n' +
        'Accept: application/json, text/plain, */*\r\n' +
        'Content-Type: application/json;charset=utf-8\r\n' +
        'User-Agent: axios/0.21.1\r\n' +
        'Content-Length: 1245\r\n' +
        'host: moodle.tite.rdc.nie.edu.sg\r\n' +
        'Connection: close\r\n' +
        '\r\n',
      _keepAliveTimeout: 0,
      _onPendingData: [Function: noopPendingOutput],
      agent: [Agent],
      socketPath: undefined,
      method: 'POST',
      maxHeaderSize: undefined,
      insecureHTTPParser: undefined,
      path: 'https://moodle.tite.rdc.nie.edu.sg/mod/lti/token.php',
      _ended: true,
      res: [IncomingMessage],
      aborted: false,
      timeoutCb: null,
      upgradeOrConnect: false,
      parser: null,
      maxHeadersCount: null,
      reusedSocket: false,
      host: '10.55.99.61',
      protocol: 'http:',
      _redirectable: [Writable],
      [Symbol(kCapture)]: false,
      [Symbol(kNeedDrain)]: false,
      [Symbol(corked)]: 0,
      [Symbol(kOutHeaders)]: [Object: null prototype]
    },
    data: { error: 'invalid_request' }
  },
  isAxiosError: true,
  toJSON: [Function: toJSON]
}

Tried on my local and thought I'll just add the expected logs here (successful generation of access_token) image

As always, thanks so much for the help!