Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Server side auth #9

Draft
wants to merge 16 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
File renamed without changes.
4 changes: 3 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ LABEL owner="goserg" \
WORKDIR /app/server

COPY bin/server /app/server
COPY cert.pem /app/server
COPY key.pem /app/server

EXPOSE 3000
EXPOSE 9865

CMD ["/app/server/server"]
44 changes: 0 additions & 44 deletions Makefile

This file was deleted.

152 changes: 152 additions & 0 deletions auth/migrations/init.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
table "log" {
schema = schema.public
column "id" {
null = false
type = serial
}
column "user_id" {
null = false
type = uuid
}
column "action" {
null = false
type = character_varying(64)
}
column "status" {
null = false
type = text
}
column "time" {
null = false
type = timestamptz
}
primary_key {
columns = [column.id]
}
foreign_key "log_users_id_fk" {
columns = [column.user_id]
ref_columns = [table.users.column.id]
on_update = NO_ACTION
on_delete = NO_ACTION
}
}
table "roles" {
schema = schema.public
column "id" {
null = false
type = character_varying(16)
}
primary_key {
columns = [column.id]
}
}
table "tokens" {
schema = schema.public
column "id" {
null = false
type = bigserial
}
column "user_id" {
null = false
type = uuid
}
column "token" {
null = false
type = uuid
}
column "created_at" {
null = false
type = timestamptz
}
column "last_active_at" {
null = false
type = timestamptz
}
column "deleted_at" {
null = true
type = timestamptz
}
primary_key {
columns = [column.id]
}
foreign_key "tokens_users_id_fk" {
columns = [column.user_id]
ref_columns = [table.users.column.id]
on_update = NO_ACTION
on_delete = NO_ACTION
}
}
table "user_roles" {
schema = schema.public
column "user_id" {
null = false
type = uuid
}
column "role" {
null = false
type = character(16)
}
primary_key {
columns = [column.user_id]
}
foreign_key "user_roles_roles_id_fk" {
columns = [column.role]
ref_columns = [table.roles.column.id]
on_update = NO_ACTION
on_delete = NO_ACTION
}
foreign_key "user_roles_users_id_fk" {
columns = [column.user_id]
ref_columns = [table.users.column.id]
on_update = NO_ACTION
on_delete = NO_ACTION
}
}
table "users" {
schema = schema.public
column "id" {
null = false
type = uuid
}
column "username" {
null = false
type = character_varying(64)
}
column "email" {
null = false
type = character_varying(320)
}
column "password_hash" {
null = false
type = character(64)
}
column "password_salt" {
null = false
type = character(16)
}
column "created_at" {
null = false
type = timestamptz
}
column "updated_at" {
null = false
type = timestamptz
}
column "deleted_at" {
null = true
type = timestamptz
}
primary_key {
columns = [column.id]
}
index "users_unique_email" {
unique = true
columns = [column.email]
}
index "users_unique_username" {
unique = true
columns = [column.username]
}
}
schema "public" {
}
12 changes: 12 additions & 0 deletions auth/service/config.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package service

import "time"

type Config struct {
SqliteFile string `toml:"sqlite_file"`
Token string `toml:"token"`
Expand All @@ -14,4 +16,14 @@ type Config struct {
Allow []string `toml:"allow"`
Order int `toml:"order"`
} `toml:"rules"`
TokenTTL time.Duration `toml:"tokenTTL"`
Storage StorageConfig `toml:"db"`
}

type StorageConfig struct {
Host string `toml:"host"`
Port int `toml:"port"`
DBName string `toml:"dbname"`
Username string `toml:"username"`
Password string `toml:"password"`
}
82 changes: 19 additions & 63 deletions auth/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ import (
"github.com/goserg/ratingserver/auth/storage"
"github.com/goserg/ratingserver/auth/users"

"github.com/gofiber/fiber/v2"
"github.com/golang-jwt/jwt"
"github.com/google/uuid"
)

Expand Down Expand Up @@ -59,46 +57,18 @@ func New(ctx context.Context, cfg Config, storage storage.AuthStorage) (*Service
return &s, nil
}

