add auth package

This commit is contained in:
Dawid Wysokiński 2021-03-06 09:32:15 +01:00
parent fdb934145a
commit 575479f236
6 changed files with 214 additions and 0 deletions

1
go.mod
View File

@ -4,6 +4,7 @@ go 1.16
require (
github.com/99designs/gqlgen v0.13.0
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/gin-contrib/cors v1.3.1
github.com/gin-gonic/gin v1.6.3
github.com/go-pg/pg/v10 v10.7.7

2
go.sum
View File

@ -13,6 +13,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:ma
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/trifles v0.0.0-20190318185328-a8d75aae118c/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=

View File

@ -0,0 +1,109 @@
package jwt
import (
"fmt"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/pkg/errors"
)
type Credentials struct {
Email string
Password string
}
type Metadata struct {
StaySignedIn bool
Credentials Credentials
}
func (opts Metadata) ToMapClaims() jwt.MapClaims {
mClaims := jwt.MapClaims{}
mClaims["email"] = opts.Credentials.Email
mClaims["password"] = opts.Credentials.Password
mClaims["stay_signed_in"] = opts.StaySignedIn
return mClaims
}
type TokenGenerator interface {
Generate(metadata Metadata) (string, error)
ExtractAccessTokenMetadata(token string) (*Metadata, error)
}
type tokenGenerator struct {
accessSecret string
}
func NewTokenGenerator(accessSecret string) TokenGenerator {
return &tokenGenerator{
accessSecret: accessSecret,
}
}
func (g *tokenGenerator) Generate(metadata Metadata) (string, error) {
atClaims := metadata.ToMapClaims()
if !metadata.StaySignedIn {
atClaims["exp"] = time.Now().Add(time.Hour * 24).Unix()
}
at := jwt.NewWithClaims(jwt.SigningMethodHS256, atClaims)
accessToken, err := at.SignedString([]byte(g.accessSecret))
if err != nil {
return "", errors.Wrap(err, "TokenGenerator.Generate")
}
return accessToken, nil
}
func (g *tokenGenerator) ExtractAccessTokenMetadata(token string) (*Metadata, error) {
return extractTokenMetadata(g.accessSecret, token)
}
func verifyToken(secret string, tokenString string) (*jwt.Token, error) {
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
//Make sure that the token method conform to "SigningMethodHMAC"
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(secret), nil
})
if err != nil {
return nil, errors.Wrap(err, "verifyToken")
}
if !token.Valid {
return nil, fmt.Errorf("Token is invalid")
}
return token, nil
}
func extractTokenMetadata(secret, tokenString string) (*Metadata, error) {
token, err := verifyToken(secret, tokenString)
if err != nil {
return nil, err
}
claims, ok := token.Claims.(jwt.MapClaims)
if ok {
staySignedIn, ok := claims["stay_signed_in"].(bool)
if !ok {
return nil, fmt.Errorf("Invalid token payload (staySignedIn should be a boolean)")
}
email, ok := claims["email"].(string)
if !ok {
return nil, fmt.Errorf("Invalid token payload (email should be a string)")
}
password, ok := claims["password"].(string)
if !ok {
return nil, fmt.Errorf("Invalid token payload (password should be a string)")
}
return &Metadata{
StaySignedIn: staySignedIn,
Credentials: Credentials{
Email: email,
Password: password,
},
}, nil
}
return nil, fmt.Errorf("Cannot extract token metadata")
}

12
internal/auth/usecase.go Normal file
View File

@ -0,0 +1,12 @@
package auth
import (
"context"
"github.com/zdam-egzamin-zawodowy/backend/internal/models"
)
type Usecase interface {
SignIn(ctx context.Context, email, password string, staySignedIn bool) (*models.User, string, error)
ExtractAccessTokenMetadata(ctx context.Context, accessToken string) (*models.User, error)
}

View File

@ -0,0 +1,6 @@
package usecase
const (
messageInvalidCredentials = "Niepoprawny email/hasło."
messageInvalidAccessToken = "Niepoprawny token."
)

View File

@ -0,0 +1,84 @@
package usecase
import (
"context"
"fmt"
"github.com/zdam-egzamin-zawodowy/backend/internal/auth"
"github.com/zdam-egzamin-zawodowy/backend/internal/auth/jwt"
"github.com/zdam-egzamin-zawodowy/backend/internal/models"
"github.com/zdam-egzamin-zawodowy/backend/internal/user"
errorutils "github.com/zdam-egzamin-zawodowy/backend/pkg/utils/error"
)
type usecase struct {
userRepository user.Repository
tokenGenerator jwt.TokenGenerator
}
type Config struct {
UserRepository user.Repository
TokenGenerator jwt.TokenGenerator
}
func New(cfg *Config) (auth.Usecase, error) {
if cfg == nil || cfg.UserRepository == nil {
return nil, fmt.Errorf("user/usecase: UserRepository is required")
}
return &usecase{
cfg.UserRepository,
cfg.TokenGenerator,
}, nil
}
func (ucase *usecase) SignIn(ctx context.Context, email, password string, staySignedIn bool) (*models.User, string, error) {
user, err := ucase.GetUserByCredentials(ctx, email, password)
if err != nil {
return nil, "", err
}
token, err := ucase.tokenGenerator.Generate(jwt.Metadata{
StaySignedIn: staySignedIn,
Credentials: jwt.Credentials{
Email: user.Email,
Password: user.Password,
},
})
if err != nil {
return nil, "", errorutils.Wrap(err, messageInvalidCredentials)
}
return user, token, nil
}
func (ucase *usecase) ExtractAccessTokenMetadata(ctx context.Context, accessToken string) (*models.User, error) {
metadata, err := ucase.tokenGenerator.ExtractAccessTokenMetadata(accessToken)
if err != nil {
return nil, errorutils.Wrap(err, messageInvalidAccessToken)
}
return ucase.GetUserByCredentials(ctx, metadata.Credentials.Email, metadata.Credentials.Password)
}
func (ucase *usecase) GetUserByCredentials(ctx context.Context, email, password string) (*models.User, error) {
users, _, err := ucase.userRepository.Fetch(ctx, &user.FetchConfig{
Limit: 1,
Count: false,
Filter: &models.UserFilter{
Email: []string{email},
},
})
if err != nil {
return nil, err
}
if len(users) <= 0 {
return nil, fmt.Errorf(messageInvalidCredentials)
}
user := users[0]
if err := user.CompareHashAndPassword(password); err != nil {
return nil, fmt.Errorf(messageInvalidCredentials)
}
return user, nil
}