Skip to content

Commit 481630b

Browse files
author
Maxim Kernozhitskiy
committed
[feat] Add X-Remote-User header
In some cases there are needs to Authorize user not in Taskcafe itself. For this reason option server.remote_user_header was added. ```toml [server] remote_user_header = true ``` With turned on Taskcafe listens X-Remote-User http header and skip password checking. But still check user existence and active flag.
1 parent d725e42 commit 481630b

File tree

8 files changed

+84
-12
lines changed

8 files changed

+84
-12
lines changed

.tmuxinator.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,4 @@ windows:
2121
- database:
2222
root: ./
2323
panes:
24-
- pgcli postgres://taskcafe:taskcafe_test@localhost:8855/taskcafe
24+
- pgcli postgres://taskcafe:taskcafe_test@localhost:8865/taskcafe

conf/taskcafe.example.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
[server]
22
hostname = '0.0.0.0:3333'
3+
remote_user_header = false
34

45
[email_notifications]
56
enabled = true

docs/remote-auth.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Remote authorize
2+
If you need to authenticate user with some proxy, you should use
3+
```toml
4+
[server]
5+
remote_user_header = true
6+
```
7+
With this option Taskcafe will take username from
8+
`X-Remote-User` HTTP header and will not check its password.

internal/commands/commands.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ func Execute() {
8181
viper.SetDefault("database.password", "taskcafe_test")
8282
viper.SetDefault("database.port", "5432")
8383
viper.SetDefault("security.token_expiration", "15m")
84+
viper.SetDefault("server.remote_user_header", "false")
8485

8586
viper.SetDefault("queue.broker", "amqp://guest:guest@localhost:5672/")
8687
viper.SetDefault("queue.store", "memcache://localhost:11211")

internal/commands/web.go

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -75,15 +75,24 @@ func newWebCmd() *cobra.Command {
7575
log.Warn("server.secret is not set, generating a random secret")
7676
secret = uuid.New().String()
7777
}
78+
7879
security, err := utils.GetSecurityConfig(viper.GetString("security.token_expiration"), []byte(secret))
79-
r, _ := route.NewRouter(db, utils.EmailConfig{
80-
From: viper.GetString("smtp.from"),
81-
Host: viper.GetString("smtp.host"),
82-
Port: viper.GetInt("smtp.port"),
83-
Username: viper.GetString("smtp.username"),
84-
Password: viper.GetString("smtp.password"),
85-
InsecureSkipVerify: viper.GetBool("smtp.skip_verify"),
86-
}, security)
80+
if err != nil {
81+
log.Error(err)
82+
}
83+
security.IsExternalAuth = viper.GetBool("server.remote_user_header")
84+
85+
r, _ := route.NewRouter(db, route.Config{
86+
Email: utils.EmailConfig{
87+
From: viper.GetString("smtp.from"),
88+
Host: viper.GetString("smtp.host"),
89+
Port: viper.GetInt("smtp.port"),
90+
Username: viper.GetString("smtp.username"),
91+
Password: viper.GetString("smtp.password"),
92+
InsecureSkipVerify: viper.GetBool("smtp.skip_verify"),
93+
},
94+
Security: security,
95+
})
8796
log.WithFields(log.Fields{"url": viper.GetString("server.hostname")}).Info("starting server")
8897
return http.ListenAndServe(viper.GetString("server.hostname"), r)
8998
},

internal/route/auth.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,15 @@ func (h *TaskcafeHandler) LogoutHandler(w http.ResponseWriter, r *http.Request)
102102

