github.com/haalcala/mattermost-server-change-repo@v0.0.0-20210713015153-16753fbeee5f/services/mfa/mfa.go (about) 1 // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. 2 // See LICENSE.txt for license information. 3 4 package mfa 5 6 import ( 7 "fmt" 8 "net/http" 9 "net/url" 10 "strings" 11 12 "github.com/dgryski/dgoogauth" 13 "github.com/mattermost/rsc/qr" 14 15 "github.com/mattermost/mattermost-server/v5/model" 16 "github.com/mattermost/mattermost-server/v5/services/configservice" 17 "github.com/mattermost/mattermost-server/v5/store" 18 ) 19 20 const ( 21 // This will result in 160 bits of entropy (base32 encoded), as recommended by rfc4226. 22 MFASecretSize = 20 23 ) 24 25 type Mfa struct { 26 ConfigService configservice.ConfigService 27 Store store.Store 28 } 29 30 func New(configService configservice.ConfigService, store store.Store) Mfa { 31 return Mfa{configService, store} 32 } 33 34 func (m *Mfa) checkConfig() *model.AppError { 35 if !*m.ConfigService.Config().ServiceSettings.EnableMultifactorAuthentication { 36 return model.NewAppError("checkConfig", "mfa.mfa_disabled.app_error", nil, "", http.StatusNotImplemented) 37 } 38 39 return nil 40 } 41 42 func getIssuerFromUrl(uri string) string { 43 issuer := "Mattermost" 44 siteUrl := strings.TrimSpace(uri) 45 46 if siteUrl != "" { 47 siteUrl = strings.TrimPrefix(siteUrl, "https://") 48 siteUrl = strings.TrimPrefix(siteUrl, "http://") 49 issuer = strings.TrimPrefix(siteUrl, "www.") 50 } 51 52 return url.QueryEscape(issuer) 53 } 54 55 func (m *Mfa) GenerateSecret(user *model.User) (string, []byte, *model.AppError) { 56 if err := m.checkConfig(); err != nil { 57 return "", nil, err 58 } 59 60 issuer := getIssuerFromUrl(*m.ConfigService.Config().ServiceSettings.SiteURL) 61 62 secret := model.NewRandomBase32String(MFASecretSize) 63 64 authLink := fmt.Sprintf("otpauth://totp/%s:%s?secret=%s&issuer=%s", issuer, user.Email, secret, issuer) 65 66 code, err := qr.Encode(authLink, qr.H) 67 68 if err != nil { 69 return "", nil, model.NewAppError("GenerateQrCode", "mfa.generate_qr_code.create_code.app_error", nil, err.Error(), http.StatusInternalServerError) 70 } 71 72 img := code.PNG() 73 74 if err := m.Store.User().UpdateMfaSecret(user.Id, secret); err != nil { 75 return "", nil, model.NewAppError("GenerateQrCode", "mfa.generate_qr_code.save_secret.app_error", nil, err.Error(), http.StatusInternalServerError) 76 } 77 78 return secret, img, nil 79 } 80 81 func (m *Mfa) Activate(user *model.User, token string) *model.AppError { 82 if err := m.checkConfig(); err != nil { 83 return err 84 } 85 86 otpConfig := &dgoogauth.OTPConfig{ 87 Secret: user.MfaSecret, 88 WindowSize: 3, 89 HotpCounter: 0, 90 } 91 92 trimmedToken := strings.TrimSpace(token) 93 94 ok, err := otpConfig.Authenticate(trimmedToken) 95 if err != nil { 96 return model.NewAppError("Activate", "mfa.activate.authenticate.app_error", nil, err.Error(), http.StatusInternalServerError) 97 } 98 99 if !ok { 100 return model.NewAppError("Activate", "mfa.activate.bad_token.app_error", nil, "", http.StatusUnauthorized) 101 } 102 103 if appErr := m.Store.User().UpdateMfaActive(user.Id, true); appErr != nil { 104 return model.NewAppError("Activate", "mfa.activate.save_active.app_error", nil, appErr.Error(), http.StatusInternalServerError) 105 } 106 107 return nil 108 } 109 110 func (m *Mfa) Deactivate(userId string) *model.AppError { 111 if err := m.checkConfig(); err != nil { 112 return err 113 } 114 115 schan := make(chan error, 1) 116 go func() { 117 schan <- m.Store.User().UpdateMfaSecret(userId, "") 118 close(schan) 119 }() 120 121 if err := m.Store.User().UpdateMfaActive(userId, false); err != nil { 122 return model.NewAppError("Deactivate", "mfa.deactivate.save_active.app_error", nil, err.Error(), http.StatusInternalServerError) 123 } 124 125 if err := <-schan; err != nil { 126 return model.NewAppError("Deactivate", "mfa.deactivate.save_secret.app_error", nil, err.Error(), http.StatusInternalServerError) 127 } 128 129 return nil 130 } 131 132 func (m *Mfa) ValidateToken(secret, token string) (bool, *model.AppError) { 133 if err := m.checkConfig(); err != nil { 134 return false, err 135 } 136 137 otpConfig := &dgoogauth.OTPConfig{ 138 Secret: secret, 139 WindowSize: 3, 140 HotpCounter: 0, 141 } 142 143 trimmedToken := strings.TrimSpace(token) 144 ok, err := otpConfig.Authenticate(trimmedToken) 145 if err != nil { 146 return false, model.NewAppError("ValidateToken", "mfa.validate_token.authenticate.app_error", nil, err.Error(), http.StatusBadRequest) 147 } 148 149 return ok, nil 150 }