[feat] Add header authentication support
In some cases there are needs to authenticate user not in Taskcafe itself. For this reason option server.remote_user_header was added. ```toml [server] remote_user_header = "X-Remote-User" ``` With turned on Taskcafe listens X-Remote-User http header and skip password checking. But still check user existence and activity flag.
This commit is contained in:
parent
d725e42adf
commit
c12a745929
@ -21,4 +21,4 @@ windows:
|
||||
- database:
|
||||
root: ./
|
||||
panes:
|
||||
- pgcli postgres://taskcafe:taskcafe_test@localhost:8855/taskcafe
|
||||
- pgcli postgres://taskcafe:taskcafe_test@localhost:8865/taskcafe
|
||||
|
@ -1,5 +1,6 @@
|
||||
[server]
|
||||
hostname = '0.0.0.0:3333'
|
||||
remote_user_header = ""
|
||||
|
||||
[email_notifications]
|
||||
enabled = true
|
||||
|
9
docs/remote-auth.md
Normal file
9
docs/remote-auth.md
Normal file
@ -0,0 +1,9 @@
|
||||
# Remote authorize
|
||||
If you need to authenticate user with some proxy, you should use
|
||||
```toml
|
||||
[server]
|
||||
remote_user_header = "X-Remote-User"
|
||||
```
|
||||
With this option Taskcafe will take username from
|
||||
`X-Remote-User` HTTP header and will not check its password.
|
||||
You can use any header you want.
|
@ -81,6 +81,7 @@ func Execute() {
|
||||
viper.SetDefault("database.password", "taskcafe_test")
|
||||
viper.SetDefault("database.port", "5432")
|
||||
viper.SetDefault("security.token_expiration", "15m")
|
||||
viper.SetDefault("server.remote_user_header", "")
|
||||
|
||||
viper.SetDefault("queue.broker", "amqp://guest:guest@localhost:5672/")
|
||||
viper.SetDefault("queue.store", "memcache://localhost:11211")
|
||||
|
@ -75,15 +75,24 @@ func newWebCmd() *cobra.Command {
|
||||
log.Warn("server.secret is not set, generating a random secret")
|
||||
secret = uuid.New().String()
|
||||
}
|
||||
|
||||
security, err := utils.GetSecurityConfig(viper.GetString("security.token_expiration"), []byte(secret))
|
||||
r, _ := route.NewRouter(db, utils.EmailConfig{
|
||||
From: viper.GetString("smtp.from"),
|
||||
Host: viper.GetString("smtp.host"),
|
||||
Port: viper.GetInt("smtp.port"),
|
||||
Username: viper.GetString("smtp.username"),
|
||||
Password: viper.GetString("smtp.password"),
|
||||
InsecureSkipVerify: viper.GetBool("smtp.skip_verify"),
|
||||
}, security)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
security.UserAuthHeader = viper.GetString("server.remote_user_header")
|
||||
|
||||
r, _ := route.NewRouter(db, route.Config{
|
||||
Email: utils.EmailConfig{
|
||||
From: viper.GetString("smtp.from"),
|
||||
Host: viper.GetString("smtp.host"),
|
||||
Port: viper.GetInt("smtp.port"),
|
||||
Username: viper.GetString("smtp.username"),
|
||||
Password: viper.GetString("smtp.password"),
|
||||
InsecureSkipVerify: viper.GetBool("smtp.skip_verify"),
|
||||
},
|
||||
Security: security,
|
||||
})
|
||||
log.WithFields(log.Fields{"url": viper.GetString("server.hostname")}).Info("starting server")
|
||||
return http.ListenAndServe(viper.GetString("server.hostname"), r)
|
||||
},
|
||||
|
@ -102,6 +102,15 @@ func (h *TaskcafeHandler) LogoutHandler(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
// LoginHandler creates a new refresh & access token for the user if given the correct credentials
|
||||
func (h *TaskcafeHandler) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if h.SecurityConfig.IsRemoteAuth() {
|
||||
h.headerAuthenticate(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
h.credentialsHandler(w, r)
|
||||
}
|
||||
|
||||
func (h *TaskcafeHandler) credentialsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var requestData LoginRequestData
|
||||
err := json.NewDecoder(r.Body).Decode(&requestData)
|
||||
if err != nil {
|
||||
@ -139,9 +148,47 @@ func (h *TaskcafeHandler) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
authCreatedAt := time.Now().UTC()
|
||||
authExpiresAt := authCreatedAt.AddDate(0, 0, 1)
|
||||
authToken, err := h.repo.CreateAuthToken(r.Context(), db.CreateAuthTokenParams{user.UserID, authCreatedAt, authExpiresAt})
|
||||
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
// TODO: should we return here?
|
||||
}
|
||||
|
||||
w.Header().Set("Content-type", "application/json")
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "authToken",
|
||||
Value: authToken.TokenID.String(),
|
||||
Expires: authExpiresAt,
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
})
|
||||
json.NewEncoder(w).Encode(LoginResponseData{Complete: true, UserID: authToken.UserID.String()})
|
||||
}
|
||||
|
||||
func (h *TaskcafeHandler) headerAuthenticate(w http.ResponseWriter, r *http.Request) {
|
||||
xRemoteUser := r.Header.Get(h.SecurityConfig.UserAuthHeader)
|
||||
user, err := h.repo.GetUserAccountByUsername(r.Context(), xRemoteUser)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"username": xRemoteUser,
|
||||
}).Warn("user account not found")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
if !user.Active {
|
||||
log.WithFields(log.Fields{
|
||||
"username": user.Username,
|
||||
}).Warn("attempt to login with inactive user")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
authCreatedAt := time.Now().UTC()
|
||||
authExpiresAt := authCreatedAt.AddDate(0, 0, 1)
|
||||
authToken, err := h.repo.CreateAuthToken(r.Context(), db.CreateAuthTokenParams{user.UserID, authCreatedAt, authExpiresAt})
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-type", "application/json")
|
||||
|
@ -65,8 +65,13 @@ type TaskcafeHandler struct {
|
||||
SecurityConfig utils.SecurityConfig
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Email utils.EmailConfig
|
||||
Security utils.SecurityConfig
|
||||
}
|
||||
|
||||
// NewRouter creates a new router for chi
|
||||
func NewRouter(dbConnection *sqlx.DB, emailConfig utils.EmailConfig, securityConfig utils.SecurityConfig) (chi.Router, error) {
|
||||
func NewRouter(dbConnection *sqlx.DB, cfg Config) (chi.Router, error) {
|
||||
formatter := new(log.TextFormatter)
|
||||
formatter.TimestampFormat = "02-01-2006 15:04:05"
|
||||
formatter.FullTimestamp = true
|
||||
@ -93,7 +98,7 @@ func NewRouter(dbConnection *sqlx.DB, emailConfig utils.EmailConfig, securityCon
|
||||
}))
|
||||
|
||||
repository := db.NewRepository(dbConnection)
|
||||
taskcafeHandler := TaskcafeHandler{*repository, securityConfig}
|
||||
taskcafeHandler := TaskcafeHandler{*repository, cfg.Security}
|
||||
|
||||
var imgServer = http.FileServer(http.Dir("./uploads/"))
|
||||
r.Group(func(mux chi.Router) {
|
||||
@ -108,7 +113,7 @@ func NewRouter(dbConnection *sqlx.DB, emailConfig utils.EmailConfig, securityCon
|
||||
r.Group(func(mux chi.Router) {
|
||||
mux.Use(auth.Middleware)
|
||||
mux.Post("/users/me/avatar", taskcafeHandler.ProfileImageUpload)
|
||||
mux.Handle("/graphql", graph.NewHandler(*repository, emailConfig))
|
||||
mux.Handle("/graphql", graph.NewHandler(*repository, cfg.Email))
|
||||
})
|
||||
|
||||
frontend := FrontendHandler{staticPath: "build", indexPath: "index.html"}
|
||||
|
@ -9,6 +9,11 @@ import (
|
||||
type SecurityConfig struct {
|
||||
AccessTokenExpiration time.Duration
|
||||
Secret []byte
|
||||
UserAuthHeader string
|
||||
}
|
||||
|
||||
func (c SecurityConfig) IsRemoteAuth() bool {
|
||||
return c.UserAuthHeader != ""
|
||||
}
|
||||
|
||||
func GetSecurityConfig(accessTokenExp string, secret []byte) (SecurityConfig, error) {
|
||||
|
Loading…
Reference in New Issue
Block a user