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 | ``` |