Skip to content

Conversation

@tdeekens
Copy link
Contributor

@tdeekens tdeekens commented Jul 4, 2020

Summary

This pull request fixes the sdk-auth package to encode the request's body at all times.

Description

The sdk-auth is used to perform request authorization in our Node.js SDK. For that it often uses parameters such as scope, refresh_token or grant_type among others.

The parameters mentioned above can often contain strings which need encoding. This holds true for e.g. the <scope>:<projectKey> or also the token itself.

So far the sdk-auth just concatinated strings to build up the request. For example as body += &scope=${scope}. Then sometimes it encodes a parameter as request.body += &refresh_token=${encodeURIComponent(token)}. Our APIs can deal with this inconsistency but it can also cause problems when a string is not rightfully detected as not encoded. This is what I think @cneijenhuis noticed when having problems with token encoding and investigating it.

As a result of this this pull request suggests to always encode the body's paramters as it's send as 'Content-Type': 'application/x-www-form-urlencoded'.

There are usually two options going about it:

  1. Use a library
  2. Use URLSearchParams (web standard)

I propose to use qss which is a tiny library which works both in Node.js and the browser. We have good experience with it in the Merchant Center.
Using URLSearchParams would need a polyfill as it's not supported in all browsers. Usually leading to larger bundles while we do not need most of it's functionality/API surface.

The pull request also updates the tests affected by this which outlines the change.

This overall instead of for example:

grant_type=client_credentials&scope=view_products:sample-project manage_types:sample-project

gives us

grant_type=client_credentials&scope=view_products%3Asample-project%20manage_types%3Asample-project

Comment on lines +110 to +115
const isNotRefreshTokenGrantType = grantType !== 'refresh_token'
const initialBody = encode({
grant_type: grantType,
...(disableRefreshToken && { refresh_token: false }),
...(isNotRefreshTokenGrantType && { scope }),
})
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Building up the initial request encoding all params.

