diff --git a/.gitignore b/.gitignore index 9535c09..e9c9d70 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /dist /uploads -/default.etcd \ No newline at end of file +/default.etcd +/keys diff --git a/go.mod b/go.mod index 80703b7..ab97beb 100644 --- a/go.mod +++ b/go.mod @@ -39,6 +39,7 @@ require ( github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v5 v5.2.1 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/consul/api v1.30.0 // indirect diff --git a/go.sum b/go.sum index 897bfc2..b246939 100644 --- a/go.sum +++ b/go.sum @@ -69,6 +69,8 @@ github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtg github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= diff --git a/pkg/internal/auth/jwt.go b/pkg/internal/auth/jwt.go deleted file mode 100644 index 8832b06..0000000 --- a/pkg/internal/auth/jwt.go +++ /dev/null @@ -1 +0,0 @@ -package auth diff --git a/pkg/internal/auth/token.go b/pkg/internal/auth/token.go new file mode 100644 index 0000000..3b7b9b7 --- /dev/null +++ b/pkg/internal/auth/token.go @@ -0,0 +1,55 @@ +package auth + +import ( + "git.solsynth.dev/hypernet/nexus/pkg/nex/sec" + "github.com/gofiber/fiber/v2" + "strings" +) + +var JReader *sec.JwtReader + +func SoftAuthMiddleware(c *fiber.Ctx) error { + atk := tokenExtract(c) + c.Locals("nex_token", atk) + + if claims, err := tokenRead(atk); err == nil && claims != nil { + c.Locals("nex_principal", claims) + } else if err != nil { + c.Locals("nex_auth_error", err) + } + + return c.Next() +} + +func HardAuthMiddleware(c *fiber.Ctx) error { + if c.Locals("nex_principal") == nil { + err := c.Locals("nex_auth_error").(error) + return fiber.NewError(fiber.StatusUnauthorized, err.Error()) + } + + return c.Next() +} + +func tokenExtract(c *fiber.Ctx) string { + var atk string + if cookie := c.Cookies(sec.CookieAccessToken); len(cookie) > 0 { + atk = cookie + } + if header := c.Get(fiber.HeaderAuthorization); len(header) > 0 { + tk := strings.Replace(header, "Bearer", "", 1) + atk = strings.TrimSpace(tk) + } + if tk := c.Query("tk"); len(tk) > 0 { + atk = strings.TrimSpace(tk) + } + return atk +} + +func tokenRead(in string) (*sec.JwtClaims, error) { + if JReader == nil { + return nil, nil + } + + claims, err := sec.ReadJwt[sec.JwtClaims](JReader, in) + return &claims, err +} diff --git a/pkg/main.go b/pkg/main.go index 67607db..3e0844d 100644 --- a/pkg/main.go +++ b/pkg/main.go @@ -2,9 +2,11 @@ package main import ( "fmt" + "git.solsynth.dev/hypernet/nexus/pkg/internal/auth" "git.solsynth.dev/hypernet/nexus/pkg/internal/database" "git.solsynth.dev/hypernet/nexus/pkg/internal/http" "git.solsynth.dev/hypernet/nexus/pkg/internal/kv" + "git.solsynth.dev/hypernet/nexus/pkg/nex/sec" "github.com/fatih/color" "os" "os/signal" @@ -70,6 +72,14 @@ func main() { } } + // Read the public key for jwt + if reader, err := sec.NewJwtReader(viper.GetString("security.public_key")); err != nil { + log.Error().Err(err).Msg("An error occurred when reading public key for jwt. Authentication related features will be disabled.") + } else { + auth.JReader = reader + log.Info().Msg("Jwt public key loaded.") + } + // Server go server.NewServer().Listen() diff --git a/pkg/nex/README.md b/pkg/nex/README.md new file mode 100644 index 0000000..8ec8bbc --- /dev/null +++ b/pkg/nex/README.md @@ -0,0 +1,15 @@ +# Nex + +The Hypernet.Nexus development SDK. +Defined the useful functions and ways to handle data for both server side and client. + +## Parts + +### Nex.Cruda + +Create Read Update Delete Accelerator, aka. Cruda. +Cruda will help you to build a simplified database access layer based on the command system in nexus. + +### Nex.Sec + +The security part of nexus, including signing and validating the tokens and much more. \ No newline at end of file diff --git a/pkg/nex/sec/const.go b/pkg/nex/sec/const.go new file mode 100644 index 0000000..ce8745c --- /dev/null +++ b/pkg/nex/sec/const.go @@ -0,0 +1,12 @@ +package sec + +const ( + CookieAccessToken = "nex_atk" + CookieRefreshToken = "nex_rtk" +) + +const ( + TokenTypeAccess = "access_token" + RefreshTokenType = "refresh_token" + IdTokenType = "id_token" +) diff --git a/pkg/nex/sec/jwt_claims.go b/pkg/nex/sec/jwt_claims.go new file mode 100644 index 0000000..e0dcb45 --- /dev/null +++ b/pkg/nex/sec/jwt_claims.go @@ -0,0 +1,27 @@ +package sec + +import ( + "github.com/golang-jwt/jwt/v5" + "time" +) + +type JwtClaims struct { + jwt.RegisteredClaims + + // Nexus Standard + Session int `json:"sed"` + CacheTTL time.Duration `json:"ttl,omitempty"` + + // OIDC Standard + Name string `json:"name,omitempty"` + Nick string `json:"preferred_username,omitempty"` + Email string `json:"email,omitempty"` + + // OAuth2 Standard + AuthorizedParties string `json:"azp,omitempty"` + Nonce string `json:"nonce,omitempty"` + + // The usage of this token + // Can be access_token, refresh_token or id_token + Type string `json:"typ"` +} diff --git a/pkg/nex/sec/jwt_reader.go b/pkg/nex/sec/jwt_reader.go new file mode 100644 index 0000000..bf12852 --- /dev/null +++ b/pkg/nex/sec/jwt_reader.go @@ -0,0 +1,61 @@ +package sec + +import ( + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "fmt" + "github.com/golang-jwt/jwt/v5" + "os" +) + +type JwtReader struct { + key *rsa.PublicKey +} + +func NewJwtReader(fp string) (*JwtReader, error) { + privateKeyBytes, err := os.ReadFile(fp) + if err != nil { + return nil, err + } + + block, _ := pem.Decode(privateKeyBytes) + if block == nil || block.Type != "PUBLIC KEY" { + return nil, fmt.Errorf("failed to decode PEM block containing private key") + } + + anyPk, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + return nil, err + } + + pk, ok := anyPk.(*rsa.PublicKey) + if !ok { + return nil, fmt.Errorf("not an RSA public key") + } + + return &JwtReader{ + key: pk, + }, nil +} + +func ReadJwt[T jwt.Claims](v *JwtReader, in string) (T, error) { + var out T + token, err := jwt.Parse(in, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + return v.key, nil + }) + if err != nil { + return out, err + } else if !token.Valid { + return out, fmt.Errorf("token is not valid") + } + + if claims, ok := token.Claims.(T); ok { + return claims, nil + } else { + return out, err + } +} diff --git a/pkg/nex/sec/jwt_writer.go b/pkg/nex/sec/jwt_writer.go new file mode 100644 index 0000000..9743fc8 --- /dev/null +++ b/pkg/nex/sec/jwt_writer.go @@ -0,0 +1,49 @@ +package sec + +import ( + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "fmt" + "github.com/golang-jwt/jwt/v5" + "os" +) + +type JwtWriter struct { + key *rsa.PrivateKey +} + +func NewJwtWriter(fp string) (*JwtWriter, error) { + rawPk, err := os.ReadFile(fp) + if err != nil { + return nil, err + } + + block, _ := pem.Decode(rawPk) + if block == nil || block.Type != "PRIVATE KEY" { + return nil, fmt.Errorf("failed to decode PEM block containing private key") + } + + anyPk, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + return nil, err + } + + pk, ok := anyPk.(*rsa.PrivateKey) + if !ok { + return nil, fmt.Errorf("not an RSA private key") + } + + return &JwtWriter{ + key: pk, + }, nil +} + +func WriteJwt[T jwt.Claims](v *JwtWriter, in T) (string, error) { + token := jwt.NewWithClaims(jwt.SigningMethodRS256, in) + ss, err := token.SignedString(v.key) + if err != nil { + return "", err + } + return ss, nil +} diff --git a/settings.toml b/settings.toml index a4b3c27..6d88a9e 100644 --- a/settings.toml +++ b/settings.toml @@ -1,25 +1,11 @@ bind = "0.0.0.0:8001" grpc_bind = "0.0.0.0:7001" domain = "localhost" -secret = "LtTjzAGFLshwXhN4ZD4nG5KlMv1MWcsvfv03TSZYnT1VhiAnLIZFTnHUwR0XhGgi" [debug] database = false print_routes = false -[mailer] -name = "Alphabot " -smtp_host = "smtp.exmail.qq.com" -smtp_port = 465 -username = "alphabot@smartsheep.studio" -password = "gz937Zxxzfcd9SeH" - -[security] -cookie_domain = "localhost" -cookie_samesite = "Lax" -access_token_duration = 300 -refresh_token_duration = 2592000 - [services] aliases = { id = "auth", uc = "files", co = "interactive", im = "messaging" } @@ -30,5 +16,5 @@ prefix = "sn_" [kv] endpoints = ["localhost:2379"] -[scraper] -user-agent = "SolarBot/1.0" \ No newline at end of file +[security] +public_key = "keys/public_key.pem"