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