TRA-4157 fix ws auth (#669)

* Update socket_routes.go, user_controller.go, and 2 more files...

* Update user_controller.go

* Switch to http-only cookies for more security
This commit is contained in:
RamiBerm 2022-01-20 14:10:25 +02:00 committed by GitHub
parent 6bab381280
commit 676e50b0b1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 50 additions and 37 deletions

View File

@ -47,9 +47,9 @@ func init() {
} }
func WebSocketRoutes(app *gin.Engine, eventHandlers EventHandlers, startTime int64) { func WebSocketRoutes(app *gin.Engine, eventHandlers EventHandlers, startTime int64) {
app.GET("/ws", func(c *gin.Context) { app.GET("/ws", middlewares.RequiresAuth(), func(c *gin.Context) {
websocketHandler(c.Writer, c.Request, eventHandlers, false, startTime) websocketHandler(c.Writer, c.Request, eventHandlers, false, startTime)
}, middlewares.RequiresAuth()) })
app.GET("/wsTapper", func(c *gin.Context) { // TODO: add m2m authentication to this route app.GET("/wsTapper", func(c *gin.Context) { // TODO: add m2m authentication to this route
websocketHandler(c.Writer, c.Request, eventHandlers, true, startTime) websocketHandler(c.Writer, c.Request, eventHandlers, true, startTime)

View File

@ -1,7 +1,9 @@
package controllers package controllers
import ( import (
"errors"
"mizuserver/pkg/providers" "mizuserver/pkg/providers"
"net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/up9inc/mizu/shared/logger" "github.com/up9inc/mizu/shared/logger"
@ -11,20 +13,44 @@ func Login(c *gin.Context) {
if token, err := providers.PerformLogin(c.PostForm("username"), c.PostForm("password"), c.Request.Context()); err != nil { if token, err := providers.PerformLogin(c.PostForm("username"), c.PostForm("password"), c.Request.Context()); err != nil {
c.AbortWithStatusJSON(401, gin.H{"error": "bad login"}) c.AbortWithStatusJSON(401, gin.H{"error": "bad login"})
} else { } else {
c.JSON(200, gin.H{"token": token}) c.SetSameSite(http.SameSiteLaxMode)
c.SetCookie("x-session-token", *token, 3600, "/", "", false, true)
c.JSON(200, "")
} }
} }
func Logout(c *gin.Context) { func Logout(c *gin.Context) {
token := c.GetHeader("x-session-token") token, err := c.Cookie("x-session-token")
if err := providers.Logout(token, c.Request.Context()); err != nil { if err != nil {
if errors.Is(err, http.ErrNoCookie) {
c.AbortWithStatusJSON(401, gin.H{"error": "could not find session cookie"})
} else {
logger.Log.Errorf("error reading cookie in logout %s", err)
c.AbortWithStatusJSON(500, gin.H{"error": "error occured while logging out, the session might still be valid"})
}
return
}
if err = providers.Logout(token, c.Request.Context()); err != nil {
c.AbortWithStatusJSON(500, gin.H{"error": "error occured while logging out, the session might still be valid"}) c.AbortWithStatusJSON(500, gin.H{"error": "error occured while logging out, the session might still be valid"})
} else { } else {
c.SetCookie("x-session-token", "", -1, "/", "", false, true)
c.JSON(200, "") c.JSON(200, "")
} }
} }
func Register(c *gin.Context) { func Register(c *gin.Context) {
// only allow one user to be created without authentication
if IsInstallNeeded, err := providers.IsInstallNeeded(); err != nil {
logger.Log.Errorf("unknown internal while checking if install is needed %s", err)
c.AbortWithStatusJSON(500, gin.H{"error": "internal error occured while checking if install is needed"})
return
} else if !IsInstallNeeded {
c.AbortWithStatusJSON(401, gin.H{"error": "cannot register when install is not needed"})
return
}
if token, _, err, formErrorMessages := providers.RegisterUser(c.PostForm("username"), c.PostForm("password"), c.Request.Context()); err != nil { if token, _, err, formErrorMessages := providers.RegisterUser(c.PostForm("username"), c.PostForm("password"), c.Request.Context()); err != nil {
if formErrorMessages != nil { if formErrorMessages != nil {
logger.Log.Infof("user attempted to register but had form errors %v %v", formErrorMessages, err) logger.Log.Infof("user attempted to register but had form errors %v %v", formErrorMessages, err)
@ -34,6 +60,8 @@ func Register(c *gin.Context) {
c.AbortWithStatusJSON(500, gin.H{"error": "internal error occured while registering"}) c.AbortWithStatusJSON(500, gin.H{"error": "internal error occured while registering"})
} }
} else { } else {
c.JSON(200, gin.H{"token": token}) c.SetSameSite(http.SameSiteLaxMode)
c.SetCookie("x-session-token", *token, 3600, "/", "", false, true)
c.JSON(200, "")
} }
} }

View File

@ -1,49 +1,51 @@
package middlewares package middlewares
import ( import (
"errors"
"mizuserver/pkg/config" "mizuserver/pkg/config"
"mizuserver/pkg/providers" "mizuserver/pkg/providers"
"time" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/patrickmn/go-cache"
"github.com/up9inc/mizu/shared/logger" "github.com/up9inc/mizu/shared/logger"
) )
const cachedValidTokensRetainmentTime = time.Minute * 1 const errorMessage = "unknown authentication error occured"
var cachedValidTokens = cache.New(cachedValidTokensRetainmentTime, cachedValidTokensRetainmentTime)
func RequiresAuth() gin.HandlerFunc { func RequiresAuth() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
// auth is irrelevant for ephermeral mizu // authentication is irrelevant for ephermeral mizu
if !config.Config.StandaloneMode { if !config.Config.StandaloneMode {
c.Next() c.Next()
return return
} }
token := c.GetHeader("x-session-token") token, err := c.Cookie("x-session-token")
if token == "" { if err != nil {
c.AbortWithStatusJSON(401, gin.H{"error": "token header is empty"}) if errors.Is(err, http.ErrNoCookie) {
c.AbortWithStatusJSON(401, gin.H{"error": "could not find session cookie"})
} else {
logger.Log.Errorf("error reading cookie %s", err)
c.AbortWithStatusJSON(500, gin.H{"error": errorMessage})
}
return return
} }
if _, isTokenCached := cachedValidTokens.Get(token); isTokenCached { if token == "" {
c.Next() c.AbortWithStatusJSON(401, gin.H{"error": "token cookie is empty"})
return return
} }
if isTokenValid, err := providers.VerifyToken(token, c.Request.Context()); err != nil { if isTokenValid, err := providers.VerifyToken(token, c.Request.Context()); err != nil {
logger.Log.Errorf("error verifying token %s", err) logger.Log.Errorf("error verifying token %s", err)
c.AbortWithStatusJSON(401, gin.H{"error": "unknown auth error occured"}) c.AbortWithStatusJSON(500, gin.H{"error": errorMessage})
return return
} else if !isTokenValid { } else if !isTokenValid {
c.AbortWithStatusJSON(401, gin.H{"error": "invalid token"}) c.AbortWithStatusJSON(401, gin.H{"error": "invalid token"})
return return
} }
cachedValidTokens.Set(token, true, cachedValidTokensRetainmentTime)
c.Next() c.Next()
} }
} }

View File

@ -22,8 +22,6 @@ export default class Api {
} }
constructor() { constructor() {
this.token = localStorage.getItem("token");
this.client = this.getAxiosClient(); this.client = this.getAxiosClient();
this.source = null; this.source = null;
} }
@ -143,7 +141,6 @@ export default class Api {
try { try {
const response = await this.client.post(`/user/register`, form); const response = await this.client.post(`/user/register`, form);
this.persistToken(response.data.token);
return response; return response;
} catch (e) { } catch (e) {
if (e.response.status === 400) { if (e.response.status === 400) {
@ -164,32 +161,18 @@ export default class Api {
form.append('password', password); form.append('password', password);
const response = await this.client.post(`/user/login`, form); const response = await this.client.post(`/user/login`, form);
if (response.status >= 200 && response.status < 300) {
this.persistToken(response.data.token);
}
return response; return response;
} }
persistToken = (token) => {
this.token = token;
this.client = this.getAxiosClient();
localStorage.setItem('token', token);
}
logout = async () => { logout = async () => {
await this.client.post(`/user/logout`); await this.client.post(`/user/logout`);
this.persistToken(null);
} }
getAxiosClient = () => { getAxiosClient = () => {
const headers = { const headers = {
Accept: "application/json" Accept: "application/json"
} }
if (this.token) {
headers['x-session-token'] = `${this.token}`; // we use `x-session-token` instead of `Authorization` because the latter is reserved by kubectl proxy, making mizu view not work
}
return axios.create({ return axios.create({
baseURL: apiURL, baseURL: apiURL,
timeout: 31000, timeout: 31000,