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