I am using the PayPal REST API in Java to make a request to the testing (sandbox) servers for an access token, which succeeds, and then sending that access token back to the same sandbox servers for a Payment which fails with a 401 Not Authorized.
I followed the PayPal REST API instructions found here: https://developer.paypal.com/webapps/developer/docs/api/#authentication--headers and here https://developer.paypal.com/webapps/developer/docs/integration/direct/make-your-first-call/
I tried using the OAuthTokenCredential
object to provide my clientId
and clientSecret
but it only returns me the access token, not the appID
provided by PayPal nor the expiresIn
time in minutes. In addition, it complains about a missing sdk_conf.properties
file in my classpath.
I then created my own class to make the call using Apache's HttpClient to get the full response and that works:
Custom PayPalAPI interface for RestEasy
@Path("v1/oauth2/token")public interface PayPalOAuthAPI { @POST @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_FORM_URLENCODED) public OAuthInfo requestAccessToken( @HeaderParam("Authorization") String basicEncodedToken, String requestBody );}
Custom OAuth token request
private OAuthInfo getOAuthInfo() { OAuthInfo info = null; PayPalOAuthAPI client = HttpClientFactory.createRestEasyClient(80, 443, 120, 2, API_ENDPOINT, PayPalOAuthAPI.class, true); info = client.requestAccessToken("Basic "+ generateBase64String(PP_API_USER, PP_API_SECRET),"grant_type=client_credentials"); if (info == null) { throw new RuntimeException(APIResultCodes.REMOTE_UNAUTHORIZED, "Could not authenticate with PayPal."); } return info;}
This all works correctly and returns me a 200 OK status with the expected JSON response including an access token.
I then take that token and provide it to my Payment
object which uses PayPal's code to communicate with their servers (under the hood I believe it's basic java.lang.net.HttpConnection code) and that returns a 401 Unauthorized.
Payment call using PayPal's REST API
// get a new tokenString accessToken = getOAuthInfo().getAccessToken();// configProps is a Properties object pre-populated with sdk_conf.properties// values except service.EndPoint, clientID, and clientSecretconfigProps.setProperty("service.EndPoint", API_ENDPOINT);// ... transaction objects omittedtry { Payment payment = new Payment(); Payment.initConfig(configProps); payment.setIntent(Intent.sale.toString()); payment.setPayer(payer); payment.setTransactions(transactions); Payment createdPayment = payment.create(accessToken);} catch (PayPalRESTException ex) { LOG.error("Failed to submit donation", ex);}
Log output
(including HttpClient request/response with IDs redacted)
2013-08-19 16:34:11,077 [qtp1005310362-44] DEBUG:org.apache.http.impl.conn.DefaultClientConnection Sending request: POST /v1/oauth2/token HTTP/1.12013-08-19 16:34:11,077 [qtp1005310362-44] DEBUG:org.apache.http.wire >> "POST /v1/oauth2/token HTTP/1.1[\r][\n]"2013-08-19 16:34:11,077 [qtp1005310362-44] DEBUG:org.apache.http.wire >> "Content-Type: application/x-www-form-urlencoded[\r][\n]"2013-08-19 16:34:11,077 [qtp1005310362-44] DEBUG:org.apache.http.wire >> "Accept: application/json[\r][\n]"2013-08-19 16:34:11,077 [qtp1005310362-44] DEBUG:org.apache.http.wire >> "Authorization: Basic QWM1UmNoQ3g3dHB1bWZSZW9rcUR3MW41bHRXaktQU0xRODIyWUJWWXpEdXpZUGJuc0J0eDZYWGlqX1pROkVJVnQ5eERmM2JnQmw1OG5KYlZ2VmtSR3JCaVZVN1BIWGtSV01mQjVqb3NxRTNkbWxCcF9TV05BdU91eA==[\r][\n]"2013-08-19 16:34:11,077 [qtp1005310362-44] DEBUG:org.apache.http.wire >> "Accept-Encoding: gzip, deflate[\r][\n]"2013-08-19 16:34:11,077 [qtp1005310362-44] DEBUG:org.apache.http.wire >> "Content-Length: 29[\r][\n]"2013-08-19 16:34:11,077 [qtp1005310362-44] DEBUG:org.apache.http.wire >> "Host: api.sandbox.paypal.com[\r][\n]"2013-08-19 16:34:11,077 [qtp1005310362-44] DEBUG:org.apache.http.wire >> "Connection: Keep-Alive[\r][\n]"2013-08-19 16:34:11,077 [qtp1005310362-44] DEBUG:org.apache.http.wire >> "User-Agent: Apache-HttpClient/4.2 (java 1.5)[\r][\n]"2013-08-19 16:34:11,078 [qtp1005310362-44] DEBUG:org.apache.http.wire >> "[\r][\n]"2013-08-19 16:34:11,078 [qtp1005310362-44] DEBUG:org.apache.http.wire >> "grant_type=client_credentials"2013-08-19 16:34:12,690 [qtp1005310362-44] DEBUG:org.apache.http.wire << "HTTP/1.1 200 OK[\r][\n]"2013-08-19 16:34:12,690 [qtp1005310362-44] DEBUG:org.apache.http.wire << "Server: Apache-Coyote/1.1[\r][\n]"2013-08-19 16:34:12,690 [qtp1005310362-44] DEBUG:org.apache.http.wire << "Date: Mon, 19 Aug 2013 20:34:11 GMT[\r][\n]"2013-08-19 16:34:12,690 [qtp1005310362-44] DEBUG:org.apache.http.wire << "PayPal-Debug-Id: d29c41eb8625a[\r][\n]"2013-08-19 16:34:12,690 [qtp1005310362-44] DEBUG:org.apache.http.wire << "Content-Type: application/json[\r][\n]"2013-08-19 16:34:12,690 [qtp1005310362-44] DEBUG:org.apache.http.wire << "Content-Length: 282[\r][\n]"2013-08-19 16:34:12,690 [qtp1005310362-44] DEBUG:org.apache.http.wire << "[\r][\n]"2013-08-19 16:34:12,690 [qtp1005310362-44] 2013-08-19 16:34:12,691 [qtp1005310362-44] DEBUG:org.apache.http.impl.client.DefaultHttpClient Connection can be kept alive indefinitely2013-08-19 16:34:12,691 [qtp1005310362-44] DEBUG:org.apache.http.wire << "{"scope":"https://api.paypal.com/v1/payments/.* https://api.paypal.com/v1/vault/credit-card https://api.paypal.com/v1/vault/credit-card/.* openid","access_token":"RVATuLOQB0WFX keSDZQw4ZnyfIduPCF2j7sMhcfspwo","token_type":"Bearer","app_id":"APP-80W284285Q519543T","expires_in":28800}"2013-08-19 16:34:12,693 [qtp1005310362-44] DEBUG:org.apache.http.impl.conn.PoolingClientConnectionManager Connection [id: 5][route: {s}->https://api.sandbox.paypal.com] can be kept alive indefinitely2013-08-19 16:34:12,693 [qtp1005310362-44] DEBUG:org.apache.http.impl.conn.PoolingClientConnectionManager Connection released: [id: 5][route: {s}->https://api.sandbox.paypal.com][total kept alive: 1; route allocated: 1 of 2; total allocated: 1 of 2]2013-08-19 16:34:15,197 [qtp1005310362-44] ERROR:com.example.gateways.paypal.payments.PayPalPaymentsProREST Failed to submit donation com.paypal.core.rest.PayPalRESTException: Error code : 401 with response : Server returned HTTP response code: 401 for URL: https://api.sandbox.paypal.com/v1/payments/payment
I noticed the scope and the URL the API sent me to are two different things. Perhaps I'm missing some configuration to sent the API calls to the sandbox URL?
In my code above you can see I'm setting the service.EndPoint
to API_ENDPOINT
which is set to the https://api.sandbox.paypal.com
in my tests. I am also not providing the clientID
or clientSecret
in my properties object, but even providing them gives me a 401.
What else am I missing?