Hello,
I followed this doc to create OAuth External Credential with the JWT Bearer Flow. I need to access Salesforce Headless API in my Salesforce org. The document states:
"We recommend that you request a refresh token or offline access. Otherwise, when the token expires, you lose access to the external system"
However when I set the scope to the "refresh_token offline_access" or "refresh_token" or "offline_access", I get the error "Unable to complete the JWT token exchange scope parameter not supported". If I don't include this scope then the access token will expire in 24 hours and the integration breaks because of an expired token.
What I am doing wrong? How can I use JWT and guarantee that access token is automatically refreshed when needed?
UPDATE
-------------
My use-case is the following: we want to use Headless Registration Flow for Private Clients for an experience site we are building. So I have only 1 salesforce org which is both source and target at the same time. For this I followed the example here Customize Your Experience Site’s Forgot Password Functionality with Headless Identity APIs. In the example JWT access token is requested via apex. But I found out that I could use External Credentials + Named Credentials, and save some apex code lines :) And it worked, BUT then after some time (~24 hours) callouts to the `/services/auth/headless/init/registration` endpoint started to fail with the error "authentication failure".
public RegistrationResponse initRegistration(RegistrationRequest requestBody) {
HttpRequest request = new HttpRequest();
request.setMethod('POST');
String url = 'callout:Headless_API_Endpoint/services/auth/headless/init/registration';
request.setEndpoint(url);
request.setBody(System.JSON.serialize(requestBody));
request.setHeader('Content-Type', 'application/json');
HttpResponse response = new Http().send(request);
RegistrationResponse registrationResponse = new RegistrationResponse();
//Do this because response structure is unknown when failure.
Map<String, Object> responseMap = (Map<String, Object>) System.JSON.deserializeUntyped(response.getBody());
if (responseMap.keySet().contains('status') && (String) responseMap.get('status') == 'success') {
registrationResponse = (RegistrationResponse) JSON.deserialize(
response.getBody(),
RegistrationResponse.class
);
} else {
registrationResponse.status = 'failed';
registrationResponse.message = response.getBody();
}
return registrationResponse;
}
Debug Logs (some values changed due to privacy):
- System.HttpRequest[Endpoint=callout:Headless_API_Endpoint/services/auth/headless/init/registration, Method=POST]
- NamedCallout[Named Credential Id=0XEEO0000002ls11, Named Credential Name=Headless_API_Endpoint, Endpoint=https://dev--admin.sandbox.my.site.com/myshopvforcesite/services/auth/headless/init/registration, Method=POST, External Credential Type=EXTERNAL, HTTP Header Authorization=Method: Bearer - Credential: b126f52df000fsadf38105bd9866851a040121a7bad1eb0brsafdfa0, Content-Type=application/json, Request Size bytes=968, Retry on 401=True]
- NamedCallout[Named Credential Id=0XEEO0000002ls11, Named Credential Name=Headless_API_Endpoint, Status Code=400, Content-Type=application/json;charset=UTF-8, Response Size bytes=62, Overall Callout Time ms=272, Connect Time ms=7
- CALLOUT_RESPONSE|[47]|System.HttpResponse[Status=Bad Request, StatusCode=400]
- EXCEPTION_THROWN|[51]|RegistrationException: {"status":"failed","message":"{\"invalid_request\":\"authentication failure\",\"status\":\"failed\"}","identifier":null,"email":null}
@Eduard Yavorovenko apologies if this has already been stated but I have not seen it:
1. Headless Identity flows are not meant to be used on experience cloud. This is made clear in our documentation (see first paragraph: https://help.salesforce.com/s/articleView?id=sf.headless_identity_customers_overview.htm&type=5). This is also stated in the blog post in the paragraph that introduces the headless APIs the talks about how to use the headless forgot password flow to fill a current gap in LWR communities. The right way to do what you are doing is to use the self reg apex handler or build a custom self reg page using apex.
2. Because headless Identity flows are not formally supported to be used from on the platform, their compatibility with named credentials is not relevant, this is not a use case we want to support or plan to support. If you wish to do this, you need use Apex. All of the above being said, we have a bug on the backlog to update our error code to a 401 not a 400, but this is a low priority bug given that it really affects named credentials and again the use of named creds in this context is not formally supported.
3. While not talked about in your implementation, I just want to make sure that you have implemented BOT protection via captcha or something else on your reg form, if you have not you need to do this, as the authenticated context mode assumes the caller has done bot protection checks before calling our APIs.
Got a reply from SF Support. Not sure what I am supposed to do now :)
_____
I have checked with the Product Team and can confirm that JWT bearer flow will not need to pass any scope as it shall be taken care by the platform only. You can consider this as a Doc BUg and this will be modified in the public facing documentation.
Now coming to the issue we have here the reason the session is getting termination and refresh token is not getting generated here as ideally the refresh token would get generated when the client response back with 401 but for our case it is 400, bad request.
"401 is the standard code to indicate a token refresh is needed".
____
Sushil Kumar (UKG) Forum Ambassador
Strange behavior..... So I was testing with this simple tooling call "callout:JWTFlowNameCredential/services/data/v58.0/tooling". It behaved as i described above in my message. And i compared your setup with mine, only difference i see that you are passing some Oauth scope as user_Registracion_api_refresh token, I am hope this is the one not causing some issue with token issuance. Maybe try removing this and see if it works. Only other thing i can recommend is reaching out to SF support as External credentials are giving different behavior in different API use cases.
Hi @Samuel Rosen, thank you for your reply. The reason why we needed to use Headless API for Self-Registration in our B2B Commerce LRW expericnce site was because we wanted (1) to have full control over OTP email design and (2) to confirm an email address of a self-registered customer before creating their user. This unfortunately, is not possible with "the self reg apex handler or build a custom self reg page using apex" option you mentioned above.
The apex solution to get access token worked (as described in Customize Your Experience Site’s Forgot Password Functionality with Headless Identity APIs).
For BOT protection we are using Friendly Captcha and it works.
Hi @José PassosJosé Passos (Capgemini), unfortunately not. I created a support case and it is still in progress. I hope SF can find a solution. I'll post updates here.
Hey @Eduard Yavorovenko,
Were you able to bypass the issue?
I'm also facing the same issue trying to access B2C commerce API using JWT named credentials. If I just change the Named Credential it just works, but like you, after some time (24h or less) I start getting a 403, which in theory means the token is valid but the seems the scopes are gone.
- 11:42:30.0 (162795285)|CALLOUT_REQUEST|[261]|System.HttpRequest[Endpoint=callout:B2C_Commerce_API/inventory/reservation/v1/organizations/<ommited mytenantGroupId from Omnichannel Inventory>/reservation-documents/JQP-20240227_0001, Method=PUT]
- 11:42:31.464 (1464282908)|NAMED_CREDENTIAL_REQUEST|NamedCallout[Named Credential Id=0XA7Y0000008s8J, Named Credential Name=B2C_Commerce_API, Endpoint=https://DE.api.commercecloud.salesforce.com/inventory/reservation/v1/organizations/<ommited mytenantGroupId from Omnichannel Inventory>/reservation-documents/JQP-20240227_0001, Method=PUT, External Credential Type=EXTERNAL, HTTP Header Authorization=Method: Bearer - Credential: fbf2bcf3a5a3d9186429a08a1c7df2ec7aa7d00e707ec1b5820cd47cd2b8adbc, Content-Type=application/json, Request Size bytes=6593, Retry on 401=True]
- 11:42:31.465 (1465264924)|NAMED_CREDENTIAL_RESPONSE|NamedCallout[Named Credential Id=0XA7Y0000008s8J, Named Credential Name=B2C_Commerce_API, Status Code=403, Content-Type=text/html;charset=utf-8, Response Size bytes=431, Overall Callout Time ms=1293, Connect Time ms=1
- 11:42:31.464 (1472888996)|CALLOUT_RESPONSE|[261]|System.HttpResponse[Status=Forbidden, StatusCode=403]
After manually recreate the Principal and readd it to the permission set it starts working
- 12:27:00.0 (154218740)|CALLOUT_REQUEST|[261]|System.HttpRequest[Endpoint=callout:B2C_Commerce_API/inventory/reservation/v1/organizations/<ommited mytenantGroupId from Omnichannel Inventory>/reservation-documents/JQP-20240227_0001, Method=PUT]
- 12:27:02.276 (2276386353)|NAMED_CREDENTIAL_REQUEST|NamedCallout[Named Credential Id=0XA7Y0000008s8J, Named Credential Name=B2C_Commerce_API, Endpoint=https://DE.api.commercecloud.salesforce.com/inventory/reservation/v1/organizations/<ommited mytenantGroupId from Omnichannel Inventory>/reservation-documents/JQP-20240227_0001, Method=PUT, External Credential Type=EXTERNAL, HTTP Header Authorization=Method: Bearer - Credential: 7cd867d5f8ed6ee9e9116b94984e96f7453687c3b33a6f2079c12982674831f9, Content-Type=application/json, Request Size bytes=6593, Retry on 401=True]
- 12:27:02.277 (2277513616)|NAMED_CREDENTIAL_RESPONSE|NamedCallout[Named Credential Id=0XA7Y0000008s8J, Named Credential Name=B2C_Commerce_API, Status Code=200, Content-Type=application/json, Response Size bytes=9813, Overall Callout Time ms=1837, Connect Time ms=0
- 12:27:02.276 (2278077982)|CALLOUT_RESPONSE|[261]|System.HttpResponse[Status=OK, StatusCode=200]
@Sushil Kumar, I changed timeout to 15 mins and this is what I see (same token after 15 mins)
- 22:37:41.1 (21909526)|CALLOUT_REQUEST|[47]|System.HttpRequest[Endpoint=callout:Headless_API_Endpoint/services/auth/headless/init/registration, Method=POST]
- 22:37:41.881 (881004030)|UNKNOWN|NamedCallout[Named Credential Id=0XDDD0000003ls1, Named Credential Name=Headless_API_Endpoint, Endpoint=https://dev--admin.sandbox.my.site.com/myshopvforcesite/services/auth/headless/init/registration, Method=POST, External Credential Type=EXTERNAL, HTTP Header Authorization=Method: Bearer - Credential: 3306c445744970931c95581ed0e0e4f697ce8226bbb9fb2f110693f3e82d864a, Content-Type=application/json, Request Size bytes=966, Retry on 401=True]
- 22:37:41.882 (882174073)|UNKNOWN|NamedCallout[Named Credential Id=0XDDD0000003ls1, Named Credential Name=Headless_API_Endpoint, Status Code=200, Content-Type=application/json;charset=UTF-8, Response Size bytes=87, Overall Callout Time ms=546, Connect Time ms=10
- 22:37:41.1 (882854907)|CALLOUT_RESPONSE|[47]|System.HttpResponse[Status=OK, StatusCode=200]
- 22:53:20.1 (21909526)|CALLOUT_REQUEST|[47]|System.HttpRequest[Endpoint=callout:Headless_API_Endpoint/services/auth/headless/init/registration, Method=POST]
- 22:53:20.466 (466693869)|UNKNOWN|NamedCallout[Named Credential Id=0XDDD0000003ls1, Named Credential Name=Headless_API_Endpoint, Endpoint=https://dev--admin.sandbox.my.site.com/myshopvforcesite/services/auth/headless/init/registration, Method=POST, External Credential Type=EXTERNAL, HTTP Header Authorization=Method: Bearer - Credential: 3306c445744970931c95581ed0e0e4f697ce8226bbb9fb2f110693f3e82d864a, Content-Type=application/json, Request Size bytes=966, Retry on 401=True]
- 22:53:20.467 (467912000)|UNKNOWN|NamedCallout[Named Credential Id=0XDDD0000003ls1, Named Credential Name=Headless_API_Endpoint, Status Code=400, Content-Type=application/json;charset=UTF-8, Response Size bytes=62, Overall Callout Time ms=189, Connect Time ms=12
- 22:53:20.1 (519881769)|CALLOUT_RESPONSE|[47]|System.HttpResponse[Status=Bad Request, StatusCode=400]
Ok, thanks! Found it in the Classic view of the Connected App. I also updated the description of the question. Provided more details about the issue.