103103
// LoginHandler creates a new refresh & access token for the user if given the correct credentials
104104
func (h *TaskcafeHandler) LoginHandler(w http.ResponseWriter, r *http.Request) {
105+
if h.SecurityConfig.IsExternalAuth {
106+
h.xHeaderAuthenticate(w, r)
107+
return
108+
}
109+
110+
h.credentialsHandler(w, r)
111+
}
112+
113+
func (h *TaskcafeHandler) credentialsHandler(w http.ResponseWriter, r *http.Request) {
105114
var requestData LoginRequestData
106115
err := json.NewDecoder(r.Body).Decode(&requestData)
107116
if err != nil {
@@ -139,9 +148,47 @@ func (h *TaskcafeHandler) LoginHandler(w http.ResponseWriter, r *http.Request) {
139148
authCreatedAt := time.Now().UTC()
140149
authExpiresAt := authCreatedAt.AddDate(0, 0, 1)
141150
authToken, err := h.repo.CreateAuthToken(r.Context(), db.CreateAuthTokenParams{user.UserID, authCreatedAt, authExpiresAt})
151+
if err != nil {
152+
w.WriteHeader(http.StatusInternalServerError)
153+
// TODO: should we return here?
154+
}
155+
156+
w.Header().Set("Content-type", "application/json")
157+
http.SetCookie(w, &http.Cookie{
158+
Name: "authToken",
159+
Value: authToken.TokenID.String(),
160+
Expires: authExpiresAt,
161+
Path: "/",
162+
HttpOnly: true,
163+
})
164+
json.NewEncoder(w).Encode(LoginResponseData{Complete: true, UserID: authToken.UserID.String()})
165+
}
142166

167+
func (h *TaskcafeHandler) xHeaderAuthenticate(w http.ResponseWriter, r *http.Request) {
168+
xRemoteUser := r.Header.Get("X-Remote-User")
169+
user, err := h.repo.GetUserAccountByUsername(r.Context(), xRemoteUser)
170+
if err != nil {
171+
log.WithFields(log.Fields{
172+
"username": xRemoteUser,
173+
}).Warn("user account not found")
174+
w.WriteHeader(http.StatusUnauthorized)
175+
return
176+
}
177+
178+
if !user.Active {
179+
log.WithFields(log.Fields{
180+
"username": user.Username,
181+
}).Warn("attempt to login with inactive user")
182+
w.WriteHeader(http.StatusUnauthorized)
183+
return
184+
}
185+
186+
authCreatedAt := time.Now().UTC()
187+
authExpiresAt := authCreatedAt.AddDate(0, 0, 1)
188+
authToken, err := h.repo.CreateAuthToken(r.Context(), db.CreateAuthTokenParams{user.UserID, authCreatedAt, authExpiresAt})
143189
if err != nil {
144190
w.WriteHeader(http.StatusInternalServerError)
191+
return
145192
}
146193

147194
w.Header().Set("Content-type", "application/json")

internal/route/route.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,13 @@ type TaskcafeHandler struct {
6565
SecurityConfig utils.SecurityConfig
6666
}
6767

68+
type Config struct {
69+
Email utils.EmailConfig
70+
Security utils.SecurityConfig
71+
}
72+
6873
// NewRouter creates a new router for chi
69-
func NewRouter(dbConnection *sqlx.DB, emailConfig utils.EmailConfig, securityConfig utils.SecurityConfig) (chi.Router, error) {
74+
func NewRouter(dbConnection *sqlx.DB, cfg Config) (chi.Router, error) {
7075
formatter := new(log.TextFormatter)
7176
formatter.TimestampFormat = "02-01-2006 15:04:05"
7277
formatter.FullTimestamp = true
@@ -93,7 +98,7 @@ func NewRouter(dbConnection *sqlx.DB, emailConfig utils.EmailConfig, securityCon
9398
}))
9499

95100
repository := db.NewRepository(dbConnection)
96-
taskcafeHandler := TaskcafeHandler{*repository, securityConfig}
101+
taskcafeHandler := TaskcafeHandler{*repository, cfg.Security}
97102

98103
var imgServer = http.FileServer(http.Dir("./uploads/"))
99104
r.Group(func(mux chi.Router) {
@@ -108,7 +113,7 @@ func NewRouter(dbConnection *sqlx.DB, emailConfig utils.EmailConfig, securityCon
108113
r.Group(func(mux chi.Router) {
109114
mux.Use(auth.Middleware)
110115
mux.Post("/users/me/avatar", taskcafeHandler.ProfileImageUpload)
111-
mux.Handle("/graphql", graph.NewHandler(*repository, emailConfig))
116+
mux.Handle("/graphql", graph.NewHandler(*repository, cfg.Email))
112117
})
113118

114119
frontend := FrontendHandler{staticPath: "build", indexPath: "index.html"}

internal/utils/security.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
type SecurityConfig struct {
1010
AccessTokenExpiration time.Duration
1111
Secret []byte
12+
IsExternalAuth bool
1213
}
1314

1415
func GetSecurityConfig(accessTokenExp string, secret []byte) (SecurityConfig, error) {

0 commit comments

Comments
 (0)