feat(otp): replace inline OTPaaS calls with provider abstraction#181
feat(otp): replace inline OTPaaS calls with provider abstraction#181vlwk wants to merge 10 commits into
otp): replace inline OTPaaS calls with provider abstraction#181Conversation
|
Possible to continue to use the global map as the temporary session store as the session middleware is still work in progress? |
This reverts commit c8ef494.
Reverted the last commit. Now back to using global map |
| EnvironmentDevelopment Environment = "development" | ||
| EnvironmentProduction Environment = "production" | ||
|
|
||
| ProviderOTPaaS Provider = "otpaas" |
There was a problem hiding this comment.
| ProviderOTPaaS Provider = "otpaas" | |
| OTPProviderOTPaaS Provider = "otpaas" |
I think we should prefix with OTP because there will be a lot of different provider services in the future.
| Environment Environment `dotenv:"TW_ENV"` | ||
| LogLevel slog.Level `dotenv:"TW_LOG_LEVEL"` | ||
|
|
||
| OTPProvider Provider `dotenv:"TW_OTP_PROVIDER"` |
There was a problem hiding this comment.
BOTP service only focusing on OTP operations so the configurations do not need to be nested. TW will have a lot of different kind of configurations so I think we should nest OTP related configurations into a struct.
For example:
type Config struct {
...
OTP OTPConfig `dotenv:",squash"`
}
type OTPConfig struct {
Provider OTPProvider `dotenv:"TW_OTP_PROVIDER"`
AllowedEmailDomains []string `dotenv:"TW_OTP_ALLOWED_EMAIL_DOMAINS"`
OTPaaS OTPaaSConfig `dotenv:",squash"`
Mock MockConfig `dotenv:",squash"`
}Then we add comments to OTPaaSConfig and MockConfig:
// OTPaaSConfig is the configuration for the OTPaaS OTP provider.
type OTPaaSConfig struct {
...
}
// MockConfig is the configuration for the mock OTP provider.
type MockConfig struct {
...
}WDYT?
| @@ -0,0 +1,365 @@ | |||
| package config | |||
There was a problem hiding this comment.
Should we have a separate PR for adding config tests?
| } | ||
| } | ||
|
|
||
| func generateMockFlowID() (string, error) { |
There was a problem hiding this comment.
We added a random package to generate random alphanumeric. Can check if we can replace this helper?
🚀 Summary
Migrate botp OTP provider logic into TW. Eliminates the cross-repo dependency for the email OTP auth flow by inlining the provider abstraction directly. Establishes an authenticated session after successful OTP verification using
the existingglobal map.session.StoreabstractionOpen Questions
config_test.go, should I addcfg.BundleDirectory = t.TempDir()infixtureConfigWithProvider(t *testing.T, provider Provider)instead? Currently, it’s in the valid environment test.http.Error()for 5xx. Should we align with botp and return json for server errors too?AUTHORIZATION_FAILED. Is 422 correct here?✏️ Changes
server/internal/otp/package with both OTPaaS and mock provider implementationserver/internal/otp/bimap/package (note: decided to nest it withinotp/since its only being used by the mock provider)global mapsession.StoreCommits
feat(otp): add provider package with OTPaaS and mock implementationsotp.Providerinterface, OTPaaS provider, mock provider, and bimap from botpfeat(config): add OTP provider selection and mock configurationTW_OTP_PROVIDER,TW_ALLOWED_EMAIL_DOMAINS,TW_MOCK_ALLOWED_EMAILSwith conditional validationfeat(env): add otp related env vars.env.examplewith new OTP-related environment variablesfeat(handler): accept OTP provider in handler constructorotpProviderfield to Handler struct, changeNew()signaturefeat(otp): make handler call providersfeat(session): integrate session store into OTP handlerssession.Store, establish authenticated session after verifyfeat(session): integrate session store into OTP handlersOld TW vs New TW (handler behavior)
http.Clientwith manual HMAC authotp.ProviderinterfacebuildAuthTokenin handlerisAllowedEmail(env-based)AllowedEmailDomainsliststrings.TrimSpace(strings.ToLower(...))application/jsonwith 415Content-Type: application/jsonX-Content-Type-Options: nosniff,charset=UTF-8200 {id}200 {id}(unchanged)204 No Content200 {id, email}context.DeadlineExceeded→ 504session.Storewith TTL (10min otp flow, 30min auth)botp vs TW comparison
{message, errors: [{field, message}]}{code, message, error: [{code, message}]}renderJSON(generic)writeClientErrorResponse+ inline JSON/verify/{flow_id})renderJSONhttp.ErrorTest coverage (botp → TW mapping)
RequestOTP
VerifyOTP
🧪 Test Plan
go test ./server/internal/otp/...go test ./server/internal/config/...go test ./server/internal/handler/...go test ./...(full suite)make lintTW_OTP_PROVIDER=mock TW_ALLOWED_EMAIL_DOMAINS=@schools.gov.sg TW_MOCK_ALLOWED_EMAILS=test@schools.gov.sg go run ./server/cmd/twcurl -sv -X POST 'http://[::1]:3000/otp/request' -H 'Content-Type: application/json' -d '{"email":"test@schools.gov.sg"}'→ 200with
{id}andSet-Cookie: session_id=...curl -s -X POST 'http://[::1]:3000/otp/verify' -H 'Content-Type: application/json' -d '{"pin":"112233"}' --cookie 'session_id=<value>'→ 200 with{id, email}"pin":"999999"→ 422AUTHORIZATION_FAILED"pin":"223344"→ 422AUTHORIZATION_FAILED"email":"hacker@example.com"→ 422INVALID_FORM