-
-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(backend): rework mail blast model
- Loading branch information
Showing
25 changed files
with
955 additions
and
710 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
package administrator | ||
|
||
import ( | ||
"crypto/ed25519" | ||
"crypto/rand" | ||
"encoding/hex" | ||
"fmt" | ||
|
||
"conf/administrator/jwt" | ||
"github.com/pquerna/otp/totp" | ||
) | ||
|
||
type Administrator struct { | ||
Username string `yaml:"username"` | ||
HashedPassword string `yaml:"hashed_password"` | ||
TotpSecret string `yaml:"totp_secret"` | ||
} | ||
|
||
func GenerateSecret(username string) (secret string, url string, err error) { | ||
generate, err := totp.Generate(totp.GenerateOpts{Issuer: "teknumconf", AccountName: username, Rand: rand.Reader}) | ||
if err != nil { | ||
return "", "", err | ||
} | ||
|
||
return generate.Secret(), generate.URL(), nil | ||
} | ||
|
||
type AdministratorDomain struct { | ||
jwt *jwt.JsonWebToken | ||
administrators []Administrator | ||
} | ||
|
||
func NewAdministratorDomain(administrators []Administrator) (*AdministratorDomain, error) { | ||
// Generate ed25519 key pairs for access and refresh tokens | ||
accessPublicKey, accessPrivateKey, err := ed25519.GenerateKey(nil) | ||
if err != nil { | ||
return nil, fmt.Errorf("generating fresh access key pair: %w", err) | ||
} | ||
|
||
refreshPublicKey, refreshPrivateKey, err := ed25519.GenerateKey(nil) | ||
if err != nil { | ||
return nil, fmt.Errorf("generating fresh refresh key pair: %w", err) | ||
} | ||
|
||
var randomIssuer = make([]byte, 18) | ||
_, _ = rand.Read(randomIssuer) | ||
|
||
var randomSubject = make([]byte, 16) | ||
_, _ = rand.Read(randomSubject) | ||
|
||
var randomAudience = make([]byte, 32) | ||
_, _ = rand.Read(randomAudience) | ||
|
||
authJwt := jwt.NewJwt(accessPrivateKey, accessPublicKey, refreshPrivateKey, refreshPublicKey, hex.EncodeToString(randomIssuer), hex.EncodeToString(randomSubject), hex.EncodeToString(randomAudience)) | ||
|
||
return &AdministratorDomain{ | ||
jwt: authJwt, | ||
administrators: administrators, | ||
}, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
package administrator | ||
|
||
import ( | ||
"context" | ||
"encoding/hex" | ||
"errors" | ||
"fmt" | ||
|
||
"github.com/getsentry/sentry-go" | ||
"github.com/pquerna/otp/totp" | ||
"golang.org/x/crypto/bcrypt" | ||
) | ||
|
||
func (a *AdministratorDomain) Authenticate(ctx context.Context, username string, plainPassword string, otpCode string) (string, bool, error) { | ||
span := sentry.StartSpan(ctx, "administrator.authenticate", sentry.WithTransactionName("Authenticate")) | ||
defer span.Finish() | ||
|
||
var administrator Administrator | ||
for _, adm := range a.administrators { | ||
if adm.Username == username { | ||
administrator = adm | ||
break | ||
} | ||
} | ||
|
||
if administrator.Username == "" { | ||
return "", false, nil | ||
} | ||
|
||
hashedPassword, err := hex.DecodeString(administrator.HashedPassword) | ||
if err != nil { | ||
return "", false, fmt.Errorf("invalid hex string") | ||
} | ||
|
||
err = bcrypt.CompareHashAndPassword(hashedPassword, []byte(plainPassword)) | ||
if err != nil { | ||
if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) { | ||
return "", false, nil | ||
} | ||
|
||
return "", false, fmt.Errorf("password: %w", err) | ||
} | ||
|
||
ok := totp.Validate(otpCode, administrator.TotpSecret) | ||
if !ok { | ||
return "", false, nil | ||
} | ||
|
||
token, err := a.jwt.Sign(username) | ||
if err != nil { | ||
return "", false, fmt.Errorf("signing token: %w", err) | ||
} | ||
|
||
return token, true, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
package jwt | ||
|
||
import ( | ||
"crypto/ed25519" | ||
"crypto/rand" | ||
"errors" | ||
"fmt" | ||
"time" | ||
|
||
"github.com/golang-jwt/jwt/v4" | ||
) | ||
|
||
type JsonWebToken struct { | ||
accessPrivateKey ed25519.PrivateKey | ||
accessPublicKey ed25519.PublicKey | ||
refreshPrivateKey ed25519.PrivateKey | ||
refreshPublicKey ed25519.PublicKey | ||
issuer string | ||
subject string | ||
audience string | ||
} | ||
|
||
func NewJwt(accessPrivateKey []byte, accessPublicKey []byte, refreshPrivateKey []byte, refreshPublicKey []byte, issuer string, subject string, audience string) *JsonWebToken { | ||
return &JsonWebToken{ | ||
accessPrivateKey: accessPrivateKey, | ||
accessPublicKey: accessPublicKey, | ||
refreshPrivateKey: refreshPrivateKey, | ||
refreshPublicKey: refreshPublicKey, | ||
issuer: issuer, | ||
subject: subject, | ||
audience: audience, | ||
} | ||
} | ||
|
||
func (j *JsonWebToken) Sign(userId string) (accessToken string, err error) { | ||
accessRandId := make([]byte, 32) | ||
_, _ = rand.Read(accessRandId) | ||
|
||
accessClaims := jwt.MapClaims{ | ||
"iss": j.issuer, | ||
"sub": j.subject, | ||
"aud": j.audience, | ||
"exp": time.Now().Add(time.Hour * 1).Unix(), | ||
"nbf": time.Now().Unix(), | ||
"iat": time.Now().Unix(), | ||
"jti": string(accessRandId), | ||
"uid": userId, | ||
} | ||
|
||
accessToken, err = jwt.NewWithClaims(jwt.SigningMethodEdDSA, accessClaims).SignedString(j.accessPrivateKey) | ||
if err != nil { | ||
return "", fmt.Errorf("failed to sign access token: %w", err) | ||
} | ||
|
||
return accessToken, nil | ||
} | ||
|
||
var ErrInvalidSigningMethod = errors.New("invalid signing method") | ||
var ErrExpired = errors.New("token expired") | ||
var ErrInvalid = errors.New("token invalid") | ||
var ErrClaims = errors.New("token claims invalid") | ||
|
||
func (j *JsonWebToken) VerifyAccessToken(token string) (userId string, err error) { | ||
if token == "" { | ||
return "", ErrInvalid | ||
} | ||
|
||
parsedToken, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) { | ||
_, ok := t.Method.(*jwt.SigningMethodEd25519) | ||
if !ok { | ||
return nil, ErrInvalidSigningMethod | ||
} | ||
return j.accessPublicKey, nil | ||
}) | ||
if err != nil { | ||
if parsedToken != nil && !parsedToken.Valid { | ||
// Check if the error is a type of jwt.ValidationError | ||
validationError, ok := err.(*jwt.ValidationError) | ||
if ok { | ||
if validationError.Errors&jwt.ValidationErrorExpired != 0 { | ||
return "", ErrExpired | ||
} | ||
|
||
if validationError.Errors&jwt.ValidationErrorSignatureInvalid != 0 { | ||
return "", ErrInvalid | ||
} | ||
|
||
if validationError.Errors&jwt.ValidationErrorClaimsInvalid != 0 { | ||
return "", ErrClaims | ||
} | ||
|
||
return "", fmt.Errorf("failed to parse access token: %w", err) | ||
} | ||
|
||
return "", fmt.Errorf("non-validation error during parsing token: %w", err) | ||
} | ||
|
||
return "", fmt.Errorf("token is valid or parsedToken is not nil: %w", err) | ||
} | ||
|
||
claims, ok := parsedToken.Claims.(jwt.MapClaims) | ||
if !ok { | ||
return "", ErrClaims | ||
} | ||
|
||
if !claims.VerifyAudience(j.audience, true) { | ||
return "", ErrInvalid | ||
} | ||
|
||
if !claims.VerifyExpiresAt(time.Now().Unix(), true) { | ||
return "", ErrExpired | ||
} | ||
|
||
if !claims.VerifyIssuer(j.issuer, true) { | ||
return "", ErrInvalid | ||
} | ||
|
||
if !claims.VerifyNotBefore(time.Now().Unix(), true) { | ||
return "", ErrInvalid | ||
} | ||
|
||
jwtId, ok := claims["jti"].(string) | ||
if !ok { | ||
return "", ErrClaims | ||
} | ||
|
||
if jwtId == "" { | ||
return "", ErrClaims | ||
} | ||
|
||
userId, ok = claims["uid"].(string) | ||
if !ok { | ||
return "", ErrClaims | ||
} | ||
|
||
return userId, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
package jwt_test | ||
|
||
import ( | ||
"crypto/ed25519" | ||
"errors" | ||
"log" | ||
"os" | ||
"testing" | ||
|
||
"conf/administrator/jwt" | ||
) | ||
|
||
var authJwt *jwt.JsonWebToken | ||
|
||
func TestMain(m *testing.M) { | ||
// Generate ed25519 key pairs for access and refresh tokens | ||
accessPublicKey, accessPrivateKey, err := ed25519.GenerateKey(nil) | ||
if err != nil { | ||
log.Fatalf("failed to generate access key pair: %v", err) | ||
} | ||
|
||
refreshPublicKey, refreshPrivateKey, err := ed25519.GenerateKey(nil) | ||
if err != nil { | ||
log.Fatalf("failed to generate refresh key pair: %v", err) | ||
} | ||
|
||
authJwt = jwt.NewJwt(accessPrivateKey, accessPublicKey, refreshPrivateKey, refreshPublicKey, "kodiiing", "user", "kodiiing") | ||
|
||
exitCode := m.Run() | ||
|
||
os.Exit(exitCode) | ||
} | ||
|
||
func TestSign(t *testing.T) { | ||
accessToken, err := authJwt.Sign("john") | ||
if err != nil { | ||
t.Errorf("failed to sign access token: %v", err) | ||
} | ||
|
||
if accessToken == "" { | ||
t.Error("access token is empty") | ||
} | ||
} | ||
|
||
func TestVerify(t *testing.T) { | ||
accessToken, err := authJwt.Sign("john") | ||
if err != nil { | ||
t.Errorf("failed to sign access token: %v", err) | ||
} | ||
|
||
if accessToken == "" { | ||
t.Error("access token is empty") | ||
} | ||
|
||
accessId, err := authJwt.VerifyAccessToken(accessToken) | ||
if err != nil { | ||
t.Errorf("failed to verify access token: %v", err) | ||
} | ||
|
||
if accessId != "john" { | ||
t.Errorf("access id is not 'john': %v", accessId) | ||
} | ||
} | ||
|
||
func TestVerifyEmpty(t *testing.T) { | ||
accessId, err := authJwt.VerifyAccessToken("") | ||
if err == nil { | ||
t.Errorf("access token is valid: %v", accessId) | ||
} | ||
|
||
if !errors.Is(err, jwt.ErrInvalid) { | ||
t.Errorf("error is not ErrInvalid: %v", err) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
package administrator | ||
|
||
import ( | ||
"context" | ||
|
||
"github.com/getsentry/sentry-go" | ||
) | ||
|
||
func (a *AdministratorDomain) Validate(ctx context.Context, token string) (Administrator, bool, error) { | ||
span := sentry.StartSpan(ctx, "administrator.validate", sentry.WithTransactionName("Validate")) | ||
defer span.Finish() | ||
|
||
if token == "" { | ||
return Administrator{}, false, nil | ||
} | ||
|
||
username, err := a.jwt.VerifyAccessToken(token) | ||
if err != nil { | ||
return Administrator{}, false, nil | ||
} | ||
|
||
var administrator Administrator | ||
for _, adm := range a.administrators { | ||
if adm.Username == username { | ||
administrator = adm | ||
break | ||
} | ||
} | ||
|
||
return administrator, true, nil | ||
} |
Oops, something went wrong.