github.com/greenpau/go-identity@v1.1.6/mfa_token_test.go (about) 1 // Copyright 2020 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-identity/internal/tests" 20 "github.com/greenpau/go-identity/pkg/errors" 21 "github.com/greenpau/go-identity/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 }