cloud-sdk-js icon indicating copy to clipboard operation
cloud-sdk-js copied to clipboard

Manual Assignment of CSRF

Open schardosin opened this issue 2 years ago • 7 comments

Hi, I am trying to use the Cloud SDK to post in an endpoint that is behind API Management. For some reason the HEAD option is NOT enabled at the entity level, but it works in the base Resource Path Level. So when using postman, I can do a HEAD call in the ResourcePath to get the CSRF token, and then call my entity passing this token.

Eg.:

  1. HEAD - https://url:80/v1/resource (get x-csrf-token from header)
  2. POST - HEAD - https://url:80/v1/resource/entity (passing the x-csrf-token from the first step)

When using the Cloud SDK, it always tries to do a HEAD call at the entity level (https://url:80/v1/resource/entity), once it fails, it tries to do the POST request without the CSRF token and it doesn't work.

I tried to do different calls, one to GET the CSRF token and then adding a custom header with this information, but, besides the fact I can inspect the payload and the CSRF is there, the call is rejected saying the token is invalid (maybe the two different calls are being done by two different clients).

I would like to ask if there is any workaround recommended that I could use to add the CSRF token manually or to replace the URL used to fetch the token.

Thank you, Rafael

schardosin avatar Apr 28 '22 17:04 schardosin

(I assumed this was a Java question)

newtork avatar Apr 28 '22 21:04 newtork

These are the logs, slightly changed to hide the service name:

[2022-04-28T21:20:18.835Z] WARN (csrf-token-header): First attempt to fetch CSRF token failed with the URL: v1/RESOURCE/IDataSet/. Retrying without trailing slash. [2022-04-28T21:20:19.065Z] WARN (csrf-token-header): Second attempt to fetch CSRF token failed with the URL: v1/RESOURCE/IDataSet. No CSRF token fetched. [2022-04-28T21:20:19.066Z] WARN (csrf-token-header): Destination did not return a CSRF token. This may cause a failure when sending the OData request. [2022-04-28T21:20:19.066Z] WARN (csrf-token-header): CSRF header response does not include cookies.

IDataset is the entity called, as it can be seen, CloudSDK (Javascript) uses the same URL to fetch the token. By the comment above it seems it is different from what happens with Java, where it fetches from the service (resourcePath).

Thank you

schardosin avatar Apr 28 '22 21:04 schardosin

Hi @schardosin , Thanks for reaching us.

The log shows the attempts to fetch the CSRF token, which can be disabled. Here is the documentation. Then, with the custom header, it should work.

If it does not, please also share your code snippets.

What I found strange was, if you used the custom header for setting the csrf token manually, the head request should be skipped already, which obviously did not.

@marikaner , we might want to align with Java for switching to the resource path, which is a breaking change.

BR, Junjie

jjtang1985 avatar Apr 29 '22 05:04 jjtang1985

Hi @jjtang1985 , thank you for your support.

The previous log were when sending and letting CloudSDK to fetch the token automatically, and it fails once HEAD is not enabled for the entity (enabled only for the resourcePath)

I and adding below a snippet about when trying to fetch the CSRF manually and then adding as a custom header, any suggestion of workaround would be acceptable at this moment.

var tokentGet = await this.serviceApi.iDataSetApi.requestBuilder().getAll().addCustomHeaders({ "X-CSRF-Token": "fetch" }).filter(FILTER).executeRaw({ destinationName: DESTINATION });
    
console.log("Headers:");
console.log(tokentGet.headers);
this.csrfToken = tokentGet.headers["x-csrf-token"]
console.log("CSRF: " + this.csrfToken);

const response = await this.serviceApi.iDataSetApi.requestBuilder()
  .create(requestBody)
  .skipCsrfTokenFetching()
  .addCustomHeaders({ 'x-csrf-token': this.csrfToken })
  .executeRaw({ destinationName: DESTINATION });

Headers for GET Token:

{
  date: 'Fri, 29 Apr 2022 17:24:51 GMT',
  'content-type': 'application/json;charset=utf-8',
  'content-length': '20',
  connection: 'close',
  'cache-control': 'no-store, no-cache',
  dataserviceversion: '2.0',
  'sap-metadata-last-modified': 'Thu, 28 Apr 2022 13:57:48 GMT',
  'sap-perf-fesrec': '57516.000000',
  'sap-processing-info': 'ODataBEP=,crp=,RAL=,st=X,MedCacheHub=SHM,MedCacheBEP=SHM,codeployed=X,softstate=',
  'sap-server': 'true',
  'set-cookie': [
    'sap-usercontext=sap-client=220; path=/',
    'SAP_SESSIONID_S4D_220=LTEhcKDd-ULV2OZIc3m8qUZ6E1DH4RHsg2UADTrBy2U%3d; path=/'
  ],
  'x-csrf-token': 'vaFlDie9NhOOKy9EBRLXjw==',
  'x-vcap-request-id': 'XYZ',
  'strict-transport-security': 'max-age=31536000; includeSubDomains; preload;'
}

CSRF: vaFlDie9NhOOKy9EBRLXjw==

Errors and Object when Posting:

[2022-04-29T17:24:52.286Z] ERROR    (integration-service): ErrorWithCause: post request to https://url.com:443/v1/RESOURCE failed! 
    at constructError (.../node_modules/@sap-cloud-sdk/odata-common/src/request/odata-request.ts:275:12)
    at .../node_modules/@sap-cloud-sdk/odata-common/src/request/odata-request.ts:241:13
    at processTicksAndRejections (internal/process/task_queues.js:95:5)
    at Connector.sendPostApi (/....ts:26:37)
    at Service.execute (/....ts:33:9)
Caused by:
Error: Request failed with status code 403
    at createError (.../node_modules/@sap-cloud-sdk/odata-common/node_modules/axios/lib/core/createError.js:16:15)
    at settle (.../node_modules/@sap-cloud-sdk/odata-common/node_modules/axios/lib/core/settle.js:17:12)
    at IncomingMessage.handleStreamEnd (.../node_modules/@sap-cloud-sdk/odata-common/node_modules/axios/lib/adapters/http.js:322:11)
    at IncomingMessage.emit (events.js:412:35)
    at IncomingMessage.emit (domain.js:470:12)
    at endReadableNT (internal/streams/readable.js:1317:12)
    at processTicksAndRejections (internal/process/task_queues.js:82:21)
Error: Request failed with status code 403
    at createError (.../node_modules/@sap-cloud-sdk/odata-common/node_modules/axios/lib/core/createError.js:16:15)
    at settle (.../node_modules/@sap-cloud-sdk/odata-common/node_modules/axios/lib/core/settle.js:17:12)
    at IncomingMessage.handleStreamEnd (.../node_modules/@sap-cloud-sdk/odata-common/node_modules/axios/lib/adapters/http.js:322:11)
    at IncomingMessage.emit (events.js:412:35)
    at IncomingMessage.emit (domain.js:470:12)
    at endReadableNT (internal/streams/readable.js:1317:12)
    at processTicksAndRejections (internal/process/task_queues.js:82:21) {
  config: {
    transitional: {
      silentJSONParsing: true,
      forcedJSONParsing: true,
      clarifyTimeoutError: false
    },
    adapter: [Function: httpAdapter],
    transformRequest: [ [Function: transformRequest] ],
    transformResponse: [ [Function: transformResponse] ],
    timeout: 0,
    xsrfCookieName: 'XSRF-TOKEN',
    xsrfHeaderName: 'X-XSRF-TOKEN',
    maxContentLength: -1,
    maxBodyLength: -1,
    validateStatus: [Function: validateStatus],
    headers: {
      Accept: 'application/json',
      'Content-Type': 'application/json',
      authorization: 'Bearer XYZ',
      'x-csrf-token': 'vaFlDie9NhOOKy9EBRLXjw==',
      'User-Agent': 'axios/0.26.0',
      'Content-Length': 2794
    },
    proxy: false,
    httpAgent: Agent {
      _events: [Object: null prototype],
      _eventsCount: 2,
      _maxListeners: undefined,
      defaultPort: 80,
      protocol: 'http:',
      options: [Object],
      requests: {},
      sockets: {},
      freeSockets: {},
      keepAliveMsecs: 1000,
      keepAlive: false,
      maxSockets: Infinity,
      maxFreeSockets: 256,
      scheduling: 'lifo',
      maxTotalSockets: Infinity,
      totalSocketCount: 0,
      [Symbol(kCapture)]: false
    },
    httpsAgent: HttpsProxyAgent {
      _events: [Object: null prototype] {},
      _eventsCount: 0,
      _maxListeners: undefined,
      timeout: null,
      maxFreeSockets: 1,
      maxSockets: 1,
      maxTotalSockets: Infinity,
      sockets: {},
      freeSockets: {},
      requests: {},
      options: {},
      secureProxy: false,
      proxy: [Object],
      promisifiedCallback: [Function: callback],
      [Symbol(kCapture)]: false
    },
    paramsSerializer: [Function: paramsSerializer],
    method: 'post',
    baseURL: 'https://url:443/',
    params: {},
    url: 'v1/RESOURCE/IDataSet',
    data: '{}'
  },
  request: <ref *1> ClientRequest {
    _events: [Object: null prototype] {
      abort: [Function (anonymous)],
      aborted: [Function (anonymous)],
      connect: [Function (anonymous)],
      error: [Function (anonymous)],
      socket: [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: TLSSocket {
      _tlsOptions: [Object],
      _secureEstablished: true,
      _securePending: false,
      _newSessionPending: false,
      _controlReleased: true,
      secureConnecting: false,
      _SNICallback: null,
      servername: 'url.com',
      alpnProtocol: false,
      authorized: true,
      authorizationError: null,
      encrypted: true,
      _events: [Object: null prototype],
      _eventsCount: 7,
      connecting: false,
      _hadError: false,
      _parent: [Socket],
      _host: null,
      _readableState: [ReadableState],
      _maxListeners: undefined,
      _writableState: [WritableState],
      allowHalfOpen: false,
      _sockname: null,
      _pendingData: null,
      _pendingEncoding: '',
      server: undefined,
      _server: null,
      ssl: [TLSWrap],
      _requestCert: true,
      _rejectUnauthorized: true,
      parser: null,
      _httpMessage: [Circular *1],
      [Symbol(res)]: [TLSWrap],
      [Symbol(verified)]: true,
      [Symbol(pendingSession)]: null,
      [Symbol(async_id_symbol)]: 668,
      [Symbol(kHandle)]: [TLSWrap],
      [Symbol(kSetNoDelay)]: false,
      [Symbol(lastWriteQueueSize)]: 0,
      [Symbol(timeout)]: null,
      [Symbol(kBuffer)]: null,
      [Symbol(kBufferCb)]: null,
      [Symbol(kBufferGen)]: null,
      [Symbol(kCapture)]: false,
      [Symbol(kBytesRead)]: 0,
      [Symbol(kBytesWritten)]: 0,
      [Symbol(connect-options)]: [Object],
      [Symbol(RequestTimeout)]: undefined
    },
    _header: 'POST /v1/RESOURCE/IDataSet HTTP/1.1\r\n' +
      'Accept: application/json\r\n' +
      'Content-Type: application/json\r\n' +
      'authorization: Bearer XYZ\r\n' +
      'x-csrf-token: vaFlDie9NhOOKy9EBRLXjw==\r\n' +
      'User-Agent: axios/0.26.0\r\n' +
      'Content-Length: 2794\r\n' +
      'Host: url.com\r\n' +
      'Connection: close\r\n' +
      '\r\n',
    _keepAliveTimeout: 0,
    _onPendingData: [Function: noopPendingOutput],
    agent: HttpsProxyAgent {
      _events: [Object: null prototype] {},
      _eventsCount: 0,
      _maxListeners: undefined,
      timeout: null,
      maxFreeSockets: 1,
      maxSockets: 1,
      maxTotalSockets: Infinity,
      sockets: {},
      freeSockets: {},
      requests: {},
      options: {},
      secureProxy: false,
      proxy: [Object],
      promisifiedCallback: [Function: callback],
      [Symbol(kCapture)]: false
    },
    socketPath: undefined,
    method: 'POST',
    maxHeaderSize: undefined,
    insecureHTTPParser: undefined,
    path: '/v1/RESOURCE/IDataSet',
    _ended: true,
    res: IncomingMessage {
      _readableState: [ReadableState],
      _events: [Object: null prototype],
      _eventsCount: 4,
      _maxListeners: undefined,
      socket: [TLSSocket],
      httpVersionMajor: 1,
      httpVersionMinor: 1,
      httpVersion: '1.1',
      complete: true,
      headers: [Object],
      rawHeaders: [Array],
      trailers: {},
      rawTrailers: [],
      aborted: false,
      upgrade: false,
      url: '',
      method: null,
      statusCode: 403,
      statusMessage: 'Forbidden',
      client: [TLSSocket],
      _consuming: false,
      _dumped: false,
      req: [Circular *1],
      responseUrl: 'https://url.com:443/v1/RESOURCE/IDataSet',
      redirects: [],
      [Symbol(kCapture)]: false,
      [Symbol(RequestTimeout)]: undefined
    },
    aborted: false,
    timeoutCb: null,
    upgradeOrConnect: false,
    parser: null,
    maxHeadersCount: null,
    reusedSocket: false,
    host: 'url.com',
    protocol: 'https:',
    _redirectable: Writable {
      _writableState: [WritableState],
      _events: [Object: null prototype],
      _eventsCount: 3,
      _maxListeners: undefined,
      _options: [Object],
      _ended: true,
      _ending: true,
      _redirectCount: 0,
      _redirects: [],
      _requestBodyLength: 2794,
      _requestBodyBuffers: [],
      _onNativeResponse: [Function (anonymous)],
      _currentRequest: [Circular *1],
      _currentUrl: 'https://url.com:443/v1/RESOURCE/IDataSet',
      [Symbol(kCapture)]: false
    },
    [Symbol(kCapture)]: false,
    [Symbol(kNeedDrain)]: false,
    [Symbol(corked)]: 0,
    [Symbol(kOutHeaders)]: [Object: null prototype] {
      accept: [Array],
      'content-type': [Array],
      authorization: [Array],
      'x-csrf-token': [Array],
      'user-agent': [Array],
      'content-length': [Array],
      host: [Array]
    }
  },
  response: {
    status: 403,
    statusText: 'Forbidden',
    headers: {
      date: 'Fri, 29 Apr 2022 17:24:52 GMT',
      'content-type': 'text/plain;charset=utf-8',
      'content-length': '28',
      connection: 'close',
      'sap-perf-fesrec': '18657.000000',
      'sap-processing-info': 'ODataBEP=,crp=,RAL=,st=,MedCacheHub=,codeployed=X,softstate=',
      'sap-server': 'true',
      'set-cookie': [Array],
      'x-csrf-token': 'Required',
      'x-vcap-request-id': 'XYZ',
      'strict-transport-security': 'max-age=31536000; includeSubDomains; preload;'
    },
    config: {
      transitional: [Object],
      adapter: [Function: httpAdapter],
      transformRequest: [Array],
      transformResponse: [Array],
      timeout: 0,
      xsrfCookieName: 'XSRF-TOKEN',
      xsrfHeaderName: 'X-XSRF-TOKEN',
      maxContentLength: -1,
      maxBodyLength: -1,
      validateStatus: [Function: validateStatus],
      headers: [Object],
      proxy: false,
      httpAgent: [Agent],
      httpsAgent: [HttpsProxyAgent],
      paramsSerializer: [Function: paramsSerializer],
      method: 'post',
      baseURL: 'https://url.com:443/',
      params: {},
      url: 'v1/RESOURCE/IDataSet',
      data: '{}'
    },
    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: [TLSSocket],
      _header: 'POST /v1/RESOURCE/IDataSet HTTP/1.1\r\n' +
        'Accept: application/json\r\n' +
        'Content-Type: application/json\r\n' +
        'authorization: Bearer XYZ\r\n' +
        'x-csrf-token: vaFlDie9NhOOKy9EBRLXjw==\r\n' +
        'User-Agent: axios/0.26.0\r\n' +
        'Content-Length: 2794\r\n' +
        'Host: url.com\r\n' +
        'Connection: close\r\n' +
        '\r\n',
      _keepAliveTimeout: 0,
      _onPendingData: [Function: noopPendingOutput],
      agent: [HttpsProxyAgent],
      socketPath: undefined,
      method: 'POST',
      maxHeaderSize: undefined,
      insecureHTTPParser: undefined,
      path: '/v1/RESOURCE/IDataSet',
      _ended: true,
      res: [IncomingMessage],
      aborted: false,
      timeoutCb: null,
      upgradeOrConnect: false,
      parser: null,
      maxHeadersCount: null,
      reusedSocket: false,
      host: 'url.com',
      protocol: 'https:',
      _redirectable: [Writable],
      [Symbol(kCapture)]: false,
      [Symbol(kNeedDrain)]: false,
      [Symbol(corked)]: 0,
      [Symbol(kOutHeaders)]: [Object: null prototype]
    },
    data: 'CSRF token validation failed'
  },
  isAxiosError: true,
  toJSON: [Function: toJSON]
}

schardosin avatar Apr 29 '22 17:04 schardosin

Hi @schardosin ,

Sorry for coming back late.

I checked your code snippets, which seems to be ok. However, I noticed that:

  • when fetching the csrf token, you use { "X-CSRF-Token": "fetch" }
  • when adding it to the POST request, you use { 'x-csrf-token': this.csrfToken } I'm not sure whether your target system accepts both cases, or maybe you can try the same key?

Also, I found the code below a bit strange, as I found ***Api twice:

this.serviceApi.iDataSetApi.requestBuilder()

As shown in the doc, you only see ***Api once like this.serviceApi.iDataSetApi.requestBuilder(). But I guess this is a copy paste issue, as your get request works.

In general, 403 might be related to the issues:

  • authentication, which should not be the case, as your get request works
  • user roles, please make sure your current user has the permission to make the POST request
  • csrf token

It's always helpful, if you can use e.g., Postman and see whether you can make a POST request, so we are sure, it's SDK issue or not. If you have the permission to check the log of the service (e.g., an On-Prem system), you may find the clues why you got the 403.

BTW, are you using an SAP OData service? Would it be fine to share the name?

Best regards, Junjie

jjtang1985 avatar Jun 07 '22 07:06 jjtang1985

Hi @jjtang1985 , thanks for the response.

The issue (for me) is how cloudSDK for javascript fetches the csrf token, my problem is solved, I asked the owner of the API to enable HEAD method in the entity, then it is working by design.

It would be nice if there were some options to override the fetch token url, at least to be able to fetch it from the resource path. But it is solved now in my case.

Thanks again! Rafael

schardosin avatar Jun 09 '22 23:06 schardosin

@schardosin ,

I'm happy that you found your solution. Thanks for your feedback and I created a feature request.

Best regards Junjie

jjtang1985 avatar Jun 10 '22 05:06 jjtang1985