under construction
← experience
apr – oct 2026 internship

software engineer intern

Omise Co., Ltd. — AI Project Team. Took an MCP server for the payment API toward production, and built JWT merchant authentication on the payment platform.

goruby on railskubernetes

Seven months on Omise’s AI Project Team, from April to October 2026. I worked on two projects: an MCP server that lets AI agents use the Omise payment API, and JWT-based authentication for merchants on the payment platform.

MCP server: payment API tools for AI agents

Go · JSON Schema · Datadog · Flux · Kubernetes (EKS)

The Model Context Protocol is how AI agents discover and call external tools. Omise’s MCP server is a multi-tenant Go service that exposes the payment API to agents as 50 tools covering charges, refunds, transfers, schedules and disputes. I extended the server and helped get it ready for production.

  • Tested every tool against the real API. I ran end-to-end test cases comparing each tool’s output with the raw payment API’s response for the same request. I then ranked the fixes by how many payment methods each one affected and fixed the schema and serialization gaps that were rejecting valid requests.
  • Filled in the missing tools. I built the transfer and recurring-schedule tools, and rebuilt file upload on multipart/form-data. Each tool has JSON Schema constraints that reject invalid input before it reaches the API, plus regression tests for both success and failure cases.
  • Got it ready for production. I set up Datadog P90/P95/P99 latency monitors and a composite error-rate monitor, aligned with the existing services to cut false alerts. I also wrote the Flux production config and standard health checks, and drafted the production readiness review docs.

Merchant API authentication with JWT

Ruby on Rails · Keycloak (OAuth 2.0 / OIDC) · Istio · Flux

The problem

The MCP server is only useful if an agent can act as the merchant, with the same permissions the merchant has on the dashboard. The merchant already signs in to the dashboard through Keycloak SSO, which gives them a short-lived JWT access token. But the payment platform, a Ruby on Rails monolith, only understood its own credentials: API keys and dashboard session keys. Sessions are stateful, which suits a monolith but becomes awkward as more services need to call the platform.

The goal: let the platform accept the merchant’s Keycloak JWT directly, so the agent can reuse the merchant’s existing sign-in instead of asking them to log in a second time or handing it a long-lived key.

Design: split the work between the mesh and the app

I researched JWT, OAuth 2.0 and OIDC, then worked through the design with engineers from several teams. The main decision was to split responsibility between Istio and the Rails app:

caller (MCP server, acting for the merchant)
  │  Authorization: Bearer <Keycloak JWT>
  ▼
load balancer
  ▼
Istio sidecar ── RequestAuthentication
  │  • pick Keycloak public key by kid (JWKS)
  │  • verify RS256 signature
  │  • pin iss + aud, check exp
  │  • forward verified claims in a header
  ▼
Rails app ── MerchantJwt authenticator
     • re-check claims, email_verified
     • user ← email claim
     • account ← account header (must be a membership)
     • reject deleted users
     • anything off → 401 (fail closed)
  • Istio verifies the token at the service mesh. A RequestAuthentication policy selects Keycloak’s public key by the token’s kid and verifies the RS256 signature. It also pins the expected issuer and audience and checks expiry. The verified claims are then forwarded to the app in a header. The app never handles signing keys and doesn’t re-verify the signature.
  • The app decides who the caller is and what they can do. A new MerchantJwt authenticator, modelled on the existing session authenticator, reads the forwarded claims. It maps the email claim to a user and the account header to one of that user’s memberships (live or test mode). The normal authorization checks then run exactly as they do for any other request.
  • It fails closed. The app rejects the request if the forwarded payload is missing, or if iss, aud, azp, exp or email_verified don’t check out. It also rejects deactivated users and accounts the user isn’t a member of. There is no fallback path that would quietly accept an unverified token.

Implementation

  • Istio, in two layers. Mode A is the RequestAuthentication policy described above: it validates Bearer tokens from SSO and forwards their claims. Mode B adds an AuthorizationPolicy that denies any JWT issued to a client other than the dashboard. Requests without a JWT, such as existing API-key and session traffic, are unaffected.
  • Rails. I added the MerchantJwt authenticator and wired it into the authentication flow. Each controller has to opt in, so JWT is accepted only on the endpoints the MCP server exposes. Every other endpoint still returns 401 for a JWT request. Session keys, secret keys and team secrets keep working alongside it.
  • A small but real detail. Istio base64url-encodes the forwarded payload without padding, so the app has to restore the padding before decoding it.

Testing

  • No live dependencies in unit tests. The unit and integration tests build the same base64url payload Istio would forward, so they run without Keycloak or Istio. Every failure path is covered: expired token, wrong client, account the user isn’t a member of, deactivated user, unverified email, and endpoints that haven’t opted in. Each one must return 401.
  • Real mesh testing. I tested against a real mesh on a local kind cluster, with a test JWKS and curl from inside the mesh. Port-forwarding bypasses the sidecar, so it would have skipped the checks being tested. Istio allows about 60 seconds of clock skew, so the “expired token” cases had to use tokens that expired at least two minutes earlier.

Rollout

I rolled the Istio policies out through Flux (GitOps), staging first and then production, under a change ticket with a rollback plan. It’s now live in production. The app gates the new path behind feature flags: one switch for JWT auth overall, plus an allow-list of users. That way it could be turned on for a few merchants at a time and switched off instantly.