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  }