Compare commits

...

6 Commits

Author SHA1 Message Date
celogeek
e02a05c254
refresh token update 2022-05-27 00:12:34 +02:00
celogeek
54c75f24e1
remove session table 2022-05-26 23:58:51 +02:00
celogeek
ff3931f083
generate jwt private key 2022-05-26 23:58:43 +02:00
celogeek
e6210640db
disable deps 2022-05-26 23:58:24 +02:00
celogeek
7ea0d08b11
use JWT token 2022-05-26 23:58:06 +02:00
celogeek
6b38b9e899
add jwt 2022-05-26 23:57:13 +02:00
6 changed files with 149 additions and 38 deletions

1
go.mod
View File

@ -17,6 +17,7 @@ require (
github.com/go-playground/locales v0.14.0 // indirect github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-playground/validator/v10 v10.11.0 // indirect github.com/go-playground/validator/v10 v10.11.0 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang/protobuf v1.5.2 // indirect github.com/golang/protobuf v1.5.2 // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgconn v1.12.1 // indirect github.com/jackc/pgconn v1.12.1 // indirect

2
go.sum
View File

@ -32,6 +32,8 @@ github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSM
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=

View File

@ -3,17 +3,26 @@ package photosapi
import ( import (
"errors" "errors"
"net/http" "net/http"
"strings"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
) )
// Errors // Errors
var ( var (
ErrAccountExists = errors.New("account exists") ErrAccountExists = errors.New("account exists")
ErrAccountAuth = errors.New("login or password incorrect") ErrAccountAuth = errors.New("login or password incorrect")
ErrAccountTokenMissing = errors.New("token missing")
ErrAccountTokenInvalid = errors.New("token invalid")
)
// Const
const (
TOKEN = 1
REFRESH_TOKEN = 2
) )
// Model // Model
@ -46,6 +55,35 @@ type LoginRequest struct {
type LoginResponse struct { type LoginResponse struct {
Token string `json:"token"` Token string `json:"token"`
RefreshToken string `json:"refresh_token"`
}
// JWT
type JWT struct {
Type int `json:"typ"`
ExpireAt int64 `json:"exp"`
AccountId uint32 `json:"acc"`
}
func (j *JWT) Valid() error {
return jwt.MapClaims{
"exp": float64(j.ExpireAt),
}.Valid()
}
func NewJWTToken(accountId uint32) *JWT {
return &JWT{
Type: TOKEN,
ExpireAt: time.Now().Add(time.Minute * 15).Unix(),
AccountId: accountId,
}
}
func NewJWTRefreshToken(accountId uint32) *JWT {
return &JWT{
Type: REFRESH_TOKEN,
ExpireAt: time.Now().Add(time.Hour * 24).Unix(),
AccountId: accountId,
}
} }
func (s *Service) Signup(c *gin.Context) { func (s *Service) Signup(c *gin.Context) {
@ -73,41 +111,89 @@ func (s *Service) Signup(c *gin.Context) {
} }
func (s *Service) Login(c *gin.Context) { func (s *Service) Login(c *gin.Context) {
var account *LoginRequest var loginRequest *LoginRequest
if c.BindJSON(&account) != nil { if c.BindJSON(&loginRequest) != nil {
return return
} }
session, err := NewSession(s.DB, account.Login, account.Password) account := &Account{}
if err != nil { if err := s.DB.Where(
if errors.Is(err, gorm.ErrRecordNotFound) || errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) { "login = ?",
loginRequest.Login,
).First(account).Error; err != nil {
c.AbortWithError(http.StatusNotFound, ErrAccountAuth) c.AbortWithError(http.StatusNotFound, ErrAccountAuth)
} else { return
}
if err := bcrypt.CompareHashAndPassword(account.Password, []byte(loginRequest.Password)); err != nil {
c.AbortWithError(http.StatusNotFound, ErrAccountAuth)
return
}
token, err := jwt.NewWithClaims(&jwt.SigningMethodEd25519{}, NewJWTToken(account.ID)).SignedString(s.SessionKey)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err) c.AbortWithError(http.StatusInternalServerError, err)
}
return return
} }
c.JSON(http.StatusOK, LoginResponse{session.Token}) refresh_token, err := jwt.NewWithClaims(&jwt.SigningMethodEd25519{}, NewJWTRefreshToken(account.ID)).SignedString(s.SessionKey)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
} }
func (s *Service) Logout(c *gin.Context) { c.JSON(http.StatusOK, LoginResponse{
res := s.DB.Where("token = ?", c.GetString("token")).Delete(&Session{}) Token: token,
if res.Error != nil { RefreshToken: refresh_token,
c.AbortWithError(http.StatusInternalServerError, res.Error) })
}
func (s *Service) LoginRefresh(c *gin.Context) {
auth := strings.Split(c.GetHeader("Authorization"), " ")
if len(auth) != 2 {
c.AbortWithError(http.StatusForbidden, ErrAccountTokenMissing)
return return
} }
if res.RowsAffected == 0 {
c.AbortWithError(http.StatusNotFound, ErrSessionNotFound) if auth[0] != "Bearer" {
c.AbortWithError(http.StatusForbidden, ErrAccountTokenMissing)
return return
} }
c.Status(http.StatusNoContent)
var claims JWT
refresh_token, err := jwt.ParseWithClaims(auth[1], &claims, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodEd25519); !ok {
return nil, ErrAccountTokenInvalid
}
return s.SessionKeyValidation, nil
})
if err != nil || !refresh_token.Valid || claims.Type != REFRESH_TOKEN {
c.AbortWithError(http.StatusForbidden, ErrAccountTokenInvalid)
return
}
token, err := jwt.NewWithClaims(&jwt.SigningMethodEd25519{}, NewJWTToken(claims.AccountId)).SignedString(s.SessionKey)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
new_refresh_token, err := jwt.NewWithClaims(&jwt.SigningMethodEd25519{}, NewJWTRefreshToken(claims.AccountId)).SignedString(s.SessionKey)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.JSON(http.StatusOK, LoginResponse{
Token: token,
RefreshToken: new_refresh_token,
})
} }
func (s *Service) AccountInit() { func (s *Service) AccountInit() {
ac := s.Gin.Group("/account") ac := s.Gin.Group("/account")
ac.POST("/signup", s.Signup) ac.POST("/signup", s.Signup)
ac.POST("/login", s.Login) ac.POST("/login", s.Login)
ac.GET("/logout", s.RequireAuthToken, s.Logout) ac.GET("/refresh", s.LoginRefresh)
} }

