SecurePay: secure payments with instant email confirmation
A small set of services that accept payments safely, show every customer only their own data, and email a confirmation after each payment, without ever slowing the payment down.
"The password is checked once, at login. After that, a signed pass proves who you are and what you may do, and every service checks that pass on its own."
The four pieces
Customer ──login──► Login service (:9000) ──signed pass (JWT)──► Customer
Customer ──pass──► Gateway (:8080) ──same pass──► Payment service (:8081) ──► Database
│ after the payment is saved
▼
Notification service (:8082) ──► Email
| Service | Job in one line |
| Login service | The only place passwords are checked. Issues short-lived signed passes (5 minutes). |
| Gateway | The front door for customers. Checks the pass and forwards the request. |
| Payment service | Saves payments. Checks the pass again (it never trusts the gateway) and keeps each payment private to its owner. |
| Notification service | Sends the confirmation email. Only accepts requests from the payment service. |
What happens when a customer pays
- Log in. The customer signs in once and approves what the app may do (read payments, create payments). The login service returns a signed pass that expires in 5 minutes.
- Send the payment. The app sends the payment with the pass. The gateway checks the signature, the expiry and the permission before anything else happens.
- Forward safely. The gateway passes the same pass to the payment service, which verifies it again on its own.
- Save it. The payment is stored and tagged with the customer it belongs to.
- Confirm by email. Only after the payment is safely saved, the payment service asks the notification service to send an email. This runs in the background, so the customer gets their answer immediately.
What this protects you from
| Risk | How it is handled |
| Someone calls the API without logging in | Rejected with 401. |
| A user does something they are not allowed to | Rejected with 403. A read-only pass cannot create payments. |
| A customer tries to see another customer's payment | Answered with 404, the same as "does not exist", so its existence is not revealed. |
| A forged or edited pass | The signature check fails, so it is rejected. |
| A stolen pass | Useless after 5 minutes. |
| One internal service is tricked into calling another | Every service verifies the pass itself. The notification service accepts only the payment service's own machine pass (scope notification.send), never a customer's. |
| Passwords leaking through the system | Only the login service ever sees a password. Mail passwords live in a local file that is never committed to the code repository. |
When something goes wrong, the customer is not hurt
- Email provider slow or down: the payment still succeeds. The failure is logged and can be retried or investigated.
- Bad input (negative amount, broken data):
400 with a clear message.
- Duplicate payment id:
409, so a payment is never created twice by accident.
- Payment service unreachable: the gateway answers
503 quickly instead of hanging.
How we prove it works
Two commands, both in PowerShell, no manual clicking in a test tool:
.\scripts\start-all.ps1 starts all four services in the right order
.\scripts\test-all.ps1 logs in for real, then runs about 27 automated checks
The checks cover login and tokens, creating and reading payments, ownership between two users, every error case above, and the notification service's own security. There is also a separate unit-test suite for the email service, and an interactive script that sends a real test email to any address.
Built with
Java 17Spring Boot 3.5Spring Security 6.5OAuth 2.0JWT (RS256)JPA + H2Thymeleaf email
Ready for production?
The design is. Before go-live we would swap the demo users and in-memory keys for a real user store and a managed signing key, and use a production database and mail provider.
Likely questions
Why a token instead of sending the password each time? The password is exposed once, not on every call, and a token can be limited in time and permissions.
Why check the token in every service? If one layer is bypassed or misconfigured, the next one still says no.
Why send email in the background? Email providers can take many seconds. The customer should never wait for, or be blocked by, an email.
Can we add another service? Yes. It validates the same signed pass against the login service's public key and declares which permission it needs.