paypal / paypal-here-sdk-ios-distribution

Add credit card (tap, insert, swipe & key-in) capabilities to your iOS app
Other
82 stars 91 forks source link

Get Access Token without Mid Tier Server #146

Closed maulikpat closed 6 years ago

maulikpat commented 6 years ago

Hello,

I am integrating PayPal Here SDK with iOS. In Documentation it says we need to have mid tier server for getting access tokens and refresh tokens.

But I want to know that is there any way without server need I can get access token ? Currently I am able to generate access by : `

NSString *clientID = @"";

NSString *secret = @"";
NSString *authString = [NSString stringWithFormat:@"%@:%@", clientID, secret];
NSData * authData = [authString dataUsingEncoding:NSUTF8StringEncoding];
NSString *credentials = [NSString stringWithFormat:@"Basic %@", [authData base64EncodedStringWithOptions:0]];

NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
[configuration setHTTPAdditionalHeaders:@{
                                          @"Accept": @"application/json",
                                          @"Accept-Language": @"en_US",
                                          @"Content-Type": @"application/x-www-form-urlencoded",
                                          @"Authorization": credentials }];
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration];

NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:
                                [NSURL URLWithString:@"https://api.sandbox.paypal.com/v1/oauth2/token"]];
request.HTTPMethod = @"POST";

NSString *dataString = @"grant_type=client_credentials";
NSData *theData = [dataString dataUsingEncoding:NSASCIIStringEncoding allowLossyConversion:YES];

__weak typeof(self) weakSelf = self;
NSURLSessionUploadTask *task = [session uploadTaskWithRequest:request fromData:theData completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
    if (!error)
    {
        NSDictionary * dict = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
        NSLog(@"data = %@", dict);
     }`

Response :

data = { "access_token" = "A101.S6WF1CZIz9T... 3K9bqCjUIFmsVy"; "app_id" = "APP-80...43T"; "expires_in" = 32042; nonce = "2016-12-26T10:24:12Z8qEQBxdSGdAbNMg2ivVmUNTUJfyFuSL30OI_W9UCgGA"; scope = "https://uri.paypal.com/services/subscriptions https://api.paypal.com/v1/payments/. https://api.paypal.com/v1/vault/credit-card https://uri.paypal.com/services/applications/webhooks openid https://uri.paypal.com/payments/payouts https://api.paypal.com/v1/vault/credit-card/."; "token_type" = Bearer; }

Now how can I use this token ? As in SDK there is 2 methods

  1. setupWithCredentials - which requires refresh_url which I don't have
  2. setupWithCompositeTokenString - I dont have composite token as I dont have refresh url

I am OK with having credential stored at app side as my app wont go at apple stores.

Any suggestions ?

djMax commented 6 years ago

It's been a long while, but if you don't have a midtier, then you can just refresh the token yourself from the client. This looks like a "first party" token - does it actually work to call the various PPH functions? i.e. can you process a transaction with it? If so, then you can just redo the call above periodically to give the SDK a new access token. If you wanted to be fancy, you could give the SDK a fake refresh url and then intercept the SDK network requests looking for that one, and refresh the token at that time.

