iris/middleware/basicauth/user_auth.go

257 lines
5.7 KiB
Go
Raw Normal View History

package basicauth
import (
"encoding/json"
"fmt"
"io/ioutil"
"reflect"
"strings"
"github.com/kataras/iris/v12/context"
"golang.org/x/crypto/bcrypt"
"gopkg.in/yaml.v3"
)
type UserAuthOptions struct {
// Defaults to plain check, can be modified for encrypted passwords, see `BCRYPT`.
ComparePassword func(stored, userPassword string) bool
}
type UserAuthOption func(*UserAuthOptions)
func BCRYPT(opts *UserAuthOptions) {
opts.ComparePassword = func(stored, userPassword string) bool {
err := bcrypt.CompareHashAndPassword([]byte(stored), []byte(userPassword))
return err == nil
}
}
func toUserAuthOptions(opts []UserAuthOption) (options UserAuthOptions) {
for _, opt := range opts {
opt(&options)
}
if options.ComparePassword == nil {
options.ComparePassword = func(stored, userPassword string) bool {
return stored == userPassword
}
}
return options
}
type User interface {
context.UserGetUsername
context.UserGetPassword
}
// Users
// - map[string]string form of: {username:password, ...} form.
// - map[string]interface{} form of: []{"username": "...", "password": "...", "other_field": ...}, ...}.
// - []T which T completes the User interface.
// - []T which T contains at least Username and Password fields.
func AllowUsers(users interface{}, opts ...UserAuthOption) AuthFunc {
// create a local user structure to be used in the map copy,
// takes longer to initialize but faster to serve.
type user struct {
password string
ref interface{}
}
cp := make(map[string]*user)
v := reflect.Indirect(reflect.ValueOf(users))
switch v.Kind() {
case reflect.Slice:
for i := 0; i < v.Len(); i++ {
elem := v.Index(i).Interface()
// MUST contain a username and password.
username, password, ok := extractUsernameAndPassword(elem)
if !ok {
continue
}
cp[username] = &user{
password: password,
ref: elem,
}
}
case reflect.Map:
elem := v.Interface()
switch m := elem.(type) {
case map[string]string:
return userMap(m, opts...)
case map[string]interface{}:
username, password, ok := mapUsernameAndPassword(m)
if !ok {
break
}
cp[username] = &user{
password: password,
ref: m,
}
default:
panic(fmt.Sprintf("unsupported type of map: %T", users))
}
default:
panic(fmt.Sprintf("unsupported type: %T", users))
}
options := toUserAuthOptions(opts)
return func(_ *context.Context, username, password string) (interface{}, bool) {
if u, ok := cp[username]; ok { // fast map access,
if options.ComparePassword(u.password, password) {
return u.ref, true
}
}
return nil, false
}
}
func userMap(usernamePassword map[string]string, opts ...UserAuthOption) AuthFunc {
options := toUserAuthOptions(opts)
return func(_ *context.Context, username, password string) (interface{}, bool) {
pass, ok := usernamePassword[username]
return nil, ok && options.ComparePassword(pass, password)
}
}
func AllowUsersFile(jsonOrYamlFilename string, opts ...UserAuthOption) AuthFunc {
var (
usernamePassword map[string]string
// no need to support too much forms, this would be for:
// "$username": { "password": "$pass", "other_field": ...}
// users map[string]map[string]interface{}
userList []map[string]interface{}
)
if err := decodeFile(jsonOrYamlFilename, &usernamePassword, &userList); err != nil {
panic(err)
}
if len(usernamePassword) > 0 {
// JSON Form: { "$username":"$pass", "$username": "$pass" }
// YAML Form: $username: $pass
// $username: $pass
return userMap(usernamePassword, opts...)
}
if len(userList) > 0 {
// JSON Form: [{"username": "$username", "password": "$pass", "other_field": ...}, {"username": ...}, ... ]
// YAML Form:
// - username: $username
// password: $password
// other_field: ...
return AllowUsers(userList, opts...)
}
panic("malformed document file: " + jsonOrYamlFilename)
}
func decodeFile(src string, dest ...interface{}) error {
data, err := ioutil.ReadFile(src)
if err != nil {
return err
}
// We use unmarshal instead of file decoder
// as we may need to read it more than once (dests, see below).
var (
unmarshal func(data []byte, v interface{}) error
ext string
)
if idx := strings.LastIndexByte(src, '.'); idx > 0 {
ext = src[idx:]
}
switch ext {
case "", ".json":
unmarshal = json.Unmarshal
case ".yml", ".yaml":
unmarshal = yaml.Unmarshal
default:
return fmt.Errorf("unexpected file extension: %s", ext)
}
var (
ok bool
lastErr error
)
for _, d := range dest {
if err = unmarshal(data, d); err == nil {
ok = true
} else {
lastErr = err
}
}
if !ok {
return lastErr
}
return nil // if at least one is succeed we are ok.
}
func extractUsernameAndPassword(s interface{}) (username, password string, ok bool) {
if s == nil {
return
}
switch u := s.(type) {
case User:
username = u.GetUsername()
password = u.GetPassword()
ok = username != "" && password != ""
return
case map[string]interface{}:
return mapUsernameAndPassword(u)
default:
b, err := json.Marshal(u)
if err != nil {
return
}
var m map[string]interface{}
if err = json.Unmarshal(b, &m); err != nil {
return
}
return mapUsernameAndPassword(m)
}
}
func mapUsernameAndPassword(m map[string]interface{}) (username, password string, ok bool) {
// type of username: password.
if len(m) == 1 {
for username, v := range m {
if password, ok := v.(string); ok {
ok := username != "" && password != ""
return username, password, ok
}
}
}
var usernameFound, passwordFound bool
for k, v := range m {
switch k {
case "username", "Username":
username, usernameFound = v.(string)
case "password", "Password":
password, passwordFound = v.(string)
}
if usernameFound && passwordFound {
ok = true
break
}
}
return
}