cloud-sdk-js
cloud-sdk-js copied to clipboard
Manual Assignment of CSRF
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.:
- HEAD - https://url:80/v1/resource (get x-csrf-token from header)
- 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
(I assumed this was a Java question)
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
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
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]
}
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
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 ,
I'm happy that you found your solution. Thanks for your feedback and I created a feature request.
Best regards Junjie