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