AUTH / SSO
Building an SSO Provider for external sites
Even after migration, the OTT (Online Technology Transfer) technology-trade site still needed to let users sign in with Badabom accounts, so I built a Provider from scratch. Single-use UUID tokens are stored in the DB so everything keeps working across multiple WAS nodes, and CI-based mapping automatically reconnects accounts that were already scattered across both sites.
- Integration target
- OTT and similar
- Token
- UUID + DB
- Multi-WAS
- Supported
- Account mapping
- CI-based
External partner sites
Single-use · 1 min expiry
DB-backed shared store
Existing members auto-linked
Problem
An OTT site that outlived migration, and scattered accounts
Part of the OTT (Online Technology Transfer) technology-trade system's functionality was migrated into Badabom, but the OTT site itself remained live and continued operating on its own. A new requirement came in: users had to be able to sign in there with their Badabom account. Bringing in a commercial SSO solution just for this would have been too expensive and too slow.
On top of that, the same person had often signed up on both sites under different IDs. The system saw them as separate accounts, but by CI (Connecting Information — Korea's national personal identifier), they were the same individual. If I was going to connect the logins, I had to solve the "same person, split accounts" situation at the same time.
External site needs to reuse Badabom authentication
Even after migration, the OTT site stayed alive on its own, so there had to be a path to sign in there with a Badabom account.
The same person exists as separate accounts on both sites
A user who had signed up separately on OTT and Badabom was the same individual by CI but had different IDs. I had to map them so the account would not look split to that user.
In-memory tokens break across multiple WAS nodes
Because of the load balancer, the WAS that handled login and the WAS that received verify could be different, so keeping tokens in an in-memory Map meant "unknown token" on the other node.
Approach
Build a lightweight Provider in-house — UUID + DB + CI mapping
A commercial solution was overkill, and for now the only integration target was OTT, so it made more sense to build a lightweight Provider myself. The shape is close to OAuth2 Implicit — once login finishes on Badabom, I attach a single-use UUID token to the redirect URL and hand it back, and when OTT checks that token against the verify endpoint it gets consumed immediately so it cannot be replayed.
The key choice was storing tokens in a DB table rather than a session or memory. That way a token issued on any WAS can be verified from any other WAS, so nothing breaks in a multi-WAS environment. I handled account mapping based on CI: if the user already has an account on OTT, the existing OTT ID is returned automatically; if not, the flow continues with the Badabom ID. From the user's side, there is no "wait, which account am I supposed to use?" moment — it just flows through.
External Site
OTT
SSO Provider
Badabom
- 1
/sso/login.do
?redirect_uri=https://ott/...
- 2
Validate redirect_uri against allow-list
Contained in ALLOWED_DOMAINS?
- 3
Render login page
Keep redirect_uri in session
- 4
POST login
ID / PW authentication
- 5
Look up existing OTT member by CI
resolveUserIdByCi()
- 6
INSERT UUID token into DB
SSO_USER_TOKEN · 1 min expiry
- 7
redirect(token=xxx)
redirect_uri?token=...
- 8
/sso/verify.do?token=xxx
Called from OTT backend
- 9
Token SELECT + DELETE
Single-use consumption (blocks reuse)
- 10
{ success: true, userId }
JSON response
Multi-WAS
DB-backed store means verification works from any node, regardless of where the token was issued
Account mapping
Links OTT and Badabom accounts via CI — prevents duplicate-account creation
Phishing defense
redirect_uri allow-list + single-use tokens
Process
Implementation steps
- 01
Designed a DB-backed token store
If tokens live in an in-memory Map, having two or more WAS nodes means the issuing and verifying nodes can differ, turning the token into "unknown." I stored the token, userId, and expiry time in a
SSO_USER_TOKENtable so every WAS node references the same store. I also added a lightweight GC that sweeps expired tokens every 10 issuances.SsoController.javajava// SsoController.java - stores tokens in the DB so multiple WAS nodes stay in syncpublic String generateToken(String userId, String userName, String email) {// Periodically clean up expired tokens (every 10 issuances)if (tokenGenerationCount.incrementAndGet() >= CLEANUP_INTERVAL) {cleanupExpiredTokens();tokenGenerationCount.set(0);}String token = UUID.randomUUID().toString().replace("-", "");long expireTime = System.currentTimeMillis() + TOKEN_EXPIRE_MS; // 1 minuteMap<String, Object> params = new HashMap<>();params.put("token", token);params.put("userId", userId);params.put("expireTime", expireTime);ssoMappingDAO.insertToken(params); // stored in the DB rather than memory = shared across multiple WAS nodesreturn token;} - 02
CI-based OTT ↔ Badabom account mapping
When the same person has signed up separately on both sites, CI (Connecting Information — Korea's national personal identifier) can identify them as the same individual. I decrypt the encrypted CI stored on Badabom, look up the OTT ID in
SSO_USER_MAPPING, and return the OTT ID if a mapping exists; otherwise I return the Badabom ID as-is. A fallback makes sure login never fails when no mapping is found, so the flow is smooth for new members too.SsoController.javajava// Resolves the user ID from the CI (Connecting Information — Korea's national personal identifier)// - Existing OTT member (row exists in SSO_USER_MAPPING): returns the OTT ID (links the two accounts)// - New member: returns the Badabom ID as-ispublic String resolveUserIdByCi(String encryptedCi, String badabomUserId) {if (StringUtils.isBlank(encryptedCi)) return badabomUserId;try {String decryptedCi = CryptoUtil.decrypt(encryptedCi);if (StringUtils.isBlank(decryptedCi)) return badabomUserId;// Look up the OTT ID in the mapping tableString ottUserId = ssoMappingDAO.selectOttUserIdByCi(decryptedCi);if (StringUtils.isNotBlank(ottUserId)) {log.info("[SSO] Existing OTT member found - CI mapping: {} -> {}", badabomUserId, ottUserId);return ottUserId;}return badabomUserId;} catch (Exception e) {log.error("[SSO] CI-based ID resolution failed, falling back to Badabom ID", e);return badabomUserId;}} - 03
Single-use verification + CORS
When OTT checks a token via
/sso/verify.do, I delete the token from the DB immediately after verification to block reuse. Because the call comes from an external domain, I also send back the necessary CORS headers.SsoController.javajava// Token verification API - called by OTT, single-use consumption@RequestMapping(value = "/sso/verify.do", method = {GET, POST})@ResponseBodypublic Map<String, Object> verify(HttpServletRequest request, HttpServletResponse response) {// Allow CORSresponse.setHeader("Access-Control-Allow-Origin", "*");response.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");Map<String, Object> result = new HashMap<>();String token = request.getParameter("token");if (StringUtils.isBlank(token)) {result.put("success", false);result.put("message", "Token is required.");return result;}// Look up the token in the DB (includes expiry check)String userId = ssoMappingDAO.selectTokenUserId(token);if (StringUtils.isBlank(userId)) {result.put("success", false);result.put("message", "Invalid token.");return result;}// Single-use: delete immediately after verification (prevents reuse)ssoMappingDAO.deleteToken(token);result.put("success", true);result.put("userId", userId);return result;} - 04
redirect_uri allow-list + logging
To defend against token theft and phishing, I only allow pre-registered domains for
redirect_uri. Disallowed domains are rejected with 403, and the attempt is recorded as a warn log so it can be used for later auditing.SsoController.javajava// Only issue tokens to allow-listed redirect_uri domainsprivate static final String[] ALLOWED_DOMAINS = {"localhost:3000", // for testing"ofris.kimst.re.kr", // OTT production"192.168.40.36:8000" // OTT internal-network testing};private boolean isAllowedRedirectUri(String redirectUri) {for (String allowed : ALLOWED_DOMAINS) {if (redirectUri.contains(allowed)) return true;}return false;}// Validate on entry to /sso/login.doif (!isAllowedRedirectUri(redirectUri)) {log.warn("[SSO] Disallowed redirect_uri: {}", redirectUri);response.sendError(HttpServletResponse.SC_FORBIDDEN, "Disallowed redirect_uri");return;}
Outcome
Results and lessons
SSO integration with the external site is complete
The flow — signing in on OTT with a Badabom account, receiving a token, and creating an OTT session — runs reliably even in a multi-WAS environment.
Accounts on both sites are automatically linked by CI
Even when an existing OTT member separately signed up on Badabom, CI mapping recognizes them as the same user. From the user's side, the confusion of seeing their account as split simply disappeared.
Learned the "tokens in DB, not session" pattern
The problem of shared state breaking in a multi-WAS environment is exactly the one I ran into with Redis session clustering. This reinforced the principle: "shared state belongs in an external store."
redirect_uri must always be allow-listed
A contains-based string match looks simple but can be fragile, so for real deployments there is room to tighten host and scheme parsing. In the next iteration I plan to use a URL parser to make the validation logic stricter.
MORE
Explore other cases
Badabom
DEVOPS / OBSERVABILITY
SSE + Cross-WAS Real-Time Log Viewer
The WAS lived in the Daejeon IDC, but network-segregation policy meant only Busan-office PCs could reach it — so pulling a log effectively meant flying to Busan. I built an SSE-based viewer inside the admin web and added a cross-WAS relay so logs from both WAS nodes stream into a single screen.
View detailBadabom
LEGACY MIGRATION
Migrating the OTT Technology-Trade System into Badabom
Moved an Oracle + MyBatis technology-trade platform (OTT) onto PostgreSQL + iBATIS. Rewrote 87 URLs, 34 JSPs, 80+ SQL queries, and 14 tables.
View detailGAIS — Government Advertising Integrated Support System
CI/CD
Automating the Build and Deploy Pipeline
Replaced a fully manual build-and-deploy workflow with a Jenkins + GitLab Webhook pipeline, cutting deploy time from 15–20 min down to around 4 min.
View detailGAIS — Government Advertising Integrated Support System
INFRA / SESSION
Redis-Backed Session Clustering
JEUS Standard doesn't support native session clustering, so I put Redis in front as an external session store. That unlocked rolling restarts across WAS nodes.
View detailGAIS — Government Advertising Integrated Support System
SECURITY / NETWORK
Applying TLS 1.3 via an Nginx Reverse Proxy
Touching the shared WebtoB SSL felt risky, so I put Nginx in front and terminated TLS there instead. Existing services kept running untouched while TLS 1.3 was rolled out.
View detailFreelance · Side Projects
CLIENT WORK / WEB
Pitched and Built a Postpartum Care Center Site Renewal
My wife had stayed at a postpartum care center whose website felt dated, so I mocked up a UI sample and pitched it myself. I built an Astro static site with a 192-frame scroll animation, Kakao Map, and SEO — then shipped it to their production domain.
View detailFreelance · Side Projects
SIDE PROJECT / AI
Family-Driven Baby Naming with AI + Tournament-Style Voting
Existing naming services are designed for solo use, so I built a way for the whole family to join in. GPT-4o suggests names aligned with Saju (birth-chart) and Ohaeng (Five-Element) rules, and the family votes tournament-style to pick the final name.
View detail