Added logout, token renewal and more stuff

This commit is contained in:
Lilleman auf Larv 2022-03-13 21:00:45 +01:00
parent 57ab820693
commit a5e5f660e6
9 changed files with 301 additions and 40 deletions

2
go.mod
View File

@ -10,7 +10,7 @@ require (
require ( require (
github.com/andybalholm/brotli v1.0.4 // indirect github.com/andybalholm/brotli v1.0.4 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/klauspost/compress v1.14.3 // indirect github.com/klauspost/compress v1.14.3 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.33.0 // indirect github.com/valyala/fasthttp v1.33.0 // indirect

View File

@ -12,7 +12,7 @@ import (
) )
// Method must be a valid REST method, like GET, POST, PUT, DELETE etc // Method must be a valid REST method, like GET, POST, PUT, DELETE etc
func (api Api) Call(method string, path string, jsonPayload []byte) (ApiRes, error) { func (api Api) Call(method string, path string, payload []byte) (ApiRes, error) {
respObj := ApiRes{} respObj := ApiRes{}
adminToken, _, err := api.getAdminToken() adminToken, _, err := api.getAdminToken()
@ -20,7 +20,7 @@ func (api Api) Call(method string, path string, jsonPayload []byte) (ApiRes, err
return ApiRes{}, err return ApiRes{}, err
} }
req, err := http.NewRequest(method, api.URL+path, bytes.NewBuffer(jsonPayload)) req, err := http.NewRequest(method, api.URL+path, bytes.NewBuffer(payload))
if err != nil { if err != nil {
api.Log.Error("Could not create request", "method", method, "err", err.Error()) api.Log.Error("Could not create request", "method", method, "err", err.Error())
return ApiRes{}, err return ApiRes{}, err
@ -36,18 +36,12 @@ func (api Api) Call(method string, path string, jsonPayload []byte) (ApiRes, err
} }
defer res.Body.Close() defer res.Body.Close()
api.Log.Debug("API res status", "status", res.StatusCode) api.Log.Debug(api.URL+path+" res status", "status", res.StatusCode)
respObj.StatusCode = res.StatusCode respObj.StatusCode = res.StatusCode
if strconv.Itoa(res.StatusCode)[0:1] == "5" { if strconv.Itoa(res.StatusCode)[0:1] == "5" {
api.Log.Error("API gave internal server error", "statusCode", res.StatusCode) api.Log.Error("API gave internal server error", "statusCode", res.StatusCode)
return ApiRes{}, errors.New("API gave internal server error") return ApiRes{}, errors.New("API gave internal server error")
} else if res.StatusCode == 403 {
api.Log.Error("API responded with 403, Forbidden")
return ApiRes{}, errors.New("API responded with 403, Forbidden")
} else if res.StatusCode == 401 {
api.Log.Error("API responded with 401, Unauthorized")
return ApiRes{}, errors.New("API responded with 403, Unauthorized")
} }
body, err := ioutil.ReadAll(res.Body) body, err := ioutil.ReadAll(res.Body)
@ -107,7 +101,7 @@ func (api Api) getAdminTokenCall() (string, error) {
} }
defer res.Body.Close() defer res.Body.Close()
api.Log.Debug("API res status", "status", res.StatusCode) api.Log.Debug(api.URL+"/auth/api-key res status", "status", res.StatusCode)
if strconv.Itoa(res.StatusCode)[0:1] == "5" { if strconv.Itoa(res.StatusCode)[0:1] == "5" {
api.Log.Error("API gave internal server error", "statusCode", res.StatusCode) api.Log.Error("API gave internal server error", "statusCode", res.StatusCode)

View File

@ -1,12 +1,88 @@
package handlers package handlers
import ( import (
"encoding/json"
"strconv"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"gitlab.larvit.se/power-plan/auth-ui/src/api"
"gitlab.larvit.se/power-plan/auth-ui/src/utils"
"gitlab.larvit.se/power-plan/auth-ui/src/views" "gitlab.larvit.se/power-plan/auth-ui/src/views"
) )
type LoginUser struct {
Username string `json:"name"`
Password string `json:"password"`
}
// Index GET
func (h Handlers) Index(c *fiber.Ctx) error { func (h Handlers) Index(c *fiber.Ctx) error {
c.Set(fiber.HeaderContentType, fiber.MIMETextHTML) c.Set(fiber.HeaderContentType, fiber.MIMETextHTML)
return c.SendString(views.Index()) return c.SendString(views.Index(views.IndexData{}))
}
// Index POST
func (h Handlers) IndexPost(c *fiber.Ctx) error {
c.Set(fiber.HeaderContentType, fiber.MIMETextHTML)
loginUser := new(LoginUser)
err := c.BodyParser(loginUser)
if err != nil {
h.Log.Debug("Invalid input data", "err", err.Error())
c.Status(400)
}
jsonPayload, err := json.Marshal(loginUser)
if err != nil {
h.Log.Error("Could not marshal loginUser struct", "err", err.Error())
return c.Status(500).SendString(views.Error500())
}
apiRes, err := h.Api.Call("POST", "/auth/password", jsonPayload)
if err != nil {
h.Log.Error("Could not call backend API", "err", err.Error())
return c.Status(500).SendString(views.Error500())
}
h.Log.Debug("API res status", "status", apiRes.StatusCode)
if apiRes.StatusCode == 200 {
err = utils.SetTokenCookies(apiRes.Body, c, h.Log)
if err != nil {
h.Log.Error("Could not unmarshal body from auth API", "err", err.Error())
return c.Status(500).SendString(views.Error500())
}
userToken := c.Cookies("userToken")
parsed, err := utils.ParseAndValidateToken(userToken, h.Api.JwtSharedSecret, h.Log)
if err != nil {
h.Log.Error("Could not parse and validate new user token after renewal", "err", err.Error())
return c.Status(500).SendString(views.Error500())
}
c.Locals("user", parsed)
return c.SendString(views.Index(views.IndexData{
OkMsg: "User logged in!",
Title: "Dashboard",
}))
} else if strconv.Itoa(apiRes.StatusCode)[0:1] == "4" {
var jsonResp []api.ApiResError
err := json.Unmarshal(apiRes.Body, &jsonResp)
if err != nil {
h.Log.Error("Could not unmarshal api error response", "err", err.Error())
return c.Status(500).SendString(views.Error500())
}
return c.Status(apiRes.StatusCode).SendString(views.Index(views.IndexData{
ErrField: jsonResp[0].Field,
ErrMsg: jsonResp[0].Error,
Title: "Login",
}))
} else {
h.Log.Error("Unexpected API response status", "status", apiRes.StatusCode)
return c.Status(500).SendString(views.Error500())
}
} }

15
src/handlers/logout.go Normal file
View File

@ -0,0 +1,15 @@
package handlers
import (
"github.com/gofiber/fiber/v2"
)
// Logout
func (h Handlers) LogoutPost(c *fiber.Ctx) error {
c.ClearCookie("userToken")
c.ClearCookie("renewalToken")
c.Locals("user", "")
c.Redirect("/")
return nil
}

View File

@ -1,13 +1,76 @@
package handlers package handlers
import ( import (
"errors"
"strings"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"gitlab.larvit.se/power-plan/auth-ui/src/utils"
"gitlab.larvit.se/power-plan/auth-ui/src/views"
) )
// Log all requests // Log all requests.
func (h Handlers) LogReq(c *fiber.Ctx) error { func (h Handlers) LogReq(c *fiber.Ctx) error {
h.Log.Debug("http request", "method", c.Method(), "url", c.OriginalURL()) h.Log.Debug("http request", "method", c.Method(), "url", c.OriginalURL())
c.Next() c.Next()
return nil return nil
} }
// Token cookie handling.
func (h Handlers) HandleCookieTokens(c *fiber.Ctx) error {
userToken := c.Cookies("userToken")
// If no userToken exists, nothing more to do, so exiting.
if len(userToken) == 0 {
return c.Next()
}
parsed, err := utils.ParseAndValidateToken(userToken, h.Api.JwtSharedSecret, h.Log)
// If parsing went fine, just set the user context local and exit.
if err == nil {
c.Locals("user", parsed)
return c.Next()
}
// Something is wrong and it is not that the token has expired.
// Log the error and exit.
if !strings.HasPrefix(err.Error(), "token is expired") {
h.Log.Error("could not parse token with claims", "err", err.Error())
return c.Next()
}
renewalToken := c.Cookies("renewalToken")
if len(renewalToken) == 0 {
h.Log.Debug("no renewalToken cookie set, can not renew expired token")
return errors.New("no renewalToken cookie set, can not renew expired token")
}
apiRes, err := h.Api.Call("POST", "/renew-token", []byte("\""+renewalToken+"\""))
if err != nil {
h.Log.Error("Could not call backend API", "err", err.Error())
return c.Status(500).SendString(views.Error500())
}
if apiRes.StatusCode != 200 {
h.Log.Error("Unexpected API response status", "status", apiRes.StatusCode)
return c.Status(500).SendString(views.Error500())
}
err = utils.SetTokenCookies(apiRes.Body, c, h.Log)
if err != nil {
h.Log.Error("Could not unmarshal body from auth API", "err", err.Error())
return c.Status(500).SendString(views.Error500())
}
userToken = c.Cookies("userToken")
parsed, err = utils.ParseAndValidateToken(userToken, h.Api.JwtSharedSecret, h.Log)
if err != nil {
h.Log.Error("Could not parse and validate new user token after renewal", "err", err.Error())
return c.Status(500).SendString(views.Error500())
}
c.Locals("user", parsed)
return c.Next()
}

View File

@ -15,16 +15,21 @@ func (h Handlers) Register(c *fiber.Ctx) error {
return c.SendString(views.Register(views.RegisterData{})) return c.SendString(views.Register(views.RegisterData{}))
} }
type NewUser struct { type field struct {
Name string `json:"name"` Name string `json:"name"`
Username string `json:"username"` Values []string `json:"values"`
Password string `json:"password"` }
RepeatPassword string `json:"-"` type newUserType struct {
Name string `json:"name"`
Username string `json:"username"`
Password string `json:"password"`
RepeatPassword string `json:"-"`
Fields []field `json:"fields"`
} }
func (h Handlers) RegisterPost(c *fiber.Ctx) error { func (h Handlers) RegisterPost(c *fiber.Ctx) error {
c.Set(fiber.HeaderContentType, fiber.MIMETextHTML) c.Set(fiber.HeaderContentType, fiber.MIMETextHTML)
newUser := new(NewUser) newUser := new(newUserType)
err := c.BodyParser(newUser) err := c.BodyParser(newUser)
if err != nil { if err != nil {
@ -32,6 +37,10 @@ func (h Handlers) RegisterPost(c *fiber.Ctx) error {
c.Status(400) c.Status(400)
} }
newUser.Name = newUser.Username newUser.Name = newUser.Username
newUser.Fields = append(newUser.Fields, field{
Name: "role",
Values: []string{"user"},
})
jsonPayload, err := json.Marshal(newUser) jsonPayload, err := json.Marshal(newUser)
if err != nil { if err != nil {

View File

@ -49,11 +49,17 @@ func main() {
// Static files // Static files
app.Static("/", "./src/public") app.Static("/", "./src/public")
// Unpack user token, refresh token when needed etc.
app.Use(handlers.HandleCookieTokens)
app.Get("/", handlers.Index) app.Get("/", handlers.Index)
app.Post("/", handlers.IndexPost)
app.Get("/register", handlers.Register) app.Get("/register", handlers.Register)
app.Post("/register", handlers.RegisterPost) app.Post("/register", handlers.RegisterPost)
app.Post("/logout", handlers.LogoutPost)
log.Info("Trying to start web server", "PORT", os.Getenv("PORT")) log.Info("Trying to start web server", "PORT", os.Getenv("PORT"))
webBindHost := ":" + os.Getenv("PORT") webBindHost := ":" + os.Getenv("PORT")

View File

@ -1,9 +1,14 @@
package utils package utils
import ( import (
"encoding/json"
"fmt"
"log" "log"
"strings"
"time" "time"
"github.com/gofiber/fiber/v2"
"github.com/golang-jwt/jwt"
"go.uber.org/zap" "go.uber.org/zap"
"go.uber.org/zap/zapcore" "go.uber.org/zap/zapcore"
) )
@ -35,3 +40,65 @@ func GetLog() *zap.SugaredLogger {
return log return log
} }
type authClaims struct {
jwt.StandardClaims
AccountId string `json:"accountId"`
AccountFields map[string][]string `json:"accountFields"`
AccountName string `json:"accountName"`
}
func ParseAndValidateToken(token string, jwtSecret string, log *zap.SugaredLogger) (authClaims, error) {
var claims authClaims
_, err := jwt.ParseWithClaims(token, &claims, func(token *jwt.Token) (interface{}, error) {
_, ok := token.Method.(*jwt.SigningMethodHMAC)
if !ok {
log.Error("unexpected signing method", "signingMethod", token.Header["alg"])
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(jwtSecret), nil
})
if err != nil {
if strings.HasPrefix(err.Error(), "token is expired") {
log.Debug("token have expired", "err", err.Error())
} else {
log.Error("could not parse token with claims", "err", err.Error())
}
return authClaims{}, err
}
return claims, nil
}
type AuthRes struct {
Jwt string `json:"jwt"`
RenewalToken string `json:"renewalToken"`
}
func SetTokenCookies(apiResBody []byte, c *fiber.Ctx, log *zap.SugaredLogger) error {
var authRes AuthRes
err := json.Unmarshal(apiResBody, &authRes)
if err != nil {
log.Error("Could not unmarshal body from auth API", "err", err.Error())
return err
}
c.Cookie(&fiber.Cookie{
Name: "userToken",
Value: authRes.Jwt,
Expires: time.Now().Add(24 * time.Hour),
HTTPOnly: false,
SameSite: "lax",
})
c.Cookie(&fiber.Cookie{
Name: "renewalToken",
Value: authRes.RenewalToken,
Expires: time.Now().Add(24 * time.Hour),
HTTPOnly: true,
SameSite: "lax",
})
return nil
}

View File

@ -1,31 +1,62 @@
package views package views
import "gitlab.larvit.se/power-plan/auth-ui/src/views/layouts" import (
vu "gitlab.larvit.se/power-plan/auth-ui/src/view-utils"
"gitlab.larvit.se/power-plan/auth-ui/src/views/layouts"
)
func Index() string { type UserData struct {
content := ` Name string
<h1 class="main-title">Login</h1> }
<form method="post" class="pure-form pure-form-aligned primary-form">
<fieldset>
<div class="pure-control-group">
<label for="username">Username</label>
<input type="text" placeholder="Username" name="username" id="username">
</div>
<div class="pure-control-group"> type IndexData struct {
<label for="password">Password</label> ErrField string
<input type="password" placeholder="Password" name="password" id="password" /> ErrMsg string
</div> OkMsg string
Title string
UserData UserData
UserLoggedIn bool
}
<div class="pure-controls"> func Index(data IndexData) string {
<a href="/register" class="pure-button">Register</a> content := ""
<button type="submit" class="pure-button pure-button-primary">Login</button>
</div> if data.UserLoggedIn {
</fieldset> content = `
</form>` <h1 class="main-title">Welcome ` + data.UserData.Name + `</h1>`
} else {
content = `
<h1 class="main-title">Login</h1>
<form method="post" class="pure-form pure-form-aligned primary-form">
` + vu.TernStr(len(data.OkMsg) != 0, "<p>"+data.OkMsg+" <a href=\"/\">Login</a></p>", "") + `
<fieldset>
<div class="pure-control-group">
<label for="username">Username</label>
<input type="text" placeholder="Username" name="username" id="username">
</div>
<div class="pure-control-group">
<label for="password">Password</label>
<input type="password" placeholder="Password" name="password" id="password" />
</div>
` + vu.TernStr(len(data.ErrMsg) != 0, "<p>ERROR! "+data.ErrMsg+"<p>", "") + `
<div class="pure-controls">
<a href="/register" class="pure-button">Register</a>
<button type="submit" class="pure-button pure-button-primary">Login</button>
</div>
</fieldset>
</form>
<form method="post" action="/logout">
<button type="submit">Logout</button>
</form>
`
}
return layouts.Default(layouts.DefaultData{ return layouts.Default(layouts.DefaultData{
Content: content, Content: content,
Title: "Login", Title: data.Title,
}) })
} }