initial commit
This commit is contained in:
commit
e8da0f7874
15 files changed
+722
No files matched your search
@@ -0,0 +1,2 @@
|
|||||||
|
go.sum
|
||||||
|
_env
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
FROM golang:alpine
|
||||||
|
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN go build -o testjwtauth .
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
ENTRYPOINT ["/testjwtauth"]
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
DSN = getEnv("DSN")
|
||||||
|
SMTPHost = getEnv("SMTP_HOST")
|
||||||
|
SMTPEmail = getEnv("SMTP_EMAIL")
|
||||||
|
SMTPPassword = getEnv("SMTP_PASSWORD")
|
||||||
|
MockSMTPRecipient = getEnv("MOCK_SMTP_RECIPIENT")
|
||||||
|
JWTSecret = getEnv("JWT_SECRET")
|
||||||
|
)
|
||||||
|
|
||||||
|
func getEnv(k string) string {
|
||||||
|
v := os.Getenv(k)
|
||||||
|
if v == "" {
|
||||||
|
log.Fatalf("enviroment variable %s is required", k)
|
||||||
|
}
|
||||||
|
|
||||||
|
return v
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
c "desq.com.ru/testjwtauth/config"
|
||||||
|
"desq.com.ru/testjwtauth/models"
|
||||||
|
|
||||||
|
"gorm.io/driver/postgres"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func initDB() (*gorm.DB, error) {
|
||||||
|
db, err := gorm.Open(postgres.Open(c.DSN), &gorm.Config{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
db.AutoMigrate(&models.RefreshToken{})
|
||||||
|
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
module desq.com.ru/testjwtauth
|
||||||
|
|
||||||
|
go 1.23.4
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/gofiber/contrib/jwt v1.0.10
|
||||||
|
github.com/gofiber/fiber/v2 v2.52.5
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
|
golang.org/x/crypto v0.30.0
|
||||||
|
gorm.io/driver/postgres v1.5.11
|
||||||
|
gorm.io/gorm v1.25.12
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/MicahParks/keyfunc/v2 v2.1.0 // indirect
|
||||||
|
github.com/andybalholm/brotli v1.1.1 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
|
github.com/jackc/pgx/v5 v5.7.1 // indirect
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
|
github.com/klauspost/compress v1.17.11 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
|
github.com/stretchr/testify v1.10.0 // indirect
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
|
github.com/valyala/fasthttp v1.57.0 // indirect
|
||||||
|
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||||
|
golang.org/x/sync v0.10.0 // indirect
|
||||||
|
golang.org/x/sys v0.28.0 // indirect
|
||||||
|
golang.org/x/text v0.21.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"desq.com.ru/testjwtauth/config"
|
||||||
|
"desq.com.ru/testjwtauth/models"
|
||||||
|
"desq.com.ru/testjwtauth/utils"
|
||||||
|
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CreateToken(DB *gorm.DB, userID uuid.UUID, IP string, expires int64) (string, string, error) {
|
||||||
|
RT, err := utils.GenerateToken(32)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", fmt.Errorf("failed to generate refresh token")
|
||||||
|
}
|
||||||
|
|
||||||
|
claims := jwt.MapClaims{
|
||||||
|
"userId": userID,
|
||||||
|
"ip": IP,
|
||||||
|
"sub": RT,
|
||||||
|
"exp": expires,
|
||||||
|
}
|
||||||
|
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS512, claims)
|
||||||
|
|
||||||
|
t, err := token.SignedString([]byte(config.JWTSecret))
|
||||||
|
if err != nil {
|
||||||
|
return "", "", fmt.Errorf("failed to sign token")
|
||||||
|
}
|
||||||
|
|
||||||
|
RTEncoded, err := utils.EncodeToken(RT)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", fmt.Errorf("failed to encode refresh token")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := DB.Create(&models.RefreshToken{
|
||||||
|
UserID: userID,
|
||||||
|
Token: RTEncoded,
|
||||||
|
Valid: true,
|
||||||
|
}).Error; err != nil {
|
||||||
|
return "", "", fmt.Errorf("failed to create refresh token")
|
||||||
|
}
|
||||||
|
|
||||||
|
return t, RT, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func RefreshToken(DB *gorm.DB, userID uuid.UUID, claims jwt.MapClaims, RT string, IP string) error {
|
||||||
|
if claims["sub"].(string) != RT {
|
||||||
|
return fmt.Errorf("refresh token does not belong to this token")
|
||||||
|
}
|
||||||
|
|
||||||
|
validRTs := []models.RefreshToken{}
|
||||||
|
if err := DB.Where("user_id = ? AND valid = ?", userID, true).Find(&validRTs).Error; err != nil {
|
||||||
|
return fmt.Errorf("failed to retrieve valid tokens")
|
||||||
|
}
|
||||||
|
|
||||||
|
var matchedRT *models.RefreshToken
|
||||||
|
for i := range validRTs {
|
||||||
|
if utils.CompareToken(validRTs[i].Token, RT) {
|
||||||
|
matchedRT = &validRTs[i]
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if matchedRT == nil || !matchedRT.Valid {
|
||||||
|
return fmt.Errorf("token is invalid")
|
||||||
|
}
|
||||||
|
|
||||||
|
matchedRT.Valid = false
|
||||||
|
if err := DB.Save(&matchedRT).Error; err != nil {
|
||||||
|
return fmt.Errorf("failed to invalidate refresh token")
|
||||||
|
}
|
||||||
|
|
||||||
|
if claims["ip"].(string) != IP {
|
||||||
|
if err := utils.NotifyEmail(IP, config.MockSMTPRecipient); err != nil {
|
||||||
|
fmt.Printf("Failed to send email: %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
c "desq.com.ru/testjwtauth/config"
|
||||||
|
"desq.com.ru/testjwtauth/models"
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/driver/postgres"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCreateToken(t *testing.T) {
|
||||||
|
DB, err := gorm.Open(postgres.Open(c.DSN), &gorm.Config{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to connect to database: %v", err)
|
||||||
|
}
|
||||||
|
DB.AutoMigrate(&models.RefreshToken{})
|
||||||
|
userID := uuid.New()
|
||||||
|
IP := "127.0.0.1"
|
||||||
|
expires := time.Now().Add(time.Hour * 24).Unix()
|
||||||
|
type args struct {
|
||||||
|
DB *gorm.DB
|
||||||
|
userID uuid.UUID
|
||||||
|
IP string
|
||||||
|
expires int64
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "CreateToken",
|
||||||
|
args: args{
|
||||||
|
DB: DB,
|
||||||
|
userID: userID,
|
||||||
|
IP: IP,
|
||||||
|
expires: expires,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
_, _, err := CreateToken(tt.args.DB, tt.args.userID, tt.args.IP, tt.args.expires)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("CreateToken() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRefreshToken(t *testing.T) {
|
||||||
|
DB, err := gorm.Open(postgres.Open(c.DSN), &gorm.Config{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to connect to database: %v", err)
|
||||||
|
}
|
||||||
|
DB.AutoMigrate(&models.RefreshToken{})
|
||||||
|
userID := uuid.New()
|
||||||
|
IP := "127.0.0.1"
|
||||||
|
expires := time.Now().Add(time.Hour * 24)
|
||||||
|
_, RT, err := CreateToken(DB, userID, IP, expires.Unix())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to generate token pair: %v", err)
|
||||||
|
}
|
||||||
|
claims := jwt.MapClaims{
|
||||||
|
"userId": userID,
|
||||||
|
"ip": IP,
|
||||||
|
"sub": RT,
|
||||||
|
"exp": expires,
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
DB *gorm.DB
|
||||||
|
userID uuid.UUID
|
||||||
|
claims jwt.MapClaims
|
||||||
|
RT string
|
||||||
|
IP string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "RefreshToken",
|
||||||
|
args: args{
|
||||||
|
DB: DB,
|
||||||
|
userID: userID,
|
||||||
|
claims: claims,
|
||||||
|
RT: RT,
|
||||||
|
IP: IP,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if err := RefreshToken(tt.args.DB, tt.args.userID, tt.args.claims, tt.args.RT, tt.args.IP); (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("RefreshToken() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"desq.com.ru/testjwtauth/config"
|
||||||
|
"desq.com.ru/testjwtauth/routes"
|
||||||
|
|
||||||
|
jwtware "github.com/gofiber/contrib/jwt"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
DB, err := initDB()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to estabilish database connection: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
app := fiber.New()
|
||||||
|
|
||||||
|
app.Use(func(c *fiber.Ctx) error {
|
||||||
|
c.Locals("db", DB)
|
||||||
|
|
||||||
|
return c.Next()
|
||||||
|
})
|
||||||
|
|
||||||
|
app.Post("/me/:id/token", routes.CreateToken)
|
||||||
|
app.Use(jwtware.New(jwtware.Config{
|
||||||
|
SigningKey: jwtware.SigningKey{Key: []byte(config.JWTSecret)},
|
||||||
|
}))
|
||||||
|
app.Post("/me/token/refresh", routes.RefreshToken)
|
||||||
|
|
||||||
|
app.Listen(":8080")
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RefreshToken struct {
|
||||||
|
ID int
|
||||||
|
UserID uuid.UUID
|
||||||
|
Token string
|
||||||
|
Valid bool
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"desq.com.ru/testjwtauth/handlers"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setRTCookie(c *fiber.Ctx, RT string, expires time.Time) {
|
||||||
|
c.Cookie(&fiber.Cookie{
|
||||||
|
Name: "refreshToken",
|
||||||
|
Value: RT,
|
||||||
|
HTTPOnly: true,
|
||||||
|
Expires: expires,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateToken(c *fiber.Ctx) error {
|
||||||
|
DB := c.Locals("db").(*gorm.DB)
|
||||||
|
|
||||||
|
uID := c.Params("id")
|
||||||
|
userID, err := uuid.Parse(uID)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(500).SendString(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
expires := time.Now().Add(time.Hour * 24)
|
||||||
|
|
||||||
|
T, RT, err := handlers.CreateToken(DB, userID, c.IP(), expires.Unix())
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(500).SendString(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
setRTCookie(c, RT, expires)
|
||||||
|
|
||||||
|
return c.Status(201).JSON(fiber.Map{
|
||||||
|
"token": T,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func RefreshToken(c *fiber.Ctx) error {
|
||||||
|
DB := c.Locals("db").(*gorm.DB)
|
||||||
|
claims := c.Locals("user").(*jwt.Token).Claims.(jwt.MapClaims)
|
||||||
|
|
||||||
|
RT := c.Cookies("refreshToken")
|
||||||
|
if RT == "" {
|
||||||
|
return c.Status(400).SendString("Refresh cookie must be set")
|
||||||
|
}
|
||||||
|
|
||||||
|
uID := claims["userId"].(string)
|
||||||
|
|
||||||
|
userID, err := uuid.Parse(uID)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(500).SendString(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
err = handlers.RefreshToken(DB, userID, claims, RT, c.IP())
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(401).SendString(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
expires := time.Now().Add(time.Hour * 24)
|
||||||
|
|
||||||
|
t, RT, err := handlers.CreateToken(DB, userID, c.IP(), expires.Unix())
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(500).SendString(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
setRTCookie(c, RT, expires)
|
||||||
|
|
||||||
|
return c.Status(200).JSON(fiber.Map{
|
||||||
|
"token": t,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"desq.com.ru/testjwtauth/config"
|
||||||
|
"desq.com.ru/testjwtauth/handlers"
|
||||||
|
"desq.com.ru/testjwtauth/models"
|
||||||
|
jwtware "github.com/gofiber/contrib/jwt"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"gorm.io/driver/postgres"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setupFiber(t *testing.T) *fiber.App {
|
||||||
|
DB, err := gorm.Open(postgres.Open(config.DSN), &gorm.Config{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to connect to database: %v", err)
|
||||||
|
}
|
||||||
|
DB.AutoMigrate(&models.RefreshToken{})
|
||||||
|
app := fiber.New()
|
||||||
|
app.Use(func(c *fiber.Ctx) error {
|
||||||
|
c.Locals("db", DB)
|
||||||
|
|
||||||
|
return c.Next()
|
||||||
|
})
|
||||||
|
app.Post("/me/:id/token", CreateToken)
|
||||||
|
app.Use(jwtware.New(jwtware.Config{
|
||||||
|
SigningKey: jwtware.SigningKey{Key: []byte(config.JWTSecret)},
|
||||||
|
}))
|
||||||
|
app.Post("/me/token/refresh", RefreshToken)
|
||||||
|
|
||||||
|
return app
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateToken(t *testing.T) {
|
||||||
|
userID := uuid.New().String()
|
||||||
|
tests := []struct {
|
||||||
|
description string
|
||||||
|
route string
|
||||||
|
expectedError bool
|
||||||
|
expectedCode int
|
||||||
|
expectedBody string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
description: "CreateTokenRoute",
|
||||||
|
route: "/me/" + userID + "/token",
|
||||||
|
expectedError: false,
|
||||||
|
expectedCode: 201,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
app := setupFiber(t)
|
||||||
|
for _, test := range tests {
|
||||||
|
req, _ := http.NewRequest("POST", test.route, nil)
|
||||||
|
res, err := app.Test(req, -1)
|
||||||
|
assert.Equalf(t, test.expectedError, err != nil, test.description)
|
||||||
|
if test.expectedError {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
assert.Equalf(t, test.expectedCode, res.StatusCode, test.description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRefreshToken(t *testing.T) {
|
||||||
|
DB, err := gorm.Open(postgres.Open(config.DSN), &gorm.Config{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to connect to database: %v", err)
|
||||||
|
}
|
||||||
|
userID := uuid.New()
|
||||||
|
IP := "127.0.0.1"
|
||||||
|
expires := time.Now().Add(time.Hour * 24)
|
||||||
|
token, RT, err := handlers.CreateToken(DB, userID, IP, expires.Unix())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to generate token pair: %v", err)
|
||||||
|
}
|
||||||
|
RTCookie := &http.Cookie{
|
||||||
|
Name: "refreshToken",
|
||||||
|
Value: RT,
|
||||||
|
Path: "/",
|
||||||
|
HttpOnly: true,
|
||||||
|
Expires: expires,
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
description string
|
||||||
|
route string
|
||||||
|
RTCookie *http.Cookie
|
||||||
|
token string
|
||||||
|
expectedError bool
|
||||||
|
expectedCode int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
description: "WithNoRefreshToken",
|
||||||
|
route: "/me/token/refresh",
|
||||||
|
RTCookie: &http.Cookie{},
|
||||||
|
token: token,
|
||||||
|
expectedError: false,
|
||||||
|
expectedCode: 400,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "WithNoToken",
|
||||||
|
route: "/me/token/refresh",
|
||||||
|
RTCookie: RTCookie,
|
||||||
|
token: "",
|
||||||
|
expectedError: false,
|
||||||
|
expectedCode: 400,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "WithInvalidToken",
|
||||||
|
route: "/me/token/refresh",
|
||||||
|
RTCookie: RTCookie,
|
||||||
|
token: "InvalidToken",
|
||||||
|
expectedError: false,
|
||||||
|
expectedCode: 401,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "WithValidTokenPair",
|
||||||
|
route: "/me/token/refresh",
|
||||||
|
RTCookie: RTCookie,
|
||||||
|
token: token,
|
||||||
|
expectedError: false,
|
||||||
|
expectedCode: 200,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
app := setupFiber(t)
|
||||||
|
for _, test := range tests {
|
||||||
|
req, _ := http.NewRequest("POST", test.route, nil)
|
||||||
|
req.Header.Add("Authorization", "Bearer "+test.token)
|
||||||
|
req.AddCookie(test.RTCookie)
|
||||||
|
res, err := app.Test(req, -1)
|
||||||
|
assert.Equalf(t, test.expectedError, err != nil, test.description)
|
||||||
|
if test.expectedError {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
assert.Equalf(t, test.expectedCode, res.StatusCode, test.description)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/smtp"
|
||||||
|
|
||||||
|
c "desq.com.ru/testjwtauth/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NotifyEmail(m string, r string) error {
|
||||||
|
a := smtp.PlainAuth("", c.SMTPEmail, c.SMTPPassword, c.SMTPHost)
|
||||||
|
|
||||||
|
message := "From: " + c.SMTPEmail + "\n" +
|
||||||
|
"To: " + r + "\n" +
|
||||||
|
"Subject: Access from an unknown IP\n\n" +
|
||||||
|
m
|
||||||
|
|
||||||
|
if err := smtp.SendMail(fmt.Sprintf("%s:%d", c.SMTPHost, 587), a, c.SMTPEmail, []string{r}, []byte(message)); err != nil {
|
||||||
|
return fmt.Errorf("failed to send email: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"desq.com.ru/testjwtauth/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNotifyEmail(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
m string
|
||||||
|
r string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "WithValidEmail",
|
||||||
|
args: args{
|
||||||
|
m: "TestNotifyEmail",
|
||||||
|
r: config.MockSMTPRecipient,
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "WithInvalidEmail",
|
||||||
|
args: args{
|
||||||
|
m: "TestNotifyEmail",
|
||||||
|
r: "",
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if err := NotifyEmail(tt.args.m, tt.args.r); (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("NotifyEmail() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GenerateToken(l int) (string, error) {
|
||||||
|
bytes := make([]byte, l)
|
||||||
|
|
||||||
|
_, err := rand.Read(bytes)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
token := base64.StdEncoding.EncodeToString(bytes)
|
||||||
|
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func EncodeToken(token string) (string, error) {
|
||||||
|
encodedToken, err := bcrypt.GenerateFromPassword([]byte(token), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(encodedToken), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CompareToken(hash string, token string) bool {
|
||||||
|
eq := bcrypt.CompareHashAndPassword([]byte(hash), []byte(token))
|
||||||
|
|
||||||
|
return eq == nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGenerateToken(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
l int
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "GenerateRefreshToken",
|
||||||
|
args: args{
|
||||||
|
l: 32,
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
_, err := GenerateToken(tt.args.l)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("GenerateToken() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeToken(t *testing.T) {
|
||||||
|
token, err := GenerateToken(32)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to generate refresh token")
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
token string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "EncodeToken",
|
||||||
|
args: args{
|
||||||
|
token: token,
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
_, err := EncodeToken(tt.args.token)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("EncodeToken() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user