github.com/mattermost/mattermost-server/v5@v5.39.3/shared/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 "crypto/rand" 8 "encoding/base32" 9 "fmt" 10 "net/url" 11 "strings" 12 13 "github.com/dgryski/dgoogauth" 14 "github.com/mattermost/rsc/qr" 15 "github.com/pkg/errors" 16 ) 17 18 // InvalidToken indicates the case where the token validation has failed. 19 var InvalidToken = errors.New("invalid mfa token") 20 21 const ( 22 // This will result in 160 bits of entropy (base32 encoded), as recommended by rfc4226. 23 mfaSecretSize = 20 24 ) 25 26 type Store interface { 27 UpdateMfaActive(userId string, active bool) error 28 UpdateMfaSecret(userId, secret string) error 29 } 30 31 type MFA struct { 32 store Store 33 } 34 35 func New(store Store) *MFA { 36 return &MFA{store} 37 } 38 39 // newRandomBase32String returns a base32 encoded string of a random slice 40 // of bytes of the given size. The resulting entropy will be (8 * size) bits. 41 func newRandomBase32String(size int) string { 42 data := make([]byte, size) 43 rand.Read(data) 44 return base32.StdEncoding.EncodeToString(data) 45 } 46 47 func getIssuerFromUrl(uri string) string { 48 issuer := "Mattermost" 49 siteUrl := strings.TrimSpace(uri) 50 51 if siteUrl != "" { 52 siteUrl = strings.TrimPrefix(siteUrl, "https://") 53 siteUrl = strings.TrimPrefix(siteUrl, "http://") 54 issuer = strings.TrimPrefix(siteUrl, "www.") 55 } 56 57 return url.QueryEscape(issuer) 58 } 59 60 // GenerateSecret generates a new user mfa secret and store it with the StoreSecret function provided 61 func (m *MFA) GenerateSecret(siteURL, userEmail, userID string) (string, []byte, error) { 62 issuer := getIssuerFromUrl(siteURL) 63 64 secret := newRandomBase32String(mfaSecretSize) 65 66 authLink := fmt.Sprintf("otpauth://totp/%s:%s?secret=%s&issuer=%s", issuer, userEmail, secret, issuer) 67 68 code, err := qr.Encode(authLink, qr.H) 69 70 if err != nil { 71 return "", nil, errors.Wrap(err, "unable to generate qr code") 72 } 73 74 img := code.PNG() 75 76 if err := m.store.UpdateMfaSecret(userID, secret); err != nil { 77 return "", nil, errors.Wrap(err, "unable to store mfa secret") 78 } 79 80 return secret, img, nil 81 } 82 83 // Activate set the mfa as active and store it with the StoreActive function provided 84 func (m *MFA) Activate(userMfaSecret, userID string, token string) error { 85 otpConfig := &dgoogauth.OTPConfig{ 86 Secret: userMfaSecret, 87 WindowSize: 3, 88 HotpCounter: 0, 89 } 90 91 trimmedToken := strings.TrimSpace(token) 92 93 ok, err := otpConfig.Authenticate(trimmedToken) 94 if err != nil { 95 return errors.Wrap(err, "unable to parse the token") 96 } 97 98 if !ok { 99 return InvalidToken 100 } 101 102 if err := m.store.UpdateMfaActive(userID, true); err != nil { 103 return errors.Wrap(err, "unable to store mfa active") 104 } 105 106 return nil 107 } 108 109 // Deactivate set the mfa as deactive, remove the mfa secret, store it with the StoreActive and StoreSecret functions provided 110 func (m *MFA) Deactivate(userId string) error { 111 if err := m.store.UpdateMfaActive(userId, false); err != nil { 112 return errors.Wrap(err, "unable to store mfa active") 113 } 114 115 if err := m.store.UpdateMfaSecret(userId, ""); err != nil { 116 return errors.Wrap(err, "unable to store mfa secret") 117 } 118 119 return nil 120 } 121 122 // Validate the provide token using the secret provided 123 func (m *MFA) ValidateToken(secret, token string) (bool, error) { 124 otpConfig := &dgoogauth.OTPConfig{ 125 Secret: secret, 126 WindowSize: 3, 127 HotpCounter: 0, 128 } 129 130 trimmedToken := strings.TrimSpace(token) 131 ok, err := otpConfig.Authenticate(trimmedToken) 132 if err != nil { 133 return false, errors.Wrap(err, "unable to parse the token") 134 } 135 136 return ok, nil 137 }