github.com/greenpau/go-authcrunch@v1.1.4/pkg/identity/mfa_token_test.go (about)

     1  // Copyright 2022 Paul Greenberg greenpau@outlook.com
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package identity
    16  
    17  import (
    18  	"fmt"
    19  	"github.com/greenpau/go-authcrunch/internal/tests"
    20  	"github.com/greenpau/go-authcrunch/pkg/errors"
    21  	"github.com/greenpau/go-authcrunch/pkg/requests"
    22  	"math"
    23  	"testing"
    24  	"time"
    25  )
    26  
    27  func generateTestPasscode(r *requests.Request, offset bool) error {
    28  	var t time.Time
    29  	if r.MfaToken.Passcode != "" || r.MfaToken.Period == 0 {
    30  		return nil
    31  	}
    32  	if offset {
    33  		t = time.Now().Add(-time.Second * time.Duration(r.MfaToken.Period)).UTC()
    34  	} else {
    35  		t = time.Now().UTC()
    36  	}
    37  	ts := uint64(math.Floor(float64(t.Unix()) / float64(r.MfaToken.Period)))
    38  	code, err := generateMfaCode(r.MfaToken.Secret, r.MfaToken.Algorithm, r.MfaToken.Digits, ts)
    39  	if err != nil {
    40  		return err
    41  	}
    42  	r.MfaToken.Passcode = code
    43  	return nil
    44  }
    45  
    46  func TestNewMfaToken(t *testing.T) {
    47  	testcases := []struct {
    48  		name      string
    49  		req       *requests.Request
    50  		shouldErr bool
    51  		err       error
    52  	}{
    53  		{
    54  			name: "valid totp app token with sha1",
    55  			req: &requests.Request{
    56  				MfaToken: requests.MfaToken{
    57  					Comment:   "ms auth app",
    58  					Type:      "totp",
    59  					Secret:    "c71ca4c68bc14ec5b4ab8d3c3b63802c",
    60  					Algorithm: "sha1",
    61  					Period:    30,
    62  					Digits:    6,
    63  				},
    64  			},
    65  		},
    66  		{
    67  			name: "valid totp app token with sha256",
    68  			req: &requests.Request{
    69  				MfaToken: requests.MfaToken{
    70  					Comment:   "ms auth app",
    71  					Type:      "totp",
    72  					Secret:    "c71ca4c68bc14ec5b4ab8d3c3b63802c",
    73  					Algorithm: "sha256",
    74  					Period:    30,
    75  					Digits:    6,
    76  				},
    77  			},
    78  		},
    79  		{
    80  			name: "valid totp app token with sha512",
    81  			req: &requests.Request{
    82  				MfaToken: requests.MfaToken{
    83  					Comment:   "ms auth app",
    84  					Type:      "totp",
    85  					Secret:    "c71ca4c68bc14ec5b4ab8d3c3b63802c",
    86  					Algorithm: "sha512",
    87  					Period:    30,
    88  					Digits:    6,
    89  				},
    90  			},
    91  		},
    92  		{
    93  			name: "valid totp app token without algo",
    94  			req: &requests.Request{
    95  				MfaToken: requests.MfaToken{
    96  					Comment: "ms auth app",
    97  					Type:    "totp",
    98  					Secret:  "c71ca4c68bc14ec5b4ab8d3c3b63802c",
    99  					//Algorithm: "sha512",
   100  					Period: 30,
   101  					Digits: 6,
   102  				},
   103  			},
   104  			shouldErr: true,
   105  			err:       errors.ErrMfaTokenEmptyAlgorithm,
   106  		},
   107  		{
   108  			name: "valid totp app token without invalid algo",
   109  			req: &requests.Request{
   110  				MfaToken: requests.MfaToken{
   111  					Comment:   "ms auth app",
   112  					Type:      "totp",
   113  					Secret:    "c71ca4c68bc14ec5b4ab8d3c3b63802c",
   114  					Algorithm: "sha2048",
   115  					Period:    30,
   116  					Digits:    6,
   117  				},
   118  			},
   119  			shouldErr: true,
   120  			err:       errors.ErrMfaTokenInvalidAlgorithm.WithArgs("sha2048"),
   121  		},
   122  		{
   123  			name: "valid mfa token with long secret",
   124  			req: &requests.Request{
   125  				MfaToken: requests.MfaToken{
   126  					Secret:    "TJhDkLuPEtRapebVbBmV81JgdxSmZhYwLisDhA2G57yju4gWH4IRJ8KCIviDaFP5lgjsBnTG7L7yeK5kb",
   127  					Comment:   "ms auth app",
   128  					Period:    30,
   129  					Digits:    6,
   130  					Type:      "totp",
   131  					Algorithm: "sha1",
   132  				},
   133  			},
   134  		},
   135  		{
   136  			name: "invalid mfa token with matching codes",
   137  			req: &requests.Request{
   138  				MfaToken: requests.MfaToken{
   139  					Secret:   "c71ca4c68bc14ec5b4ab8d3c3b63802c",
   140  					Comment:  "ms auth app",
   141  					Period:   30,
   142  					Type:     "totp",
   143  					Passcode: "1234",
   144  				},
   145  			},
   146  			shouldErr: true,
   147  			err:       errors.ErrMfaTokenInvalidPasscode.WithArgs("digits length mismatch"),
   148  		},
   149  		{
   150  			name: "invalid mfa token with codes being too long",
   151  			req: &requests.Request{
   152  				MfaToken: requests.MfaToken{
   153  					Secret:   "c71ca4c68bc14ec5b4ab8d3c3b63802c",
   154  					Comment:  "ms auth app",
   155  					Period:   30,
   156  					Type:     "totp",
   157  					Passcode: "987654321",
   158  				},
   159  			},
   160  			shouldErr: true,
   161  			err:       errors.ErrMfaTokenInvalidPasscode.WithArgs("not 4-8 characters long"),
   162  		},
   163  		{
   164  			name: "invalid mfa token with codes being too short",
   165  			req: &requests.Request{
   166  				MfaToken: requests.MfaToken{
   167  					Secret:   "c71ca4c68bc14ec5b4ab8d3c3b63802c",
   168  					Comment:  "ms auth app",
   169  					Period:   30,
   170  					Type:     "totp",
   171  					Passcode: "123",
   172  				},
   173  			},
   174  			shouldErr: true,
   175  			err:       errors.ErrMfaTokenInvalidPasscode.WithArgs("not 4-8 characters long"),
   176  		},
   177  		{
   178  			name: "valid u2f token",
   179  			req: &requests.Request{
   180  				MfaToken: requests.MfaToken{
   181  					Comment: "u2f token",
   182  					Type:    "u2f",
   183  				},
   184  				WebAuthn: requests.WebAuthn{
   185  					Challenge: "gBRjbIXJu7YtwaHy5eM1MgpxeYIrbpxroOkGw0D7qFxW6HDA85Wxfnh3isb2utUPnVxW",
   186  					Register: "eyJpZCI6ImZjZWNmN2FkLTk0MDMtNGYzZi05ZTE0LWJiYTZkN2FhNTc0YiIsInR5cGUiOiJwdWJs" +
   187  						"aWMta2V5Iiwic3VjY2VzcyI6dHJ1ZSwiYXR0ZXN0YXRpb25PYmplY3QiOnsiYXR0U3RtdCI6eyJh" +
   188  						"bGciOi03LCJzaWciOiJNRVFDSUJSUU1tMUdsUmdLKzdVUVhZY3VjMElXRXNNOW5XZWpTaTBjeWFR" +
   189  						"UVV2RHlBaUJIdzlCZ1BkdDl0Qzd3NUl0cjI5eEZwb2RaZ204RHZYRkpuTE9veXM2R1p3PT0iLCJ4" +
   190  						"NWMiOlsiTUlJQ3ZUQ0NBYVdnQXdJQkFnSUVOY1JURGpBTkJna3Foa2lHOXcwQkFRc0ZBREF1TVN3" +
   191  						"d0tnWURWUVFERXlOWmRXSnBZMjhnVlRKR0lGSnZiM1FnUTBFZ1UyVnlhV0ZzSURRMU56SXdNRFl6" +
   192  						"TVRBZ0Z3MHhOREE0TURFd01EQXdNREJhR0E4eU1EVXdNRGt3TkRBd01EQXdNRm93YmpFTE1Ba0dB" +
   193  						"MVVFQmhNQ1UwVXhFakFRQmdOVkJBb01DVmwxWW1samJ5QkJRakVpTUNBR0ExVUVDd3daUVhWMGFH" +
   194  						"VnVkR2xqWVhSdmNpQkJkSFJsYzNSaGRHbHZiakVuTUNVR0ExVUVBd3dlV1hWaWFXTnZJRlV5UmlC" +
   195  						"RlJTQlRaWEpwWVd3Z09UQXlNRFU0TnpZMk1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNE" +
   196  						"UWdBRVpxN05yaVVZamtvamx3QllRWVIvWmEzeDhJc0VJL3FGWTBxN3FZWXVGQzMzdWZRSjN5NU9Y" +
   197  						"cDRHcjNvWE9lRlIxWGVRTUxXSzEzRzFYMngxWW40ckI2TnNNR293SWdZSkt3WUJCQUdDeEFvQ0JC" +
   198  						"VXhMak11Tmk0eExqUXVNUzQwTVRRNE1pNHhMamN3RXdZTEt3WUJCQUdDNVJ3Q0FRRUVCQU1DQlNB" +
   199  						"d0lRWUxLd1lCQkFHQzVSd0JBUVFFRWdRUTdvZ29lWEljU1JPWGRUMzh6cGNIS2pBTUJnTlZIUk1C" +
   200  						"QWY4RUFqQUFNQTBHQ1NxR1NJYjNEUUVCQ3dVQUE0SUJBUUNxeUk4MmVCeERvOXRRbTNGaXJ0S1dL" +
   201  						"OXN1dnBtcFVCUithcnBDaVZYRS9JdHdqc0w4cmtJaUczd0RRTnNHeENQc0VNNmhhVHM5WjhKaXlJ" +
   202  						"TjVOOHFtb3JEKzNzRFBiMFNxejBmcGkzMUgybnJuV3diTUlnVmZKZEpJdC9sNkpTOHdrRFh1cU5E" +
   203  						"NmJNeUlzMmxaMUpjb3dBY1lLSVBkNTRGUy9HWXhMVzB0bDlUWGFCK0RDZG9UQUZCYjdBNTBoVWFy" +
   204  						"ZFQ4ZTF3WmhlNVZ4UVluSjZtZzlITjF2SjlVWUVOMC9ORWJtQlZnNnpFV0h5YkRNMlFySU4ySnpj" +
   205  						"Y2JlcWRhVEI0UzBKdGdZVWhnb1IzdEN1QzRFeFk3cU4zcmJMUlUxbFNJa0NYQ2VLQ2d6TzZ2aDZz" +
   206  						"OGZSR1BhaUdkRytOMFBjcHFHdU9LSkcrZXhEUS9IK1pBbiJdfSwiYXV0aERhdGEiOnsicnBJZEhh" +
   207  						"c2giOiI0OTk2MGRlNTg4MGU4YzY4NzQzNDE3MGY2NDc2NjA1YjhmZTRhZWI5YTI4NjMyYzc5OTVj" +
   208  						"ZjNiYTgzMWQ5NzYzIiwiZmxhZ3MiOnsiVVAiOnRydWUsIlJGVTEiOmZhbHNlLCJVViI6ZmFsc2Us" +
   209  						"IlJGVTJhIjpmYWxzZSwiUkZVMmIiOmZhbHNlLCJSRlUyYyI6ZmFsc2UsIkFUIjp0cnVlLCJFRCI6" +
   210  						"ZmFsc2V9LCJzaWduYXR1cmVDb3VudGVyIjozLCJjcmVkZW50aWFsRGF0YSI6eyJhYWd1aWQiOiI3" +
   211  						"b2dvZVhJY1NST1hkVDM4enBjSEtnPT0iLCJjcmVkZW50aWFsSWQiOiJzU3RHTjA3NFNBVTAiLCJw" +
   212  						"dWJsaWNLZXkiOnsia2V5X3R5cGUiOjIsImFsZ29yaXRobSI6LTcsImN1cnZlX3R5cGUiOjEsImN1" +
   213  						"cnZlX3giOiJlYlU4cXZZTXZjSHhYTFQ1OEdkeDZLTjFMVldObFpvNjVmSjJxM1NzQnJBPSIsImN1" +
   214  						"cnZlX3kiOiJZTDB3c1BhSTdRZUJsZXlFWFJOdFpqQU9PZUZiSlJ6MXg2aVZZUkx4RFlNPSJ9fSwi" +
   215  						"ZXh0ZW5zaW9ucyI6e319LCJmbXQiOiJwYWNrZWQifSwiY2xpZW50RGF0YSI6eyJ0eXBlIjoid2Vi" +
   216  						"YXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiQUFBTEFBQUFBQUJlQUFBQURnQUxBQUFBQU5jQUFB" +
   217  						"YmFoUUFQQUFDeUFBQUFBQSIsIm9yaWdpbiI6Imh0dHBzOi8vbG9jYWxob3N0Ojg0NDMiLCJjcm9z" +
   218  						"c09yaWdpbiI6ZmFsc2V9LCJkZXZpY2UiOnsibmFtZSI6IlVua25vd24gZGV2aWNlIiwidHlwZSI6" +
   219  						"InVua25vd24ifX0K",
   220  				},
   221  			},
   222  		},
   223  
   224  		{
   225  			name: "invalid mfa token type",
   226  			req: &requests.Request{
   227  				MfaToken: requests.MfaToken{
   228  					Type: "foobar",
   229  				},
   230  			},
   231  			shouldErr: true,
   232  			err:       errors.ErrMfaTokenInvalidType.WithArgs("foobar"),
   233  		},
   234  		{
   235  			name: "empty mfa token type",
   236  			req: &requests.Request{
   237  				MfaToken: requests.MfaToken{},
   238  			},
   239  			shouldErr: true,
   240  			err:       errors.ErrMfaTokenTypeEmpty,
   241  		},
   242  		{
   243  			name: "app token with invalid algorithm",
   244  			req: &requests.Request{
   245  				MfaToken: requests.MfaToken{
   246  					Type:      "totp",
   247  					Algorithm: "foobar",
   248  				},
   249  			},
   250  			shouldErr: true,
   251  			err:       errors.ErrMfaTokenInvalidAlgorithm.WithArgs("foobar"),
   252  		},
   253  		{
   254  			name: "app token with invalid period",
   255  			req: &requests.Request{
   256  				MfaToken: requests.MfaToken{
   257  					Type:      "totp",
   258  					Algorithm: "sha1",
   259  					Period:    10,
   260  				},
   261  			},
   262  			shouldErr: true,
   263  			err:       errors.ErrMfaTokenInvalidPeriod.WithArgs(10),
   264  		},
   265  		{
   266  			name: "app token with invalid digits",
   267  			req: &requests.Request{
   268  				MfaToken: requests.MfaToken{
   269  					Type:      "totp",
   270  					Algorithm: "sha1",
   271  					Period:    30,
   272  					Digits:    2,
   273  				},
   274  			},
   275  			shouldErr: true,
   276  			err:       errors.ErrMfaTokenInvalidDigits.WithArgs(2),
   277  		},
   278  	}
   279  
   280  	for _, tc := range testcases {
   281  		t.Run(tc.name, func(t *testing.T) {
   282  			msgs := []string{fmt.Sprintf("test name: %s", tc.name)}
   283  			if tc.req.MfaToken.Type == "totp" && tc.req.MfaToken.Passcode == "" {
   284  				if err := generateTestPasscode(tc.req, true); err != nil {
   285  					if tests.EvalErrWithLog(t, err, "mfa token passcode", tc.shouldErr, tc.err, msgs) {
   286  						return
   287  					}
   288  					t.Fatalf("unexpected failure during passcode generation: %v", err)
   289  				}
   290  			}
   291  
   292  			token, err := NewMfaToken(tc.req)
   293  			if tests.EvalErrWithLog(t, err, "new mfa token", tc.shouldErr, tc.err, msgs) {
   294  				return
   295  			}
   296  			// t.Logf("token: %v", token)
   297  
   298  			if tc.req.MfaToken.Type == "totp" {
   299  				generateTestPasscode(tc.req, false)
   300  				if err := token.ValidateCode(tc.req.MfaToken.Passcode); err != nil {
   301  					t.Fatalf("unexpected failure during passcode validation: %v", err)
   302  				}
   303  				if err := token.ValidateCode("123456"); err == nil {
   304  					t.Fatalf("unexpected success during passcode validation: %v", err)
   305  				}
   306  				if err := token.ValidateCode(""); err == nil {
   307  					t.Fatalf("unexpected success during passcode validation: %v", err)
   308  				}
   309  				token.Algorithm = "sha2048"
   310  				if err := token.ValidateCode(tc.req.MfaToken.Passcode); err == nil {
   311  					t.Fatalf("unexpected success during passcode validation: %v", err)
   312  				}
   313  			}
   314  
   315  			bundle := NewMfaTokenBundle()
   316  			bundle.Add(token)
   317  			bundle.Get()
   318  			token.Disable()
   319  		})
   320  	}
   321  }