Added logout, token renewal and more stuff
This commit is contained in:
parent
57ab820693
commit
a5e5f660e6
2
go.mod
2
go.mod
|
@ -10,7 +10,7 @@ require (
|
|||
|
||||
require (
|
||||
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/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.33.0 // indirect
|
||||
|
|
|
@ -12,7 +12,7 @@ import (
|
|||
)
|
||||
|
||||
// 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{}
|
||||
|
||||
adminToken, _, err := api.getAdminToken()
|
||||
|
@ -20,7 +20,7 @@ func (api Api) Call(method string, path string, jsonPayload []byte) (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 {
|
||||
api.Log.Error("Could not create request", "method", method, "err", err.Error())
|
||||
return ApiRes{}, err
|
||||
|
@ -36,18 +36,12 @@ func (api Api) Call(method string, path string, jsonPayload []byte) (ApiRes, err
|
|||
}
|
||||
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
|
||||
|
||||
if strconv.Itoa(res.StatusCode)[0:1] == "5" {
|
||||
api.Log.Error("API gave internal server error", "statusCode", res.StatusCode)
|
||||
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)
|
||||
|
@ -107,7 +101,7 @@ func (api Api) getAdminTokenCall() (string, error) {
|
|||
}
|
||||
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" {
|
||||
api.Log.Error("API gave internal server error", "statusCode", res.StatusCode)
|
||||
|
|
|
@ -1,12 +1,88 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
type LoginUser struct {
|
||||
Username string `json:"name"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
// Index GET
|
||||
func (h Handlers) Index(c *fiber.Ctx) error {
|
||||
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
15
src/handlers/logout.go
Normal 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
|
||||
}
|
|
@ -1,13 +1,76 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"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 {
|
||||
h.Log.Debug("http request", "method", c.Method(), "url", c.OriginalURL())
|
||||
|
||||
c.Next()
|
||||
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()
|
||||
}
|
||||
|
|
|
@ -15,16 +15,21 @@ func (h Handlers) Register(c *fiber.Ctx) error {
|
|||
return c.SendString(views.Register(views.RegisterData{}))
|
||||
}
|
||||
|
||||
type NewUser struct {
|
||||
Name string `json:"name"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
RepeatPassword string `json:"-"`
|
||||
type field struct {
|
||||
Name string `json:"name"`
|
||||
Values []string `json:"values"`
|
||||
}
|
||||
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 {
|
||||
c.Set(fiber.HeaderContentType, fiber.MIMETextHTML)
|
||||
newUser := new(NewUser)
|
||||
newUser := new(newUserType)
|
||||
|
||||
err := c.BodyParser(newUser)
|
||||
if err != nil {
|
||||
|
@ -32,6 +37,10 @@ func (h Handlers) RegisterPost(c *fiber.Ctx) error {
|
|||
c.Status(400)
|
||||
}
|
||||
newUser.Name = newUser.Username
|
||||
newUser.Fields = append(newUser.Fields, field{
|
||||
Name: "role",
|
||||
Values: []string{"user"},
|
||||
})
|
||||
|
||||
jsonPayload, err := json.Marshal(newUser)
|
||||
if err != nil {
|
||||
|
|
|
@ -49,11 +49,17 @@ func main() {
|
|||
// Static files
|
||||
app.Static("/", "./src/public")
|
||||
|
||||
// Unpack user token, refresh token when needed etc.
|
||||
app.Use(handlers.HandleCookieTokens)
|
||||
|
||||
app.Get("/", handlers.Index)
|
||||
app.Post("/", handlers.IndexPost)
|
||||
|
||||
app.Get("/register", handlers.Register)
|
||||
app.Post("/register", handlers.RegisterPost)
|
||||
|
||||
app.Post("/logout", handlers.LogoutPost)
|
||||
|
||||
log.Info("Trying to start web server", "PORT", os.Getenv("PORT"))
|
||||
|
||||
webBindHost := ":" + os.Getenv("PORT")
|
||||
|
|
|
@ -1,9 +1,14 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/golang-jwt/jwt"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
@ -35,3 +40,65 @@ func GetLog() *zap.SugaredLogger {
|
|||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -1,31 +1,62 @@
|
|||
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 {
|
||||
content := `
|
||||
<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>
|
||||
type UserData struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" placeholder="Password" name="password" id="password" />
|
||||
</div>
|
||||
type IndexData struct {
|
||||
ErrField string
|
||||
ErrMsg string
|
||||
OkMsg string
|
||||
Title string
|
||||
UserData UserData
|
||||
UserLoggedIn bool
|
||||
}
|
||||
|
||||
<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>`
|
||||
func Index(data IndexData) string {
|
||||
content := ""
|
||||
|
||||
if data.UserLoggedIn {
|
||||
content = `
|
||||
<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{
|
||||
Content: content,
|
||||
Title: "Login",
|
||||
Title: data.Title,
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user