Skip to content

Commit be569ae

Browse files
authored
Support for Cookie authentication (#390)
- Added support for cookie authentication (authorization header will have priority) - Removed local storage token management from UI - cookie hardening (sameSite, httpOnly, secure) - login endpoint sets cookie via header, logout endpoint expires cookie - Refactor Routes and ProtectedRoutes components, improvement on the way application check if user session is valid Future improvements - look for all places in backend that returns 401 unauthorized, and destroy session there (not a priority since cookie its invalid anyway) - Downloading objects in object browser can be simplified since is just a GET request and users will be authenticated via Cookies, no need to craft additional requests
1 parent 419e94c commit be569ae

File tree

14 files changed

+330
-197
lines changed

14 files changed

+330
-197
lines changed

portal-ui/bindata_assetfs.go

Lines changed: 149 additions & 103 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

portal-ui/src/ProtectedRoutes.tsx

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// This file is part of MinIO Console Server
2+
// Copyright (c) 2020 MinIO, Inc.
3+
//
4+
// This program is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Affero General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// This program is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Affero General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Affero General Public License
15+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
17+
import React, { useEffect, useState } from "react";
18+
import { Redirect } from "react-router-dom";
19+
import { connect } from "react-redux";
20+
import { AppState } from "./store";
21+
import { userLoggedIn } from "./actions";
22+
import api from "./common/api";
23+
import { clearSession } from "./common/utils";
24+
import { saveSessionResponse } from "./screens/Console/actions";
25+
26+
const mapState = (state: AppState) => ({
27+
loggedIn: state.system.loggedIn,
28+
});
29+
30+
const connector = connect(mapState, {
31+
userLoggedIn,
32+
saveSessionResponse,
33+
});
34+
35+
interface ProtectedRouteProps {
36+
loggedIn: boolean;
37+
Component: any;
38+
userLoggedIn: typeof userLoggedIn;
39+
saveSessionResponse: typeof saveSessionResponse;
40+
}
41+
42+
const ProtectedRoute = ({
43+
Component,
44+
loggedIn,
45+
userLoggedIn,
46+
saveSessionResponse,
47+
}: ProtectedRouteProps) => {
48+
const [sessionLoading, setSessionLoading] = useState<boolean>(true);
49+
useEffect(() => {
50+
api
51+
.invoke("GET", `/api/v1/session`)
52+
.then((res) => {
53+
saveSessionResponse(res);
54+
userLoggedIn(true);
55+
setSessionLoading(false);
56+
})
57+
.catch(() => setSessionLoading(false));
58+
}, [saveSessionResponse]);
59+
60+
// if we still trying to retrieve user session render nothing
61+
if (sessionLoading) {
62+
return null;
63+
}
64+
// redirect user to the right page based on session status
65+
return loggedIn ? <Component /> : <Redirect to={{ pathname: "/login" }} />;
66+
};
67+
68+
export default connector(ProtectedRoute);

portal-ui/src/Routes.tsx

Lines changed: 11 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -15,65 +15,24 @@
1515
// along with this program. If not, see <http://www.gnu.org/licenses/>.
1616

1717
import React from "react";
18-
import { Redirect, Route, Router, Switch } from "react-router-dom";
18+
import { Route, Router, Switch } from "react-router-dom";
1919
import history from "./history";
2020
import Login from "./screens/LoginPage/LoginPage";
2121
import Console from "./screens/Console/Console";
22-
import storage from "local-storage-fallback";
23-
import { connect } from "react-redux";
24-
import { AppState } from "./store";
25-
import { userLoggedIn } from "./actions";
2622
import LoginCallback from "./screens/LoginPage/LoginCallback";
2723
import { hot } from "react-hot-loader/root";
24+
import ProtectedRoute from "./ProtectedRoutes";
2825

29-
interface ProtectedRouteProps {
30-
loggedIn: boolean;
31-
component: any;
32-
}
33-
34-
export class ProtectedRoute extends React.Component<ProtectedRouteProps> {
35-
render() {
36-
const Component = this.props.component;
37-
return this.props.loggedIn ? (
38-
<Component />
39-
) : (
40-
<Redirect to={{ pathname: "/login" }} />
41-
);
42-
}
43-
}
44-
45-
const isLoggedIn = () => {
26+
const Routes = () => {
4627
return (
47-
storage.getItem("token") !== undefined &&
48-
storage.getItem("token") !== null &&
49-
storage.getItem("token") !== ""
28+
<Router history={history}>
29+
<Switch>
30+
<Route exact path="/oauth_callback" component={LoginCallback} />
31+
<Route exact path="/login" component={Login} />
32+
<ProtectedRoute Component={Console} />
33+
</Switch>
34+
</Router>
5035
);
5136
};
5237

53-
const mapState = (state: AppState) => ({
54-
loggedIn: state.system.loggedIn,
55-
});
56-
57-
const connector = connect(mapState, { userLoggedIn });
58-
59-
interface RoutesProps {
60-
loggedIn: boolean;
61-
userLoggedIn: typeof userLoggedIn;
62-
}
63-
64-
class Routes extends React.Component<RoutesProps> {
65-
render() {
66-
const loggedIn = isLoggedIn();
67-
return (
68-
<Router history={history}>
69-
<Switch>
70-
<Route exact path="/oauth_callback" component={LoginCallback} />
71-
<Route exact path="/login" component={Login} />
72-
<ProtectedRoute component={Console} loggedIn={loggedIn} />
73-
</Switch>
74-
</Router>
75-
);
76-
}
77-
}
78-
79-
export default hot(connector(Routes));
38+
export default hot(Routes);

portal-ui/src/common/api/index.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,13 @@
1414
// You should have received a copy of the GNU Affero General Public License
1515
// along with this program. If not, see <http://www.gnu.org/licenses/>.
1616

17-
import storage from "local-storage-fallback";
1817
import request from "superagent";
1918
import get from "lodash/get";
2019
import { clearSession } from "../utils";
2120

2221
export class API {
2322
invoke(method: string, url: string, data?: object) {
24-
const token: string = storage.getItem("token")!;
2523
return request(method, url)
26-
.set("Authorization", `Bearer ${token}`)
2724
.send(data)
2825
.then((res) => res.body)
2926
.catch((err) => {

portal-ui/src/common/utils.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,17 +62,12 @@ export const deleteCookie = (name: string) => {
6262
document.cookie = name + "=; expires=Thu, 01 Jan 1970 00:00:01 GMT;";
6363
};
6464

65-
export const setSession = (token: string) => {
66-
setCookie("token", token);
67-
storage.setItem("token", token);
68-
};
69-
7065
export const clearSession = () => {
7166
storage.removeItem("token");
7267
deleteCookie("token");
7368
};
7469

75-
// timeFromdate gets time string from date input
70+
// timeFromDate gets time string from date input
7671
export const timeFromDate = (d: Date) => {
7772
let h = d.getHours() < 10 ? `0${d.getHours()}` : `${d.getHours()}`;
7873
let m = d.getMinutes() < 10 ? `0${d.getMinutes()}` : `${d.getMinutes()}`;

portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ListObjects.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,6 @@ const ListObjects = ({
227227
let xhr = new XMLHttpRequest();
228228

229229
xhr.open("POST", uploadUrl, true);
230-
xhr.setRequestHeader("Authorization", `Bearer ${token}`);
231230

232231
xhr.withCredentials = false;
233232
xhr.onload = function (event) {
@@ -276,7 +275,6 @@ const ListObjects = ({
276275
`/api/v1/buckets/${bucketName}/objects/download?prefix=${objectName}`,
277276
true
278277
);
279-
xhr.setRequestHeader("Authorization", `Bearer ${token}`);
280278
xhr.responseType = "blob";
281279

282280
xhr.onload = function (e) {

portal-ui/src/screens/Console/Console.tsx

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,7 @@ import Heal from "./Heal/Heal";
4646
import Watch from "./Watch/Watch";
4747
import ListTenants from "./Tenants/ListTenants/ListTenants";
4848
import { ISessionResponse } from "./types";
49-
import { saveSessionResponse } from "./actions";
5049
import TenantDetails from "./Tenants/TenantDetails/TenantDetails";
51-
import { clearSession } from "../../common/utils";
5250
import ObjectBrowser from "./ObjectBrowser/ObjectBrowser";
5351
import ListObjects from "./Buckets/ListBuckets/Objects/ListObjects/ListObjects";
5452
import License from "./License/License";
@@ -160,7 +158,6 @@ interface IConsoleProps {
160158
setMenuOpen: typeof setMenuOpen;
161159
serverNeedsRestart: typeof serverNeedsRestart;
162160
serverIsLoading: typeof serverIsLoading;
163-
saveSessionResponse: typeof saveSessionResponse;
164161
session: ISessionResponse;
165162
}
166163

@@ -171,24 +168,8 @@ const Console = ({
171168
isServerLoading,
172169
serverNeedsRestart,
173170
serverIsLoading,
174-
saveSessionResponse,
175171
session,
176172
}: IConsoleProps) => {
177-
useEffect(() => {
178-
api
179-
.invoke("GET", `/api/v1/session`)
180-
.then((res) => {
181-
saveSessionResponse(res);
182-
})
183-
.catch(() => {
184-
// if server returns 401 for /api/v1/session call invoke function will internally call clearSession()
185-
// and redirecto to window.location.href = "/"; and this code will be not reached
186-
// in case that not happen we clear session here and redirect as well
187-
clearSession();
188-
window.location.href = "/login";
189-
});
190-
}, [saveSessionResponse]);
191-
192173
const restartServer = () => {
193174
serverIsLoading(true);
194175
api
@@ -375,7 +356,6 @@ const connector = connect(mapState, {
375356
setMenuOpen,
376357
serverNeedsRestart,
377358
serverIsLoading,
378-
saveSessionResponse,
379359
});
380360

381361
export default connector(withStyles(styles)(Console));

portal-ui/src/screens/LoginPage/LoginPage.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ import { SystemState } from "../../types";
3838
import { userLoggedIn } from "../../actions";
3939
import api from "../../common/api";
4040
import { ILoginDetails, loginStrategyType } from "./types";
41-
import { setSession } from "../../common/utils";
4241
import history from "../../history";
4342
import { OutlinedInputProps } from "@material-ui/core/OutlinedInput";
4443

@@ -225,10 +224,7 @@ const Login = ({ classes, userLoggedIn }: ILoginProps) => {
225224
.send(loginStrategyPayload[loginStrategy.loginStrategy])
226225
.then((res: any) => {
227226
const bodyResponse = res.body;
228-
if (bodyResponse.sessionId) {
229-
// store the jwt token
230-
setSession(bodyResponse.sessionId);
231-
} else if (bodyResponse.error) {
227+
if (bodyResponse.error) {
232228
setLoginSending(false);
233229
// throw will be moved to catch block once bad login returns 403
234230
throw bodyResponse.error;

restapi/config.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"fmt"
2222
"strconv"
2323
"strings"
24+
"time"
2425

2526
"github.com/minio/minio/pkg/certs"
2627
"github.com/minio/minio/pkg/env"
@@ -41,6 +42,8 @@ var TLSPort = "9443"
4142
// TLSRedirect console tls redirect rule
4243
var TLSRedirect = "off"
4344

45+
var SessionDuration = 45 * time.Minute
46+
4447
func getAccessKey() string {
4548
return env.Get(ConsoleAccessKey, "minioadmin")
4649
}

restapi/configure_console.go

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ package restapi
2121
import (
2222
"bytes"
2323
"crypto/tls"
24+
"fmt"
2425
"log"
2526
"net/http"
2627
"strings"
@@ -168,8 +169,10 @@ func setupMiddlewares(handler http.Handler) http.Handler {
168169
// The middleware configuration happens before anything, this middleware also applies to serving the swagger.json document.
169170
// So this is a good place to plug in a panic handling middleware, logging and metrics
170171
func setupGlobalMiddleware(handler http.Handler) http.Handler {
172+
// handle cookie or authorization header for session
173+
next := AuthenticationMiddleware(handler)
171174
// serve static files
172-
next := FileServerMiddleware(handler)
175+
next = FileServerMiddleware(next)
173176
// Secure middleware, this middleware wrap all the previous handlers and add
174177
// HTTP security headers
175178
secureOptions := secure.Options{
@@ -200,6 +203,31 @@ func setupGlobalMiddleware(handler http.Handler) http.Handler {
200203
return app
201204
}
202205

206+
func AuthenticationMiddleware(next http.Handler) http.Handler {
207+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
208+
// prioritize authorization header and skip
209+
if r.Header.Get("Authorization") != "" {
210+
next.ServeHTTP(w, r)
211+
return
212+
}
213+
tokenCookie, err := r.Cookie("token")
214+
if err != nil {
215+
next.ServeHTTP(w, r)
216+
return
217+
}
218+
currentTime := time.Now()
219+
if tokenCookie.Expires.After(currentTime) {
220+
next.ServeHTTP(w, r)
221+
return
222+
}
223+
token := tokenCookie.Value
224+
if token != "" {
225+
r.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
226+
}
227+
next.ServeHTTP(w, r)
228+
})
229+
}
230+
203231
// FileServerMiddleware serves files from the static folder
204232
func FileServerMiddleware(next http.Handler) http.Handler {
205233
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

0 commit comments

Comments
 (0)