(Disclaimer: I don't work at PayPal anymore)

maulikpat commented 6 years ago

@djMax Well, as I can see in demo project there is no way set access token directly to the SDK. There is 2 methods to setup SDK setupWithCredentials and setupWithCompositeTokenString.

Is there a way to set token without these methods ?

I only wanted to accept payments via card readers.

ppmtscory commented 6 years ago

First party tokens won't work for the PPH SDK, unfortunately. You'll need to build out the onboarding flow, simply to get your own refresh token for the first time and then you'll use that, going forward, to generate your access tokens. More info on that process can be found here. If you already have PPH enabled on your PayPal account, then you only need to do the 'Permissions flow' piece from that page.

Secondly, if you're simply generating the access token from the refresh token directly, then you'll utilize the setupWithCredentials method and pass in the access token. You can also pass in a refresh URL to automatically update the access token when needed, else you can do a timer of sorts to know when you need to refresh the access token. More on that process can be found here

It should be in those docs, but I will state it here just for the record. For security purposes, we do not recommend storing client credentials (client ID & secret) along with refresh tokens within the app itself. This is the reason for a server-side integration, cloud or otherwise, to handle such token management.

maulikpat commented 6 years ago

@ppmtscory how can I use setupWithCredentials without refresh url ? Should I pass fake url as djmax mentioned ?

maulikpat commented 6 years ago

I tried to provide fake url like below [PayPalHereSDK setupWithCredentials:[dict valueForKey:@"access_token"] refreshUrl:@"www.google.com" tokenExpiryOrNil:[dict valueForKey:@"expires_in"] thenCompletionHandler:^(PPHInitResultType status, PPHError *error, PPHMerchantInfo *info)

App is crashes :

2017-12-04 12:51:21.816534+0530 TakePayment[783:43621] Assertion failure in -[PPHAccessAccount fetchMerchantInfoAndThen:], /Users/schandrashekar/Documents/Workspace/ios-int-dist/source/PayPalHereSDK/Core/PPHAccessAccount.m:156 2017-12-04 12:51:21.828128+0530 TakePayment[783:43621] Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'OpenID userInfo request failed to return email. Perhaps the scope was invalid?' *** First throw call stack: ( 0 CoreFoundation 0x00000001130391cb exceptionPreprocess + 171 1 libobjc.A.dylib 0x000000011299bf41 objc_exception_throw + 48 2 CoreFoundation 0x000000011303e362 +[NSException raise:format:arguments:] + 98 3 Foundation 0x00000001117da089 -[NSAssertionHandler handleFailureInMethod:object:file:lineNumber:description:] + 193 4 TakePayment 0x000000010fad767b 45-[PPHAccessAccount fetchMerchantInfoAndThen:]_block_invoke + 3019 5 TakePayment 0x000000010f98a9d3 83+[PPSDKCoreServices addNetworkRequest:withID:allowExternalReqHandlers:withHandler:]_block_invoke232 + 1763 6 TakePayment 0x000000010fa2c40a 57-[_OBSCURO_EMFNSNetOperation connectionDidFinishLoading:]_block_invoke_2 + 58 7 libdispatch.dylib 0x0000000115ed03f7 _dispatch_call_block_and_release + 12 8 libdispatch.dylib 0x0000000115ed143c _dispatch_client_callout + 8 9 libdispatch.dylib 0x0000000115edc6f0 _dispatch_main_queue_callback_4CF + 628 10 CoreFoundation 0x0000000112ffbef9 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE + 9 11 CoreFoundation 0x0000000112fc0662 __CFRunLoopRun + 2402 12 CoreFoundation 0x0000000112fbfa89 CFRunLoopRunSpecific + 409 13 GraphicsServices 0x000000011818d9c6 GSEventRunModal + 62 14 UIKit 0x0000000113c6dd30 UIApplicationMain + 159 15 TakePayment 0x000000010f9646ff main + 111 16 libdyld.dylib 0x0000000115f4dd81 start + 1 ) libc++abi.dylib: terminating with uncaught exception of type NSException

App Permissions :

screen shot 2017-12-04 at 1 31 52 pm
ppmtscory commented 6 years ago

That error doesn't have anything to do with the refresh URL. For that, you can just submit an empty string. That error has to do with the scopes selected on your REST app that you posted a screen shot of.

You'll need to click 'Advanced Options' next to Log In with PayPal and then further expand those options that open to make sure that you have all the address fields and email fields selected.

djMax commented 6 years ago

I can't remember - are first party tokens a supported use case?

ppmtscory commented 6 years ago

@djMax Nope. I've tried to talk to the folks responsible (and I believe you did when you were here as well) but it's still sitting as feedback at this point, unfortunately.

maulikpat commented 6 years ago

@ppmtscory I am still getting crash and same crash log. Below is my setup

1 2 3 4

I am providing Return url fake is it ok in Live as well ?

Also, will this approach work with Card Readers or PPH ? I mean getting access token without server.

maulikpat commented 6 years ago

@djMax @ppmtscory any help ? suggestion here ?

ppmtscory commented 6 years ago

Please let me know the sandbox account email address and client ID that you are using so that I can take a look.

maulikpat commented 6 years ago

Sure. email : maulik@dataimpactsol.com Client ID : AaJvdrSKIvQ3tYPJM0oaYLl6Oc1ihKatXNelBYeXJXNwD3Aj77ad2nXuPjf8EzVBTqowk22oW_e64X29

@ppmtscory

maulikpat commented 6 years ago

Any help ?

ppmtscory commented 6 years ago

My apologies on the delay. Your REST app looks ok, can you provide a code sample of how you're generating the access token? Can you validate that you're passing all the right scopes to the /authorize endpoint as outlined here?

maulikpat commented 6 years ago
 @ppmtscory  Please take a look.

 `    [PayPalHereSDK selectEnvironmentWithType:ePPHSDKServiceType_Sandbox];

   `- (void)getToken
  {
NSString *clientID = @"AaJvdrSKIvQ3tYPJM0oaYLl6Oc1ihKatXNelBYeXJXNwD3Aj77ad2nXuPjf8EzVBTqowk22oW_e64X29";
NSString *secret = @"EOirpfzTMY......3CMC4Cy";

NSString *authString = [NSString stringWithFormat:@"%@:%@", clientID, secret];
NSData * authData = [authString dataUsingEncoding:NSUTF8StringEncoding];
NSString *credentials = [NSString stringWithFormat:@"Basic %@", [authData base64EncodedStringWithOptions:0]];

NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
[configuration setHTTPAdditionalHeaders:@{
                                          @"Accept": @"application/json",
                                          @"Accept-Language": @"en_US",
                                          @"Content-Type": @"application/x-www-form-urlencoded",
                                          @"Authorization": credentials }];
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration];

NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:
                                [NSURL URLWithString:@"https://api.sandbox.paypal.com/v1/oauth2/token"]];
request.HTTPMethod = @"POST";

NSString *dataString = @"grant_type=client_credentials";
NSData *theData = [dataString dataUsingEncoding:NSASCIIStringEncoding allowLossyConversion:YES];

__weak typeof(self) weakSelf = self;
NSURLSessionUploadTask *task = [session uploadTaskWithRequest:request fromData:theData completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
    if (!error)
    {
        NSDictionary * dict = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
        NSLog(@"data = %@", dict);
        [PayPalHereSDK setupWithCredentials:[dict valueForKey:@"access_token"] refreshUrl:@"" tokenExpiryOrNil:[dict valueForKey:@"expires_in"] thenCompletionHandler:^(PPHInitResultType status, PPHError *error, PPHMerchantInfo *info) {

            if (error) {
                [weakSelf loginWithPayPal];
            } else {
                [weakSelf gotoPaymentScreen];
            }
        }];
    }
}];

