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  }