forked from go-goyave/goyave
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbasic.go
More file actions
141 lines (118 loc) Β· 4.55 KB
/
Copy pathbasic.go
File metadata and controls
141 lines (118 loc) Β· 4.55 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
package auth
import (
"crypto/subtle"
"errors"
"fmt"
"reflect"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
"goyave.dev/goyave/v5"
"goyave.dev/goyave/v5/config"
errorutil "goyave.dev/goyave/v5/util/errors"
)
// BasicAuthenticator implementation of Authenticator with the Basic
// authentication method.
//
// The T parameter represents the user DTO and should not be a pointer. The DTO used should be
// different from the DTO returned to clients as a response because it needs to contain the user's password.
type BasicAuthenticator[T any] struct {
goyave.Component
UserService UserService[T]
// PasswordField the name of T's struct field that holds the user's hashed password.
// It will be used to compare the password hash with the user input.
PasswordField string
// Optional defines if the authenticator allows requests that
// don't provide credentials. Handlers should therefore check
// if `request.User` is not `nil` before accessing it.
Optional bool
}
// NewBasicAuthenticator create a new authenticator for the Basic authentication flow.
//
// The T parameter represents the user DTO and should not be a pointer. The DTO used should be
// different from the DTO returned to clients as a response because it needs to contain the user's password.
//
// The `passwordField` corresponds to the name of T's struct field that holds the user's hashed password.
// It will be used to compare the password hash with the user input.
func NewBasicAuthenticator[T any](userService UserService[T], passwordField string) *BasicAuthenticator[T] {
return &BasicAuthenticator[T]{
UserService: userService,
PasswordField: passwordField,
}
}
// Authenticate fetch the user corresponding to the credentials
// found in the given request and returns it.
// If no user can be authenticated, returns an error.
// The password is checked using bcrypt.
func (a *BasicAuthenticator[T]) Authenticate(request *goyave.Request) (*T, error) {
username, password, ok := request.BasicAuth()
if !ok {
if a.Optional {
return nil, nil
}
return nil, fmt.Errorf("%s", request.Lang.Get("auth.no-credentials-provided"))
}
user, err := a.UserService.FindByUsername(request.Context(), username)
notFound := errors.Is(err, gorm.ErrRecordNotFound)
if err != nil && !notFound {
panic(errorutil.New(err))
}
t := reflect.Indirect(reflect.ValueOf(user))
for t.Kind() == reflect.Ptr {
t = t.Elem()
}
pass := t.FieldByName(a.PasswordField)
if pass.Kind() == reflect.Invalid {
panic(errorutil.Errorf("could not find valid field/column %q in type %T", a.PasswordField, user))
}
if notFound || bcrypt.CompareHashAndPassword([]byte(pass.String()), []byte(password)) != nil {
return nil, fmt.Errorf("%s", request.Lang.Get("auth.invalid-credentials"))
}
return user, nil
}
//--------------------------------------------
func init() {
config.Register("auth.basic.username", config.Entry{
Value: nil,
Type: reflect.String,
IsSlice: false,
AuthorizedValues: []any{},
})
config.Register("auth.basic.password", config.Entry{
Value: nil,
Type: reflect.String,
IsSlice: false,
AuthorizedValues: []any{},
})
}
// BasicUser a simple user for config-based basic authentication.
type BasicUser struct {
Name string
}
// ConfigBasicAuthenticator implementation of Authenticator with the Basic
// authentication method, using username and password from the configuration.
type ConfigBasicAuthenticator struct {
goyave.Component
}
// Authenticate check if the request basic auth header matches the
// "auth.basic.username" and "auth.basic.password" config entries.
func (a *ConfigBasicAuthenticator) Authenticate(request *goyave.Request) (*BasicUser, error) {
username, password, ok := request.BasicAuth()
if !ok {
return nil, fmt.Errorf("%s", request.Lang.Get("auth.no-credentials-provided"))
}
if subtle.ConstantTimeCompare([]byte(a.Config().GetString("auth.basic.username")), []byte(username)) != 1 ||
subtle.ConstantTimeCompare([]byte(a.Config().GetString("auth.basic.password")), []byte(password)) != 1 {
return nil, fmt.Errorf("%s", request.Lang.Get("auth.invalid-credentials"))
}
return &BasicUser{
Name: username,
}, nil
}
// ConfigBasicAuth create a new authenticator middleware for
// config-based Basic authentication. On auth success, the request
// user is set to a `*BasicUser`.
// The user is authenticated if the "auth.basic.username" and "auth.basic.password" config entries
// match the request's Authorization header.
func ConfigBasicAuth() *Handler[BasicUser] {
return Middleware(&ConfigBasicAuthenticator{})
}