Commit b9fd2573 authored by ale's avatar ale

Merge branch 'multi-2fa' into 'master'

Return all supported 2FA mechanisms in the authentication response

See merge request !6
parents e29f41a8 dab5b7f1
Pipeline #2832 passed with stages
in 1 minute and 43 seconds
......@@ -58,7 +58,6 @@ type Request struct {
U2FAppID string
U2FResponse *u2f.SignResponse
DeviceInfo *DeviceInfo
//Extra map[string]string
}
func (r *Request) EncodeToMap(m map[string]string, prefix string) {
......@@ -98,6 +97,25 @@ type UserInfo struct {
Groups []string
}
func encodeStringList(m map[string]string, prefix string, l []string) {
for i, elem := range l {
m[fmt.Sprintf("%s.%d.", prefix, i)] = elem
}
}
func decodeStringList(m map[string]string, prefix string) (out []string) {
i := 0
for {
s, ok := m[fmt.Sprintf("%s.%d.", prefix, i)]
if !ok {
break
}
out = append(out, s)
i++
}
return
}
func (u *UserInfo) EncodeToMap(m map[string]string, prefix string) {
if u.Email != "" {
m[prefix+"email"] = u.Email
......@@ -105,24 +123,14 @@ func (u *UserInfo) EncodeToMap(m map[string]string, prefix string) {
if u.Shard != "" {
m[prefix+"shard"] = u.Shard
}
for i, g := range u.Groups {
m[fmt.Sprintf("%sgroup.%d.", prefix, i)] = g
}
encodeStringList(m, prefix+"group", u.Groups)
}
func decodeUserInfoFromMap(m map[string]string, prefix string) *UserInfo {
u := UserInfo{
Email: m[prefix+"email"],
Shard: m[prefix+"shard"],
}
i := 0
for {
s, ok := m[fmt.Sprintf("%sgroup.%d.", prefix, i)]
if !ok {
break
}
u.Groups = append(u.Groups, s)
i++
Email: m[prefix+"email"],
Shard: m[prefix+"shard"],
Groups: decodeStringList(m, prefix+"group"),
}
if u.Email == "" && u.Shard == "" && len(u.Groups) == 0 {
return nil
......@@ -133,16 +141,49 @@ func decodeUserInfoFromMap(m map[string]string, prefix string) *UserInfo {
// Response to an authentication request.
type Response struct {
Status Status
TFAMethod TFAMethod
TFAMethods []TFAMethod
U2FSignRequest *u2f.WebSignRequest
UserInfo *UserInfo
}
// Has2FAMethod checks for the presence of a two-factor authentication
// method in the Response.
func (r *Response) Has2FAMethod(needle TFAMethod) bool {
for _, m := range r.TFAMethods {
if m == needle {
return true
}
}
return false
}
func encodeTFAMethodList(m map[string]string, prefix string, l []TFAMethod) {
if len(l) == 0 {
return
}
tmp := make([]string, 0, len(l))
for _, el := range l {
tmp = append(tmp, string(el))
}
encodeStringList(m, prefix, tmp)
}
func decodeTFAMethodList(m map[string]string, prefix string) []TFAMethod {
l := decodeStringList(m, prefix)
if len(l) == 0 {
return nil
}
out := make([]TFAMethod, 0, len(l))
for _, el := range l {
out = append(out, TFAMethod(el))
}
return out
}
func (r *Response) EncodeToMap(m map[string]string, prefix string) {
m[prefix+"status"] = r.Status.String()
m[prefix+"2fa_method"] = string(r.TFAMethod)
encodeTFAMethodList(m, prefix+"2fa_methods", r.TFAMethods)
if r.U2FSignRequest != nil {
// External type.
encodeU2FSignRequestToMap(r.U2FSignRequest, m, prefix+"u2f_req.")
}
if r.UserInfo != nil {
......@@ -152,7 +193,7 @@ func (r *Response) EncodeToMap(m map[string]string, prefix string) {
func (r *Response) DecodeFromMap(m map[string]string, prefix string) {
r.Status = parseAuthStatus(m[prefix+"status"])
r.TFAMethod = TFAMethod(m[prefix+"2fa_method"])
r.TFAMethods = decodeTFAMethodList(m, prefix+"2fa_methods")
r.U2FSignRequest = decodeU2FSignRequestFromMap(m, prefix+"u2f_req.")
r.UserInfo = decodeUserInfoFromMap(m, prefix+"user.")
}
......
package auth
import (
"encoding/json"
"reflect"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/tstranex/u2f"
)
......@@ -40,10 +39,8 @@ func TestProtocol_SerializeRequest(t *testing.T) {
if err != nil {
t.Fatal("Decode():", err)
}
if !reflect.DeepEqual(req, &req2) {
d1, _ := json.MarshalIndent(req, "", " ")
d2, _ := json.MarshalIndent(req2, "", " ")
t.Errorf("decode results differ: %+v vs %+v", string(d1), string(d2))
if diffs := cmp.Diff(req, &req2); diffs != "" {
t.Errorf("decode results differ:\n%s", diffs)
}
}
......@@ -51,8 +48,8 @@ func TestProtocol_SerializeResponse(t *testing.T) {
c := &kvCodec{}
resp := &Response{
Status: StatusInsufficientCredentials,
TFAMethod: TFAMethodU2F,
Status: StatusInsufficientCredentials,
TFAMethods: []TFAMethod{TFAMethodU2F},
U2FSignRequest: &u2f.WebSignRequest{
AppID: "https://some-app-id",
Challenge: "u2fChallenge",
......@@ -83,9 +80,7 @@ func TestProtocol_SerializeResponse(t *testing.T) {
if err != nil {
t.Fatal("Decode():", err)
}
if !reflect.DeepEqual(resp, &resp2) {
d1, _ := json.MarshalIndent(resp, "", " ")
d2, _ := json.MarshalIndent(resp2, "", " ")
t.Errorf("decode results differ: %+v vs %+v", string(d1), string(d2))
if diffs := cmp.Diff(resp, &resp2); diffs != "" {
t.Errorf("decode results differ: %s", diffs)
}
}
......@@ -435,15 +435,18 @@ func (s *Server) authenticateUserWith2FA(user *backend.User, req *auth.Request)
resp := &auth.Response{
Status: auth.StatusInsufficientCredentials,
}
// Two-factor mechanisms are returned in order of
// decreasing preference, so start with U2F.
if req.U2FAppID != "" && user.HasU2F() {
resp.TFAMethod = auth.TFAMethodU2F
resp.TFAMethods = append(resp.TFAMethods, auth.TFAMethodU2F)
signReq, err := s.u2fSignRequest(user, req.U2FAppID)
if err != nil {
return nil, err
}
resp.U2FSignRequest = signReq
} else if user.HasOTP() {
resp.TFAMethod = auth.TFAMethodOTP
}
if user.HasOTP() {
resp.TFAMethods = append(resp.TFAMethods, auth.TFAMethodOTP)
}
return resp, nil
}
......
......@@ -171,8 +171,8 @@ func runAuthenticationTest(t *testing.T, client client.Client) {
if resp.Status != td.expectedStatus {
t.Errorf("authentication error: s=interactive u=%s p=%s, expected=%v got=%v", td.username, td.password, td.expectedStatus, resp.Status)
}
if resp.TFAMethod != td.expectedTFAMethod {
t.Errorf("mismatch in TFAMethod hint in authentication response: s=interactive u=%s p=%s, expected=%v got=%v", td.username, td.password, td.expectedTFAMethod, resp.TFAMethod)
if td.expectedTFAMethod != auth.TFAMethodNone && !resp.Has2FAMethod(td.expectedTFAMethod) {
t.Errorf("mismatch in TFAMethod hint in authentication response: s=interactive u=%s p=%s, expected=%v got=%v", td.username, td.password, td.expectedTFAMethod, resp.TFAMethods)
}
}
}
......
Copyright (c) 2017 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
// Copyright 2017, The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE.md file.
// Package cmp determines equality of values.
//
// This package is intended to be a more powerful and safer alternative to
// reflect.DeepEqual for comparing whether two values are semantically equal.
//
// The primary features of cmp are:
//
// • When the default behavior of equality does not suit the needs of the test,
// custom equality functions can override the equality operation.
// For example, an equality function may report floats as equal so long as they
// are within some tolerance of each other.
//
// • Types that have an Equal method may use that method to determine equality.
// This allows package authors to determine the equality operation for the types
// that they define.
//
// • If no custom equality functions are used and no Equal method is defined,
// equality is determined by recursively comparing the primitive kinds on both
// values, much like reflect.DeepEqual. Unlike reflect.DeepEqual, unexported
// fields are not compared by default; they result in panics unless suppressed
// by using an Ignore option (see cmpopts.IgnoreUnexported) or explicitly compared
// using the AllowUnexported option.
package cmp
import (
"fmt"
"reflect"
"strings"
"github.com/google/go-cmp/cmp/internal/diff"
"github.com/google/go-cmp/cmp/internal/function"
"github.com/google/go-cmp/cmp/internal/value"
)
// BUG(dsnet): Maps with keys containing NaN values cannot be properly compared due to
// the reflection package's inability to retrieve such entries. Equal will panic
// anytime it comes across a NaN key, but this behavior may change.
//
// See https://golang.org/issue/11104 for more details.
var nothing = reflect.Value{}
// Equal reports whether x and y are equal by recursively applying the
// following rules in the given order to x and y and all of their sub-values:
//
// • If two values are not of the same type, then they are never equal
// and the overall result is false.
//
// • Let S be the set of all Ignore, Transformer, and Comparer options that
// remain after applying all path filters, value filters, and type filters.
// If at least one Ignore exists in S, then the comparison is ignored.
// If the number of Transformer and Comparer options in S is greater than one,
// then Equal panics because it is ambiguous which option to use.
// If S contains a single Transformer, then use that to transform the current
// values and recursively call Equal on the output values.
// If S contains a single Comparer, then use that to compare the current values.
// Otherwise, evaluation proceeds to the next rule.
//
// • If the values have an Equal method of the form "(T) Equal(T) bool" or
// "(T) Equal(I) bool" where T is assignable to I, then use the result of
// x.Equal(y) even if x or y is nil.
// Otherwise, no such method exists and evaluation proceeds to the next rule.
//
// • Lastly, try to compare x and y based on their basic kinds.
// Simple kinds like booleans, integers, floats, complex numbers, strings, and
// channels are compared using the equivalent of the == operator in Go.
// Functions are only equal if they are both nil, otherwise they are unequal.
// Pointers are equal if the underlying values they point to are also equal.
// Interfaces are equal if their underlying concrete values are also equal.
//
// Structs are equal if all of their fields are equal. If a struct contains
// unexported fields, Equal panics unless the AllowUnexported option is used or
// an Ignore option (e.g., cmpopts.IgnoreUnexported) ignores that field.
//
// Arrays, slices, and maps are equal if they are both nil or both non-nil
// with the same length and the elements at each index or key are equal.
// Note that a non-nil empty slice and a nil slice are not equal.
// To equate empty slices and maps, consider using cmpopts.EquateEmpty.
// Map keys are equal according to the == operator.
// To use custom comparisons for map keys, consider using cmpopts.SortMaps.
func Equal(x, y interface{}, opts ...Option) bool {
s := newState(opts)
s.compareAny(reflect.ValueOf(x), reflect.ValueOf(y))
return s.result.Equal()
}
// Diff returns a human-readable report of the differences between two values.
// It returns an empty string if and only if Equal returns true for the same
// input values and options. The output string will use the "-" symbol to
// indicate elements removed from x, and the "+" symbol to indicate elements
// added to y.
//
// Do not depend on this output being stable.
func Diff(x, y interface{}, opts ...Option) string {
r := new(defaultReporter)
opts = Options{Options(opts), r}
eq := Equal(x, y, opts...)
d := r.String()
if (d == "") != eq {
panic("inconsistent difference and equality results")
}
return d
}
type state struct {
// These fields represent the "comparison state".
// Calling statelessCompare must not result in observable changes to these.
result diff.Result // The current result of comparison
curPath Path // The current path in the value tree
reporter reporter // Optional reporter used for difference formatting
// recChecker checks for infinite cycles applying the same set of
// transformers upon the output of itself.
recChecker recChecker
// dynChecker triggers pseudo-random checks for option correctness.
// It is safe for statelessCompare to mutate this value.
dynChecker dynChecker
// These fields, once set by processOption, will not change.
exporters map[reflect.Type]bool // Set of structs with unexported field visibility
opts Options // List of all fundamental and filter options
}
func newState(opts []Option) *state {
s := new(state)
for _, opt := range opts {
s.processOption(opt)
}
return s
}
func (s *state) processOption(opt Option) {
switch opt := opt.(type) {
case nil:
case Options:
for _, o := range opt {
s.processOption(o)
}
case coreOption:
type filtered interface {
isFiltered() bool
}
if fopt, ok := opt.(filtered); ok && !fopt.isFiltered() {
panic(fmt.Sprintf("cannot use an unfiltered option: %v", opt))
}
s.opts = append(s.opts, opt)
case visibleStructs:
if s.exporters == nil {
s.exporters = make(map[reflect.Type]bool)
}
for t := range opt {
s.exporters[t] = true
}
case reporter:
if s.reporter != nil {
panic("difference reporter already registered")
}
s.reporter = opt
default:
panic(fmt.Sprintf("unknown option %T", opt))
}
}
// statelessCompare compares two values and returns the result.
// This function is stateless in that it does not alter the current result,
// or output to any registered reporters.
func (s *state) statelessCompare(vx, vy reflect.Value) diff.Result {
// We do not save and restore the curPath because all of the compareX
// methods should properly push and pop from the path.
// It is an implementation bug if the contents of curPath differs from
// when calling this function to when returning from it.
oldResult, oldReporter := s.result, s.reporter
s.result = diff.Result{} // Reset result
s.reporter = nil // Remove reporter to avoid spurious printouts
s.compareAny(vx, vy)
res := s.result
s.result, s.reporter = oldResult, oldReporter
return res
}
func (s *state) compareAny(vx, vy reflect.Value) {
// TODO: Support cyclic data structures.
s.recChecker.Check(s.curPath)
// Rule 0: Differing types are never equal.
if !vx.IsValid() || !vy.IsValid() {
s.report(vx.IsValid() == vy.IsValid(), vx, vy)
return
}
if vx.Type() != vy.Type() {
s.report(false, vx, vy) // Possible for path to be empty
return
}
t := vx.Type()
if len(s.curPath) == 0 {
s.curPath.push(&pathStep{typ: t})
defer s.curPath.pop()
}
vx, vy = s.tryExporting(vx, vy)
// Rule 1: Check whether an option applies on this node in the value tree.
if s.tryOptions(vx, vy, t) {
return
}
// Rule 2: Check whether the type has a valid Equal method.
if s.tryMethod(vx, vy, t) {
return
}
// Rule 3: Recursively descend into each value's underlying kind.
switch t.Kind() {
case reflect.Bool:
s.report(vx.Bool() == vy.Bool(), vx, vy)
return
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
s.report(vx.Int() == vy.Int(), vx, vy)
return
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
s.report(vx.Uint() == vy.Uint(), vx, vy)
return
case reflect.Float32, reflect.Float64:
s.report(vx.Float() == vy.Float(), vx, vy)
return
case reflect.Complex64, reflect.Complex128:
s.report(vx.Complex() == vy.Complex(), vx, vy)
return
case reflect.String:
s.report(vx.String() == vy.String(), vx, vy)
return
case reflect.Chan, reflect.UnsafePointer:
s.report(vx.Pointer() == vy.Pointer(), vx, vy)
return
case reflect.Func:
s.report(vx.IsNil() && vy.IsNil(), vx, vy)
return
case reflect.Ptr:
if vx.IsNil() || vy.IsNil() {
s.report(vx.IsNil() && vy.IsNil(), vx, vy)
return
}
s.curPath.push(&indirect{pathStep{t.Elem()}})
defer s.curPath.pop()
s.compareAny(vx.Elem(), vy.Elem())
return
case reflect.Interface:
if vx.IsNil() || vy.IsNil() {
s.report(vx.IsNil() && vy.IsNil(), vx, vy)
return
}
if vx.Elem().Type() != vy.Elem().Type() {
s.report(false, vx.Elem(), vy.Elem())
return
}
s.curPath.push(&typeAssertion{pathStep{vx.Elem().Type()}})
defer s.curPath.pop()
s.compareAny(vx.Elem(), vy.Elem())
return
case reflect.Slice:
if vx.IsNil() || vy.IsNil() {
s.report(vx.IsNil() && vy.IsNil(), vx, vy)
return
}
fallthrough
case reflect.Array:
s.compareArray(vx, vy, t)
return
case reflect.Map:
s.compareMap(vx, vy, t)
return
case reflect.Struct:
s.compareStruct(vx, vy, t)
return
default:
panic(fmt.Sprintf("%v kind not handled", t.Kind()))
}
}
func (s *state) tryExporting(vx, vy reflect.Value) (reflect.Value, reflect.Value) {
if sf, ok := s.curPath[len(s.curPath)-1].(*structField); ok && sf.unexported {
if sf.force {
// Use unsafe pointer arithmetic to get read-write access to an
// unexported field in the struct.
vx = unsafeRetrieveField(sf.pvx, sf.field)
vy = unsafeRetrieveField(sf.pvy, sf.field)
} else {
// We are not allowed to export the value, so invalidate them
// so that tryOptions can panic later if not explicitly ignored.
vx = nothing
vy = nothing
}
}
return vx, vy
}
func (s *state) tryOptions(vx, vy reflect.Value, t reflect.Type) bool {
// If there were no FilterValues, we will not detect invalid inputs,
// so manually check for them and append invalid if necessary.
// We still evaluate the options since an ignore can override invalid.
opts := s.opts
if !vx.IsValid() || !vy.IsValid() {
opts = Options{opts, invalid{}}
}
// Evaluate all filters and apply the remaining options.
if opt := opts.filter(s, vx, vy, t); opt != nil {
opt.apply(s, vx, vy)
return true
}
return false
}
func (s *state) tryMethod(vx, vy reflect.Value, t reflect.Type) bool {
// Check if this type even has an Equal method.
m, ok := t.MethodByName("Equal")
if !ok || !function.IsType(m.Type, function.EqualAssignable) {
return false
}
eq := s.callTTBFunc(m.Func, vx, vy)
s.report(eq, vx, vy)
return true
}
func (s *state) callTRFunc(f, v reflect.Value) reflect.Value {
v = sanitizeValue(v, f.Type().In(0))
if !s.dynChecker.Next() {
return f.Call([]reflect.Value{v})[0]
}
// Run the function twice and ensure that we get the same results back.
// We run in goroutines so that the race detector (if enabled) can detect
// unsafe mutations to the input.
c := make(chan reflect.Value)
go detectRaces(c, f, v)
want := f.Call([]reflect.Value{v})[0]
if got := <-c; !s.statelessCompare(got, want).Equal() {
// To avoid false-positives with non-reflexive equality operations,
// we sanity check whether a value is equal to itself.
if !s.statelessCompare(want, want).Equal() {
return want
}
fn := getFuncName(f.Pointer())
panic(fmt.Sprintf("non-deterministic function detected: %s", fn))
}
return want
}
func (s *state) callTTBFunc(f, x, y reflect.Value) bool {
x = sanitizeValue(x, f.Type().In(0))
y = sanitizeValue(y, f.Type().In(1))
if !s.dynChecker.Next() {
return f.Call([]reflect.Value{x, y})[0].Bool()
}
// Swapping the input arguments is sufficient to check that
// f is symmetric and deterministic.
// We run in goroutines so that the race detector (if enabled) can detect
// unsafe mutations to the input.
c := make(chan reflect.Value)
go detectRaces(c, f, y, x)
want := f.Call([]reflect.Value{x, y})[0].Bool()
if got := <-c; !got.IsValid() || got.Bool() != want {
fn := getFuncName(f.Pointer())
panic(fmt.Sprintf("non-deterministic or non-symmetric function detected: %s", fn))
}
return want
}
func detectRaces(c chan<- reflect.Value, f reflect.Value, vs ...reflect.Value) {
var ret reflect.Value
defer func() {
recover() // Ignore panics, let the other call to f panic instead
c <- ret
}()
ret = f.Call(vs)[0]
}
// sanitizeValue converts nil interfaces of type T to those of type R,