[task resume];
 }`
ppmtscory commented 6 years ago

So like I explained above, you are getting a first party token and those do not work within the SDK. Even though you are processing on your own behalf, it needs to be a third party token that's used so, therefore, you need to implement some internal version of the onboarding so that you can generate a refresh token for your account. You'll then use that refresh token to generate the access tokens going forward. Please review the links in my previous comments above for more info on how to handle the onboarding to get your own refresh token.

maulikpat commented 6 years ago

oh ok :(

So I need to have mid tier server right ?

Also can you suggest any .NET/C# code example to implement at server side ?

@ppmtscory

ppmtscory commented 6 years ago

I don't see the option for the initial redirect to the /authorize endpoint within the SDK, but I see code, here, to turn the authorization code into a refresh token and also refresh the access token based on the refresh token.

ppmtscory commented 6 years ago

We're revisiting the issue of first/third party token management within the SDK so will mark this as an enhancement and report back with the results of those talks.

maulikpat commented 6 years ago

@ppmtscory thanks for your reply !

kodejack commented 6 years ago

I have been trialing this with a bound Xamarin library on version 1.6 and the composite token required consists of AccessToken+TheTokenExpiry+RefreshUrl it would be great not have a dependency on a mid tier server to achieve this.

maulikpat commented 6 years ago

@ppmtscory @DJ92 Issue closed ? So it means there is no chance of this enhancement ?

DJ92 commented 6 years ago

Hey @maulikpat, we have marked this as an enhancement in our internal tasks. Will send an update once this is released.

maulikpat commented 6 years ago

@DJ92 hi. Any update on this ?

DJ92 commented 6 years ago

Hi @maulikpat , the enhancement to eliminate mid-tier server is still in planning and will take time to get prioritized. For now, the suggestion by @ppmtscory in the last comment should be a valid workaround.

maulikpat commented 6 years ago

@DJ92 thanks for your reply. I had already tried that way but as mentioned 1st party token is not working with SDK.

maulikpat commented 6 years ago

@DJ92 also can you tell me if PayPal has any solution like Square has ? Like our app opens PayPal app to take payments and after process of payment PayPal opens our app with info like success or fail ?

DJ92 commented 6 years ago

Hey @maulikpat . Sorry in the delay of my response. I'm checking internally with the team to check on first party authentication using the .NET SDK. For your use case, yes we do have a browser based solution at Sideloader. Here's a demo webapp showing the same use case: Demo App

maulikpat commented 6 years ago

@DJ92 thanks for reply. I will look into it.

maulikpat commented 4 years ago

@DJ92 @ppmtscory Hi, is there any news regarding this? I am still looking for this.

ppmtscory commented 4 years ago

You can use a first party token with the SDK now, but you will still have to have some sort of service to generate the access token and provide it to the SDK. Also, for token expiry, it's good to have it set up so that the SDK can reach out to the URL you provide to get the access token automatically when it expires (access tokens have an 8 hour expiry).

maulikpat commented 4 years ago

@ppmtscory thanks for the quick reply. So mid tire server is required. Is there any demo available for server-side code implementation?

ppmtscory commented 4 years ago

We have multiple code samples in our documentation.

maulikpat commented 4 years ago

@ppmtscory I have implemented the new SDK v2 and running the sample App. I am able to do test transactions as well. Thanks for the help.

In the sample app, it opens http://pph-retail-sdk-sample.herokuapp.com/toPayPal/ for the login page but I don't have that page so do I need to follow the same step and create that page to open login? I mean can I bypass this with a Client ID and secrete?

maulikpat commented 4 years ago

@ppmtscory if my uses only first-party transactions do I need to have mid-tier server? I mean First Party requires an access token to be refreshed?

ppmtscory commented 4 years ago

Yes, access tokens only last 8 hours so you need a way to either have the SDK refresh it (refreshUrl) or you would have to reinitialize every 8 hours when the token expires.

maulikpat commented 4 years ago

@ppmtscory is there any way to check access token is valid or expired?

ppmtscory commented 4 years ago

There's an expiry param that comes back in the response from generating the access token that you can keep track of else if it expires in the process of a HTTP request from the SDK to the PayPal backend, the SDK will provide a 401 unauthorized error back if the access token is expired or invalid. There's a token expired listener that you can set which will allow you to simply provide an access token if you don't have a refresh URL.

maulikpat commented 4 years ago

@ppmtscory ok thank you so much for help. I have set up a mid-tier server (https://github.com/paypal/paypal-retail-node) with Heroku and login flow is working and transactions as well for sandbox by using a card reader.

I want to test the refresh token but as you said token will expire after 8 hours so do I need to wait for 8 hours? or is there any way to check before 8 hours? My code is as below

SdkCredential *sdkCreds = [[SdkCredential alloc] initWithAccessToken:accessToken refreshUrl:[tokenDefault stringForKey:@"https://distestapp123.herokuapp.com/refresh/"] environment:@"sandbox"];

ppmtscory commented 4 years ago

You can test refresh logic by passing an invalid token there. If you pass in a string as "accessToken" for example, it's not valid so the SDK will get an invalid token back from the PayPal back end and then reach out to your refreshUrl.

maulikpat commented 4 years ago

@ppmtscory ok. I did like below

SdkCredential *sdkCreds = [[SdkCredential alloc] initWithAccessToken:@"abcd" refreshUrl:[tokenDefault stringForKey:@"https://distestapp123.herokuapp.com/refresh/"] environment:@"sandbox"];

It gave me errors as below:

Debug ID: (null) Error Message: 401 Error Code: Could not initialize merchant with token

Does it mean an invalid token? I guess no.

To be clear, by https://distestapp123.herokuapp.com/refresh/ means endpoint from https://github.com/paypal/paypal-retail-node/blob/master/index.js

Am I doing it right?

ppmtscory commented 4 years ago

Yes, 401 is an invalid token error. The SDK got that response from the PayPal backend when you called initializeMerchant and the SDK is giving that back to you. At this point, the SDK would reach out to that refresh URL, and you should see the same in the console logs. If that refresh is not happening, then there's something wrong with your heroku instance. You can check those logs to see what happened there. Keep in mind the sample heroku instance that you linked to is meant for instructional purposes only and not for any production deployments.

ppmtscory commented 4 years ago

You can always just host code on your heroku instance that will generate an access token and echo the response.

maulikpat commented 4 years ago

@ppmtscory Heroku doesn't show any error logs for /refresh endpoint

image

I guess the refresh URL is not working. The demo paypal-retail-node has any issue? Because I am just pointing to https://github.com/paypal/paypal-retail-node/blob/0fb2c14fb35d35f5b6710a83427a8253832e752e/index.js#L111 to get refresh token. So demo is not working? or I am doing anything wrong here?

Is there any working demo or more focused document that can guide to create refresh ULR function in Node? So I can add that function code to Heroku.

ppmtscory commented 4 years ago

The sample service is, by default, meant for third party token management and you can see that by the code parameter in your logs (code is returned and then used to get a refresh token which is a third party use case). In order to use first party with that, you'll need to enable the setup flow like it explains in the README. Otherwise, my recommendation, would be to just set your heroku instance to generate an access token via your own code.. https://developer.paypal.com/docs/api/get-an-access-token-curl/

maulikpat commented 4 years ago

@ppmtscory I think, to invalid the token by passing random string is not the correct way to check refresh URL token. Because when I do login 1st time, everything is working and I getting access_token, refresh_url, and env from Paypal login. And log says:

2020-04-22 16:46:38.577723-0700 PPHSDKSampleApp[29412:2723787] TRACK: [merchant] MerchantInitSuccessful { "appId": "None", "appVersion": "None", "appBuild": "None", "appName": "None", "osName": "None", "sdkVersion": "None", "partnerType": "NOREF", "isSideloader": false, "partnerId": "NA", "softDescriptor": "NA", "storeId": "NA", "firmwareImageClientType": "sdk", "payerId": "XGG89PYVT6D86", "isOffline": false }

Now when I change the access token here, SdkCredential *sdkCreds = [[SdkCredential alloc] initWithAccessToken:@"fasdf" refreshUrl:[tokenDefault stringForKey:@"REFRESH_URL"] environment:[tokenDefault stringForKey:@"ENVIRONMENT"]];

log says:

2020-04-22 16:52:11.560616-0700 PPHSDKSampleApp[29433:2727876] INFO: [paypalRest] Attempting an AccessToken refresh from RefreshURL 2020-04-22 16:52:11.568378-0700 PPHSDKSampleApp[29433:2727876] Task <6202C01F-9434-42C5-AB60-1829D60D8870>.<10> finished with error [-1002] Error Domain=NSURLErrorDomain Code=-1002 "unsupported URL" UserInfo={NSUnderlyingError=0x6000015ad5c0 {Error Domain=kCFErrorDomainCFNetwork Code=-1002 "(null)"}, NSErrorFailingURLStringKey=https%3A%2F%2Fdistestapp123.herokuapp.com%2Frefresh%3F%26token%3DM8S2MclCjOfINY%252FSrbmsMdKYoMyudA66CzCLCua02zWeXnXAIPZLSeEIKhIavEoNejqVfY1kzikV9B%252BXguMN%252BP8Ja721QUA8jIPUnWVYprfjxWYHwEsAvO2mOH4P1%252BI7GsubUapyFwu5ibksxZhrUgH553AwmYSMBI0cwMyJD9mRDkTPIclIDX%252FB0utOjID07zkvziVDc5Ee%252F2wZggZexYRKcG7pypF9neyjl7W8cjT7JYzpMcqJGzpRPsSE63jjjSvan%252FVyWF4YBMgV%252FpDTyGmqruO%252FIkv0ZHY3vihZiSKT%252FPV2, NSErrorFailingURLKey=https%3A%2F%2Fdistestapp123.herokuapp.com%2Frefresh%3F%26token%3DM8S2MclCjOfINY%252FSrbmsMdKYoMyudA66CzCLCua02zWeXnXAIPZLSeEIKhIavEoNejqVfY1kzikV9B%252BXguMN%252BP8Ja721QUA8jIPUnWVYprfjxWYHwEsAvO2mOH4P1%252BI7GsubUapyFwu5ibksxZhrUgH553AwmYSMBI0cwMyJD9mRDkTPIclIDX%252FB0utOjID07zkvziVDc5Ee%252F2wZggZexYRKcG7pypF9neyjl7W8cjT7JYzpMcqJGzpRPsSE63jjjSvan%252FVyWF4YBMgV%252FpDTyGmqruO%252FIkv0ZHY3vihZiSKT%252FPV2, NSLocalizedDescription=unsupported URL} 2020-04-22 16:52:11.568781-0700 PPHSDKSampleApp[29433:2727876] ERROR: [paypalRest] Failed to refresh token: { "errMessage": "NSURLErrorDomain", "rzDesc": "{\"headers\":null,\"statusCode\":0,\"body\":null}" }

I confused now. What is the correct implementation at mid-tier server for refresh URL? Any working example available?

ppmtscory commented 4 years ago

Again, for first party, my recommendation would be to just set your heroku instance to generate an access token via your own code. This doc page has curl and postman examples for you to deploy. The refresh URL would just be the link to your heroku instance that has the code to generate the access token.

maulikpat commented 4 years ago

@ppmtscory I think its working now. Though, I didn't change anything in the paypal-retail-node demo code it's still running as it was. But I created another project on Heroku and add just 1 function for the refresh token. And it's returning the access token if I pass any random string in SDK.

So I am opening the Paypal login page by the paypal-retail-node demo code https://distestapp123.herokuapp.com/toPayPal/.

And I am passing refresh URL of my another app/project at Heroku https://pp-integration.herokuapp.com/refresh, which has curl code to get access token, to the SDK.

SdkCredential *sdkCreds = [[SdkCredential alloc] initWithAccessToken:@"dfasd" refreshUrl:@"https://pp-integration.herokuapp.com/refresh" environment:@"sandbox"];

I will try to put it all together on the server-side.

Thanks for your help.