feat: complete registration invitation and auth redesign
This commit is contained in:
+117
-15
@@ -37,6 +37,7 @@ const (
|
||||
imReasonChallengeExpired = "CHALLENGE_EXPIRED"
|
||||
imReasonCodeInvalid = "CODE_INVALID"
|
||||
imReasonTooManyAttempts = "TOO_MANY_ATTEMPTS"
|
||||
imReasonSMSDeliveryFailed = "SMS_DELIVERY_FAILED"
|
||||
imReasonPasswordPolicy = "PASSWORD_POLICY_VIOLATION"
|
||||
imReasonAuthRequired = "AUTH_REQUIRED"
|
||||
imReasonRealNameRequired = "REALNAME_REQUIRED"
|
||||
@@ -51,7 +52,6 @@ const (
|
||||
|
||||
const (
|
||||
imVerificationCodeLength = 6
|
||||
imVerificationTestCode = "123456"
|
||||
imVerificationExpiresInSeconds = 300
|
||||
imVerificationResendAfterSeconds = 60
|
||||
imVerificationMaxAttempts = 5
|
||||
@@ -523,6 +523,7 @@ func serveIMTenantResolve(wrt http.ResponseWriter, req *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
endpoints := imPublicEndpoints(req)
|
||||
writeIMSuccess(wrt, req, map[string]any{
|
||||
"tenant": map[string]any{
|
||||
"code": tenant.Code,
|
||||
@@ -530,11 +531,7 @@ func serveIMTenantResolve(wrt http.ResponseWriter, req *http.Request) {
|
||||
"tenant_desc": tenant.TenantDesc,
|
||||
"branding": map[string]any{},
|
||||
},
|
||||
"endpoints": map[string]any{
|
||||
"websocket": strings.TrimRight(globals.servingAt, "/") + "/v0/channels",
|
||||
"api": strings.TrimRight(globals.servingAt, "/") + "/v1/im",
|
||||
"file": strings.TrimRight(globals.servingAt, "/") + "/v0/file",
|
||||
},
|
||||
"endpoints": endpoints,
|
||||
"auth_methods": []string{"password", "sms"},
|
||||
"password_policy": map[string]any{
|
||||
"min_length": 8,
|
||||
@@ -544,6 +541,75 @@ func serveIMTenantResolve(wrt http.ResponseWriter, req *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
func imPublicEndpoints(req *http.Request) map[string]any {
|
||||
base := imPublicHTTPBase(req)
|
||||
wsBase := base
|
||||
if strings.HasPrefix(wsBase, "https://") {
|
||||
wsBase = "wss://" + strings.TrimPrefix(wsBase, "https://")
|
||||
} else if strings.HasPrefix(wsBase, "http://") {
|
||||
wsBase = "ws://" + strings.TrimPrefix(wsBase, "http://")
|
||||
}
|
||||
return map[string]any{
|
||||
"websocket": strings.TrimRight(wsBase, "/") + "/v0/channels",
|
||||
"api": strings.TrimRight(base, "/") + "/v1/im",
|
||||
"file": strings.TrimRight(base, "/") + "/v0/file",
|
||||
}
|
||||
}
|
||||
|
||||
func imPublicHTTPBase(req *http.Request) string {
|
||||
configured := strings.TrimRight(strings.TrimSpace(globals.servingAt), "/")
|
||||
parsed, _ := url.Parse(configured)
|
||||
basePath := ""
|
||||
if parsed != nil {
|
||||
basePath = strings.TrimRight(parsed.Path, "/")
|
||||
if parsed.Scheme != "" && isPublicEndpointHost(parsed.Host) {
|
||||
return strings.TrimRight(parsed.Scheme+"://"+parsed.Host+basePath, "/")
|
||||
}
|
||||
}
|
||||
if basePath == "" && req != nil && req.URL != nil {
|
||||
if idx := strings.Index(req.URL.Path, "/v1/im/"); idx > 0 {
|
||||
basePath = strings.TrimRight(req.URL.Path[:idx], "/")
|
||||
}
|
||||
}
|
||||
|
||||
scheme := "http"
|
||||
if req != nil && req.TLS != nil {
|
||||
scheme = "https"
|
||||
}
|
||||
if req != nil {
|
||||
if forwarded := firstForwardedHeader(req.Header.Get("X-Forwarded-Proto")); forwarded == "http" || forwarded == "https" {
|
||||
scheme = forwarded
|
||||
}
|
||||
}
|
||||
|
||||
host := ""
|
||||
if req != nil {
|
||||
host = firstForwardedHeader(req.Header.Get("X-Forwarded-Host"))
|
||||
if host == "" {
|
||||
host = strings.TrimSpace(req.Host)
|
||||
}
|
||||
}
|
||||
if host == "" && parsed != nil {
|
||||
host = parsed.Host
|
||||
}
|
||||
if host == "" {
|
||||
return configured
|
||||
}
|
||||
return strings.TrimRight(scheme+"://"+host+basePath, "/")
|
||||
}
|
||||
|
||||
func isPublicEndpointHost(host string) bool {
|
||||
host = strings.TrimSpace(host)
|
||||
return host != "" && !strings.HasPrefix(host, ":") && !strings.HasPrefix(host, "0.0.0.0:")
|
||||
}
|
||||
|
||||
func firstForwardedHeader(value string) string {
|
||||
if idx := strings.Index(value, ","); idx >= 0 {
|
||||
value = value[:idx]
|
||||
}
|
||||
return strings.TrimSpace(strings.ToLower(value))
|
||||
}
|
||||
|
||||
func serveIMVerificationCodes(wrt http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodPost {
|
||||
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
|
||||
@@ -598,8 +664,12 @@ func serveIMVerificationCodes(wrt http.ResponseWriter, req *http.Request) {
|
||||
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
|
||||
return
|
||||
}
|
||||
// Temporary fixed code for app integration until the SMS provider is connected.
|
||||
code := imVerificationTestCode
|
||||
code, err := randomIMNumericCode(imVerificationCodeLength)
|
||||
if err != nil {
|
||||
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
|
||||
return
|
||||
}
|
||||
deliveryEnabled := scene != "register" || registrationSMSEnabled(now)
|
||||
vc := &types.VerificationCode{
|
||||
TenantID: tenant.ID,
|
||||
ChallengeID: challengeID,
|
||||
@@ -613,8 +683,8 @@ func serveIMVerificationCodes(wrt http.ResponseWriter, req *http.Request) {
|
||||
ResendAfter: now.Add(imVerificationResendAfterSeconds * time.Second),
|
||||
}
|
||||
if scene == "reset_password" {
|
||||
if uid, err := store.Users.GetByCred(tenant.ID, "tel", phone); err != nil {
|
||||
logs.Warn.Println("im verification create: credential lookup failed", tenant.Code, err)
|
||||
if uid, err := store.Users.GetByPhone(tenant.ID, phone); err != nil {
|
||||
logs.Warn.Println("im verification create: phone lookup failed", tenant.Code, err)
|
||||
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
|
||||
return
|
||||
} else {
|
||||
@@ -626,8 +696,18 @@ func serveIMVerificationCodes(wrt http.ResponseWriter, req *http.Request) {
|
||||
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
|
||||
return
|
||||
}
|
||||
if deliveryEnabled {
|
||||
if err := sendIMVerificationCode(tenant.ID, phone, "", scene, code); err != nil {
|
||||
_ = store.Verifications.Fail(tenant.ID, vc.ChallengeID, types.VerificationStateExpired)
|
||||
logs.Warn.Println("im verification delivery failed:", tenant.Code, scene, vc.PhoneMask, err)
|
||||
writeIMError(wrt, req, http.StatusBadGateway, 30033, imReasonSMSDeliveryFailed, "SMS delivery failed", nil)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
logs.Info.Println("im verification delivery suppressed:", tenant.Code, scene, vc.PhoneMask)
|
||||
}
|
||||
|
||||
logs.Info.Println("im verification code generated:", tenant.Code, scene, vc.PhoneMask)
|
||||
logs.Info.Println("im verification challenge created:", tenant.Code, scene, vc.PhoneMask)
|
||||
writeIMSuccess(wrt, req, map[string]any{
|
||||
"challenge_id": vc.ChallengeID,
|
||||
"phone_mask": vc.PhoneMask,
|
||||
@@ -794,7 +874,7 @@ func serveIMPasswordResetConfirm(wrt http.ResponseWriter, req *http.Request) {
|
||||
writeIMSuccess(wrt, req, map[string]any{"status": "ok"})
|
||||
return
|
||||
}
|
||||
login, authLevel, _, _, err := store.Users.GetAuthRecord(tenant.ID, vc.TargetUser, "basic")
|
||||
login, authLevel, _, _, err := store.Users.GetAuthRecord(tenant.ID, vc.TargetUser)
|
||||
if err != nil || login == "" {
|
||||
writeIMError(wrt, req, http.StatusGone, 30030, imReasonChallengeExpired, "challenge expired", nil)
|
||||
return
|
||||
@@ -830,6 +910,19 @@ func serveIMPasswordResetConfirm(wrt http.ResponseWriter, req *http.Request) {
|
||||
writeIMError(wrt, req, http.StatusGone, 30030, imReasonChallengeExpired, "challenge expired", nil)
|
||||
return
|
||||
}
|
||||
if account, getErr := store.Users.Get(tenant.ID, vc.TargetUser); getErr == nil && account != nil && account.Phone != "" {
|
||||
if _, credErr := store.Users.UpsertCred(tenant.ID, &types.Credential{
|
||||
ObjHeader: types.ObjHeader{CreatedAt: now, UpdatedAt: now},
|
||||
User: vc.TargetUser.String(),
|
||||
Method: "tel",
|
||||
Value: account.Phone,
|
||||
Done: true,
|
||||
}); credErr != nil {
|
||||
logs.Warn.Println("im password reset confirm: failed to backfill phone credential", tenant.Code, credErr)
|
||||
}
|
||||
} else if getErr != nil {
|
||||
logs.Warn.Println("im password reset confirm: failed to load account phone", tenant.Code, getErr)
|
||||
}
|
||||
if globals.sessionStore != nil {
|
||||
globals.sessionStore.EvictUser(vc.TargetUser, "")
|
||||
}
|
||||
@@ -4892,8 +4985,11 @@ func createSensitiveChallenge(wrt http.ResponseWriter, req *http.Request, princi
|
||||
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
|
||||
return
|
||||
}
|
||||
// Temporary fixed code for app integration until the SMS provider is connected.
|
||||
code := imVerificationTestCode
|
||||
code, err := randomIMNumericCode(imVerificationCodeLength)
|
||||
if err != nil {
|
||||
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
|
||||
return
|
||||
}
|
||||
vc := &types.VerificationCode{
|
||||
TenantID: principal.TenantID,
|
||||
ChallengeID: challengeID,
|
||||
@@ -4912,7 +5008,13 @@ func createSensitiveChallenge(wrt http.ResponseWriter, req *http.Request, princi
|
||||
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
|
||||
return
|
||||
}
|
||||
logs.Info.Println("im security verification code generated:", action, vc.PhoneMask)
|
||||
if err := sendIMVerificationCode(principal.TenantID, phone, "", "sensitive_action", code); err != nil {
|
||||
_ = store.Verifications.Fail(principal.TenantID, vc.ChallengeID, types.VerificationStateExpired)
|
||||
logs.Warn.Println("im security verification delivery failed:", action, vc.PhoneMask, err)
|
||||
writeIMError(wrt, req, http.StatusBadGateway, 30033, imReasonSMSDeliveryFailed, "SMS delivery failed", nil)
|
||||
return
|
||||
}
|
||||
logs.Info.Println("im security verification challenge created:", action, vc.PhoneMask)
|
||||
writeIMSuccess(wrt, req, map[string]any{
|
||||
"challenge_id": vc.ChallengeID,
|
||||
"phone_mask": vc.PhoneMask,
|
||||
|
||||
Reference in New Issue
Block a user