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
Checkout V2: Retain and refresh OAuth access token #5098
Checkout V2: Retain and refresh OAuth access token #5098
Conversation
auth_token = @access_token ? "Bearer #{@access_token}" : @options[:secret_key] | ||
auth_token = "Bearer #{@options[:access_token] || @options[:secret_key]}" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could be a breaking change as this is altering the authentication from Basic
to Bearer
auth for gateway tokens that only have a secret_key. Why are we making this change? Is it compatible to use Bearer auth with a secret_key?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This change is because the following guide and this ticket that described it, to continue supporting the secret_key
in current merchants, I could add a condition that when the secret_key
includes sk_sbox_
add Bearer
and otherwise do not include the Bearer
way
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have an assumption that the previous secret_key
ends with a uuid v4, so I am going to upload my changes so that all the tests in CORE work and once I have confirmation if my assumption is correct or not I will update the code (if it is necessary) and mention it to continue with the review :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I removed these changes from this PR, they caused a lot of confusion and they will be in a new PR :)
11f1e79
to
4189241
Compare
response = parse(ssl_post(access_token_url, 'grant_type=client_credentials', access_token_header)) | ||
@options[:access_token] = response['access_token'] | ||
rescue ResponseError => e | ||
@multiresponse = MultiResponse.new |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why add a MultiResponse here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The initial idea was to obtain the response when the access_token
is created so that we can debug better in case something fails and know what is related to the access_token
, I moved this logic elsewhere and I think it looks better :)
@@ -1,11 +1,13 @@ | |||
require 'pry' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can probably be removed now
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, thanks!
@@ -508,8 +510,17 @@ def response(action, succeeded, response, options = {}, source_id = nil) | |||
end | |||
|
|||
def headers(action, options) | |||
auth_token = @access_token ? "Bearer #{@access_token}" : @options[:secret_key] | |||
auth_token = @options[:public_key] if action == :tokens | |||
auth_token = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would prefer if these was it's own PR like it was before. It would make it a lot easier to diagnose a problem if something goes wrong when it's deployed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, I will
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please don't forget to make this into it's on PR
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did it, it's just that in this same PR I added the changes from the ticket: ECS-3487 about Update Authorization from Basic to Bearer for Checkout gateway
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know but it's better if these are in two separate PRs. One for retaining and refreshing the access_token and another for updating the authorization. Keeping this two changes separate will make it easier to diagnose if something goes wrong when we merge it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok, I will
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I removed these changes from this PR, they caused a lot of confusion and they will be in a new PR
rescue ResponseError => e | ||
if e.response.to_s.match?(/Unauthorized/) && @options[:access_token] && action != :tokens | ||
@options[:access_token] = nil | ||
return commit(action, post, options, authorization, method) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a bit worried this could cause a constant loop if a transaction fails and it going through commit again and again. I'm not sure if that possible but wanted to bring it up.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also don't think Unauthorized is a good error message to retry since I think this is a clear indication that the credentials are incorrect. The error that brought this ticket is Failed with 429 Too Many Requests so I'm not sure if this is a better one
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I changed the implementation of this part, to do this I added a new field in the gateway options called expires
, now we prevent the loop and we know better what the error is in case it occurs
4189241
to
d9ae367
Compare
end | ||
|
||
def commit(action, post, options, authorization = nil, method = :post) | ||
def access_token_valid? | ||
@options[:access_token] && @options.fetch(:expires, 0) > DateTime.now.strftime('%Q').to_i |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would suggest making sure the the default value for expires
is 0. Currently @options.fetch(:expires, 0)
could be nil which cause a NoMethodError when checking if it's less than
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh it think that's what you are trying to do with fetch and make 0 the default but for some reason it's not working
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will change it to:
@options[:access_token] && @options[:expires] && @options[:expires] > DateTime.now.strftime('%Q').to_i
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done!
response = parse(raw_response) | ||
response = parse(ssl_post(access_token_url, 'grant_type=client_credentials', access_token_header)) | ||
@options[:access_token] = response['access_token'] | ||
@options[:expires] = (DateTime.now + response.fetch('expires_in', 0).seconds).strftime('%Q').to_i |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some thing for this fetch it needs to be updated since it's not going to default to 0
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done!
error_code: error_code_from(false, response, {}) | ||
) | ||
rescue ResponseError => e | ||
Response.new(false, e.response.body.to_s, {}, test: test?) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Considering the way that y'all wrote this PR think this could be an OAuthResponseError instead
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, I will
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done!
@almalee24 @sinourain I'd like to explore options other than tracking an expiration date before we merge this - will chat offline. |
def response(action, succeeded, response, options = {}, source_id = nil) | ||
def commit(action, post, options, authorization = nil, method = :post) | ||
if @options[:secret_key] || access_token_valid? | ||
peform_payin(action, post, options, authorization, method) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What will happen if the token is valid when this is executed, but due to communications lag, it expires before the gateway receives it? It will not attempt to refresh the token, and will just fail with an auth failure, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The token is valid for 60mins so if there's a communication lag it would be a few minutes at most but yes if it expires it would fail with an auth failure. But if the customer is retries the transaction we would refresh the token.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To prevent transactions from being made very close to the expiration and therefore failing due to lag, I was thinking that we could, for example, subtract a minute or two from the expiration time (expires
) and with this we prevent there from being a lower failure rate. What do you think?
d9ae367
to
006d44e
Compare
f32fabf
to
372ecc2
Compare
@options[:access_token].present? && @options[:expires] && @options[:expires].to_i > DateTime.now.strftime('%Q').to_i | ||
end | ||
|
||
def peform_payin(action, post, options, authorization = nil, method = :post) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is a small typo but it should be perform_payin
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks!
def commit(action, post, options, authorization = nil, method = :post) | ||
MultiResponse.run do |r| | ||
r.process { setup_access_token } unless @options[:secret_key] || access_token_valid? | ||
r.process { peform_payin(action, post, options, authorization, method) } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same typo here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks!
end | ||
|
||
def commit(action, post, options, authorization = nil, method = :post) | ||
def access_token_valid? | ||
@options[:access_token].present? && @options[:expires] && @options[:expires].to_i > DateTime.now.strftime('%Q').to_i |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since you've defaulted expires to be 0 i don't think the extract @options[:expires] is necessary
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
make sense
rescue ResponseError => e | ||
@options[:access_token] = '' if e.response.code == '401' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would suggest just to added check here for access_token in case the the transaction fails when using the secret_key
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good catch!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@almalee24 I'm adding a test for this but it doesn't make sense since the access_token
takes precedence over secret_key
according to this line of code
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I guess it's not a big issue but if secret_key transaction fails with 401 then we do @options[:access_token] = ''
which we don't need to do because we didn't use access_token. It's not breaking anything so it's not something you need to change but a nice improvement I guess.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh, ok, i got it
372ecc2
to
086c17c
Compare
end | ||
Response.new( | ||
response['access_token'].present?, | ||
message_from(true, response, {}), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just another small issue is that this should only be true if response['access_token'].present?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will I use access_token_valid?
as a verifier since we assign parts of the response
that allow us to validate the presence of the access_token
and the expires
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, you just also have to update message_from(true, response, {})
to be message_from(access_token_valid?, response, {})
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done!
086c17c
to
bd03d37
Compare
message_from(true, response, {}), | ||
response.merge({ expires: @options[:expires] }), | ||
test: test?, | ||
error_code: error_code_from(false, response, {}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be the same here, it should be error_code_from(access_token_valid?, response, {})
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done!
bd03d37
to
e8d04a1
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me. Thanks for all the hard work!
response = parse(raw_response) | ||
response = parse(ssl_post(access_token_url, 'grant_type=client_credentials', access_token_header)) | ||
@options[:access_token] = response['access_token'] | ||
@options[:expires] = response['expires_in'] && response['expires_in'] > 0 && (DateTime.now + (response['expires_in'] - 120).seconds).strftime('%Q').to_i |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is confusing to me with the &&
behavior with integers mixed with booleans. Could you add a comment that explains the purpose? Or maybe extract to named helper methods?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I'm going to create a comment and a new method
@options[:access_token].present? && @options[:expires].to_i > DateTime.now.strftime('%Q').to_i | ||
end | ||
|
||
def perform_payin(action, post, options, authorization = nil, method = :post) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
payin
is an unusual term for AM - I'd suggest just perform_request
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok, thanks!
e8d04a1
to
09ce962
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the tweaks!
Retain OAuth access token to be used on subsequent transactions and refreshed when it expires Spreedly reference: [ECS-2996](https://spreedly.atlassian.net/browse/ECS-2996) Unit: 66 tests, 403 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 100% passed 2902.89 tests/s, 17725.19 assertions/s Remote: 103 tests, 254 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 100% passed 0.63 tests/s, 1.56 assertions/s
09ce962
to
311d29a
Compare
This changes contains:
Spreedly reference:
ECS-2996
Unit:
66 tests, 403 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 100% passed
2902.89 tests/s, 17725.19 assertions/s
Remote:
103 tests, 254 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 100% passed
0.63 tests/s, 1.56 assertions/s