[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:
|
- database:
|
||||||
root: ./
|
root: ./
|
||||||
panes:
|
panes:
|
||||||
- pgcli postgres://taskcafe:taskcafe_test@localhost:8855/taskcafe
|
- pgcli postgres://taskcafe:taskcafe_test@localhost:8865/taskcafe
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
[server]
|
[server]
|
||||||
hostname = '0.0.0.0:3333'
|
hostname = '0.0.0.0:3333'
|
||||||
|
remote_user_header = ""
|
||||||
|
|
||||||
[email_notifications]
|
[email_notifications]
|
||||||
enabled = true
|
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.password", "taskcafe_test")
|
||||||
viper.SetDefault("database.port", "5432")
|
viper.SetDefault("database.port", "5432")
|
||||||
viper.SetDefault("security.token_expiration", "15m")
|
viper.SetDefault("security.token_expiration", "15m")
|
||||||
|
viper.SetDefault("server.remote_user_header", "")
|
||||||
|
|
||||||
viper.SetDefault("queue.broker", "amqp://guest:guest@localhost:5672/")
|
viper.SetDefault("queue.broker", "amqp://guest:guest@localhost:5672/")
|
||||||
viper.SetDefault("queue.store", "memcache://localhost:11211")
|
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")
|
log.Warn("server.secret is not set, generating a random secret")
|
||||||
secret = uuid.New().String()
|
secret = uuid.New().String()
|
||||||
}
|
}
|
||||||
|
|
||||||
security, err := utils.GetSecurityConfig(viper.GetString("security.token_expiration"), []byte(secret))
|
security, err := utils.GetSecurityConfig(viper.GetString("security.token_expiration"), []byte(secret))
|
||||||
r, _ := route.NewRouter(db, utils.EmailConfig{
|
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"),
|
From: viper.GetString("smtp.from"),
|
||||||
Host: viper.GetString("smtp.host"),
|
Host: viper.GetString("smtp.host"),
|
||||||
Port: viper.GetInt("smtp.port"),
|
Port: viper.GetInt("smtp.port"),
|
||||||
Username: viper.GetString("smtp.username"),
|
Username: viper.GetString("smtp.username"),
|
||||||
Password: viper.GetString("smtp.password"),
|
Password: viper.GetString("smtp.password"),
|
||||||
InsecureSkipVerify: viper.GetBool("smtp.skip_verify"),
|
InsecureSkipVerify: viper.GetBool("smtp.skip_verify"),
|
||||||
}, security)
|
},
|
||||||
|
Security: security,
|
||||||
|
})
|
||||||
log.WithFields(log.Fields{"url": viper.GetString("server.hostname")}).Info("starting server")
|
log.WithFields(log.Fields{"url": viper.GetString("server.hostname")}).Info("starting server")
|
||||||
return http.ListenAndServe(viper.GetString("server.hostname"), r)
|
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
|
// 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) {
|
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
|
var requestData LoginRequestData
|
||||||
err := json.NewDecoder(r.Body).Decode(&requestData)
|
err := json.NewDecoder(r.Body).Decode(&requestData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -139,9 +148,47 @@ func (h *TaskcafeHandler) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
authCreatedAt := time.Now().UTC()
|
authCreatedAt := time.Now().UTC()
|
||||||
authExpiresAt := authCreatedAt.AddDate(0, 0, 1)
|
authExpiresAt := authCreatedAt.AddDate(0, 0, 1)
|
||||||
authToken, err := h.repo.CreateAuthToken(r.Context(), db.CreateAuthTokenParams{user.UserID, authCreatedAt, authExpiresAt})
|
authToken, err := h.repo.CreateAuthToken(r.Context(), db.CreateAuthTokenParams{user.UserID, authCreatedAt, authExpiresAt})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
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")
|
w.Header().Set("Content-type", "application/json")
|
||||||
|
@ -65,8 +65,13 @@ type TaskcafeHandler struct {
|
|||||||
SecurityConfig utils.SecurityConfig
|
SecurityConfig utils.SecurityConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Email utils.EmailConfig
|
||||||
|
Security utils.SecurityConfig
|
||||||
|
}
|
||||||
|
|
||||||
// NewRouter creates a new router for chi
|
// 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 := new(log.TextFormatter)
|
||||||
formatter.TimestampFormat = "02-01-2006 15:04:05"
|
formatter.TimestampFormat = "02-01-2006 15:04:05"
|
||||||
formatter.FullTimestamp = true
|
formatter.FullTimestamp = true
|
||||||
@ -93,7 +98,7 @@ func NewRouter(dbConnection *sqlx.DB, emailConfig utils.EmailConfig, securityCon
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
repository := db.NewRepository(dbConnection)
|
repository := db.NewRepository(dbConnection)
|
||||||
taskcafeHandler := TaskcafeHandler{*repository, securityConfig}
|
taskcafeHandler := TaskcafeHandler{*repository, cfg.Security}
|
||||||
|
|
||||||
var imgServer = http.FileServer(http.Dir("./uploads/"))
|
var imgServer = http.FileServer(http.Dir("./uploads/"))
|
||||||
r.Group(func(mux chi.Router) {
|
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) {
|
r.Group(func(mux chi.Router) {
|
||||||
mux.Use(auth.Middleware)
|
mux.Use(auth.Middleware)
|
||||||
mux.Post("/users/me/avatar", taskcafeHandler.ProfileImageUpload)
|
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"}
|
frontend := FrontendHandler{staticPath: "build", indexPath: "index.html"}
|
||||||
|
@ -9,6 +9,11 @@ import (
|
|||||||
type SecurityConfig struct {
|
type SecurityConfig struct {
|
||||||
AccessTokenExpiration time.Duration
|
AccessTokenExpiration time.Duration
|
||||||
Secret []byte
|
Secret []byte
|
||||||
|
UserAuthHeader string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c SecurityConfig) IsRemoteAuth() bool {
|
||||||
|
return c.UserAuthHeader != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetSecurityConfig(accessTokenExp string, secret []byte) (SecurityConfig, error) {
|
func GetSecurityConfig(accessTokenExp string, secret []byte) (SecurityConfig, error) {
|
||||||
|
Loading…
Reference in New Issue
Block a user