feat: complete registration invitation and auth redesign
This commit is contained in:
+154
-6
@@ -34,13 +34,27 @@ func replyCreateUser(s *Session, msg *ClientComMessage, rec *auth.Rec) {
|
||||
|
||||
// Find authenticator for the requested scheme.
|
||||
authhdl := store.Store.GetLogicalAuthHandler(msg.Acc.Scheme)
|
||||
if authhdl == nil {
|
||||
if authhdl == nil || msg.Acc.Scheme != "basic" {
|
||||
// New accounts must have an authentication scheme
|
||||
s.queueOut(ErrMalformed(msg.Id, "", msg.Timestamp))
|
||||
logs.Warn.Println("create user: unknown auth handler, sid=", s.sid)
|
||||
return
|
||||
}
|
||||
|
||||
account, phone, phoneRegistration, err := resolveRegistrationAccount(msg.Acc.Username, msg.Acc.Phone)
|
||||
if err != nil || !registrationSecretMatches(msg.Acc.Secret, account) {
|
||||
logs.Warn.Println("create user: invalid registration account fields, sid=", s.sid)
|
||||
s.queueOut(decodeStoreError(types.ErrPolicy, msg.Id, msg.Timestamp,
|
||||
map[string]any{"what": "account", "reason": "REGISTRATION_ACCOUNT_INVALID"}))
|
||||
return
|
||||
}
|
||||
inviteCode := strings.TrimSpace(msg.Acc.InviteCode)
|
||||
if msg.Acc.InviteCode != inviteCode || (inviteCode != "" && !isValidIMInviteCode(inviteCode)) {
|
||||
s.queueOut(decodeStoreError(types.ErrPolicy, msg.Id, msg.Timestamp,
|
||||
map[string]any{"what": "invite_code", "reason": "INVITE_CODE_INVALID"}))
|
||||
return
|
||||
}
|
||||
|
||||
// Check if login is unique and compliance with the policy (not too long or too short).
|
||||
authCtx := auth.AuthContext{TenantID: s.tenantID, RemoteAddr: s.remoteAddr}
|
||||
if ok, err := authhdl.IsUnique(authCtx, msg.Acc.Secret); !ok {
|
||||
@@ -52,6 +66,8 @@ func replyCreateUser(s *Session, msg *ClientComMessage, rec *auth.Rec) {
|
||||
|
||||
var user types.User
|
||||
user.TenantID = s.tenantID
|
||||
user.Username = account
|
||||
user.Phone = phone
|
||||
var private any
|
||||
|
||||
// If account state is being assigned, make sure the sender is a root user.
|
||||
@@ -85,9 +101,15 @@ func replyCreateUser(s *Session, msg *ClientComMessage, rec *auth.Rec) {
|
||||
// consequently cannot check presence of required credentials. Must do that later.
|
||||
creds := normalizeCredentials(msg.Acc.Cred, true)
|
||||
var registrationPhone string
|
||||
var registrationTelCredential bool
|
||||
for i := range creds {
|
||||
cr := &creds[i]
|
||||
vld := store.Store.GetValidator(cr.Method)
|
||||
if vld == nil || !vld.IsInitialized() {
|
||||
s.queueOut(decodeStoreError(types.ErrCredentials, msg.Id, msg.Timestamp,
|
||||
map[string]any{"what": cr.Method}))
|
||||
return
|
||||
}
|
||||
normalized, err := vld.PreCheck(cr.Value, cr.Params)
|
||||
if err != nil {
|
||||
logs.Warn.Println("create user: failed credential pre-check", cr, err, "sid=", s.sid)
|
||||
@@ -105,15 +127,40 @@ func replyCreateUser(s *Session, msg *ClientComMessage, rec *auth.Rec) {
|
||||
return
|
||||
}
|
||||
registrationPhone = phone
|
||||
registrationTelCredential = true
|
||||
}
|
||||
}
|
||||
if registrationPhone == "" && store.Verifications != nil {
|
||||
logs.Warn.Println("create user: verified phone is required, sid=", s.sid)
|
||||
if !phoneRegistration && registrationTelCredential {
|
||||
logs.Warn.Println("create user: username registration cannot attach a phone credential, sid=", s.sid)
|
||||
s.queueOut(decodeStoreError(types.ErrCredentials, msg.Id, msg.Timestamp,
|
||||
map[string]any{"creds": []string{"tel"}}))
|
||||
map[string]any{"creds": []string{"tel"}, "reason": "PHONE_NOT_SELECTED"}))
|
||||
return
|
||||
}
|
||||
user.Phone = registrationPhone
|
||||
if phoneRegistration && registrationTelCredential && registrationPhone != phone {
|
||||
s.queueOut(decodeStoreError(types.ErrCredentials, msg.Id, msg.Timestamp,
|
||||
map[string]any{"creds": []string{"tel"}, "reason": "PHONE_MISMATCH"}))
|
||||
return
|
||||
}
|
||||
smsEnabled := registrationSMSEnabled(time.Now().UTC())
|
||||
if phoneRegistration && smsEnabled && !registrationTelCredential {
|
||||
logs.Warn.Println("create user: verified phone is required while registration SMS is enabled, sid=", s.sid)
|
||||
s.queueOut(decodeStoreError(types.ErrCredentials, msg.Id, msg.Timestamp,
|
||||
map[string]any{"creds": []string{"tel"}, "reason": "PHONE_VERIFICATION_REQUIRED"}))
|
||||
return
|
||||
}
|
||||
inviteRequired := !phoneRegistration || (!smsEnabled && !registrationTelCredential)
|
||||
if inviteRequired && inviteCode == "" {
|
||||
s.queueOut(decodeStoreError(types.ErrCredentials, msg.Id, msg.Timestamp,
|
||||
map[string]any{"what": "invite_code", "reason": "INVITE_CODE_REQUIRED"}))
|
||||
return
|
||||
}
|
||||
if inviteCode != "" {
|
||||
if err := validateRegistrationInvite(s.tenantID, inviteCode, time.Now().UTC()); err != nil {
|
||||
s.queueOut(decodeStoreError(types.ErrCredentials, msg.Id, msg.Timestamp,
|
||||
map[string]any{"what": "invite_code", "reason": registrationInviteReason(err)}))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Assign default access values in case the acc creator has not provided them
|
||||
user.Access.Auth = getDefaultAccess(types.TopicCatP2P, true, false) |
|
||||
@@ -155,7 +202,7 @@ func replyCreateUser(s *Session, msg *ClientComMessage, rec *auth.Rec) {
|
||||
}
|
||||
|
||||
// Add authentication record.
|
||||
rec, err := authhdl.AddRecord(authCtx,
|
||||
rec, err = authhdl.AddRecord(authCtx,
|
||||
&auth.Rec{TenantID: s.tenantID, Uid: user.Uid()}, msg.Acc.Secret)
|
||||
if err != nil {
|
||||
logs.Warn.Println("create user: add auth record failed", err, "sid=", s.sid)
|
||||
@@ -201,6 +248,34 @@ func replyCreateUser(s *Session, msg *ClientComMessage, rec *auth.Rec) {
|
||||
}
|
||||
return
|
||||
}
|
||||
if registrationTelCredential && !slices.Contains(validated, "tel") {
|
||||
logs.Warn.Println("create user: phone credential was not validated, sid=", s.sid)
|
||||
s.queueOut(decodeStoreError(types.ErrCredentials, msg.Id, msg.Timestamp,
|
||||
map[string]any{"creds": []string{"tel"}, "reason": "PHONE_VERIFICATION_REQUIRED"}))
|
||||
if err = store.Users.Delete(s.tenantID, user.Uid(), true); err != nil {
|
||||
logs.Warn.Println("create user: failed to delete incomplete user record", err, "sid=", s.sid)
|
||||
}
|
||||
return
|
||||
}
|
||||
if inviteCode != "" {
|
||||
requestID := strings.TrimSpace(msg.Id)
|
||||
if requestID == "" {
|
||||
requestID = "acc:" + s.sid
|
||||
}
|
||||
if len(requestID) > 64 {
|
||||
requestID = requestID[:64]
|
||||
}
|
||||
if err = store.RegistrationInvites.RecordUse(s.tenantID, inviteCode, user.Uid(), requestID, time.Now().UTC()); err != nil {
|
||||
logs.Warn.Println("create user: record invite use failed", err, "sid=", s.sid)
|
||||
s.queueOut(decodeStoreError(types.ErrCredentials, msg.Id, msg.Timestamp,
|
||||
map[string]any{"what": "invite_code", "reason": registrationInviteReason(err)}))
|
||||
if deleteErr := store.Users.Delete(s.tenantID, user.Uid(), true); deleteErr != nil {
|
||||
logs.Warn.Println("create user: failed to delete incomplete user record", deleteErr, "sid=", s.sid)
|
||||
}
|
||||
return
|
||||
}
|
||||
dispatchIMBusinessEvents(s.tenantID)
|
||||
}
|
||||
|
||||
if msg.Extra != nil && len(msg.Extra.Attachments) > 0 {
|
||||
if err := store.Files.LinkAttachments(s.tenantID, user.Uid().UserId(), types.ZeroUid, msg.Extra.Attachments); err != nil {
|
||||
@@ -240,6 +315,79 @@ func replyCreateUser(s *Session, msg *ClientComMessage, rec *auth.Rec) {
|
||||
pluginAccount(&user, plgActCreate)
|
||||
}
|
||||
|
||||
func resolveRegistrationAccount(username, phone string) (account, canonicalPhone string, phoneRegistration bool, err error) {
|
||||
if phone != "" {
|
||||
if len(phone) != 11 || !asciiRegistrationDigits(phone) {
|
||||
return "", "", false, types.ErrPolicy
|
||||
}
|
||||
canonicalPhone, err = normalizeIMPhone(phone, "CN")
|
||||
if err != nil {
|
||||
return "", "", false, err
|
||||
}
|
||||
return canonicalPhone, canonicalPhone, true, nil
|
||||
}
|
||||
if username == "" || len(username) > 10 {
|
||||
return "", "", false, types.ErrPolicy
|
||||
}
|
||||
for i := range username {
|
||||
if (username[i] < 'a' || username[i] > 'z') && (username[i] < '0' || username[i] > '9') {
|
||||
return "", "", false, types.ErrPolicy
|
||||
}
|
||||
}
|
||||
return username, "", false, nil
|
||||
}
|
||||
|
||||
func asciiRegistrationDigits(value string) bool {
|
||||
for i := range value {
|
||||
if value[i] < '0' || value[i] > '9' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return value != ""
|
||||
}
|
||||
|
||||
func registrationSecretMatches(secret []byte, account string) bool {
|
||||
separator := strings.IndexByte(string(secret), ':')
|
||||
return separator > 0 && string(secret[:separator]) == account
|
||||
}
|
||||
|
||||
func validateRegistrationInvite(tenantID types.TenantID, code string, now time.Time) error {
|
||||
if store.RegistrationInvites == nil {
|
||||
return types.ErrInternal
|
||||
}
|
||||
invite, err := store.RegistrationInvites.GetByCode(tenantID, code)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if invite == nil {
|
||||
return types.ErrInviteInvalid
|
||||
}
|
||||
switch invite.Status {
|
||||
case types.RegistrationInviteRevoked:
|
||||
return types.ErrInviteRevoked
|
||||
case types.RegistrationInviteExpired:
|
||||
return types.ErrInviteExpired
|
||||
case types.RegistrationInviteActive:
|
||||
if !now.Before(invite.ExpiresAt) {
|
||||
return types.ErrInviteExpired
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return types.ErrInviteInvalid
|
||||
}
|
||||
}
|
||||
|
||||
func registrationInviteReason(err error) string {
|
||||
switch err {
|
||||
case types.ErrInviteExpired:
|
||||
return "INVITE_CODE_EXPIRED"
|
||||
case types.ErrInviteRevoked:
|
||||
return "INVITE_CODE_REVOKED"
|
||||
default:
|
||||
return "INVITE_CODE_INVALID"
|
||||
}
|
||||
}
|
||||
|
||||
// Process update to an account:
|
||||
// * Authentication update, i.e. login/password change
|
||||
// * Credentials update
|
||||
|
||||
Reference in New Issue
Block a user