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
This commit is contained in:
Lenin Alevski
2020-11-13 16:26:03 -08:00
committed by GitHub
parent 419e94ccec
commit be569aee4f
14 changed files with 330 additions and 197 deletions

View File

@@ -21,6 +21,7 @@ import (
"fmt"
"strconv"
"strings"
"time"
"github.com/minio/minio/pkg/certs"
"github.com/minio/minio/pkg/env"
@@ -41,6 +42,8 @@ var TLSPort = "9443"
// TLSRedirect console tls redirect rule
var TLSRedirect = "off"
var SessionDuration = 45 * time.Minute
func getAccessKey() string {
return env.Get(ConsoleAccessKey, "minioadmin")
}

View File

@@ -21,6 +21,7 @@ package restapi
import (
"bytes"
"crypto/tls"
"fmt"
"log"
"net/http"
"strings"
@@ -168,8 +169,10 @@ func setupMiddlewares(handler http.Handler) http.Handler {
// The middleware configuration happens before anything, this middleware also applies to serving the swagger.json document.
// So this is a good place to plug in a panic handling middleware, logging and metrics
func setupGlobalMiddleware(handler http.Handler) http.Handler {
// handle cookie or authorization header for session
next := AuthenticationMiddleware(handler)
// serve static files
next := FileServerMiddleware(handler)
next = FileServerMiddleware(next)
// Secure middleware, this middleware wrap all the previous handlers and add
// HTTP security headers
secureOptions := secure.Options{
@@ -200,6 +203,31 @@ func setupGlobalMiddleware(handler http.Handler) http.Handler {
return app
}
func AuthenticationMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// prioritize authorization header and skip
if r.Header.Get("Authorization") != "" {
next.ServeHTTP(w, r)
return
}
tokenCookie, err := r.Cookie("token")
if err != nil {
next.ServeHTTP(w, r)
return
}
currentTime := time.Now()
if tokenCookie.Expires.After(currentTime) {
next.ServeHTTP(w, r)
return
}
token := tokenCookie.Value
if token != "" {
r.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
}
next.ServeHTTP(w, r)
})
}
// FileServerMiddleware serves files from the static folder
func FileServerMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

View File

@@ -96,7 +96,7 @@ func Test_ResourceQuota(t *testing.T) {
wantErr: false,
},
{
name: "Handle error while fetching storage quota elementss",
name: "Handle error while fetching storage quota elements",
args: args{
ctx: ctx,
client: kClient,

View File

@@ -19,7 +19,9 @@ package restapi
import (
"context"
"log"
"net/http"
"github.com/go-openapi/runtime"
"github.com/go-openapi/runtime/middleware"
"github.com/minio/console/models"
"github.com/minio/console/pkg/acl"
@@ -45,21 +47,36 @@ func registerLoginHandlers(api *operations.ConsoleAPI) {
if err != nil {
return user_api.NewLoginDefault(int(err.Code)).WithPayload(err)
}
return user_api.NewLoginCreated().WithPayload(loginResponse)
// Custom response writer to set the session cookies
return middleware.ResponderFunc(func(w http.ResponseWriter, p runtime.Producer) {
cookie := NewSessionCookieForConsole(loginResponse.SessionID)
http.SetCookie(w, &cookie)
user_api.NewLoginCreated().WithPayload(loginResponse).WriteResponse(w, p)
})
})
api.UserAPILoginOauth2AuthHandler = user_api.LoginOauth2AuthHandlerFunc(func(params user_api.LoginOauth2AuthParams) middleware.Responder {
loginResponse, err := getLoginOauth2AuthResponse(params.Body)
if err != nil {
return user_api.NewLoginOauth2AuthDefault(int(err.Code)).WithPayload(err)
}
return user_api.NewLoginOauth2AuthCreated().WithPayload(loginResponse)
// Custom response writer to set the session cookies
return middleware.ResponderFunc(func(w http.ResponseWriter, p runtime.Producer) {
cookie := NewSessionCookieForConsole(loginResponse.SessionID)
http.SetCookie(w, &cookie)
user_api.NewLoginOauth2AuthCreated().WithPayload(loginResponse).WriteResponse(w, p)
})
})
api.UserAPILoginOperatorHandler = user_api.LoginOperatorHandlerFunc(func(params user_api.LoginOperatorParams) middleware.Responder {
loginResponse, err := getLoginOperatorResponse(params.Body)
if err != nil {
return user_api.NewLoginOperatorDefault(int(err.Code)).WithPayload(err)
}
return user_api.NewLoginOperatorCreated().WithPayload(loginResponse)
// Custom response writer to set the session cookies
return middleware.ResponderFunc(func(w http.ResponseWriter, p runtime.Producer) {
cookie := NewSessionCookieForConsole(loginResponse.SessionID)
http.SetCookie(w, &cookie)
user_api.NewLoginOperatorCreated().WithPayload(loginResponse).WriteResponse(w, p)
})
})
}

View File

@@ -17,6 +17,9 @@
package restapi
import (
"net/http"
"github.com/go-openapi/runtime"
"github.com/go-openapi/runtime/middleware"
"github.com/minio/console/models"
"github.com/minio/console/restapi/operations"
@@ -27,7 +30,14 @@ func registerLogoutHandlers(api *operations.ConsoleAPI) {
// logout from console
api.UserAPILogoutHandler = user_api.LogoutHandlerFunc(func(params user_api.LogoutParams, session *models.Principal) middleware.Responder {
getLogoutResponse(session)
return user_api.NewLogoutOK()
// Custom response writer to expire the session cookies
return middleware.ResponderFunc(func(w http.ResponseWriter, p runtime.Producer) {
expiredCookie := ExpireSessionCookie()
// this will tell the browser to clear the cookie and invalidate user session
// additionally we are deleting the cookie from the client side
http.SetCookie(w, &expiredCookie)
user_api.NewLogoutOK().WriteResponse(w, p)
})
})
}

View File

@@ -19,8 +19,10 @@ package restapi
import (
"crypto/rand"
"io"
"net/http"
"os"
"strings"
"time"
)
// Do not use:
@@ -102,3 +104,37 @@ func FileExists(filename string) bool {
}
return !info.IsDir()
}
func NewSessionCookieForConsole(token string) http.Cookie {
expiration := time.Now().Add(SessionDuration)
return http.Cookie{
Path: "/api", // browser will send cookie only for HTTP request under api path
Name: "token",
Value: token,
MaxAge: int(SessionDuration.Seconds()), // 45 minutes
Expires: expiration,
HttpOnly: true,
// if len(GlobalPublicCerts) > 0 is true, that means Console is running with TLS enable and the browser
// should not leak any cookie if we access the site using HTTP
Secure: len(GlobalPublicCerts) > 0,
// read more: https://web.dev/samesite-cookies-explained/
SameSite: http.SameSiteLaxMode,
}
}
func ExpireSessionCookie() http.Cookie {
return http.Cookie{
Path: "/api", // browser will send cookie only for HTTP request under api path
Name: "token",
Value: "",
MaxAge: -1,
Expires: time.Now().Add(-100 * time.Hour),
HttpOnly: true,
// if len(GlobalPublicCerts) > 0 is true, that means Console is running with TLS enable and the browser
// should not leak any cookie if we access the site using HTTP
Secure: len(GlobalPublicCerts) > 0,
// read more: https://web.dev/samesite-cookies-explained/
SameSite: http.SameSiteLaxMode,
}
}