Skip to content

feat: add OTPaaS-backed OTP request/verify endpoints#65

Open
kellylimmm wants to merge 18 commits intomainfrom
feat/add-otpaas-api-endpoint
Open

feat: add OTPaaS-backed OTP request/verify endpoints#65
kellylimmm wants to merge 18 commits intomainfrom
feat/add-otpaas-api-endpoint

Conversation

@kellylimmm
Copy link
Contributor

🚀 Summary

Adds OTPaaS-backed OTP flows to the server by introducing /api/otp/request and /api/otp/verify. The routes use a session_id cookie to persist the otp_flow_id, read OTPaaS credentials from config/env, and return consistent structured JSON error responses (including explicit timeout handling).

✏️ Changes

  • Added OTP request + verify endpoints wired into route registration (POST /api/otp/request, POST /api/otp/verify).
  • Added OTP route handler implementation integrating with OTPaaS (auth token/headers, configurable host).
  • Added session handling via session_id cookie and an in-memory store for otp_flow_id.
  • Added structured JSON error responses and expanded error handling for OTPaaS failures and non-200 responses.
  • Added config support for OTPaaS + configurable HTTP client timeout; passed config into routes (routes.Register(mux, cfg)).
  • Changed successful OTP endpoints to return 204 No Content (instead of 200).
  • Updated Index route to be a method on the routes handler to align with the handler struct pattern.
  • Tests added/expanded OTP route tests and adjusted Index/tests naming.

📑 Notes

  • Copy pending (UXD): Some log/error messages are placeholder text and will be updated once UXD finalizes copy.
  • OTPaaS: Need confirmation on OTPaaS semantics for expired/invalid flows, e.g. should expiry map to 404 Not Found or 410 Gone

@yiningsoong yiningsoong force-pushed the feat/add-otpaas-api-endpoint branch from 6d476ff to d491f98 Compare February 13, 2026 17:02
LogLevel slog.Level `dotenv:"TW_LOG_LEVEL"`

Server ServerConfig `dotenv:",squash"`

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Remove new line.


OTPaas OTPaasConfig `dotenv:",squash"`

Client ClientConfig `dotenv:",squash"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need to have a config just to configure the HTTP client timeout used by OTPaas flow. Either we inline the timeout when creating the HTTP client or place the timeout in OTPassConfig.

ID string `dotenv:"TW_APP_ID"`
Namespace string `dotenv:"TW_APP_NAMESPACE"`
Secret string `dotenv:"TW_APP_SECRET"`
Host string `dotenv:"TW_TECHPASS_OTP_HOST"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Host string `dotenv:"TW_TECHPASS_OTP_HOST"`
Host string `dotenv:"TW_OTPASS_HOST"`

)

func TestIndex(t *testing.T) {
h := &Handler{cfg: config.Default()}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
h := &Handler{cfg: config.Default()}
h := &Handler{}

Remove cfg since index handler do no need cfg yet.

}
}

func buildAuthToken(appSecret string, appId string, appNamespace string) string {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func buildAuthToken(appSecret string, appId string, appNamespace string) string {
func buildAuthToken(appID string, appNamespace string, appSecret string) string {

nit: Prefer having the arguments to be ordered like that. Since all 3 arguments have the same type, the first two argument can don't write the type.


var otpResp OTPResponse
if err := json.Unmarshal(body, &otpResp); err != nil {
w.WriteHeader(http.StatusBadGateway)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the response status is bad gateway but the body is internal server error?

func (h *Handler) RequestOTP(w http.ResponseWriter, r *http.Request) {
log := middleware.LoggerFromContext(r.Context())
var otpURL = h.cfg.OTPaas.Host + "/otp"
var sessionID string
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Place this variable close to where it is being used.

sessionID = c.Value
}

if otpResp.ID == "" {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If otpResp.ID is empty, do we still need to read the cookie and generate if it does not exist?

Name: "session_id",
Value: sessionID,
Path: "/",
Secure: true,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We cannot hardcode true to the secure flag. During development, we are using HTTP and not HTTPS so if the secure flag is always true we will not be able to send the cookies for other requests.

We should have an environment variable GO_ENV with values of development and production, similar to how we have NODE_ENV in Javascript and RAILS_ENV in Rails. This way, we can set the secure flag correctly depending on the environment.

http.SetCookie(w, &cookie)

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusNoContent)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We figured out that we need to send the id to the frontend else we could not show the prefix in the OTP page.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants