Skip to content

Commit 45b1a4b

Browse files
committed
[app] Implement Authentication
1 parent dcb86ee commit 45b1a4b

13 files changed

+318
-188
lines changed

CHANGELOG.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ NOTE: As semantic versioning states all 0.y.z releases can contain breaking chan
2020
- [#399](https://github.com/kobsio/kobs/pull/399): [github] Add new `usernotifications` panel and allow users to use the plugin within the Notifications.
2121
- [#401](https://github.com/kobsio/kobs/pull/401): [app] Add integrations for Kubernetes Resource, which allows administrators to define a set of default dashboards, which are added to each resource.
2222
- [#402](https://github.com/kobsio/kobs/pull/402): [app] Add `mongodb` driver as alternative to the existing `bolt` driver.
23-
- [#406](https://github.com/kobsio/kobs/pull/406): [app] Implement authentication, so that no third party service like [OAuth2-Proxy](https://oauth2-proxy.github.io/oauth2-proxy/) is required to grant users access to kobs.
23+
- [#406](https://github.com/kobsio/kobs/pull/406): [app] :warning: _Breaking change:_ :warning: Implement authentication, so that no third party service like [OAuth2-Proxy](https://oauth2-proxy.github.io/oauth2-proxy/) is required to grant users access to kobs.
2424
- [#407](https://github.com/kobsio/kobs/pull/407): [sql] Add `singlestats` chart to render single values returned by a query.
2525
- [#411](https://github.com/kobsio/kobs/pull/411): [sql] Add `yAxisGroup` property for charts.
2626

pkg/hub/auth/auth.go

+45-26
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"context"
55
"encoding/json"
66
"net/http"
7+
"net/url"
8+
"strings"
79
"time"
810

911
authContext "github.com/kobsio/kobs/pkg/hub/auth/context"
@@ -112,7 +114,7 @@ func (c *client) getUserFromConfig(email string) *UserConfig {
112114
// handle this within other API calls, because we will always have a valid user object there.
113115
func (c *client) getUserFromRequest(r *http.Request) (*authContext.User, error) {
114116
if c.config.Enabled {
115-
cookie, err := r.Cookie("kobs-auth")
117+
cookie, err := r.Cookie("kobs")
116118
if err != nil {
117119
return nil, err
118120
}
@@ -145,11 +147,11 @@ func (c *client) userHandler(w http.ResponseWriter, r *http.Request) {
145147
render.JSON(w, r, user)
146148
}
147149

148-
// loginHandler handles the login of users, which are provided via the configuration file of the hub. For that we have
150+
// signinHandler handles the login of users, which are provided via the configuration file of the hub. For that we have
149151
// to check if the user from the request is present in the configuration and if the provided password matches the
150152
// configured password. If this is the case we are are creating a user object with all the users permissions and using
151153
// it in the session token. The session token is then set as cookie, so it can be validated with each request.
152-
func (c *client) loginHandler(w http.ResponseWriter, r *http.Request) {
154+
func (c *client) signinHandler(w http.ResponseWriter, r *http.Request) {
153155
var loginRequest LoginRequest
154156

155157
err := json.NewDecoder(r.Body).Decode(&loginRequest)
@@ -193,42 +195,51 @@ func (c *client) loginHandler(w http.ResponseWriter, r *http.Request) {
193195
}
194196

195197
http.SetCookie(w, &http.Cookie{
196-
Name: "kobs-auth",
198+
Name: "kobs",
197199
Value: token,
198200
Path: "/",
199-
Secure: false,
201+
Secure: true,
200202
HttpOnly: true,
201203
Expires: time.Now().Add(c.config.Session.ParsedInterval),
202204
})
203205

204-
render.JSON(w, r, user)
206+
render.JSON(w, r, nil)
205207
}
206208

207-
// logoutHandler handles the logout for an user. For this we are setting the value of the auth cookie to an empty string
208-
// and we adjust the expiration date of the cookie. Finally we redirect the user to the start page of kobs, we the auth
209-
// context fails and the user has to do the auth flow again.
210-
func (c *client) logoutHandler(w http.ResponseWriter, r *http.Request) {
209+
// signoutHandler handles the logout for an user. For this we are setting the value of the auth cookie to an empty
210+
// string and we adjust the expiration date of the cookie.
211+
func (c *client) signoutHandler(w http.ResponseWriter, r *http.Request) {
211212
http.SetCookie(w, &http.Cookie{
212-
Name: "kobs-auth",
213+
Name: "kobs",
213214
Value: "",
214215
Path: "/",
215-
Secure: false,
216+
Secure: true,
216217
HttpOnly: true,
217218
Expires: time.Unix(0, 0),
218219
})
219220

220-
http.Redirect(w, r, "/", http.StatusSeeOther)
221+
render.JSON(w, r, nil)
221222
}
222223

223-
// oidcRedirectHandler redirects a user to the configured OIDC provider to start the login flow. If no OIDC provider was
224-
// configured we redirect the user to the frontend.
225-
func (c *client) oidcRedirectHandler(w http.ResponseWriter, r *http.Request) {
224+
// oidcRedirectHandler returns the login for the OIDC provider, which can then be used by the user to authenticate via
225+
// the configured provider. If no OIDC provider is configured this will return an error.
226+
//
227+
// We also "abusing" the state parameter by adding a redirect url, so that we have access to this url in the
228+
// oidcCallbackHandler and that we can redirect the user to this url.
229+
func (c *client) oidcHandler(w http.ResponseWriter, r *http.Request) {
226230
if c.oidcConfig == nil || c.oidcProvider == nil {
227-
http.Redirect(w, r, "/", http.StatusSeeOther)
231+
log.Warn(r.Context(), "OIDC provider is not configured")
232+
errresponse.Render(w, r, nil, http.StatusBadRequest, "OIDC provider is not configured")
228233
return
229234
}
230235

231-
http.Redirect(w, r, c.oidcConfig.AuthCodeURL(c.config.OIDC.State), http.StatusFound)
236+
data := struct {
237+
URL string `json:"url"`
238+
}{
239+
c.oidcConfig.AuthCodeURL(c.config.OIDC.State + url.QueryEscape(r.URL.Query().Get("redirect"))),
240+
}
241+
242+
render.JSON(w, r, data)
232243
}
233244

234245
// oidcCallbackHandler handles the callback from the OIDC login flow. Once we finished the OIDC flow and retrieved the
@@ -240,8 +251,10 @@ func (c *client) oidcCallbackHandler(w http.ResponseWriter, r *http.Request) {
240251
return
241252
}
242253

243-
if r.URL.Query().Get("state") != c.config.OIDC.State {
244-
log.Warn(r.Context(), "Invalid state")
254+
state := r.URL.Query().Get("state")
255+
256+
if !strings.HasPrefix(state, c.config.OIDC.State) {
257+
log.Warn(r.Context(), "Invalid state", zap.String("state", state))
245258
errresponse.Render(w, r, nil, http.StatusBadRequest, "Invalid state")
246259
return
247260
}
@@ -295,15 +308,21 @@ func (c *client) oidcCallbackHandler(w http.ResponseWriter, r *http.Request) {
295308
}
296309

297310
http.SetCookie(w, &http.Cookie{
298-
Name: "kobs-auth",
311+
Name: "kobs",
299312
Value: token,
300313
Path: "/",
301-
Secure: false,
314+
Secure: true,
302315
HttpOnly: true,
303316
Expires: time.Now().Add(c.config.Session.ParsedInterval),
304317
})
305318

306-
render.JSON(w, r, user)
319+
data := struct {
320+
URL string `json:"url"`
321+
}{
322+
strings.TrimPrefix(state, c.config.OIDC.State),
323+
}
324+
325+
render.JSON(w, r, data)
307326
}
308327

309328
// NewClient returns a new auth client for handling authentication and authorization within the kobs hub. The auth
@@ -349,9 +368,9 @@ func NewClient(config Config, storeClient store.Client) (Client, error) {
349368
}
350369

351370
c.router.Get("/", c.userHandler)
352-
c.router.Post("/login", c.loginHandler)
353-
c.router.Get("/logout", c.logoutHandler)
354-
c.router.Get("/oidc", c.oidcRedirectHandler)
371+
c.router.Post("/signin", c.signinHandler)
372+
c.router.Get("/signout", c.signoutHandler)
373+
c.router.Get("/oidc", c.oidcHandler)
355374
c.router.Get("/oidc/callback", c.oidcCallbackHandler)
356375

357376
return c, nil

pkg/hub/auth/auth_test.go

+15-15
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ func TestMiddlewareHandler(t *testing.T) {
5151
expectedStatusCode: http.StatusUnauthorized,
5252
expectedBody: "{\"error\":\"Unauthorized: token contains an invalid number of segments\"}\n",
5353
prepareRequest: func(r *http.Request) {
54-
r.AddCookie(&http.Cookie{Name: "kobs-auth", Value: "fake"})
54+
r.AddCookie(&http.Cookie{Name: "kobs", Value: "fake"})
5555
},
5656
},
5757
{
@@ -61,7 +61,7 @@ func TestMiddlewareHandler(t *testing.T) {
6161
expectedBody: "null\n",
6262
prepareRequest: func(r *http.Request) {
6363
token, _ := jwt.CreateToken(authContext.User{Email: "user1@kobs.io"}, "", 10*time.Minute)
64-
r.AddCookie(&http.Cookie{Name: "kobs-auth", Value: token})
64+
r.AddCookie(&http.Cookie{Name: "kobs", Value: token})
6565
},
6666
},
6767
} {
@@ -183,7 +183,7 @@ func TestUserHandler(t *testing.T) {
183183
expectedStatusCode: http.StatusUnauthorized,
184184
expectedBody: "{\"error\":\"Unauthorized: token contains an invalid number of segments\"}\n",
185185
prepareRequest: func(r *http.Request) {
186-
r.AddCookie(&http.Cookie{Name: "kobs-auth", Value: "fake"})
186+
r.AddCookie(&http.Cookie{Name: "kobs", Value: "fake"})
187187
},
188188
},
189189
{
@@ -193,7 +193,7 @@ func TestUserHandler(t *testing.T) {
193193
expectedBody: "{\"email\":\"user1@kobs.io\",\"teams\":null,\"permissions\":{}}\n",
194194
prepareRequest: func(r *http.Request) {
195195
token, _ := jwt.CreateToken(authContext.User{Email: "user1@kobs.io"}, "", 10*time.Minute)
196-
r.AddCookie(&http.Cookie{Name: "kobs-auth", Value: token})
196+
r.AddCookie(&http.Cookie{Name: "kobs", Value: token})
197197
},
198198
},
199199
} {
@@ -209,7 +209,7 @@ func TestUserHandler(t *testing.T) {
209209
}
210210
}
211211

212-
func TestLoginHandler(t *testing.T) {
212+
func TestSigninHandler(t *testing.T) {
213213
for _, tt := range []struct {
214214
name string
215215
client client
@@ -277,7 +277,7 @@ func TestLoginHandler(t *testing.T) {
277277
client: client{config: Config{Enabled: true, Users: []UserConfig{{Email: "admin", Password: "$2y$10$UPPBv.HThEllgJZINbFwYOsru62d.LT0EqG3XLug2pG81IvemopH2"}}}},
278278
requestBody: "{\"email\":\"admin\", \"password\":\"fakepassword\"}\n",
279279
expectedStatusCode: http.StatusOK,
280-
expectedBody: "{\"email\":\"admin\",\"teams\":null,\"permissions\":{}}\n",
280+
expectedBody: "null\n",
281281
prepareStoreClient: func(mockStoreClient *store.MockClient) {
282282
mockStoreClient.On("GetUsersByEmail", mock.Anything, mock.Anything).Return(nil, nil)
283283
mockStoreClient.AssertNotCalled(t, "GetTeamsByGroups", mock.Anything, mock.Anything)
@@ -292,7 +292,7 @@ func TestLoginHandler(t *testing.T) {
292292

293293
req, _ := http.NewRequestWithContext(context.Background(), http.MethodPost, "/", bytes.NewBuffer([]byte(tt.requestBody)))
294294
w := httptest.NewRecorder()
295-
tt.client.loginHandler(w, req)
295+
tt.client.signinHandler(w, req)
296296

297297
require.Equal(t, tt.expectedStatusCode, w.Code)
298298
require.Equal(t, tt.expectedBody, string(w.Body.Bytes()))
@@ -301,27 +301,27 @@ func TestLoginHandler(t *testing.T) {
301301
}
302302
}
303303

304-
func TestLogoutHandler(t *testing.T) {
304+
func TestSignoutHandler(t *testing.T) {
305305
c := client{
306306
router: chi.NewRouter(),
307307
}
308308

309309
req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/", nil)
310310
w := httptest.NewRecorder()
311-
c.logoutHandler(w, req)
312-
require.Equal(t, http.StatusSeeOther, w.Code)
311+
c.signoutHandler(w, req)
312+
require.Equal(t, http.StatusOK, w.Code)
313313
}
314314

315-
func TestOidcRedirectHandler(t *testing.T) {
315+
func TestOidcHandler(t *testing.T) {
316316
t.Run("oidc not configured", func(t *testing.T) {
317317
c := client{
318318
router: chi.NewRouter(),
319319
}
320320

321321
req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/", nil)
322322
w := httptest.NewRecorder()
323-
c.oidcRedirectHandler(w, req)
324-
require.Equal(t, http.StatusSeeOther, w.Code)
323+
c.oidcHandler(w, req)
324+
require.Equal(t, http.StatusBadRequest, w.Code)
325325
})
326326

327327
t.Run("redirect", func(t *testing.T) {
@@ -342,8 +342,8 @@ func TestOidcRedirectHandler(t *testing.T) {
342342

343343
req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/", nil)
344344
w := httptest.NewRecorder()
345-
c.oidcRedirectHandler(w, req)
346-
require.Equal(t, http.StatusFound, w.Code)
345+
c.oidcHandler(w, req)
346+
require.Equal(t, http.StatusOK, w.Code)
347347
})
348348
}
349349

0 commit comments

Comments
 (0)