View File

@ -33,7 +33,6 @@ func (d *DBConfig) DSN() string {
func (s *Service) Migrate() { func (s *Service) Migrate() {
tx := s.DB tx := s.DB
tx.AutoMigrate(&Account{}) tx.AutoMigrate(&Account{})
tx.AutoMigrate(&Session{})
tx.AutoMigrate(&File{}) tx.AutoMigrate(&File{})
} }

View File

@ -257,8 +257,8 @@ var (
// } // }
func (s *Service) FileInit() { func (s *Service) FileInit() {
file := s.Gin.Group("/file") // file := s.Gin.Group("/file")
file.Use(s.RequireSession) // file.Use(s.RequireSession)
// file.POST("", s.FileCreate) // file.POST("", s.FileCreate)
// file.HEAD("/:checksum", s.FileExists) // file.HEAD("/:checksum", s.FileExists)
// file.GET("/:checksum", s.FileGet) // file.GET("/:checksum", s.FileGet)

View File

@ -2,11 +2,13 @@ package photosapi
import ( import (
"context" "context"
"crypto/ed25519"
"errors" "errors"
"math/rand" "math/rand"
"net/http" "net/http"
"os" "os"
"os/signal" "os/signal"
"path/filepath"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@ -26,6 +28,8 @@ type Service struct {
StorageUpload *Storage StorageUpload *Storage
LogOk *Logger LogOk *Logger
LogErr *Logger LogErr *Logger
SessionKey ed25519.PrivateKey
SessionKeyValidation ed25519.PublicKey
} }
type ServiceConfig struct { type ServiceConfig struct {
@ -34,7 +38,25 @@ type ServiceConfig struct {
StorePath string StorePath string
} }
func GetOrGenerateKey(storePath string) ed25519.PrivateKey {
p := filepath.Join(storePath, "photo.key")
key, err := os.ReadFile(p)
if errors.Is(err, os.ErrNotExist) {
_, key, err = ed25519.GenerateKey(nil)
if err != nil {
panic(err)
}
err = os.WriteFile(p, key, 0600)
if err != nil {
panic(err)
}
}
return key
}
func New(config *ServiceConfig) *Service { func New(config *ServiceConfig) *Service {
key := GetOrGenerateKey(config.StorePath)
pubKey := key.Public().(ed25519.PublicKey)
return &Service{ return &Service{
Gin: gin.New(), Gin: gin.New(),
Config: config, Config: config,
@ -42,6 +64,8 @@ func New(config *ServiceConfig) *Service {
StorageUpload: NewStorage(config.StorePath, "upload"), StorageUpload: NewStorage(config.StorePath, "upload"),
LogOk: &Logger{os.Stdout, "Photos"}, LogOk: &Logger{os.Stdout, "Photos"},
LogErr: &Logger{os.Stderr, "Photos"}, LogErr: &Logger{os.Stderr, "Photos"},
SessionKey: key,
SessionKeyValidation: pubKey,
} }
} }
@ -78,7 +102,6 @@ func (s *Service) Run() error {
s.PrepareStore() s.PrepareStore()
s.SetupRoutes() s.SetupRoutes()
s.SetupDB() s.SetupDB()
go s.SessionCleaner()
srv := &http.Server{ srv := &http.Server{
Addr: s.Config.Listen, Addr: s.Config.Listen,