Changes for page Payment API Client
Last modified by Thomas Warren on 2020/01/23 13:42
From empty
To version 7.1
edited by Thomas Warren
on 2020/01/23 13:27
on 2020/01/23 13:27
Change comment: There is no comment for this version
Summary
-
Page properties (6 modified, 0 added, 0 removed)
Details
- Page properties
-
- Title
-
... ... @@ -1,0 +1,1 @@ 1 +Payment API Client - Parent
-
... ... @@ -1,0 +1,1 @@ 1 +Main.API.Public Payment API.WebHome - Author
-
... ... @@ -1,1 +1,1 @@ 1 - XWiki.XWikiGuest1 +xwiki:XWiki.ahz - Syntax
-
... ... @@ -1,1 +1,1 @@ 1 - XWiki2.11 +Markdown 1.1 - Tags
-
... ... @@ -1,0 +1,1 @@ 1 +px-custom-page-content - Content
-
... ... @@ -1,0 +1,272 @@ 1 +## Prerequisites 2 + 3 +--- 4 + 5 +* Java 11 6 +* VueJS 7 +* Maven 8 +* Postgres 9 + 10 +## Project setup 11 + 12 +--- 13 + 14 + vas-payment-api-client 15 + ├─┬ backend → backend module with Spring Boot code 16 + │ ├── src 17 + │ └── pom.xml 18 + ├─┬ frontend → frontend module with Vue.js code 19 + │ ├── src 20 + │ └── pom.xml 21 + └── pom.xml → Maven parent pom managing both modules 22 + 23 +## Security 24 + 25 +--- 26 + 27 +<details> 28 +<summary>Oauth2:</summary> 29 + 30 +VasPublicPaymentApi requires an OAuth2 access token for interaction. 31 +This application automatically handles token fetching and refreshing by using [Spring Security](https://docs.spring.io/spring-security-oauth2-boot/docs/current/reference/htmlsingle/#boot-features-security-custom-user-info-client). 32 +Configuration values are set in [application.yml](https://github.com/PayEx/vas-payment-api-client/blob/master/backend/src/main/resources/application.yml): 33 + 34 +```yaml 35 +# "XXX" Should be replaced by value provided by PayEx 36 +# CLIENT_ID/CLIENT_SECRET/VAS_AUTH_SERVER_URL can also be set in docker-compose.yml as environment variables if running with docker 37 +# The application will see if environment variables are present, if not fall back to "XXX" values. 38 +vas-payment-api: 39 + oauth2: 40 + client: 41 + grantType: client_credentials 42 + clientId: "${CLIENT_ID}:XXX" 43 + clientSecret: "${CLIENT_SECRET}:XXX" 44 + accessTokenUri: "${VAS_AUTH_SERVER_URL}:XXX" 45 + scope: publicapi 46 + 47 +``` 48 + 49 +And the implementation of these are located in [Oauth2RestTemplateConfiguration.java](https://github.com/PayEx/vas-payment-api-client/blob/master/backend/src/main/java/com/payex/vas/demo/config/security/Oauth2RestTemplateConfiguration.java): 50 + 51 +```java 52 +public class Oauth2RestTemplateConfiguration { 53 + //... 54 + @Bean 55 + @ConfigurationProperties("vas-payment-api.oauth2.client") 56 + protected ClientCredentialsResourceDetails oAuthDetails() { 57 + return new ClientCredentialsResourceDetails(); 58 + } 59 + 60 + @Bean 61 + protected RestTemplate restTemplate() { 62 + var restTemplate = new OAuth2RestTemplate(oAuthDetails()); 63 + restTemplate.setInterceptors(ImmutableList.of(externalRequestInterceptor())); 64 + restTemplate.setRequestFactory(httpRequestFactory()); 65 + return restTemplate; 66 + } 67 + //... 68 +} 69 +``` 70 +</details> 71 + 72 +<details> 73 + 74 +<summary>HMAC:</summary> 75 + 76 +The API also requires HMAC authentication to be present in a request. 77 +In this client the HMAC value is automatically calculated by [HmacSignatureBuilder.java](https://github.com/PayEx/vas-payment-api-client/blob/master/backend/src/main/java/com/payex/vas/demo/config/security/HmacSignatureBuilder.java) and added to all outgoing requests in [ExternalRequestInterceptor.java](https://github.com/PayEx/vas-payment-api-client/blob/master/backend/src/main/java/com/payex/vas/demo/config/ExternalRequestInterceptor.java) 78 + 79 +HMAC is implemented using SHA-512 secure hash algorithm. 80 + 81 +Expected `Hmac` header format is: 82 + 83 +```text 84 +HmacSHA512 <user>:<nonce>:<digest> 85 +``` 86 + 87 +where `digest` is a Base64 formatted HMAC SHA512 digest of the following string: 88 + 89 +```text 90 +METHOD\n 91 +RESOURCE\n 92 +USER\ 93 +NONCE\n 94 +DATE\n 95 +PAYLOAD\n 96 +``` 97 + 98 +`METHOD` (mandatory) the requested method (in upper case) `RESOURCE` (mandatory) the path to desired resource (without hostname and any query parameters) 99 +`NONSE` (mandatory) a unique value for each request ([UUID](https://tools.ietf.org/rfc/rfc4122.txt)) `DATE`(optional) same as `Transmission-Time` if provided as seperate header. Uses [ISO8601 standard](https://en.wikipedia.org/wiki/ISO_8601) `PAYLOAD` (optional) body of request 100 + 101 +Example request: 102 + 103 +```bash 104 +curl -X POST \ 105 + https://stage-evc.payex.com/payment-api/api/payments/payment-account/balance \ 106 + -H 'Accept: */*' \ 107 + -H 'Agreement-Merchant-Id: XXX' \ 108 + -H 'Authorization: Bearer XXX' \ 109 + -H 'Hmac: HmacSHA512 user:21a0213e-30eb-85ab-b355-a310d31af30e:oY5Q5Rf1anCz7DRm3GyWR0dvJDnhl/psylfnNCn6FA0NOrQS3L0fvyUsQ1IQ9gQPeLUt9J3IM2zwoSfZpDgRJA==' \ 110 + -H 'Transmission-Time: 2019-06-18T09:19:15.208257Z' \ 111 + -H 'Session-Id: e0447bd2-ab64-b456-b17b-da274bb8428e' \ 112 + -d '{ 113 + "accountIdentifier": { 114 + "accountKey": "7013369000000000000", 115 + "cvc": "123", 116 + "expiryDate": "2019-12-31", 117 + "instrument": "GC" 118 + } 119 +}' 120 +``` 121 + 122 +In this example `USER` is user and `SECRET` is secret. 123 + 124 +The plain string to `digest` would then be: 125 + 126 +```text 127 +POST 128 +/payment-api/api/payments/payment-account/balance 129 +user 130 +21a0213e-30eb-85ab-b355-a310d31af30e 131 +2019-06-18T09:19:15.208257Z 132 +{ 133 + "accountIdentifier": { 134 + "accountKey": "7013360000000000000", 135 + "cvc": "123", 136 + "expiryDate": "2020-12-31", 137 + "instrument": "CC" 138 + } 139 +} 140 +``` 141 + 142 +The plain `digest` string is then hashed with `HmacSHA512` algorithm and the `SECRET`. Finally we Base64 encode the hashed value. This is the final `digest` to be provided in the `Hmac` header. 143 + 144 +Final `Hmac` header value: 145 + 146 +```text 147 +HmacSHA512 user:21a0213e-30eb-85ab-b355-a310d31af30e:oY5Q5Rf1anCz7DRm3GyWR0dvJDnhl/psylfnNCn6FA0NOrQS3L0fvyUsQ1IQ9gQPeLUt9J3IM2zwoSfZpDgRJA== 148 +``` 149 + 150 +#### Postman example script 151 + 152 +In pre-request script copy/paste the following snippet: 153 + 154 +```javascript 155 + 156 +var user = 'user'; 157 +var secret = 'secret'; 158 +var transmissionTime = (new Date()).toISOString(); 159 +var sessionId = guid(); 160 + 161 +var hmac = generateHMAC(user, secret, transmissionTime); 162 +console.log('hmac: ' + hmac); 163 + 164 +//Set header values 165 +pm.request.headers.add({key: 'Hmac', value: hmac }); 166 +pm.request.headers.add({key: 'Transmission-Time', value: transmissionTime }); 167 +pm.request.headers.add({key: 'Session-Id', value: sessionId }); 168 + 169 +function generateHMAC(user, secret, transmissionTime) { 170 + 171 + var algorithm = "HmacSHA512"; 172 + var separator = ":"; 173 + var method = request.method.toUpperCase(); 174 + var nonce = generateNonce(); //UUID 175 + var date = transmissionTime; 176 + 177 + var uri_path = replaceRequestEnv(request.url.trim()).trim().replace(new RegExp('^https?://[^/]+/'), '/'); // strip hostname 178 + uri_path = uri_path.split("?")[0]; //Remove query paramters 179 + var payload = _.isEmpty(request.data) ? "" : request.data; 180 + var macData = method + '\n' 181 + + uri_path + '\n' 182 + + user + '\n' 183 + + nonce + '\n' 184 + + date + '\n' 185 + + payload + '\n'; 186 + 187 + macData = replaceRequestEnv(macData); 188 + console.log('data to mac: ' + macData); 189 + 190 + var hash = CryptoJS.HmacSHA512(macData, secret); 191 + var digest = CryptoJS.enc.Base64.stringify(hash); 192 + return algorithm + " " + user + separator + nonce + separator + digest; 193 +} 194 + 195 +function replaceRequestEnv(input) { //manually set environments to they are populated before hashing 196 + return input.replace(/{{([^)]+)}}/g, function (str, key) { 197 + var value = pm.environment.get(key); 198 + return value === null ? pm.varables.get(key) : value; 199 + }); 200 +} 201 + 202 +function generateNonce() { 203 + return guid(); 204 +} 205 + 206 +function guid() { 207 + function s4() { 208 + return Math.floor((1 + Math.random()) * 0x10000) 209 + .toString(16) 210 + .substring(1); 211 + } 212 + 213 + return s4() + s4() + '-' + s4() + '-' + s4() + '-' + 214 + s4() + '-' + s4() + s4() + s4(); 215 +} 216 + 217 +``` 218 +</details> 219 + 220 +### Security documentation 221 + 222 +--- 223 + 224 +* [OAuth2](https://oauth.net/2/) 225 +* [Client Credentials](https://www.oauth.com/oauth2-servers/access-tokens/client-credentials/) 226 +* [The RESTful CookBook: HMAC](http://restcookbook.com/Basics/loggingin/) 227 +* [HMAC - Wikipedia](https://en.wikipedia.org/wiki/HMAC) 228 + 229 +## First App run 230 + 231 +--- 232 + 233 +**NB! The application expects a PostgreSQL server to be running on localhost with a username `test` and password `test` to exist.** 234 +**This can automatically be configured if PostgreSQL server is started in docker with environment variables `POSTGRES_USER=test` and `POSTGRES_PASSWORD=test` are set (See [docker-compose.yml](https://github.com/PayEx/vas-payment-api-client/blob/master/docker-compose.yml)).** 235 + 236 +Inside the root directory, do a: 237 + 238 +```bash 239 +mvn clean install 240 +``` 241 + 242 +Run the Spring Boot App: 243 + 244 +```bash 245 +mvn --projects backend spring-boot:run 246 +``` 247 + 248 +Now go to <http://localhost:8080/> and have a look at your new client. 249 + 250 +## Testing application 251 + 252 +--- 253 + 254 +1. Add a new card with provided details from PayEx. 255 +1. Click on newly added Card 256 +1. Click on "initiate payment" to create a new transaction 257 + 258 +## Build docker image: 259 + 260 +--- 261 + 262 +```bash 263 +mvn --projects backend clean compile jib:dockerBuild 264 +``` 265 + 266 +## Deploy to local docker: 267 + 268 +--- 269 + 270 +```bash 271 +docker-compose up -d 272 +```