-
-
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 for 2024 (#84)
* feat(backend): nocodb client * feat(backend): realign structure, move to chi * feat(backend): implement UpdateTableRecords for nocodb * feat(backend): start to move database implementation to nocodb * feat(backend): rework ticketing to use nocodb * feat(backend): rework mail blast model * ci: move trufflehog as separate job * chore(backend): update lock file * fix(backend): wrong sprintf format * test(backend): fix test for nocodb related
- Loading branch information
Showing
57 changed files
with
3,646 additions
and
2,463 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
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 | ||
} |
Oops, something went wrong.