Comment on lines 120 to 131
static _appendToRequestBody(
request: AuthRequest,
toAppend: Object
): AuthRequest {
const previousDecodedRequestBody = request.body ? decode(request.body) : {}
const nextEncdedRequestBody = encode({
...previousDecodedRequestBody,
...toAppend,
})
request.body = nextEncdedRequestBody

return request
Copy link
Contributor Author

Choose a reason for hiding this comment

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

A small helper reused within the module just to append to the request cause that's what we do most of the time here.

static _appendToRequestBody(
request: AuthRequest,
toAppend: Object
): AuthRequest {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We return the request not just the body here. That makes this easier to compose from my perspective.

Copy link
Contributor

Choose a reason for hiding this comment

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

I like this change a lot 👍

anonymousFlow(anonymousId: string = '', config: CustomAuthOptions = {}) {
const _config = this._getRequestConfig(config)
const request = SdkAuth._buildRequest(
let request = SdkAuth._buildRequest(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We build the request, then append to its body and get a new request. That's what I mean by composing.

@codecov
Copy link

codecov bot commented Jul 4, 2020

Codecov Report

Merging #1581 into master will increase coverage by 0.00%.
The diff coverage is 100.00%.

Impacted file tree graph

@@           Coverage Diff           @@
##           master    #1581   +/-   ##
=======================================
  Coverage   98.63%   98.63%           
=======================================
  Files         128      128           
  Lines        3305     3307    +2     
  Branches      761      760    -1     
=======================================
+ Hits         3260     3262    +2     
  Misses         41       41           
  Partials        4        4           
Impacted Files Coverage Δ
packages/sdk-auth/src/auth.js 100.00% <100.00%> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 7371b71...2913cfe. Read the comment docs.

@tdeekens
Copy link
Contributor Author

tdeekens commented Jul 4, 2020

@daern91 please assign anybody else if needed. You seem to be most familiar with this however so maybe a good reviewer 😄.

@tdeekens tdeekens marked this pull request as ready for review July 4, 2020 17:57
Copy link
Contributor

@daern91 daern91 left a comment

Choose a reason for hiding this comment

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

Thanks for this one. Much cleaner now with a proper lib and not the hacky template strings 👍

Would also be nice to have the ability to use the, supposedly faster, querystring.stringify native solution in node. As described here: https://github.com/lukeed/qss#qss-

But OTOH it's probably not necessary and can be implemented at a later stage if needed.

toAppend: Object
): AuthRequest {
const previousDecodedRequestBody = request.body ? decode(request.body) : {}
const nextEncdedRequestBody = encode({
Copy link
Contributor

Choose a reason for hiding this comment

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

nitpick typo:
nextEncdedRequestBody
nextEncodedRequestBody

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks. Feel free to use GitHub's review suggestions next time. Helps to get better diffs of what you carefully markdowned yerself :)

static _appendToRequestBody(
request: AuthRequest,
toAppend: Object
): AuthRequest {
Copy link
Contributor

Choose a reason for hiding this comment

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

I like this change a lot 👍

username: string,
password: string
): string {
): AuthRequest {
Copy link
Contributor

Choose a reason for hiding this comment

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

Here too 👍


if (anonymousId) request.body += `&anonymous_id=${anonymousId}`
if (anonymousId)
request = SdkAuth._appendToRequestBody(request, {
Copy link
Contributor

Choose a reason for hiding this comment

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

I guess we could technically leave request as a constant and return here instead of re-assigning and returning below. Just a matter of flavor tho.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, didn't want to change such things much in this PR. Generally the composition in this code can be improve. Ideally, the AuthRequeust could have a map and value function. Then you could just request.map(SdkAuth._appendToRequestBody(...)) which gives you a mapped request and then value() when you pass it somewhere. That's another story though.

username,
password
)
request = SdkAuth._appendUserCredentialsToBody(request, username, password)
Copy link
Contributor

Choose a reason for hiding this comment

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

same with this one, but might just make things too nested and unreadable.

request.body = SdkAuth._appendUserCredentialsToBody(
request.body,
request = SdkAuth._appendUserCredentialsToBody(
request,
Copy link
Contributor

Choose a reason for hiding this comment

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

I see, we keep this pattern throughout 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The conditionals are a bit hard to improve without better patterns of compositions. I will adjust those which don't involve them.

@daern91 daern91 requested a review from a team July 5, 2020 19:15
@daern91
Copy link
Contributor

daern91 commented Jul 5, 2020

Adding @commercetools/clients-team as FYI

@cneijenhuis
Copy link
Contributor

Encoding LGTM 👍

@tdeekens
Copy link
Contributor Author

tdeekens commented Jul 6, 2020

Would also be nice to have the ability to use the, supposedly faster, querystring.stringify native solution in node. As described here: lukeed/qss#qss-

That's a Node.js utility. We also run in the browser. Given we encode three things - as you mentioned - I don't see this, comparing to other impacts, relevant for performance of this lib. With 100k ops yes, not with 3.

Thanks for this one. Much cleaner now with a proper lib and not the hacky template strings 👍

I am still not super happy with this. Encapsulation is sub-optimal:

  1. The AuthRequest should have a map and value and sort of act as a Functor
  2. The encoding chould be "inside of" it. Not part of the surrouding code

Still this is better than before and step-wise is how to improve things 🐱.

@daern91
Copy link
Contributor

daern91 commented Jul 7, 2020

@jenschude any comments on this one before we merge?

Copy link
Contributor

@jenschude jenschude left a comment

Choose a reason for hiding this comment

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

Just a comment about the testing part

`grant_type=password&scope=manage_project:${config.projectKey}` +
`&username=user%204%5El*aJ%40ETso%2B%2F%5CHdE1!x0u4q5` + // encoded
`&password=pass%204%5El*aJ%40ETso%2B%2F%5CHdE1!x0u4q5` // encoded
encode({
Copy link
Contributor

Choose a reason for hiding this comment

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

Also it may make the test more readable you are testing the encoding with the function to encode itself. I'm pretty sure that this works correct but a non working the encode function would result in a still working test.

But I also can see that the encode function may not produce a canonical string to compare it to a static string.

Copy link
Contributor Author

@tdeekens tdeekens Jul 7, 2020

Choose a reason for hiding this comment

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

Good point. Let me suggest something later :)

I played around a bit. Somehow having the encode feels alright to me. The nock library will throw if a mock is not defined. When expecating on a string I would use Inline Snapshots.

Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yet another good point 💋. Thanks for being persistent. Here we go: 2913cfe.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I take the thumbs up as an approval.

@tdeekens tdeekens merged commit 0e9ca25 into master Jul 7, 2020
@tdeekens tdeekens deleted the fix/sdk-auth-encoding branch July 7, 2020 11:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants