Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

@W-17386338 - Secure SSR Endpoints by Verifying SLAS Callback Requests #2180

Conversation

yunakim714
Copy link
Collaborator

@yunakim714 yunakim714 commented Jan 2, 2025

Description

To ensure the security of the POST endpoints written in ssr.js, we need to validate that incoming requests originate from SLAS. This will involve validating the x-slas-callback-token header, which is a JWT provided by SLAS during passwordless login and reset callbacks. Unauthorized requests should be rejected with a 401 Unauthorized response.

Types of Changes

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Documentation update
  • Breaking change (could cause existing functionality to not work as expected)
  • Other changes (non-breaking changes that does not fit any of the above)

Breaking changes include:

  • Removing a public function or component or prop
  • Adding a required argument to a function
  • Changing the data type of a function parameter or return value
  • Adding a new peer dependency to package.json

Changes

  • Add jwt-utils module that will take the x-slas-callback-token header and verify it against the jwks fetched for that SLAS instance
  • In ssr, fetch the JWKS for the SLAS instance with caching
  • Add new jose dependency

How to Test-Drive This PR

  • Go to https://wasatch-mrt-reset-password-poc.mrt-storefront-staging.com/
  • Start tail logs by running the following command: npx @salesforce/pwa-kit-dev@latest tail-logs --project wasatch-mrt --environment reset-password-poc --cloud-origin https://cloud-staging.mrt-staging.com --credentialsFile <pathToFileHere>
  • Click through the reset password flow
  • Verify that you see successful calls to both /jwks and /reset-password-callback
Screenshot 2025-01-14 at 11 22 13 AM

Checklists

General

  • Changes are covered by test cases
  • CHANGELOG.md updated with a short description of changes (not required for documentation updates)

Accessibility Compliance

You must check off all items in one of the follow two lists:

  • There are no changes to UI

or...

Localization

  • Changes include a UI text update in the Retail React App (which requires translation)

@yunakim714 yunakim714 requested a review from hajinsuha1 January 14, 2025 16:25
@yunakim714 yunakim714 marked this pull request as ready for review January 14, 2025 16:26
@yunakim714 yunakim714 requested a review from a team as a code owner January 14, 2025 16:26
* @param {string} options.tenantId - The Tenant ID for the ECOM instance.
* @returns {Promise<*>} Promise with the JWKS data.
*/
async function jwksCaching(req, res, options) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: would we consider moving this method, tenantIdRegExp, and shortCodeRegExp into jwt-utils to reduce the code in ssr.js?

@hajinsuha1
Copy link
Collaborator

hajinsuha1 commented Jan 17, 2025

Verified that /jwks endpoint is only called for the first pasword reset or passwordless call
And verfiied both reset password and passwordless still work and send the email!
Screenshot 2025-01-17 at 4 18 04 PM

@yunakim714 yunakim714 requested a review from adamraya January 21, 2025 15:57
Comment on lines 58 to 60
headers: {
'User-Agent': 'OctoperfMercuryPerfTest'
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: Remove before merging into feature branch.

@adamraya When testing with staging, this header was required to bypass some Cloudflare restrictions. Was this something you saw when testing with Storefront Preview as well?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. At the time we implemented this feature in the private BFF layer, we did not need to add any header.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed with Pratik that customer ECOM instances should hit production CDN so there should be no Cloudflare WAFs blocking this API call!

Comment on lines 16 to 19
const appOrigin = getAppOrigin()
const {app: appConfig} = getConfig()
const shortCode = appConfig.commerceAPI.parameters.shortCode
const tenantId = appConfig.commerceAPI.parameters.organizationId.replace(/^f_ecom_/, '')
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code assumes the values shortCode and tenantId from the static PWA Kit config instead of extracting and validating them directly from the SLAS token.

Probably the assumption is correct in most cases, but it’s not the safest approach. My understanding is that tokens should be validated based on the data they carry to ensure they match their intended context.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@adamraya I am using the values from the PWA Kit config because I want to make sure that we are retrieving the JWKs from the tenant that the customer has configured and make sure that the JWT from the callback matches that.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you consider doing both things? i.e.

  1. Extract the shortCode and tenantId from the token.
  2. Validate the values match the PWA Kit config values.

export const validateSlasCallbackToken = async (token) => {
try {
const jwks = createRemoteJWKSet()
const {payload} = await jwtVerify(token, jwks, {})
Copy link
Collaborator

@adamraya adamraya Jan 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we only relying on jwtVerify() to validate the token? Should we also check headers, such as kid, alg, or jku?

I'm not an expert in this area, but I recommend reaching out to a SLAS expert who can guide you on what is the CC standard to consider a token valid.

Another question for the SLAS expert is whether it is safe to share these validation logic in a public repo.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will ask Blair for his input on this!

Copy link
Collaborator Author

@yunakim714 yunakim714 Jan 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@adamraya I believe it is sufficient to call jwtVerify and have the validation logic in this repo. Unless the PWA team has preferences on where this logic is placed, security wise I believe it is not a risk.

From Blair: "There is no need to validate the header portion of the JWT. The kid is the most important header claim as that is the KeyID of the public key to validate the body signature. Calling jwtVerify should be fine.
I think it would be okay to add the validation of the JWT signature to the repo. After all it is using the Public keys and not exposing the private key or how the JWT was signed."

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. I mention this because we usually have an additional layer of validation that checks those claims to ensure the token context is valid, which jwtVerify does not validate. But let's continue with what the experts say.

Comment on lines 58 to 60
headers: {
'User-Agent': 'OctoperfMercuryPerfTest'
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. At the time we implemented this feature in the private BFF layer, we did not need to add any header.

Comment on lines +36 to +39
const payload = decodeJwt(token)
const subClaim = payload[CLAIM.ISSUER]
const tokens = subClaim.split(DELIMITER.ISSUER)
const tenantId = tokens[2]
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extracting the tenantId from the JWT here by reading the iss claim

Comment on lines +28 to +30
if (tenantId !== configTenantId) {
throw new Error(`The tenant ID in your PWA Kit configuration ("${configTenantId}") does not match the tenant ID in the SLAS callback token ("${tenantId}").`)
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the configured tenantId and the JWT tenantId do not match, an error is thrown.

The short code is not provided as part of the JWT, so we can only rely on the configured value in the config file.

@yunakim714 yunakim714 added the skip changelog Skip the "Changelog Check" GitHub Actions step even if the Changelog.md files are not updated label Jan 22, 2025
@yunakim714 yunakim714 merged commit b70a97c into feature-passwordless-social-login Jan 23, 2025
4 of 29 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
skip changelog Skip the "Changelog Check" GitHub Actions step even if the Changelog.md files are not updated
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants