mirror of
https://github.com/kataras/iris.git
synced 2025-01-23 18:51:03 +01:00
305 lines
7.5 KiB
Go
305 lines
7.5 KiB
Go
package basicauth
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"reflect"
|
|
"strings"
|
|
|
|
"github.com/kataras/iris/v12/context"
|
|
|
|
"golang.org/x/crypto/bcrypt"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// ReadFile can be used to customize the way the
|
|
// AllowUsersFile function is loading the filename from.
|
|
// Example of usage: embedded users.yml file.
|
|
// Defaults to the `os.ReadFile` which reads the file from the physical disk.
|
|
var ReadFile = os.ReadFile
|
|
|
|
// User is a partial part of the iris.User interface.
|
|
// It's used to declare a static slice of registered User for authentication.
|
|
type User interface {
|
|
context.UserGetUsername
|
|
context.UserGetPassword
|
|
}
|
|
|
|
// UserAuthOptions holds optional user authentication options
|
|
// that can be given to the builtin Default and Load (and AllowUsers, AllowUsersFile) functions.
|
|
type UserAuthOptions struct {
|
|
// Defaults to plain check, can be modified for encrypted passwords,
|
|
// see the BCRYPT optional function.
|
|
ComparePassword func(stored, userPassword string) bool
|
|
}
|
|
|
|
// UserAuthOption is the option function type
|
|
// for the Default and Load (and AllowUsers, AllowUsersFile) functions.
|
|
//
|
|
// See BCRYPT for an implementation.
|
|
type UserAuthOption func(*UserAuthOptions)
|
|
|
|
// BCRYPT it is a UserAuthOption, it compares a bcrypt hashed password with its user input.
|
|
// Reports true on success and false on failure.
|
|
//
|
|
// Useful when the users passwords are encrypted
|
|
// using the Provos and Mazières's bcrypt adaptive hashing algorithm.
|
|
// See https://www.usenix.org/legacy/event/usenix99/provos/provos.pdf.
|
|
//
|
|
// Usage:
|
|
//
|
|
// Default(..., BCRYPT) OR
|
|
// Load(..., BCRYPT) OR
|
|
// Options.Allow = AllowUsers(..., BCRYPT) OR
|
|
// OPtions.Allow = AllowUsersFile(..., BCRYPT)
|
|
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
|
|
}
|
|
|
|
// AllowUsers is an AuthFunc which authenticates user input based on a (static) user list.
|
|
// The "users" input parameter can be one of the following forms:
|
|
//
|
|
// map[string]string e.g. {username: password, username: password...}.
|
|
// []map[string]interface{} e.g. []{"username": "...", "password": "...", "other_field": ...}, ...}.
|
|
// []T which T completes the User interface.
|
|
// []T which T contains at least Username and Password fields.
|
|
//
|
|
// Usage:
|
|
// New(Options{Allow: AllowUsers(..., [BCRYPT])})
|
|
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)
|
|
}
|
|
}
|
|
|
|
// AllowUsersFile is an AuthFunc which authenticates user input based on a (static) user list
|
|
// loaded from a file on initialization.
|
|
//
|
|
// Example Code:
|
|
//
|
|
// New(Options{Allow: AllowUsersFile("users.yml", BCRYPT)})
|
|
//
|
|
// The users.yml file looks like the following:
|
|
// - username: kataras
|
|
// password: kataras_pass
|
|
// age: 27
|
|
// role: admin
|
|
// - username: makis
|
|
// password: makis_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": ...}
|
|
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 := 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
|
|
}
|