Wiki source code of Payment API Client
Last modified by Thomas Warren on 2020/01/23 13:42
| |
7.1 | 1 | ## Prerequisites |
| |
3.1 | 2 | |
| |
7.1 | 3 | --- |
| |
3.1 | 4 | |
| |
7.1 | 5 | * Java 11 |
| 6 | * VueJS | ||
| 7 | * Maven | ||
| 8 | * Postgres | ||
| |
3.1 | 9 | |
| |
7.2 | 10 | ## GitHub |
| 11 | --- | ||
| 12 | |||
| |
7.4 | 13 | * [Payment Api Client](https://github.com/PayEx/vas-payment-api-client) |
| |
7.2 | 14 | |
| |
7.3 | 15 | ## Swagger documentation |
| 16 | |||
| 17 | --- | ||
| 18 | |||
| 19 | * [Payment API Swagger](https://stage-evc.payex.com/payment-api/swagger-ui.html) | ||
| 20 | |||
| |
7.1 | 21 | ## Project setup |
| |
3.1 | 22 | |
| |
7.1 | 23 | --- |
| |
3.1 | 24 | |
| |
7.1 | 25 | vas-payment-api-client |
| |
6.1 | 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 | ||
| |
7.1 | 32 | └── pom.xml → Maven parent pom managing both modules |
| |
3.1 | 33 | |
| |
7.1 | 34 | ## Security |
| |
3.1 | 35 | |
| |
7.1 | 36 | --- |
| |
3.1 | 37 | |
| |
7.1 | 38 | <details> |
| 39 | <summary>Oauth2:</summary> | ||
| |
3.1 | 40 | |
| |
7.1 | 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): | ||
| |
3.1 | 44 | |
| |
7.1 | 45 | ```yaml |
| |
6.1 | 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" | ||
| |
7.1 | 56 | scope: publicapi |
| 57 | |||
| 58 | ``` | ||
| |
3.1 | 59 | |
| |
7.1 | 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): |
| |
3.1 | 61 | |
| |
7.1 | 62 | ```java |
| |
6.1 | 63 | public class Oauth2RestTemplateConfiguration { |
| |
7.1 | 64 | //... |
| |
6.1 | 65 | @Bean |
| 66 | @ConfigurationProperties("vas-payment-api.oauth2.client") | ||
| 67 | protected ClientCredentialsResourceDetails oAuthDetails() { | ||
| 68 | return new ClientCredentialsResourceDetails(); | ||
| |
7.1 | 69 | } |
| |
3.1 | 70 | |
| |
7.1 | 71 | @Bean |
| |
6.1 | 72 | protected RestTemplate restTemplate() { |
| 73 | var restTemplate = new OAuth2RestTemplate(oAuthDetails()); | ||
| |
7.1 | 74 | restTemplate.setInterceptors(ImmutableList.of(externalRequestInterceptor())); |
| |
6.1 | 75 | restTemplate.setRequestFactory(httpRequestFactory()); |
| 76 | return restTemplate; | ||
| 77 | } | ||
| |
7.1 | 78 | //... |
| |
6.1 | 79 | } |
| 80 | ``` | ||
| |
7.1 | 81 | </details> |
| |
3.1 | 82 | |
| |
7.1 | 83 | <details> |
| |
3.1 | 84 | |
| |
7.1 | 85 | <summary>HMAC:</summary> |
| |
3.1 | 86 | |
| |
7.1 | 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) | ||
| |
3.1 | 89 | |
| |
7.1 | 90 | HMAC is implemented using SHA-512 secure hash algorithm. |
| |
3.1 | 91 | |
| |
7.1 | 92 | Expected `Hmac` header format is: |
| |
3.1 | 93 | |
| |
7.1 | 94 | ```text |
| 95 | HmacSHA512 <user>:<nonce>:<digest> | ||
| 96 | ``` | ||
| |
3.1 | 97 | |
| |
7.1 | 98 | where `digest` is a Base64 formatted HMAC SHA512 digest of the following string: |
| |
3.1 | 99 | |
| |
7.1 | 100 | ```text |
| |
6.1 | 101 | METHOD\n |
| 102 | RESOURCE\n | ||
| 103 | USER\ | ||
| 104 | NONCE\n | ||
| 105 | DATE\n | ||
| 106 | PAYLOAD\n | ||
| |
7.1 | 107 | ``` |
| |
3.1 | 108 | |
| |
7.1 | 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 | ||
| |
3.1 | 111 | |
| |
7.1 | 112 | Example request: |
| |
3.1 | 113 | |
| |
7.1 | 114 | ```bash |
| |
6.1 | 115 | curl -X POST \ |
| |
7.1 | 116 | https://stage-evc.payex.com/payment-api/api/payments/payment-account/balance \ |
| |
6.1 | 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 | }' | ||
| |
7.1 | 131 | ``` |
| |
3.1 | 132 | |
| |
7.1 | 133 | In this example `USER` is user and `SECRET` is secret. |
| |
3.1 | 134 | |
| |
7.1 | 135 | The plain string to `digest` would then be: |
| |
3.1 | 136 | |
| |
7.1 | 137 | ```text |
| |
6.1 | 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 | } | ||
| |
7.1 | 151 | ``` |
| |
3.1 | 152 | |
| |
7.1 | 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. |
| |
3.1 | 154 | |
| |
7.1 | 155 | Final `Hmac` header value: |
| |
3.1 | 156 | |
| |
7.1 | 157 | ```text |
| |
6.1 | 158 | HmacSHA512 user:21a0213e-30eb-85ab-b355-a310d31af30e:oY5Q5Rf1anCz7DRm3GyWR0dvJDnhl/psylfnNCn6FA0NOrQS3L0fvyUsQ1IQ9gQPeLUt9J3IM2zwoSfZpDgRJA== |
| |
7.1 | 159 | ``` |
| |
3.1 | 160 | |
| |
7.1 | 161 | #### Postman example script |
| |
3.1 | 162 | |
| |
7.1 | 163 | In pre-request script copy/paste the following snippet: |
| |
3.1 | 164 | |
| |
7.1 | 165 | ```javascript |
| |
3.1 | 166 | |
| |
7.1 | 167 | var user = 'user'; |
| |
6.1 | 168 | var secret = 'secret'; |
| 169 | var transmissionTime = (new Date()).toISOString(); | ||
| |
7.1 | 170 | var sessionId = guid(); |
| |
3.1 | 171 | |
| |
7.1 | 172 | var hmac = generateHMAC(user, secret, transmissionTime); |
| 173 | console.log('hmac: ' + hmac); | ||
| |
3.1 | 174 | |
| |
7.1 | 175 | //Set header values |
| |
6.1 | 176 | pm.request.headers.add({key: 'Hmac', value: hmac }); |
| 177 | pm.request.headers.add({key: 'Transmission-Time', value: transmissionTime }); | ||
| |
7.1 | 178 | pm.request.headers.add({key: 'Session-Id', value: sessionId }); |
| |
3.1 | 179 | |
| |
7.1 | 180 | function generateHMAC(user, secret, transmissionTime) { |
| |
3.1 | 181 | |
| |
7.1 | 182 | var algorithm = "HmacSHA512"; |
| |
6.1 | 183 | var separator = ":"; |
| 184 | var method = request.method.toUpperCase(); | ||
| |
7.1 | 185 | var nonce = generateNonce(); //UUID |
| |
6.1 | 186 | var date = transmissionTime; |
| |
7.1 | 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 | ||
| |
6.1 | 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' | ||
| |
7.1 | 196 | + payload + '\n'; |
| |
3.1 | 197 | |
| |
7.1 | 198 | macData = replaceRequestEnv(macData); |
| 199 | console.log('data to mac: ' + macData); | ||
| |
3.1 | 200 | |
| |
7.1 | 201 | var hash = CryptoJS.HmacSHA512(macData, secret); |
| |
6.1 | 202 | var digest = CryptoJS.enc.Base64.stringify(hash); |
| 203 | return algorithm + " " + user + separator + nonce + separator + digest; | ||
| |
7.1 | 204 | } |
| |
3.1 | 205 | |
| |
7.1 | 206 | function replaceRequestEnv(input) { //manually set environments to they are populated before hashing |
| 207 | return input.replace(/{{([^)]+)}}/g, function (str, key) { | ||
| |
6.1 | 208 | var value = pm.environment.get(key); |
| 209 | return value === null ? pm.varables.get(key) : value; | ||
| 210 | }); | ||
| |
7.1 | 211 | } |
| |
3.1 | 212 | |
| |
7.1 | 213 | function generateNonce() { |
| |
6.1 | 214 | return guid(); |
| |
7.1 | 215 | } |
| |
3.1 | 216 | |
| |
7.1 | 217 | function guid() { |
| |
6.1 | 218 | function s4() { |
| 219 | return Math.floor((1 + Math.random()) * 0x10000) | ||
| 220 | .toString(16) | ||
| 221 | .substring(1); | ||
| |
7.1 | 222 | } |
| |
3.1 | 223 | |
| |
7.1 | 224 | return s4() + s4() + '-' + s4() + '-' + s4() + '-' + |
| |
6.1 | 225 | s4() + '-' + s4() + s4() + s4(); |
| |
7.1 | 226 | } |
| |
3.1 | 227 | |
| |
7.1 | 228 | ``` |
| 229 | </details> | ||
| |
3.1 | 230 | |
| |
7.1 | 231 | ### Security documentation |
| |
3.1 | 232 | |
| |
7.1 | 233 | --- |
| |
3.1 | 234 | |
| |
7.1 | 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) | ||
| |
3.1 | 239 | |
| |
7.1 | 240 | ## First App run |
| |
3.1 | 241 | |
| |
7.1 | 242 | --- |
| |
3.1 | 243 | |
| |
7.1 | 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)).** | ||
| |
3.1 | 246 | |
| |
7.1 | 247 | Inside the root directory, do a: |
| |
3.1 | 248 | |
| |
7.1 | 249 | ```bash |
| |
6.1 | 250 | mvn clean install |
| |
7.1 | 251 | ``` |
| |
3.1 | 252 | |
| |
7.1 | 253 | Run the Spring Boot App: |
| |
3.1 | 254 | |
| |
7.1 | 255 | ```bash |
| 256 | mvn --projects backend spring-boot:run | ||
| 257 | ``` | ||
| |
3.1 | 258 | |
| |
7.1 | 259 | Now go to <http://localhost:8080/> and have a look at your new client. |
| |
3.1 | 260 | |
| |
7.1 | 261 | ## Testing application |
| |
3.1 | 262 | |
| |
7.1 | 263 | --- |
| |
3.1 | 264 | |
| |
7.1 | 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 | ||
| |
3.1 | 268 | |
| |
7.1 | 269 | ## Build docker image: |
| |
3.1 | 270 | |
| |
7.1 | 271 | --- |
| |
3.1 | 272 | |
| |
7.1 | 273 | ```bash |
| 274 | mvn --projects backend clean compile jib:dockerBuild | ||
| 275 | ``` | ||
| |
3.1 | 276 | |
| |
7.1 | 277 | ## Deploy to local docker: |
| |
3.1 | 278 | |
| |
7.1 | 279 | --- |
| |
3.1 | 280 | |
| |
7.1 | 281 | ```bash |
| |
6.1 | 282 | docker-compose up -d |
| |
7.1 | 283 | ``` |