func (s *Service) Login(ctx context.Context, name string, password string) (users.User, error) {
func (s *Service) Login(ctx context.Context, name string, password string) (token uuid.UUID, err error) {
userSecret, err := s.storage.GetUserSecret(ctx, users.User{Name: name})
if err != nil {
return users.User{}, err
return uuid.Nil, err
}
secret := generateSecret(password, s.cfg.PasswordPepper, userSecret.Salt)
if err != nil {
return users.User{}, err
return uuid.Nil, err
}
return s.storage.SignIn(ctx, name, secret.PasswordHash)
}

func (s *Service) GenerateJWTCookie(userID uuid.UUID, host string) (*fiber.Cookie, error) {
expiresIn, err := time.ParseDuration(s.cfg.Expiration)
if err != nil {
return nil, err
}
expirationTime := time.Now().Add(expiresIn)
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.StandardClaims{
ExpiresAt: expirationTime.Unix(),
IssuedAt: time.Now().Unix(),
Subject: userID.String(),
})
tokenString, err := token.SignedString([]byte(s.cfg.Token))
if err != nil {
return nil, err
}
return &fiber.Cookie{
Name: "token",
Value: tokenString,
Path: "/",
Domain: host,
Expires: expirationTime,
Secure: false,
HTTPOnly: true,
SameSite: "",
SessionOnly: false,
}, nil
}

func (s *Service) Auth(ctx context.Context, cookie string, method string, url string) (users.User, error) {
user, err := s.getUserFromToken(ctx, cookie)
if err != nil {
Expand All @@ -118,7 +88,7 @@ func (s *Service) Auth(ctx context.Context, cookie string, method string, url st
return user, nil
}
for _, userRole := range user.Roles {
if role == userRole {
if role == strings.TrimSpace(userRole) { // "admin " ??
return user, nil
}
}
Expand All @@ -133,44 +103,25 @@ func (s *Service) Auth(ctx context.Context, cookie string, method string, url st

func (s *Service) getUserFromToken(ctx context.Context, cookie string) (users.User, error) {
if cookie == "" {
return users.User{}, nil
return users.User{
Roles: []string{"user"},
}, nil
}
token, err := jwt.ParseWithClaims(cookie, &jwt.StandardClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(s.cfg.Token), nil
})
token, err := uuid.Parse(cookie)
if err != nil {
return users.User{}, err
}
if token.Valid {
claims, ok := token.Claims.(*jwt.StandardClaims)
if !ok {
return users.User{}, errors.New("bad request")
}
userID := claims.Subject
id, err := uuid.Parse(userID)
if err != nil {
return users.User{}, err
}
user, err := s.storage.GetUser(ctx, id)
if err != nil {
return users.User{}, err
}
return user, nil
}
ve := jwt.ValidationError{}
if ok := errors.As(err, &ve); !ok {
user, err := s.storage.Me(ctx, token)
if err != nil {
return users.User{}, err
}
if ve.Errors&jwt.ValidationErrorMalformed != 0 {
return users.User{}, errors.New("bad request")
} else if ve.Errors&(jwt.ValidationErrorExpired|jwt.ValidationErrorNotValidYet) != 0 {
return users.User{}, errors.New("token expired")
} else {
return users.User{}, err
if user.ID == uuid.Nil {
return user, ErrNotAuthorized
}
return user, nil
}

func (s *Service) SignUp(ctx context.Context, name string, password string) error {
func (s *Service) SignUp(ctx context.Context, name string, email string, password string) error {
salt, err := randomSalt()
if err != nil {
return err
Expand All @@ -179,6 +130,7 @@ func (s *Service) SignUp(ctx context.Context, name string, password string) erro
err = s.storage.CreateUser(ctx, users.User{
ID: uuid.New(),
Name: name,
Email: email,
Roles: []string{"user"},
RegisteredAt: time.Now(),
}, secret)
Expand All @@ -191,6 +143,10 @@ func (s *Service) SignUp(ctx context.Context, name string, password string) erro
return nil
}

func (s *Service) Logout(ctx context.Context, token uuid.UUID) error {
return s.storage.LogOut(ctx, token)
}

func randomSalt() ([]byte, error) {
salt := make([]byte, 8)
_, err := rand.Read(salt)
Expand Down
5 changes: 4 additions & 1 deletion auth/storage/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import (
type AuthStorage interface {
GetUserSecret(ctx context.Context, user users.User) (users.Secret, error)
CreateUser(ctx context.Context, user users.User, secret users.Secret) error
SignIn(ctx context.Context, name string, pwdHash []byte) (users.User, error)
SignIn(ctx context.Context, name string, pwdHash []byte) (token uuid.UUID, err error)
GetUser(ctx context.Context, id uuid.UUID) (users.User, error)

Me(ctx context.Context, token uuid.UUID) (users.User, error)
LogOut(ctx context.Context, token uuid.UUID) error
}
Loading