From 9706d6aa29f9a2a622fca31db330e11b73b589cb Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Tue, 15 Jul 2025 16:05:44 +0800 Subject: [PATCH 01/32] re-tag --- .github/workflows/deploy.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 3a977b78..1e9ecd9c 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -81,6 +81,10 @@ jobs: docker pull ghcr.io/oullin/oullin_api:$IMAGE_TAG docker pull ghcr.io/oullin/oullin_proxy:$IMAGE_TAG + echo "๐Ÿท๏ธ Retagging for Composeโ€ฆ" + docker tag ghcr.io/oullin/oullin_api:$IMAGE_TAG api-api:latest + docker tag ghcr.io/oullin/oullin_proxy:$IMAGE_TAG api-caddy_prod:latest + echo "๐Ÿงน Pruning old, unused Docker images ..." docker image prune -af From 1ffd35f8456b7846d3e21d44a77a3ac53a917a9c Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Tue, 15 Jul 2025 16:06:31 +0800 Subject: [PATCH 02/32] Empty - Commit From adb8677ea4c76ac616a9c845488f981dfbc89a01 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Tue, 15 Jul 2025 16:19:57 +0800 Subject: [PATCH 03/32] debug code --- .github/workflows/deploy.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 1e9ecd9c..15ffd69d 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -81,11 +81,19 @@ jobs: docker pull ghcr.io/oullin/oullin_api:$IMAGE_TAG docker pull ghcr.io/oullin/oullin_proxy:$IMAGE_TAG + echo "----- Images before re-tag -----" + docker images | grep api-api + echo "-------------------------------" + echo "๐Ÿท๏ธ Retagging for Composeโ€ฆ" docker tag ghcr.io/oullin/oullin_api:$IMAGE_TAG api-api:latest docker tag ghcr.io/oullin/oullin_proxy:$IMAGE_TAG api-caddy_prod:latest + echo "----- Images after re-tag -----" + docker images | grep api-api + echo "-------------------------------" + echo "๐Ÿงน Pruning old, unused Docker images ..." - docker image prune -af + docker image prune -f echo "โœ… Latest images pulled successfully to VPS!" From 8caac8151b21dd9ef94ab75bbd0aba6ca2a81424 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Tue, 15 Jul 2025 16:54:13 +0800 Subject: [PATCH 04/32] tweak caddy --- caddy/Caddyfile.prod | 57 +++++++++++++++++++++++++++----------------- 1 file changed, 35 insertions(+), 22 deletions(-) diff --git a/caddy/Caddyfile.prod b/caddy/Caddyfile.prod index ca41dd7e..76b48451 100644 --- a/caddy/Caddyfile.prod +++ b/caddy/Caddyfile.prod @@ -2,34 +2,47 @@ # Caddy will automatically provision a Let's Encrypt certificate. oullin.io { - # Enable compression to reduce bandwidth usage. - encode gzip zstd - - # Add security-related headers to protect against common attacks. - header { - # Enable HSTS to ensure browsers only connect via HTTPS. - Strict-Transport-Security "max-age=31536000;" - # Prevent clickjacking attacks. - X-Frame-Options "SAMEORIGIN" - # Prevent content type sniffing. - X-Content-Type-Options "nosniff" - # Enhances user privacy. - Referrer-Policy "strict-origin-when-cross-origin" - } - - log { + # Enable compression to reduce bandwidth usage. + encode gzip zstd + + # Add security-related headers to protect against common attacks. + header { + # Enable HSTS to ensure browsers only connect via HTTPS. + Strict-Transport-Security "max-age=31536000;" + # Prevent clickjacking attacks. + X-Frame-Options "SAMEORIGIN" + # Prevent content type sniffing. + X-Content-Type-Options "nosniff" + # Enhances user privacy. + Referrer-Policy "strict-origin-when-cross-origin" + } + + log { output file /var/log/caddy/oullin.io.log { - roll_size 10mb # Rotate logs after they reach 10MB - roll_keep 5 # Keep the last 5 rotated log files + roll_size 10mb # Rotate logs after they reach 10MB + roll_keep 5 # Keep the last 5 rotated log files } format json } - # Reverse proxy all requests to the Go application service. - # 'api' is the service name defined in docker-compose.yml. - reverse_proxy api:8080 { - # Set timeouts to prevent slow backends from holding up resources. + # Reverse-proxy all requests to the Go API, forwarding Host + auth headers + reverse_proxy { + # Tell Caddy which upstream to send to + to api:8080 + + # Preserve the original Host header + header_up Host {host} + + # Forward the client-sent auth headers + header_up X-API-Username {>X-API-Username} + header_up X-API-Key {>X-API-Key} + + # *** DEBUG: echo back to client what Caddy actually saw *** + header_down X-Debug-Username {http.request.header.X-API-Username} + header_down X-Debug-Key {http.request.header.X-API-Key} + + # Transport timeouts transport http { dial_timeout 10s response_header_timeout 30s From 2c6e68164fb0586629111ae34790dacfec594e81 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Tue, 15 Jul 2025 17:19:37 +0800 Subject: [PATCH 05/32] fix token logic --- pkg/auth/token.go | 9 +++++---- pkg/http/middleware/token.go | 7 +++++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/pkg/auth/token.go b/pkg/auth/token.go index df6c798a..7b218440 100644 --- a/pkg/auth/token.go +++ b/pkg/auth/token.go @@ -17,20 +17,21 @@ func (t Token) IsInvalid(seed string) bool { func (t Token) IsValid(seed string) bool { token := strings.TrimSpace(t.Public) - salt := strings.TrimSpace(t.Private) externalSalt := strings.TrimSpace(seed) - if salt != externalSalt { + if token != externalSalt { return false } + salt := strings.TrimSpace(t.Private) + hash := sha256.New() - hash.Write([]byte(externalSalt)) + hash.Write([]byte(salt)) bytes := hash.Sum(hash.Sum(nil)) encodeToString := strings.TrimSpace( hex.EncodeToString(bytes), ) - return token == encodeToString + return salt == encodeToString } diff --git a/pkg/http/middleware/token.go b/pkg/http/middleware/token.go index d1807afe..37d146bd 100644 --- a/pkg/http/middleware/token.go +++ b/pkg/http/middleware/token.go @@ -5,6 +5,7 @@ import ( "github.com/oullin/pkg/http" "log/slog" baseHttp "net/http" + "strings" ) const tokenHeader = "X-API-Key" @@ -22,12 +23,14 @@ func MakeTokenMiddleware(token auth.Token) TokenCheckMiddleware { func (t TokenCheckMiddleware) Handle(next http.ApiHandler) http.ApiHandler { return func(w baseHttp.ResponseWriter, r *baseHttp.Request) *http.ApiError { - if t.token.IsInvalid(r.Header.Get(tokenHeader)) { + salt := strings.TrimSpace(r.Header.Get(tokenHeader)) + + if t.token.IsInvalid(salt) { message := "Forbidden: Invalid API key" slog.Error(message) return &http.ApiError{ - Message: message, + Message: message + " | " + salt, Status: baseHttp.StatusForbidden, } } From 9f3cd8fe514ad1ef1f8da29e47a44954d70d2d99 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Tue, 15 Jul 2025 17:22:44 +0800 Subject: [PATCH 06/32] caddy --- caddy/Caddyfile.prod | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/caddy/Caddyfile.prod b/caddy/Caddyfile.prod index 76b48451..a1e017af 100644 --- a/caddy/Caddyfile.prod +++ b/caddy/Caddyfile.prod @@ -35,8 +35,8 @@ oullin.io { header_up Host {host} # Forward the client-sent auth headers - header_up X-API-Username {>X-API-Username} - header_up X-API-Key {>X-API-Key} + header_up X-API-Username {http.request.header.X-API-Username} + header_up X-API-Key {http.request.header.X-API-Key} # *** DEBUG: echo back to client what Caddy actually saw *** header_down X-Debug-Username {http.request.header.X-API-Username} From 9f626a689de898a36a466064d554788afba5d685 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Wed, 16 Jul 2025 10:33:35 +0800 Subject: [PATCH 07/32] start workin on Bearer-Token --- .../infra/migrations/000001_schema.up.sql | 14 +- .../infra/migrations/000002_api_keys.up.sql | 17 ++ token/main.go | 152 ++++++++++++++++++ 3 files changed, 176 insertions(+), 7 deletions(-) create mode 100644 database/infra/migrations/000002_api_keys.up.sql create mode 100644 token/main.go diff --git a/database/infra/migrations/000001_schema.up.sql b/database/infra/migrations/000001_schema.up.sql index b85b5703..fead4b94 100644 --- a/database/infra/migrations/000001_schema.up.sql +++ b/database/infra/migrations/000001_schema.up.sql @@ -4,13 +4,13 @@ CREATE TABLE IF NOT EXISTS users ( id BIGSERIAL PRIMARY KEY, uuid UUID UNIQUE NOT NULL, - first_name varchar(250) NOT NULL, - last_name varchar(250) NOT NULL, - username VARCHAR(50) UNIQUE NOT NULL , + first_name VARCHAR(250) NOT NULL, + last_name VARCHAR(250) NOT NULL, + username VARCHAR(50) UNIQUE NOT NULL, display_name VARCHAR(255), - email varchar(250) UNIQUE NOT NULL, + email VARCHAR(250) UNIQUE NOT NULL, password_hash VARCHAR(255) NOT NULL, - public_token varchar(250) NOT NULL, + public_token VARCHAR(250) NOT NULL, bio TEXT, picture_file_name VARCHAR(2048), profile_picture_url VARCHAR(2048), @@ -156,8 +156,8 @@ CREATE INDEX idx_likes_user_post ON likes (user_id, post_id); CREATE TABLE newsletters ( id BIGSERIAL PRIMARY KEY, email VARCHAR(255) UNIQUE NOT NULL, - first_name varchar(250) NOT NULL, - last_name varchar(250) NOT NULL, + first_name VARCHAR(250) NOT NULL, + last_name VARCHAR(250) NOT NULL, subscribed_at TIMESTAMP DEFAULT NULL, unsubscribed_at TIMESTAMP DEFAULT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, diff --git a/database/infra/migrations/000002_api_keys.up.sql b/database/infra/migrations/000002_api_keys.up.sql new file mode 100644 index 00000000..55f8991f --- /dev/null +++ b/database/infra/migrations/000002_api_keys.up.sql @@ -0,0 +1,17 @@ +CREATE TABLE api_keys ( + id BIGSERIAL PRIMARY KEY, + uuid UUID UNIQUE NOT NULL, + account_name VARCHAR(50) UNIQUE NOT NULL, + public_key VARCHAR(50) UNIQUE NOT NULL, + secret_key VARCHAR(50) UNIQUE NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP DEFAULT NULL, + + CONSTRAINT uq_account_keys UNIQUE (account_name, public_key, secret_key) +); + +CREATE INDEX idx_account_name ON api_keys(account_name); +CREATE INDEX idx_public_key ON api_keys(public_key); +CREATE INDEX idx_secret_key ON api_keys(secret_key); +CREATE INDEX idx_deleted_at ON api_keys(deleted_at); diff --git a/token/main.go b/token/main.go new file mode 100644 index 00000000..fbcc8249 --- /dev/null +++ b/token/main.go @@ -0,0 +1,152 @@ +package main + +import ( + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "fmt" + "strings" +) + +// Constants for our token prefixes. +const ( + PublicKeyPrefix = "pk_" + SecretKeyPrefix = "sk_" + AuthLevelPublic = "public" + AuthLevelSecret = "secret" +) + +// A simple in-memory store to hold our valid tokens and map them to an account ID. +// In a real application, this would be a database table. +var tokenStore = make(map[string]string) + +// generateSecureToken creates a secure, SHA256-hashed token from random data. +func generateSecureToken(saltLength int) (string, error) { + // Create a random salt to ensure the hash is unique. + salt := make([]byte, saltLength) + if _, err := rand.Read(salt); err != nil { + return "", err + } + + // Create a new SHA256 hasher. + hasher := sha256.New() + + // Write the random salt to the hasher. + hasher.Write(salt) + + // Get the resulting hash and encode it as a hex string. + hashBytes := hasher.Sum(nil) + return hex.EncodeToString(hashBytes), nil +} + +// setupNewAccount generates a new set of public and secret keys for a given account ID +// and stores them in our tokenStore. +func setupNewAccount(accountID string) (publicKey, secretKey string, err error) { + // Generate the core of the keys using a SHA256 hash. + // The input length (16) is for the random salt; the output length is fixed by SHA256. + publicKeyPart, err := generateSecureToken(16) + if err != nil { + return "", "", fmt.Errorf("failed to generate public key: %w", err) + } + secretKeyPart, err := generateSecureToken(16) + if err != nil { + return "", "", fmt.Errorf("failed to generate secret key: %w", err) + } + + // Add the prefixes + publicKey = PublicKeyPrefix + publicKeyPart + secretKey = SecretKeyPrefix + secretKeyPart + + // Store the valid tokens, mapping them back to the account ID + tokenStore[publicKey] = accountID + tokenStore[secretKey] = accountID + + return publicKey, secretKey, nil +} + +// validateBearerToken performs a proper validation of a given token. +// It returns the accountID, authentication level, and an error if validation fails. +func validateBearerToken(token string) (accountID string, authLevel string, err error) { + // 1. Check if the token exists in our store. This is the primary validation. + accountID, found := tokenStore[token] + if !found { + return "", "", fmt.Errorf("token not found or invalid") + } + + // 2. Determine the authentication level based on the token's prefix. + if strings.HasPrefix(token, PublicKeyPrefix) { + return accountID, AuthLevelPublic, nil + } + + if strings.HasPrefix(token, SecretKeyPrefix) { + return accountID, AuthLevelSecret, nil + } + + // 3. If the token was found but has no valid prefix, it's a format error. + // This case should be rare if tokens are always generated by our system. + return "", "", fmt.Errorf("token has an unknown format") +} + +// simulateApiRequest checks a token and simulates API calls. +func simulateApiRequest(token string) { + // Safely create a truncated token for display purposes to avoid logging secrets. + displayToken := token + const maxDisplayLen = 20 + if len(displayToken) > maxDisplayLen { + displayToken = displayToken[:maxDisplayLen] + "..." + } + fmt.Printf("--- Simulating request with token '%s' ---\n", displayToken) + + // 1. Perform validation by calling the dedicated validation function. + accountID, authLevel, err := validateBearerToken(token) + if err != nil { + // If the validation function returns an error, the request fails. + fmt.Printf(" => โŒ VALIDATION FAILED: %v.\n", err) + fmt.Println("-------------------------------------------------") + return + } + + // If we get here, the token is valid. + fmt.Printf(" Token validated for Account '%s' with '%s' level auth.\n", accountID, authLevel) + + // 2. Simulate accessing a public resource (always allowed with a valid token). + fmt.Println(" Attempting to access PUBLIC data...") + fmt.Printf(" => โœ… SUCCESS: Public data accessible.\n") + + // 3. Simulate accessing a protected resource (requires secret level auth). + fmt.Println(" Attempting to perform a SECRET action...") + if authLevel == AuthLevelSecret { + fmt.Printf(" => ๐Ÿ”‘ SUCCESS: Protected action allowed.\n") + } else { + fmt.Printf(" => โŒ FAILURE: Forbidden. A secret key is required for this action.\n") + } + + fmt.Println("-------------------------------------------------") +} + +func main() { + // --- Setup --- + // For demonstration, we'll create one account and its keys. + accountID := "acct_1A2B3C4D" + publicKey, secretKey, err := setupNewAccount(accountID) + if err != nil { + panic(fmt.Sprintf("Failed to set up initial account: %v", err)) + } + + fmt.Println("๐Ÿš€ CLI Token Authentication Simulation") + fmt.Println("=================================================") + fmt.Printf("Test Account ID: %s\n", accountID) + fmt.Printf("Public Key: %s\n", publicKey) + fmt.Printf("Secret Key: %s\n", secretKey) + fmt.Println("=================================================") + + // --- Test Cases --- + // 1. Simulate a request with the PUBLIC key. + simulateApiRequest(publicKey) + + // 2. Simulate a request with the SECRET key. + simulateApiRequest(secretKey) + + // 3. Simulate a request with a BAD token. + simulateApiRequest("pk_badtoken12345") +} From 779bf7fbf5db239f68bbbd3a9ebc7de92e356936 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Wed, 16 Jul 2025 10:39:11 +0800 Subject: [PATCH 08/32] update model --- database/attrs.go | 6 ++++++ database/model.go | 14 +++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/database/attrs.go b/database/attrs.go index d905a42f..d949da1e 100644 --- a/database/attrs.go +++ b/database/attrs.go @@ -4,6 +4,12 @@ import ( "time" ) +type APIKeyAttr struct { + AccountName string + PublicKey string + SecretKey string +} + type UsersAttrs struct { Username string Name string diff --git a/database/model.go b/database/model.go index b1e98c2a..0b0ef2e2 100644 --- a/database/model.go +++ b/database/model.go @@ -1,6 +1,7 @@ package database import ( + "github.com/google/uuid" "gorm.io/gorm" "slices" "time" @@ -12,7 +13,7 @@ var schemaTables = []string{ "users", "posts", "categories", "post_categories", "tags", "post_tags", "post_views", "post_views", "comments", - "likes", "newsletters", + "likes", "newsletters", "api_keys", } func GetSchemaTables() []string { @@ -23,6 +24,17 @@ func isValidTable(seed string) bool { return slices.Contains(schemaTables, seed) } +type APIKey struct { + ID int64 `gorm:"primaryKey"` + UUID uuid.UUID `gorm:"type:uuid;unique;not null"` + AccountName string `gorm:"column:account_name;size:50;not null;unique;uniqueIndex:uq_account_keys"` + PublicKey string `gorm:"column:public_key;size:50;not null;unique;index;uniqueIndex:uq_account_keys"` + SecretKey string `gorm:"column:secret_key;size:50;not null;unique;index;uniqueIndex:uq_account_keys"` + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt gorm.DeletedAt `gorm:"index"` +} + type User struct { ID uint64 `gorm:"primaryKey;autoIncrement"` UUID string `gorm:"type:uuid;unique;not null"` From a87019dde1c19151f5fa2c64f3254a3bd62448da Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Wed, 16 Jul 2025 11:14:01 +0800 Subject: [PATCH 09/32] token structure --- pkg/auth/schema.go | 21 +++++++++++++ pkg/auth/token.go | 73 ++++++++++++++++++++++++++++++++++------------ 2 files changed, 75 insertions(+), 19 deletions(-) create mode 100644 pkg/auth/schema.go diff --git a/pkg/auth/schema.go b/pkg/auth/schema.go new file mode 100644 index 00000000..15d2cdfd --- /dev/null +++ b/pkg/auth/schema.go @@ -0,0 +1,21 @@ +package auth + +const ( + PublicKeyPrefix = "pk_" + SecretKeyPrefix = "sk_" + LevelPublic = "public" + LevelSecret = "secret" + TokenMinLength = 16 +) + +type Token struct { + AccountName string `validate:"required,min=5"` + PublicKey string `validate:"required,len=16"` + PrivateKey string `validate:"required,len=16"` + Length int `validate:"required"` +} + +type ValidatedToken struct { + AuthLevel string `validate:"required,oneof=public secret"` + Token string `validate:"required,len=16"` +} diff --git a/pkg/auth/token.go b/pkg/auth/token.go index 7b218440..8d9c5b40 100644 --- a/pkg/auth/token.go +++ b/pkg/auth/token.go @@ -1,37 +1,72 @@ package auth import ( + "crypto/rand" "crypto/sha256" "encoding/hex" + "fmt" "strings" ) -type Token struct { - Public string `validate:"required,min=10"` - Private string `validate:"required,min=10"` -} +func SetupNewAccount(accountName string, TokenLength int) (*Token, error) { + token := Token{} + + pk, err := generateSecureToken(TokenLength) + if err != nil { + return nil, fmt.Errorf("failed to generate public key: %w", err) + } + + sk, err := generateSecureToken(TokenLength) + if err != nil { + return nil, fmt.Errorf("failed to generate secret key: %w", err) + } -func (t Token) IsInvalid(seed string) bool { - return !t.IsValid(seed) + token.PublicKey = pk + token.PrivateKey = sk + token.Length = TokenLength + token.AccountName = accountName + + return &token, nil } -func (t Token) IsValid(seed string) bool { - token := strings.TrimSpace(t.Public) - externalSalt := strings.TrimSpace(seed) +func generateSecureToken(length int) (string, error) { + if length < TokenMinLength { + return "", fmt.Errorf("the token length should be >= %d", length) + } + + salt := make([]byte, length) - if token != externalSalt { - return false + if _, err := rand.Read(salt); err != nil { + return "", fmt.Errorf("failed to generate secure tokens salt: %v", err) } - salt := strings.TrimSpace(t.Private) + hasher := sha256.New() + hasher.Write(salt) - hash := sha256.New() - hash.Write([]byte(salt)) - bytes := hash.Sum(hash.Sum(nil)) + // Get the resulting hash and encode it as a hex string. + hashBytes := hasher.Sum(nil) - encodeToString := strings.TrimSpace( - hex.EncodeToString(bytes), - ) + return hex.EncodeToString(hashBytes), nil +} + +func ValidateBearerToken(seed string) (*ValidatedToken, error) { + validated := ValidatedToken{ + Token: strings.TrimSpace(seed), + } + + if validated.Token == "" { + return nil, fmt.Errorf("token not found or invalid") + } + + if strings.HasPrefix(validated.Token, PublicKeyPrefix) { + validated.AuthLevel = strings.TrimSpace(LevelPublic) + return &validated, nil + } + + if strings.HasPrefix(validated.Token, SecretKeyPrefix) { + validated.AuthLevel = strings.TrimSpace(LevelSecret) + return &validated, nil + } - return salt == encodeToString + return nil, fmt.Errorf("the given token [%s] is not valid", validated.Token) } From 9036e897940dbb8b151c88196f4be67f89cf8f0c Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Wed, 16 Jul 2025 11:18:44 +0800 Subject: [PATCH 10/32] comment old token --- boost/factory.go | 21 ++++++++++----------- boost/router.go | 8 ++++---- cli/main.go | 31 +++++++++++++++++-------------- cli/panel/menu.go | 2 +- env/app.go | 8 +++----- pkg/http/middleware/token.go | 23 +++++++++++------------ 6 files changed, 46 insertions(+), 47 deletions(-) diff --git a/boost/factory.go b/boost/factory.go index 09f74044..60b0f24f 100644 --- a/boost/factory.go +++ b/boost/factory.go @@ -6,7 +6,6 @@ import ( "github.com/oullin/database" "github.com/oullin/env" "github.com/oullin/pkg" - "github.com/oullin/pkg/auth" "github.com/oullin/pkg/llogs" "log" "strconv" @@ -60,15 +59,15 @@ func MakeEnv(validate *pkg.Validator) *env.Environment { port, _ := strconv.Atoi(env.GetEnvVar("ENV_DB_PORT")) - token := auth.Token{ - Public: env.GetEnvVar("ENV_APP_TOKEN_PUBLIC"), - Private: env.GetEnvVar("ENV_APP_TOKEN_PRIVATE"), - } + //token := auth.Token{ + // Public: env.GetEnvVar("ENV_APP_TOKEN_PUBLIC"), + // Private: env.GetEnvVar("ENV_APP_TOKEN_PRIVATE"), + //} app := env.AppEnvironment{ - Name: env.GetEnvVar("ENV_APP_NAME"), - Type: env.GetEnvVar("ENV_APP_ENV_TYPE"), - Credentials: token, + Name: env.GetEnvVar("ENV_APP_NAME"), + Type: env.GetEnvVar("ENV_APP_ENV_TYPE"), + //Credentials: token, } db := env.DBEnvironment{ @@ -106,9 +105,9 @@ func MakeEnv(validate *pkg.Validator) *env.Environment { panic(errorSuffix + "invalid [Sql] model: " + validate.GetErrorsAsJason()) } - if _, err := validate.Rejects(token); err != nil { - panic(errorSuffix + "invalid [token] model: " + validate.GetErrorsAsJason()) - } + //if _, err := validate.Rejects(token); err != nil { + // panic(errorSuffix + "invalid [token] model: " + validate.GetErrorsAsJason()) + //} if _, err := validate.Rejects(logsCreds); err != nil { panic(errorSuffix + "invalid [logs Creds] model: " + validate.GetErrorsAsJason()) diff --git a/boost/router.go b/boost/router.go index 98d0d98c..a9076a5d 100644 --- a/boost/router.go +++ b/boost/router.go @@ -15,15 +15,15 @@ type Router struct { } func (r *Router) PipelineFor(apiHandler http.ApiHandler) baseHttp.HandlerFunc { - tokenMiddleware := middleware.MakeTokenMiddleware( - r.Env.App.Credentials, - ) + //tokenMiddleware := middleware.MakeTokenMiddleware( + // r.Env.App.Credentials, + //) return http.MakeApiHandler( r.Pipeline.Chain( apiHandler, middleware.UsernameCheck, - tokenMiddleware.Handle, + //tokenMiddleware.Handle, ), ) } diff --git a/cli/main.go b/cli/main.go index 57d9726b..2bc19104 100644 --- a/cli/main.go +++ b/cli/main.go @@ -1,39 +1,38 @@ package main import ( + "fmt" "github.com/oullin/boost" - "github.com/oullin/cli/gate" "github.com/oullin/cli/panel" "github.com/oullin/cli/posts" "github.com/oullin/env" "github.com/oullin/pkg" "github.com/oullin/pkg/cli" - "os" "time" ) -var guard gate.Guard +// var guard gate.Guard var environment *env.Environment func init() { secrets := boost.Ignite("./../.env", pkg.GetDefaultValidator()) environment = secrets - guard = gate.MakeGuard(environment.App.Credentials) + //guard = gate.MakeGuard(environment.App.Credentials) } func main() { cli.ClearScreen() - if err := guard.CaptureInput(); err != nil { - cli.Errorln(err.Error()) - return - } - - if guard.Rejects() { - cli.Errorln("Invalid credentials") - os.Exit(1) - } + //if err := guard.CaptureInput(); err != nil { + // cli.Errorln(err.Error()) + // return + //} + // + //if guard.Rejects() { + // cli.Errorln("Invalid credentials") + // os.Exit(1) + //} menu := panel.MakeMenu() @@ -64,7 +63,7 @@ func main() { return case 2: - showTime() + generateToken() case 3: timeParse() case 0: @@ -80,6 +79,10 @@ func main() { } } +func generateToken() { + fmt.Println("Generating token...") +} + func showTime() { now := time.Now().Format("2006-01-02 15:04:05") diff --git a/cli/panel/menu.go b/cli/panel/menu.go index 2b1c03a9..593f3a35 100644 --- a/cli/panel/menu.go +++ b/cli/panel/menu.go @@ -85,7 +85,7 @@ func (p *Menu) Print() { fmt.Println(divider) p.PrintOption("1) Parse Posts", inner) - p.PrintOption("2) Show Time", inner) + p.PrintOption("2) Create new account", inner) p.PrintOption("3) Show Date", inner) p.PrintOption("0) Exit", inner) diff --git a/env/app.go b/env/app.go index d296a84a..f6142c3e 100644 --- a/env/app.go +++ b/env/app.go @@ -1,15 +1,13 @@ package env -import "github.com/oullin/pkg/auth" - const local = "local" const staging = "staging" const production = "production" type AppEnvironment struct { - Name string `validate:"required,min=4"` - Type string `validate:"required,lowercase,oneof=local production staging"` - Credentials auth.Token `validate:"required"` + Name string `validate:"required,min=4"` + Type string `validate:"required,lowercase,oneof=local production staging"` + //Credentials auth.Token `validate:"required"` } func (e AppEnvironment) IsProduction() bool { diff --git a/pkg/http/middleware/token.go b/pkg/http/middleware/token.go index 37d146bd..9101119e 100644 --- a/pkg/http/middleware/token.go +++ b/pkg/http/middleware/token.go @@ -5,7 +5,6 @@ import ( "github.com/oullin/pkg/http" "log/slog" baseHttp "net/http" - "strings" ) const tokenHeader = "X-API-Key" @@ -23,17 +22,17 @@ func MakeTokenMiddleware(token auth.Token) TokenCheckMiddleware { func (t TokenCheckMiddleware) Handle(next http.ApiHandler) http.ApiHandler { return func(w baseHttp.ResponseWriter, r *baseHttp.Request) *http.ApiError { - salt := strings.TrimSpace(r.Header.Get(tokenHeader)) - - if t.token.IsInvalid(salt) { - message := "Forbidden: Invalid API key" - slog.Error(message) - - return &http.ApiError{ - Message: message + " | " + salt, - Status: baseHttp.StatusForbidden, - } - } + //salt := strings.TrimSpace(r.Header.Get(tokenHeader)) + // + //if t.token.IsInvalid(salt) { + // message := "Forbidden: Invalid API key" + // slog.Error(message) + // + // return &http.ApiError{ + // Message: message + " | " + salt, + // Status: baseHttp.StatusForbidden, + // } + //} slog.Info("Token validation successful") From 1444a27e60535d25c19e4f4b61283bb45f495ad7 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Wed, 16 Jul 2025 12:00:06 +0800 Subject: [PATCH 11/32] wire cli menu --- cli/accounts/factory.go | 23 +++++++ cli/accounts/handler.go | 27 ++++++++ cli/main.go | 67 +++++++++++++------ cli/panel/menu.go | 18 +++++ cli/posts/factory.go | 7 +- .../infra/migrations/000002_api_keys.up.sql | 6 +- database/model.go | 11 ++- database/repository/api_keys.go | 32 +++++++++ pkg/auth/schema.go | 13 ++-- pkg/auth/token.go | 2 +- 10 files changed, 164 insertions(+), 42 deletions(-) create mode 100644 cli/accounts/factory.go create mode 100644 cli/accounts/handler.go create mode 100644 database/repository/api_keys.go diff --git a/cli/accounts/factory.go b/cli/accounts/factory.go new file mode 100644 index 00000000..896ad478 --- /dev/null +++ b/cli/accounts/factory.go @@ -0,0 +1,23 @@ +package accounts + +import ( + "github.com/oullin/database" + "github.com/oullin/database/repository" + "github.com/oullin/pkg/auth" +) + +type Handler struct { + Tokens *repository.ApiKeys + TokenLength int + IsDebugging bool +} + +func MakeHandler(db *database.Connection) Handler { + tokens := repository.ApiKeys{DB: db} + + return Handler{ + IsDebugging: false, + TokenLength: auth.TokenMinLength, + Tokens: &tokens, + } +} diff --git a/cli/accounts/handler.go b/cli/accounts/handler.go new file mode 100644 index 00000000..84bf6649 --- /dev/null +++ b/cli/accounts/handler.go @@ -0,0 +1,27 @@ +package accounts + +import ( + "fmt" + "github.com/oullin/database" + "github.com/oullin/pkg/auth" +) + +func (h Handler) CreateAccount(accountName string) error { + token, err := auth.SetupNewAccount(accountName, h.TokenLength) + + if err != nil { + return fmt.Errorf("failed to create account tokens pair: %v", err) + } + + _, err = h.Tokens.Create(database.APIKeyAttr{ + AccountName: token.AccountName, + SecretKey: token.SecretKey, + PublicKey: token.PublicKey, + }) + + if err != nil { + return fmt.Errorf("failed to create account: %v", err) + } + + return nil +} diff --git a/cli/main.go b/cli/main.go index 2bc19104..763ae66b 100644 --- a/cli/main.go +++ b/cli/main.go @@ -1,39 +1,30 @@ package main import ( - "fmt" "github.com/oullin/boost" + "github.com/oullin/cli/accounts" "github.com/oullin/cli/panel" "github.com/oullin/cli/posts" + "github.com/oullin/database" "github.com/oullin/env" "github.com/oullin/pkg" "github.com/oullin/pkg/cli" "time" ) -// var guard gate.Guard var environment *env.Environment +var dbConn *database.Connection func init() { secrets := boost.Ignite("./../.env", pkg.GetDefaultValidator()) environment = secrets - //guard = gate.MakeGuard(environment.App.Credentials) + dbConn = boost.MakeDbConnection(environment) } func main() { cli.ClearScreen() - //if err := guard.CaptureInput(); err != nil { - // cli.Errorln(err.Error()) - // return - //} - // - //if guard.Rejects() { - // cli.Errorln("Invalid credentials") - // os.Exit(1) - //} - menu := panel.MakeMenu() for { @@ -54,7 +45,7 @@ func main() { } httpClient := pkg.MakeDefaultClient(nil) - handler := posts.MakeHandler(input, httpClient, environment) + handler := posts.MakeHandler(input, httpClient, dbConn) if _, err := handler.NotParsed(); err != nil { cli.Errorln(err.Error()) @@ -63,7 +54,28 @@ func main() { return case 2: - generateToken() + //input, err := menu.CaptureAccountName() + + //if err != nil { + // cli.Errorln(err.Error()) + // continue + //} + // + //handler := accounts.MakeHandler(dbConn) + // + //if err := handler.CreateAccount(input); err != nil { + // cli.Errorln(err.Error()) + // continue + //} + + if err = createNewAccount(menu); err != nil { + cli.Errorln(err.Error()) + continue + } + + return + + //generateToken()\\\ case 3: timeParse() case 0: @@ -79,16 +91,29 @@ func main() { } } -func generateToken() { - fmt.Println("Generating token...") -} +func createNewAccount(menu panel.Menu) error { + var err error + var account string + + if account, err = menu.CaptureAccountName(); err != nil { + return err + } -func showTime() { - now := time.Now().Format("2006-01-02 15:04:05") + handler := accounts.MakeHandler(dbConn) - cli.Cyanln("\nThe current time is: " + now) + if err = handler.CreateAccount(account); err != nil { + return err + } + + return nil } +//func showTime() { +// now := time.Now().Format("2006-01-02 15:04:05") +// +// cli.Cyanln("\nThe current time is: " + now) +//} + func timeParse() { s := pkg.MakeStringable("2025-04-12") diff --git a/cli/panel/menu.go b/cli/panel/menu.go index 593f3a35..a4f60836 100644 --- a/cli/panel/menu.go +++ b/cli/panel/menu.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/oullin/cli/posts" "github.com/oullin/pkg" + "github.com/oullin/pkg/auth" "github.com/oullin/pkg/cli" "golang.org/x/term" "net/url" @@ -117,6 +118,23 @@ func (p *Menu) CenterText(s string, width int) string { return strings.Repeat(" ", left) + s + strings.Repeat(" ", right) } +func (p *Menu) CaptureAccountName() (string, error) { + fmt.Print("Enter the account name: ") + + account, err := p.Reader.ReadString('\n') + + if err != nil { + return "", fmt.Errorf("%sError reading the account name: %v %s", cli.RedColour, err, cli.Reset) + } + + account = strings.TrimSpace(account) + if account == "" || len(account) < auth.AccountNameMinLength { + return "", fmt.Errorf("%sError: no account name provided or has an invalid length: %s", cli.RedColour, cli.Reset) + } + + return account, nil +} + func (p *Menu) CapturePostURL() (*posts.Input, error) { fmt.Print("Enter the post markdown file URL: ") diff --git a/cli/posts/factory.go b/cli/posts/factory.go index e4a1859f..8d986234 100644 --- a/cli/posts/factory.go +++ b/cli/posts/factory.go @@ -3,9 +3,8 @@ package posts import ( "context" "fmt" - "github.com/oullin/boost" + "github.com/oullin/database" "github.com/oullin/database/repository" - "github.com/oullin/env" "github.com/oullin/pkg" "github.com/oullin/pkg/markdown" "net/http" @@ -20,9 +19,7 @@ type Handler struct { IsDebugging bool } -func MakeHandler(input *Input, client *pkg.Client, env *env.Environment) Handler { - db := boost.MakeDbConnection(env) - +func MakeHandler(input *Input, client *pkg.Client, db *database.Connection) Handler { tags := &repository.Tags{DB: db} categories := &repository.Categories{DB: db} diff --git a/database/infra/migrations/000002_api_keys.up.sql b/database/infra/migrations/000002_api_keys.up.sql index 55f8991f..46b3afcf 100644 --- a/database/infra/migrations/000002_api_keys.up.sql +++ b/database/infra/migrations/000002_api_keys.up.sql @@ -1,9 +1,9 @@ CREATE TABLE api_keys ( id BIGSERIAL PRIMARY KEY, uuid UUID UNIQUE NOT NULL, - account_name VARCHAR(50) UNIQUE NOT NULL, - public_key VARCHAR(50) UNIQUE NOT NULL, - secret_key VARCHAR(50) UNIQUE NOT NULL, + account_name VARCHAR(255) UNIQUE NOT NULL, + public_key VARCHAR(255) UNIQUE NOT NULL, + secret_key VARCHAR(255) UNIQUE NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, deleted_at TIMESTAMP DEFAULT NULL, diff --git a/database/model.go b/database/model.go index 0b0ef2e2..56413fe7 100644 --- a/database/model.go +++ b/database/model.go @@ -1,7 +1,6 @@ package database import ( - "github.com/google/uuid" "gorm.io/gorm" "slices" "time" @@ -25,11 +24,11 @@ func isValidTable(seed string) bool { } type APIKey struct { - ID int64 `gorm:"primaryKey"` - UUID uuid.UUID `gorm:"type:uuid;unique;not null"` - AccountName string `gorm:"column:account_name;size:50;not null;unique;uniqueIndex:uq_account_keys"` - PublicKey string `gorm:"column:public_key;size:50;not null;unique;index;uniqueIndex:uq_account_keys"` - SecretKey string `gorm:"column:secret_key;size:50;not null;unique;index;uniqueIndex:uq_account_keys"` + ID int64 `gorm:"primaryKey"` + UUID string `gorm:"type:uuid;unique;not null"` + AccountName string `gorm:"column:account_name;size:50;not null;unique;uniqueIndex:uq_account_keys"` + PublicKey string `gorm:"column:public_key;size:50;not null;unique;index;uniqueIndex:uq_account_keys"` + SecretKey string `gorm:"column:secret_key;size:50;not null;unique;index;uniqueIndex:uq_account_keys"` CreatedAt time.Time UpdatedAt time.Time DeletedAt gorm.DeletedAt `gorm:"index"` diff --git a/database/repository/api_keys.go b/database/repository/api_keys.go new file mode 100644 index 00000000..05c6c27d --- /dev/null +++ b/database/repository/api_keys.go @@ -0,0 +1,32 @@ +package repository + +import ( + "fmt" + "github.com/google/uuid" + "github.com/oullin/database" + "github.com/oullin/pkg/gorm" +) + +type ApiKeys struct { + DB *database.Connection +} + +func (p ApiKeys) Create(attrs database.APIKeyAttr) (*database.APIKey, error) { + key := database.APIKey{ + UUID: uuid.NewString(), + AccountName: attrs.AccountName, + PublicKey: attrs.PublicKey, + SecretKey: attrs.SecretKey, + } + + if result := p.DB.Sql().Create(&key); gorm.HasDbIssues(result.Error) { + return nil, fmt.Errorf( + "issue creating the given api key pair [%s, %s]: %s", + attrs.PublicKey, + attrs.SecretKey, + result.Error, + ) + } + + return &key, nil +} diff --git a/pkg/auth/schema.go b/pkg/auth/schema.go index 15d2cdfd..010ecb68 100644 --- a/pkg/auth/schema.go +++ b/pkg/auth/schema.go @@ -1,17 +1,18 @@ package auth const ( - PublicKeyPrefix = "pk_" - SecretKeyPrefix = "sk_" - LevelPublic = "public" - LevelSecret = "secret" - TokenMinLength = 16 + PublicKeyPrefix = "pk_" + SecretKeyPrefix = "sk_" + LevelPublic = "public" + LevelSecret = "secret" + TokenMinLength = 16 + AccountNameMinLength = 5 ) type Token struct { AccountName string `validate:"required,min=5"` PublicKey string `validate:"required,len=16"` - PrivateKey string `validate:"required,len=16"` + SecretKey string `validate:"required,len=16"` Length int `validate:"required"` } diff --git a/pkg/auth/token.go b/pkg/auth/token.go index 8d9c5b40..17620d9c 100644 --- a/pkg/auth/token.go +++ b/pkg/auth/token.go @@ -22,7 +22,7 @@ func SetupNewAccount(accountName string, TokenLength int) (*Token, error) { } token.PublicKey = pk - token.PrivateKey = sk + token.SecretKey = sk token.Length = TokenLength token.AccountName = accountName From 7a23ae375722e172ed7c49b165ede60e055203cd Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Wed, 16 Jul 2025 12:02:28 +0800 Subject: [PATCH 12/32] add prefix --- cli/main.go | 6 ------ pkg/auth/token.go | 4 ++-- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/cli/main.go b/cli/main.go index 763ae66b..c6631930 100644 --- a/cli/main.go +++ b/cli/main.go @@ -108,12 +108,6 @@ func createNewAccount(menu panel.Menu) error { return nil } -//func showTime() { -// now := time.Now().Format("2006-01-02 15:04:05") -// -// cli.Cyanln("\nThe current time is: " + now) -//} - func timeParse() { s := pkg.MakeStringable("2025-04-12") diff --git a/pkg/auth/token.go b/pkg/auth/token.go index 17620d9c..913fbd42 100644 --- a/pkg/auth/token.go +++ b/pkg/auth/token.go @@ -21,8 +21,8 @@ func SetupNewAccount(accountName string, TokenLength int) (*Token, error) { return nil, fmt.Errorf("failed to generate secret key: %w", err) } - token.PublicKey = pk - token.SecretKey = sk + token.PublicKey = PublicKeyPrefix + pk + token.SecretKey = SecretKeyPrefix + sk token.Length = TokenLength token.AccountName = accountName From 4e2c81e5c1842d0f816be715bbfc54a6e498ef92 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Wed, 16 Jul 2025 12:05:16 +0800 Subject: [PATCH 13/32] remove old token refs --- .env.example | 4 ---- .env.prod.example | 4 ---- boost/factory.go | 10 ---------- env/app.go | 1 - 4 files changed, 19 deletions(-) diff --git a/.env.example b/.env.example index 9e6630fe..732eb804 100644 --- a/.env.example +++ b/.env.example @@ -5,10 +5,6 @@ ENV_APP_LOG_LEVEL=debug ENV_APP_LOGS_DIR="./storage/logs/logs_%s.log" ENV_APP_LOGS_DATE_FORMAT="2006_02_01" -# --- Auth -ENV_APP_TOKEN_PUBLIC="foo" -ENV_APP_TOKEN_PRIVATE="bar" - # --- DB ENV_DB_USER_NAME="gus" ENV_DB_USER_PASSWORD="password" diff --git a/.env.prod.example b/.env.prod.example index 95bc4727..ebbf82b7 100644 --- a/.env.prod.example +++ b/.env.prod.example @@ -5,10 +5,6 @@ ENV_APP_LOG_LEVEL=debug ENV_APP_LOGS_DIR="./storage/logs/logs_%s.log" ENV_APP_LOGS_DATE_FORMAT="2006_02_01" -# --- Auth -ENV_APP_TOKEN_PUBLIC="" -ENV_APP_TOKEN_PRIVATE="" - # --- DB ENV_DB_PORT= ENV_DB_HOST= diff --git a/boost/factory.go b/boost/factory.go index 60b0f24f..101e1054 100644 --- a/boost/factory.go +++ b/boost/factory.go @@ -59,15 +59,9 @@ func MakeEnv(validate *pkg.Validator) *env.Environment { port, _ := strconv.Atoi(env.GetEnvVar("ENV_DB_PORT")) - //token := auth.Token{ - // Public: env.GetEnvVar("ENV_APP_TOKEN_PUBLIC"), - // Private: env.GetEnvVar("ENV_APP_TOKEN_PRIVATE"), - //} - app := env.AppEnvironment{ Name: env.GetEnvVar("ENV_APP_NAME"), Type: env.GetEnvVar("ENV_APP_ENV_TYPE"), - //Credentials: token, } db := env.DBEnvironment{ @@ -105,10 +99,6 @@ func MakeEnv(validate *pkg.Validator) *env.Environment { panic(errorSuffix + "invalid [Sql] model: " + validate.GetErrorsAsJason()) } - //if _, err := validate.Rejects(token); err != nil { - // panic(errorSuffix + "invalid [token] model: " + validate.GetErrorsAsJason()) - //} - if _, err := validate.Rejects(logsCreds); err != nil { panic(errorSuffix + "invalid [logs Creds] model: " + validate.GetErrorsAsJason()) } diff --git a/env/app.go b/env/app.go index f6142c3e..9038eb44 100644 --- a/env/app.go +++ b/env/app.go @@ -7,7 +7,6 @@ const production = "production" type AppEnvironment struct { Name string `validate:"required,min=4"` Type string `validate:"required,lowercase,oneof=local production staging"` - //Credentials auth.Token `validate:"required"` } func (e AppEnvironment) IsProduction() bool { From 9253828ba12de783af36aa7aa1e83d354758dbac Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Wed, 16 Jul 2025 12:13:27 +0800 Subject: [PATCH 14/32] format --- cli/main.go | 45 ++++++++++++++++++--------------------------- cli/panel/menu.go | 2 +- 2 files changed, 19 insertions(+), 28 deletions(-) diff --git a/cli/main.go b/cli/main.go index c6631930..a7897612 100644 --- a/cli/main.go +++ b/cli/main.go @@ -37,45 +37,19 @@ func main() { switch menu.GetChoice() { case 1: - input, err := menu.CapturePostURL() - - if err != nil { - cli.Errorln(err.Error()) - continue - } - - httpClient := pkg.MakeDefaultClient(nil) - handler := posts.MakeHandler(input, httpClient, dbConn) - - if _, err := handler.NotParsed(); err != nil { + if err = createBlogPost(menu); err != nil { cli.Errorln(err.Error()) continue } return case 2: - //input, err := menu.CaptureAccountName() - - //if err != nil { - // cli.Errorln(err.Error()) - // continue - //} - // - //handler := accounts.MakeHandler(dbConn) - // - //if err := handler.CreateAccount(input); err != nil { - // cli.Errorln(err.Error()) - // continue - //} - if err = createNewAccount(menu); err != nil { cli.Errorln(err.Error()) continue } return - - //generateToken()\\\ case 3: timeParse() case 0: @@ -91,6 +65,23 @@ func main() { } } +func createBlogPost(menu panel.Menu) error { + input, err := menu.CapturePostURL() + + if err != nil { + return err + } + + httpClient := pkg.MakeDefaultClient(nil) + handler := posts.MakeHandler(input, httpClient, dbConn) + + if _, err = handler.NotParsed(); err != nil { + return err + } + + return nil +} + func createNewAccount(menu panel.Menu) error { var err error var account string diff --git a/cli/panel/menu.go b/cli/panel/menu.go index a4f60836..6b365bf3 100644 --- a/cli/panel/menu.go +++ b/cli/panel/menu.go @@ -85,7 +85,7 @@ func (p *Menu) Print() { fmt.Println(title) fmt.Println(divider) - p.PrintOption("1) Parse Posts", inner) + p.PrintOption("1) Parse Blog Posts", inner) p.PrintOption("2) Create new account", inner) p.PrintOption("3) Show Date", inner) p.PrintOption("0) Exit", inner) From 3c584687f67bb113fa10efa34fdeee6090097842 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Wed, 16 Jul 2025 12:37:53 +0800 Subject: [PATCH 15/32] start working on token middleware logic --- boost/app.go | 8 ++- boost/router.go | 9 ++-- database/repository/api_keys.go | 23 ++++++++- pkg/http/middleware/pipeline.go | 4 +- pkg/http/middleware/token.go | 41 --------------- pkg/http/middleware/token_middleware.go | 66 +++++++++++++++++++++++++ pkg/http/middleware/username.go | 31 ------------ 7 files changed, 100 insertions(+), 82 deletions(-) delete mode 100644 pkg/http/middleware/token.go create mode 100644 pkg/http/middleware/token_middleware.go delete mode 100644 pkg/http/middleware/username.go diff --git a/boost/app.go b/boost/app.go index 903fb40c..b8d1d757 100644 --- a/boost/app.go +++ b/boost/app.go @@ -2,6 +2,7 @@ package boost import ( "github.com/oullin/database" + "github.com/oullin/database/repository" "github.com/oullin/env" "github.com/oullin/pkg" "github.com/oullin/pkg/http/middleware" @@ -19,19 +20,22 @@ type App struct { } func MakeApp(env *env.Environment, validator *pkg.Validator) *App { + db := MakeDbConnection(env) + app := App{ env: env, validator: validator, logs: MakeLogs(env), sentry: MakeSentry(env), - db: MakeDbConnection(env), + db: db, } router := Router{ Env: env, Mux: baseHttp.NewServeMux(), Pipeline: middleware.Pipeline{ - Env: env, + Env: env, + ApiKeys: &repository.ApiKeys{DB: db}, }, } diff --git a/boost/router.go b/boost/router.go index a9076a5d..4061e485 100644 --- a/boost/router.go +++ b/boost/router.go @@ -15,15 +15,14 @@ type Router struct { } func (r *Router) PipelineFor(apiHandler http.ApiHandler) baseHttp.HandlerFunc { - //tokenMiddleware := middleware.MakeTokenMiddleware( - // r.Env.App.Credentials, - //) + tokenMiddleware := middleware.MakeTokenMiddleware( + r.Pipeline.ApiKeys, + ) return http.MakeApiHandler( r.Pipeline.Chain( apiHandler, - middleware.UsernameCheck, - //tokenMiddleware.Handle, + tokenMiddleware.Handle, ), ) } diff --git a/database/repository/api_keys.go b/database/repository/api_keys.go index 05c6c27d..3a9ffb1a 100644 --- a/database/repository/api_keys.go +++ b/database/repository/api_keys.go @@ -5,13 +5,14 @@ import ( "github.com/google/uuid" "github.com/oullin/database" "github.com/oullin/pkg/gorm" + "strings" ) type ApiKeys struct { DB *database.Connection } -func (p ApiKeys) Create(attrs database.APIKeyAttr) (*database.APIKey, error) { +func (a ApiKeys) Create(attrs database.APIKeyAttr) (*database.APIKey, error) { key := database.APIKey{ UUID: uuid.NewString(), AccountName: attrs.AccountName, @@ -19,7 +20,7 @@ func (p ApiKeys) Create(attrs database.APIKeyAttr) (*database.APIKey, error) { SecretKey: attrs.SecretKey, } - if result := p.DB.Sql().Create(&key); gorm.HasDbIssues(result.Error) { + if result := a.DB.Sql().Create(&key); gorm.HasDbIssues(result.Error) { return nil, fmt.Errorf( "issue creating the given api key pair [%s, %s]: %s", attrs.PublicKey, @@ -30,3 +31,21 @@ func (p ApiKeys) Create(attrs database.APIKeyAttr) (*database.APIKey, error) { return &key, nil } + +func (a ApiKeys) FindBy(accountName string) *database.APIKey { + key := database.APIKey{} + + result := a.DB.Sql(). + Where("LOWER(account_name) = ?", strings.ToLower(accountName)). + First(&key) + + if gorm.HasDbIssues(result.Error) { + return nil + } + + if strings.Trim(key.UUID, " ") != "" { + return &key + } + + return nil +} diff --git a/pkg/http/middleware/pipeline.go b/pkg/http/middleware/pipeline.go index 5ca24c5d..b3895cc0 100644 --- a/pkg/http/middleware/pipeline.go +++ b/pkg/http/middleware/pipeline.go @@ -1,12 +1,14 @@ package middleware import ( + "github.com/oullin/database/repository" "github.com/oullin/env" "github.com/oullin/pkg/http" ) type Pipeline struct { - Env *env.Environment + Env *env.Environment + ApiKeys *repository.ApiKeys } // Chain applies a list of middleware handlers to a final ApiHandler. diff --git a/pkg/http/middleware/token.go b/pkg/http/middleware/token.go deleted file mode 100644 index 9101119e..00000000 --- a/pkg/http/middleware/token.go +++ /dev/null @@ -1,41 +0,0 @@ -package middleware - -import ( - "github.com/oullin/pkg/auth" - "github.com/oullin/pkg/http" - "log/slog" - baseHttp "net/http" -) - -const tokenHeader = "X-API-Key" - -type TokenCheckMiddleware struct { - token auth.Token -} - -func MakeTokenMiddleware(token auth.Token) TokenCheckMiddleware { - return TokenCheckMiddleware{ - token: token, - } -} - -func (t TokenCheckMiddleware) Handle(next http.ApiHandler) http.ApiHandler { - return func(w baseHttp.ResponseWriter, r *baseHttp.Request) *http.ApiError { - - //salt := strings.TrimSpace(r.Header.Get(tokenHeader)) - // - //if t.token.IsInvalid(salt) { - // message := "Forbidden: Invalid API key" - // slog.Error(message) - // - // return &http.ApiError{ - // Message: message + " | " + salt, - // Status: baseHttp.StatusForbidden, - // } - //} - - slog.Info("Token validation successful") - - return next(w, r) - } -} diff --git a/pkg/http/middleware/token_middleware.go b/pkg/http/middleware/token_middleware.go new file mode 100644 index 00000000..83d67e51 --- /dev/null +++ b/pkg/http/middleware/token_middleware.go @@ -0,0 +1,66 @@ +package middleware + +import ( + "fmt" + "github.com/oullin/database/repository" + "github.com/oullin/pkg/auth" + "github.com/oullin/pkg/http" + "log/slog" + baseHttp "net/http" + "strings" +) + +const tokenHeader = "X-API-Key" +const usernameHeader = "X-API-Username" + +type TokenCheckMiddleware struct { + ApiKeys *repository.ApiKeys +} + +func MakeTokenMiddleware(apiKeys *repository.ApiKeys) TokenCheckMiddleware { + return TokenCheckMiddleware{ + ApiKeys: apiKeys, + } +} + +func (t TokenCheckMiddleware) Handle(next http.ApiHandler) http.ApiHandler { + return func(w baseHttp.ResponseWriter, r *baseHttp.Request) *http.ApiError { + + accountName := strings.TrimSpace(r.Header.Get(usernameHeader)) + publicToken := strings.TrimSpace(r.Header.Get(tokenHeader)) + + if accountName == "" || publicToken == "" { + return &http.ApiError{ + Message: fmt.Sprintf("invalid request. Please, provide a valid token and accout name"), + Status: baseHttp.StatusForbidden, + } + } + + validPublicToken, err := auth.ValidateBearerToken(publicToken) + if err != nil { + return &http.ApiError{ + Message: fmt.Sprintf("invalid token format: [token: %s]", publicToken), + Status: baseHttp.StatusForbidden, + } + } + + item := t.ApiKeys.FindBy(accountName) + if item == nil { + return &http.ApiError{ + Message: fmt.Sprintf("the provided account does not exist: [account: %s]", accountName), + Status: baseHttp.StatusForbidden, + } + } + + if item.PublicKey != validPublicToken.Token { + return &http.ApiError{ + Message: fmt.Sprintf("the provided token does not match your provided account: [token: %s, account name: %s]", publicToken, accountName), + Status: baseHttp.StatusForbidden, + } + } + + slog.Info("Token validation successful") + + return next(w, r) + } +} diff --git a/pkg/http/middleware/username.go b/pkg/http/middleware/username.go deleted file mode 100644 index 73565ad0..00000000 --- a/pkg/http/middleware/username.go +++ /dev/null @@ -1,31 +0,0 @@ -package middleware - -import ( - "fmt" - "github.com/oullin/pkg/http" - "log/slog" - baseHttp "net/http" - "strings" -) - -const usernameHeader = "X-API-Username" - -func UsernameCheck(next http.ApiHandler) http.ApiHandler { - return func(w baseHttp.ResponseWriter, r *baseHttp.Request) *http.ApiError { - username := strings.TrimSpace(r.Header.Get(usernameHeader)) - - if username != "gocanto" { - message := fmt.Sprintf("Unauthorized: Invalid API username received ('%s')", username) - slog.Error(message) - - return &http.ApiError{ - Message: message, - Status: baseHttp.StatusUnauthorized, - } - } - - slog.Info("Successfully authenticated user: gocanto") - - return next(w, r) - } -} From dab85aee1d33187744b4901a1455ea11d87a159b Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Wed, 16 Jul 2025 12:55:46 +0800 Subject: [PATCH 16/32] format --- cli/accounts/handler.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cli/accounts/handler.go b/cli/accounts/handler.go index 84bf6649..3d171592 100644 --- a/cli/accounts/handler.go +++ b/cli/accounts/handler.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/oullin/database" "github.com/oullin/pkg/auth" + "github.com/oullin/pkg/cli" ) func (h Handler) CreateAccount(accountName string) error { @@ -23,5 +24,7 @@ func (h Handler) CreateAccount(accountName string) error { return fmt.Errorf("failed to create account: %v", err) } + cli.Successln("Account created successfully") + return nil } From 76dffe6bb389f636ea87b03a2050870ca99a977d Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Wed, 16 Jul 2025 12:59:51 +0800 Subject: [PATCH 17/32] makefile --- config/makefile/app.mk | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/config/makefile/app.mk b/config/makefile/app.mk index 46c994d9..44dcb571 100644 --- a/config/makefile/app.mk +++ b/config/makefile/app.mk @@ -1,4 +1,6 @@ -.PHONY: fresh audit watch format run-cli +.PHONY: fresh audit watch format run-cli validate-caddy + +APP_CADDY_CONFIG_FILE ?= caddy/Caddyfile.prod format: gofmt -w -s . @@ -48,3 +50,9 @@ run-cli: DB_SECRET_PASSWORD="$(DB_SECRET_PASSWORD)" \ DB_SECRET_DBNAME="$(DB_SECRET_DBNAME)" \ docker compose run --rm api-runner go run ./cli/main.go + +# --- Mac: +# Needs to be locally installed: https://formulae.brew.sh/formula/caddy +validate-caddy: + caddy fmt --overwrite $(APP_CADDY_CONFIG_FILE) + caddy validate --config $(APP_CADDY_CONFIG_FILE) From 86516d60028a6db936ee157e781f530e4c5356f2 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Wed, 16 Jul 2025 13:01:10 +0800 Subject: [PATCH 18/32] remove this --- token/main.go | 152 -------------------------------------------------- 1 file changed, 152 deletions(-) delete mode 100644 token/main.go diff --git a/token/main.go b/token/main.go deleted file mode 100644 index fbcc8249..00000000 --- a/token/main.go +++ /dev/null @@ -1,152 +0,0 @@ -package main - -import ( - "crypto/rand" - "crypto/sha256" - "encoding/hex" - "fmt" - "strings" -) - -// Constants for our token prefixes. -const ( - PublicKeyPrefix = "pk_" - SecretKeyPrefix = "sk_" - AuthLevelPublic = "public" - AuthLevelSecret = "secret" -) - -// A simple in-memory store to hold our valid tokens and map them to an account ID. -// In a real application, this would be a database table. -var tokenStore = make(map[string]string) - -// generateSecureToken creates a secure, SHA256-hashed token from random data. -func generateSecureToken(saltLength int) (string, error) { - // Create a random salt to ensure the hash is unique. - salt := make([]byte, saltLength) - if _, err := rand.Read(salt); err != nil { - return "", err - } - - // Create a new SHA256 hasher. - hasher := sha256.New() - - // Write the random salt to the hasher. - hasher.Write(salt) - - // Get the resulting hash and encode it as a hex string. - hashBytes := hasher.Sum(nil) - return hex.EncodeToString(hashBytes), nil -} - -// setupNewAccount generates a new set of public and secret keys for a given account ID -// and stores them in our tokenStore. -func setupNewAccount(accountID string) (publicKey, secretKey string, err error) { - // Generate the core of the keys using a SHA256 hash. - // The input length (16) is for the random salt; the output length is fixed by SHA256. - publicKeyPart, err := generateSecureToken(16) - if err != nil { - return "", "", fmt.Errorf("failed to generate public key: %w", err) - } - secretKeyPart, err := generateSecureToken(16) - if err != nil { - return "", "", fmt.Errorf("failed to generate secret key: %w", err) - } - - // Add the prefixes - publicKey = PublicKeyPrefix + publicKeyPart - secretKey = SecretKeyPrefix + secretKeyPart - - // Store the valid tokens, mapping them back to the account ID - tokenStore[publicKey] = accountID - tokenStore[secretKey] = accountID - - return publicKey, secretKey, nil -} - -// validateBearerToken performs a proper validation of a given token. -// It returns the accountID, authentication level, and an error if validation fails. -func validateBearerToken(token string) (accountID string, authLevel string, err error) { - // 1. Check if the token exists in our store. This is the primary validation. - accountID, found := tokenStore[token] - if !found { - return "", "", fmt.Errorf("token not found or invalid") - } - - // 2. Determine the authentication level based on the token's prefix. - if strings.HasPrefix(token, PublicKeyPrefix) { - return accountID, AuthLevelPublic, nil - } - - if strings.HasPrefix(token, SecretKeyPrefix) { - return accountID, AuthLevelSecret, nil - } - - // 3. If the token was found but has no valid prefix, it's a format error. - // This case should be rare if tokens are always generated by our system. - return "", "", fmt.Errorf("token has an unknown format") -} - -// simulateApiRequest checks a token and simulates API calls. -func simulateApiRequest(token string) { - // Safely create a truncated token for display purposes to avoid logging secrets. - displayToken := token - const maxDisplayLen = 20 - if len(displayToken) > maxDisplayLen { - displayToken = displayToken[:maxDisplayLen] + "..." - } - fmt.Printf("--- Simulating request with token '%s' ---\n", displayToken) - - // 1. Perform validation by calling the dedicated validation function. - accountID, authLevel, err := validateBearerToken(token) - if err != nil { - // If the validation function returns an error, the request fails. - fmt.Printf(" => โŒ VALIDATION FAILED: %v.\n", err) - fmt.Println("-------------------------------------------------") - return - } - - // If we get here, the token is valid. - fmt.Printf(" Token validated for Account '%s' with '%s' level auth.\n", accountID, authLevel) - - // 2. Simulate accessing a public resource (always allowed with a valid token). - fmt.Println(" Attempting to access PUBLIC data...") - fmt.Printf(" => โœ… SUCCESS: Public data accessible.\n") - - // 3. Simulate accessing a protected resource (requires secret level auth). - fmt.Println(" Attempting to perform a SECRET action...") - if authLevel == AuthLevelSecret { - fmt.Printf(" => ๐Ÿ”‘ SUCCESS: Protected action allowed.\n") - } else { - fmt.Printf(" => โŒ FAILURE: Forbidden. A secret key is required for this action.\n") - } - - fmt.Println("-------------------------------------------------") -} - -func main() { - // --- Setup --- - // For demonstration, we'll create one account and its keys. - accountID := "acct_1A2B3C4D" - publicKey, secretKey, err := setupNewAccount(accountID) - if err != nil { - panic(fmt.Sprintf("Failed to set up initial account: %v", err)) - } - - fmt.Println("๐Ÿš€ CLI Token Authentication Simulation") - fmt.Println("=================================================") - fmt.Printf("Test Account ID: %s\n", accountID) - fmt.Printf("Public Key: %s\n", publicKey) - fmt.Printf("Secret Key: %s\n", secretKey) - fmt.Println("=================================================") - - // --- Test Cases --- - // 1. Simulate a request with the PUBLIC key. - simulateApiRequest(publicKey) - - // 2. Simulate a request with the SECRET key. - simulateApiRequest(secretKey) - - // 3. Simulate a request with a BAD token. - simulateApiRequest("pk_badtoken12345") -} From f289f369e7ab7f2bb91086e871949594f908aa9c Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Wed, 16 Jul 2025 14:31:03 +0800 Subject: [PATCH 19/32] check private token hash too --- pkg/auth/token.go | 57 +++++++++++++---- pkg/http/middleware/token_middleware.go | 85 ++++++++++++++++++------- 2 files changed, 106 insertions(+), 36 deletions(-) diff --git a/pkg/auth/token.go b/pkg/auth/token.go index 913fbd42..18015e54 100644 --- a/pkg/auth/token.go +++ b/pkg/auth/token.go @@ -1,6 +1,7 @@ package auth import ( + "crypto/hmac" "crypto/rand" "crypto/sha256" "encoding/hex" @@ -49,24 +50,56 @@ func generateSecureToken(length int) (string, error) { return hex.EncodeToString(hashBytes), nil } -func ValidateBearerToken(seed string) (*ValidatedToken, error) { - validated := ValidatedToken{ - Token: strings.TrimSpace(seed), +func ValidateTokenFormat(seed string) error { + token := strings.TrimSpace(seed) + + if token == "" || len(token) < TokenMinLength { + return fmt.Errorf("token not found or invalid") } - if validated.Token == "" { - return nil, fmt.Errorf("token not found or invalid") + if !strings.HasPrefix(token, PublicKeyPrefix) || !strings.HasPrefix(token, SecretKeyPrefix) { + return fmt.Errorf("invalid token prefix") } - if strings.HasPrefix(validated.Token, PublicKeyPrefix) { - validated.AuthLevel = strings.TrimSpace(LevelPublic) - return &validated, nil + return fmt.Errorf("the given token [%s] is not valid", token) +} + +func CreateSignatureFrom(message, secretKey string) string { + mac := hmac.New(sha256.New, []byte(secretKey)) + mac.Write([]byte(message)) + + return hex.EncodeToString(mac.Sum(nil)) +} + +func SafeDisplay(secret string) string { + var prefixLen int + visibleChars := 8 + + if strings.HasPrefix(secret, PublicKeyPrefix) { + prefixLen = len(PublicKeyPrefix) + } else { + prefixLen = len(SecretKeyPrefix) } - if strings.HasPrefix(validated.Token, SecretKeyPrefix) { - validated.AuthLevel = strings.TrimSpace(LevelSecret) - return &validated, nil + if len(secret) <= prefixLen+visibleChars { + return secret } - return nil, fmt.Errorf("the given token [%s] is not valid", validated.Token) + return secret[:prefixLen+visibleChars] + "..." +} + +func (t Token) HasInValidSignature(receivedSignature string) bool { + return !t.HasValidSignature(receivedSignature) +} + +func (t Token) HasValidSignature(receivedSignature string) bool { + signature := CreateSignatureFrom( + t.AccountName, + t.SecretKey, + ) + + return hmac.Equal( + []byte(signature), + []byte(receivedSignature), + ) } diff --git a/pkg/http/middleware/token_middleware.go b/pkg/http/middleware/token_middleware.go index 83d67e51..54e58b90 100644 --- a/pkg/http/middleware/token_middleware.go +++ b/pkg/http/middleware/token_middleware.go @@ -2,6 +2,7 @@ package middleware import ( "fmt" + "github.com/oullin/database" "github.com/oullin/database/repository" "github.com/oullin/pkg/auth" "github.com/oullin/pkg/http" @@ -12,6 +13,7 @@ import ( const tokenHeader = "X-API-Key" const usernameHeader = "X-API-Username" +const signatureHeader = "X-API-Signature" type TokenCheckMiddleware struct { ApiKeys *repository.ApiKeys @@ -28,35 +30,18 @@ func (t TokenCheckMiddleware) Handle(next http.ApiHandler) http.ApiHandler { accountName := strings.TrimSpace(r.Header.Get(usernameHeader)) publicToken := strings.TrimSpace(r.Header.Get(tokenHeader)) + signature := strings.TrimSpace(r.Header.Get(signatureHeader)) - if accountName == "" || publicToken == "" { - return &http.ApiError{ - Message: fmt.Sprintf("invalid request. Please, provide a valid token and accout name"), - Status: baseHttp.StatusForbidden, - } + if accountName == "" || publicToken == "" || signature == "" { + return t.getInvalidRequestError(accountName, publicToken, signature) } - validPublicToken, err := auth.ValidateBearerToken(publicToken) - if err != nil { - return &http.ApiError{ - Message: fmt.Sprintf("invalid token format: [token: %s]", publicToken), - Status: baseHttp.StatusForbidden, - } + if err := auth.ValidateTokenFormat(publicToken); err != nil { + return t.getInvalidTokenFormatError(publicToken) } - item := t.ApiKeys.FindBy(accountName) - if item == nil { - return &http.ApiError{ - Message: fmt.Sprintf("the provided account does not exist: [account: %s]", accountName), - Status: baseHttp.StatusForbidden, - } - } - - if item.PublicKey != validPublicToken.Token { - return &http.ApiError{ - Message: fmt.Sprintf("the provided token does not match your provided account: [token: %s, account name: %s]", publicToken, accountName), - Status: baseHttp.StatusForbidden, - } + if t.shallReject(accountName, signature) { + return t.getUnauthenticatedError(accountName, publicToken, signature) } slog.Info("Token validation successful") @@ -64,3 +49,55 @@ func (t TokenCheckMiddleware) Handle(next http.ApiHandler) http.ApiHandler { return next(w, r) } } + +func (t TokenCheckMiddleware) shallReject(accountName, signature string) bool { + var item *database.APIKey + + if item = t.ApiKeys.FindBy(accountName); item == nil { + return true + } + + token := auth.Token{ + AccountName: item.AccountName, + SecretKey: item.SecretKey, + PublicKey: item.PublicKey, + Length: len(item.PublicKey), + } + + return token.HasInValidSignature(signature) +} + +func (t TokenCheckMiddleware) getInvalidRequestError(accountName, publicToken, signature string) *http.ApiError { + message := fmt.Sprintf( + "invalid request. Please, provide a valid token, signature and accout name headers. [account: %s, public token: %s, signature: %s]", + accountName, + auth.SafeDisplay(publicToken), + auth.SafeDisplay(signature), + ) + + return &http.ApiError{ + Message: message, + Status: baseHttp.StatusForbidden, + } +} + +func (t TokenCheckMiddleware) getInvalidTokenFormatError(publicToken string) *http.ApiError { + return &http.ApiError{ + Message: fmt.Sprintf("invalid token format: [token: %s]", auth.SafeDisplay(publicToken)), + Status: baseHttp.StatusForbidden, + } +} + +func (t TokenCheckMiddleware) getUnauthenticatedError(accountName, publicToken, signature string) *http.ApiError { + message := fmt.Sprintf( + "Unauthenticated, please check your credentials and signature headers: [token: %s, account name: %s, signature: %s]", + auth.SafeDisplay(publicToken), + accountName, + auth.SafeDisplay(signature), + ) + + return &http.ApiError{ + Message: message, + Status: baseHttp.StatusForbidden, + } +} From 73ab9e9fbf15f5eede1b586b014e800365dcce31 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Wed, 16 Jul 2025 14:39:52 +0800 Subject: [PATCH 20/32] format --- database/model.go | 2 +- pkg/auth/schema.go | 11 ++--------- pkg/auth/token.go | 4 ++++ 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/database/model.go b/database/model.go index 56413fe7..b20c2e6a 100644 --- a/database/model.go +++ b/database/model.go @@ -11,7 +11,7 @@ const DriverName = "postgres" var schemaTables = []string{ "users", "posts", "categories", "post_categories", "tags", "post_tags", - "post_views", "post_views", "comments", + "post_views", "comments", "likes", "newsletters", "api_keys", } diff --git a/pkg/auth/schema.go b/pkg/auth/schema.go index 010ecb68..6022718f 100644 --- a/pkg/auth/schema.go +++ b/pkg/auth/schema.go @@ -3,20 +3,13 @@ package auth const ( PublicKeyPrefix = "pk_" SecretKeyPrefix = "sk_" - LevelPublic = "public" - LevelSecret = "secret" TokenMinLength = 16 AccountNameMinLength = 5 ) type Token struct { AccountName string `validate:"required,min=5"` - PublicKey string `validate:"required,len=16"` - SecretKey string `validate:"required,len=16"` + PublicKey string `validate:"required"` + SecretKey string `validate:"required"` Length int `validate:"required"` } - -type ValidatedToken struct { - AuthLevel string `validate:"required,oneof=public secret"` - Token string `validate:"required,len=16"` -} diff --git a/pkg/auth/token.go b/pkg/auth/token.go index 18015e54..ab340daf 100644 --- a/pkg/auth/token.go +++ b/pkg/auth/token.go @@ -12,6 +12,10 @@ import ( func SetupNewAccount(accountName string, TokenLength int) (*Token, error) { token := Token{} + if len(accountName) < AccountNameMinLength { + return nil, fmt.Errorf("account name must be at least %d characters", AccountNameMinLength) + } + pk, err := generateSecureToken(TokenLength) if err != nil { return nil, fmt.Errorf("failed to generate public key: %w", err) From f5e2636b445f881b961507a2b5f58bd86f8edd91 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Wed, 16 Jul 2025 14:47:50 +0800 Subject: [PATCH 21/32] remove old guard --- cli/gate/guard.go | 57 ----------------------------------------------- cli/main.go | 13 ----------- cli/panel/menu.go | 3 ++- 3 files changed, 2 insertions(+), 71 deletions(-) delete mode 100644 cli/gate/guard.go diff --git a/cli/gate/guard.go b/cli/gate/guard.go deleted file mode 100644 index 4a359da6..00000000 --- a/cli/gate/guard.go +++ /dev/null @@ -1,57 +0,0 @@ -package gate - -import ( - "bufio" - "fmt" - "github.com/oullin/pkg/auth" - "github.com/oullin/pkg/cli" - "os" - "strings" -) - -type Guard struct { - salt *string - token auth.Token - reader *bufio.Reader -} - -func MakeGuard(token auth.Token) Guard { - return Guard{ - token: token, - reader: bufio.NewReader(os.Stdin), - } -} - -func (g *Guard) CaptureInput() error { - cli.Warning("Type the public token: ") - - input, err := g.reader.ReadString('\n') - - if err != nil { - return fmt.Errorf("error reading input: %v", err) - } - - input = strings.TrimSpace(input) - - if len(input) == 0 { - return fmt.Errorf("token cannot be empty") - } - - if len(input) > 1024 { - return fmt.Errorf("token is too long") - } - - g.salt = &input - - return nil -} - -func (g *Guard) Rejects() bool { - if g.salt == nil { - return true - } - - salt := *g.salt - - return g.token.IsInvalid(salt) -} diff --git a/cli/main.go b/cli/main.go index a7897612..ea4ab209 100644 --- a/cli/main.go +++ b/cli/main.go @@ -9,7 +9,6 @@ import ( "github.com/oullin/env" "github.com/oullin/pkg" "github.com/oullin/pkg/cli" - "time" ) var environment *env.Environment @@ -50,8 +49,6 @@ func main() { } return - case 3: - timeParse() case 0: cli.Successln("Goodbye!") return @@ -98,13 +95,3 @@ func createNewAccount(menu panel.Menu) error { return nil } - -func timeParse() { - s := pkg.MakeStringable("2025-04-12") - - if seed, err := s.ToDatetime(); err != nil { - panic(err) - } else { - cli.Magentaln(seed.Format(time.DateTime)) - } -} diff --git a/cli/panel/menu.go b/cli/panel/menu.go index 6b365bf3..08a23ad3 100644 --- a/cli/panel/menu.go +++ b/cli/panel/menu.go @@ -86,8 +86,9 @@ func (p *Menu) Print() { fmt.Println(divider) p.PrintOption("1) Parse Blog Posts", inner) + p.PrintOption(" ", inner) p.PrintOption("2) Create new account", inner) - p.PrintOption("3) Show Date", inner) + p.PrintOption(" ", inner) p.PrintOption("0) Exit", inner) fmt.Println(footer + cli.Reset) From aaa526d6918290d3f92c20de46e84795767812d1 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Wed, 16 Jul 2025 15:19:56 +0800 Subject: [PATCH 22/32] fix validation --- .env.example | 7 +++++++ cli/main.go | 11 +++++++++++ cli/panel/menu.go | 2 ++ config/makefile/build.mk | 6 +++++- pkg/auth/token.go | 4 ++-- pkg/http/middleware/token_middleware.go | 6 +++--- 6 files changed, 30 insertions(+), 6 deletions(-) diff --git a/.env.example b/.env.example index 732eb804..aaa50536 100644 --- a/.env.example +++ b/.env.example @@ -20,3 +20,10 @@ CADDY_LOGS_PATH="./storage/logs/caddy" # --- Docker (Local envs) ENV_DOCKER_USER="gocanto" ENV_DOCKER_USER_GROUP="ggroup" + +# --- Testing Token +# These variables are not intended to be used in production, but in the console interface (option: 3). +# This procedure facilitates the signatures creation for local testing/development. +# For more info, please see: cli/main.go +ENV_LOCAL_TOKEN_ACCOUNT= +ENV_LOCAL_TOKEN_SECRET= diff --git a/cli/main.go b/cli/main.go index ea4ab209..4aed52f5 100644 --- a/cli/main.go +++ b/cli/main.go @@ -8,7 +8,9 @@ import ( "github.com/oullin/database" "github.com/oullin/env" "github.com/oullin/pkg" + "github.com/oullin/pkg/auth" "github.com/oullin/pkg/cli" + "os" ) var environment *env.Environment @@ -48,6 +50,15 @@ func main() { continue } + return + case 3: + signature := auth.CreateSignatureFrom( + os.Getenv("ENV_LOCAL_TOKEN_ACCOUNT"), + os.Getenv("ENV_LOCAL_TOKEN_SECRET"), + ) + + cli.Successln("Signature: " + signature) + return case 0: cli.Successln("Goodbye!") diff --git a/cli/panel/menu.go b/cli/panel/menu.go index 08a23ad3..f6a5848a 100644 --- a/cli/panel/menu.go +++ b/cli/panel/menu.go @@ -89,6 +89,8 @@ func (p *Menu) Print() { p.PrintOption(" ", inner) p.PrintOption("2) Create new account", inner) p.PrintOption(" ", inner) + p.PrintOption("3) Create account HTTP signature", inner) + p.PrintOption(" ", inner) p.PrintOption("0) Exit", inner) fmt.Println(footer + cli.Reset) diff --git a/config/makefile/build.mk b/config/makefile/build.mk index 4fd5c311..3a987377 100644 --- a/config/makefile/build.mk +++ b/config/makefile/build.mk @@ -1,4 +1,4 @@ -.PHONY: build-local build-ci build-prod build-release build-deploy +.PHONY: build-local build-ci build-prod build-release build-deploy build-local-restart BUILD_VERSION ?= latest BUILD_PACKAGE_OWNER := oullin @@ -8,6 +8,10 @@ DB_INFRA_SCRIPTS_PATH ?= $(DB_INFRA_ROOT_PATH)/scripts build-local: docker compose --profile local up --build -d +build-local-restart: + docker compose --profile local down && \ + docker compose --profile local up --build -d + build-ci: @printf "\n$(CYAN)Building production images for CI$(NC)\n" # This 'build' command only builds the images; it does not run them. diff --git a/pkg/auth/token.go b/pkg/auth/token.go index ab340daf..6870997e 100644 --- a/pkg/auth/token.go +++ b/pkg/auth/token.go @@ -61,8 +61,8 @@ func ValidateTokenFormat(seed string) error { return fmt.Errorf("token not found or invalid") } - if !strings.HasPrefix(token, PublicKeyPrefix) || !strings.HasPrefix(token, SecretKeyPrefix) { - return fmt.Errorf("invalid token prefix") + if strings.HasPrefix(token, PublicKeyPrefix) || strings.HasPrefix(token, SecretKeyPrefix) { + return nil } return fmt.Errorf("the given token [%s] is not valid", token) diff --git a/pkg/http/middleware/token_middleware.go b/pkg/http/middleware/token_middleware.go index 54e58b90..d70d7d36 100644 --- a/pkg/http/middleware/token_middleware.go +++ b/pkg/http/middleware/token_middleware.go @@ -37,7 +37,7 @@ func (t TokenCheckMiddleware) Handle(next http.ApiHandler) http.ApiHandler { } if err := auth.ValidateTokenFormat(publicToken); err != nil { - return t.getInvalidTokenFormatError(publicToken) + return t.getInvalidTokenFormatError(publicToken, err) } if t.shallReject(accountName, signature) { @@ -81,9 +81,9 @@ func (t TokenCheckMiddleware) getInvalidRequestError(accountName, publicToken, s } } -func (t TokenCheckMiddleware) getInvalidTokenFormatError(publicToken string) *http.ApiError { +func (t TokenCheckMiddleware) getInvalidTokenFormatError(publicToken string, err error) *http.ApiError { return &http.ApiError{ - Message: fmt.Sprintf("invalid token format: [token: %s]", auth.SafeDisplay(publicToken)), + Message: fmt.Sprintf("invalid token format [token: %s]: %v", auth.SafeDisplay(publicToken), err), Status: baseHttp.StatusForbidden, } } From 861f178d220dbf5aa13998c84482b2edcc457f45 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Wed, 16 Jul 2025 15:46:18 +0800 Subject: [PATCH 23/32] pass signature header too --- caddy/Caddyfile.prod | 3 +++ 1 file changed, 3 insertions(+) diff --git a/caddy/Caddyfile.prod b/caddy/Caddyfile.prod index a1e017af..8a1fc153 100644 --- a/caddy/Caddyfile.prod +++ b/caddy/Caddyfile.prod @@ -37,10 +37,13 @@ oullin.io { # Forward the client-sent auth headers header_up X-API-Username {http.request.header.X-API-Username} header_up X-API-Key {http.request.header.X-API-Key} + header_up X-API-Signature {http.request.header.X-API-Signature} + # Todo: Remove! # *** DEBUG: echo back to client what Caddy actually saw *** header_down X-Debug-Username {http.request.header.X-API-Username} header_down X-Debug-Key {http.request.header.X-API-Key} + header_down X-Debug-Signature {http.request.header.X-API-Signature} # Transport timeouts transport http { From 7f3975ddc8f9915a9edde1c59c12be4026518703 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Wed, 16 Jul 2025 15:54:28 +0800 Subject: [PATCH 24/32] apply tweaks --- database/model.go | 6 +++--- database/repository/api_keys.go | 2 +- database/repository/categories.go | 2 +- database/repository/tags.go | 2 +- database/repository/users.go | 2 +- pkg/http/middleware/token_middleware.go | 8 ++++++-- 6 files changed, 13 insertions(+), 9 deletions(-) diff --git a/database/model.go b/database/model.go index b20c2e6a..c8092e60 100644 --- a/database/model.go +++ b/database/model.go @@ -26,9 +26,9 @@ func isValidTable(seed string) bool { type APIKey struct { ID int64 `gorm:"primaryKey"` UUID string `gorm:"type:uuid;unique;not null"` - AccountName string `gorm:"column:account_name;size:50;not null;unique;uniqueIndex:uq_account_keys"` - PublicKey string `gorm:"column:public_key;size:50;not null;unique;index;uniqueIndex:uq_account_keys"` - SecretKey string `gorm:"column:secret_key;size:50;not null;unique;index;uniqueIndex:uq_account_keys"` + AccountName string `gorm:"column:account_name;not null;unique;uniqueIndex:uq_account_keys"` + PublicKey string `gorm:"column:public_key;not null;unique;index;uniqueIndex:uq_account_keys"` + SecretKey string `gorm:"column:secret_key;not null;unique;index;uniqueIndex:uq_account_keys"` CreatedAt time.Time UpdatedAt time.Time DeletedAt gorm.DeletedAt `gorm:"index"` diff --git a/database/repository/api_keys.go b/database/repository/api_keys.go index 3a9ffb1a..746a52b4 100644 --- a/database/repository/api_keys.go +++ b/database/repository/api_keys.go @@ -43,7 +43,7 @@ func (a ApiKeys) FindBy(accountName string) *database.APIKey { return nil } - if strings.Trim(key.UUID, " ") != "" { + if result.RowsAffected > 0 { return &key } diff --git a/database/repository/categories.go b/database/repository/categories.go index 53739b1f..b7e65d2c 100644 --- a/database/repository/categories.go +++ b/database/repository/categories.go @@ -23,7 +23,7 @@ func (c Categories) FindBy(slug string) *database.Category { return nil } - if strings.Trim(category.UUID, " ") != "" { + if result.RowsAffected > 0 { return &category } diff --git a/database/repository/tags.go b/database/repository/tags.go index eba250f7..e1ad435a 100644 --- a/database/repository/tags.go +++ b/database/repository/tags.go @@ -45,7 +45,7 @@ func (t Tags) FindBy(slug string) *database.Tag { return nil } - if strings.Trim(tag.UUID, " ") != "" { + if result.RowsAffected > 0 { return &tag } diff --git a/database/repository/users.go b/database/repository/users.go index f4143c40..50502e0e 100644 --- a/database/repository/users.go +++ b/database/repository/users.go @@ -23,7 +23,7 @@ func (u Users) FindBy(username string) *database.User { return nil } - if strings.Trim(user.UUID, " ") != "" { + if result.RowsAffected > 0 { return &user } diff --git a/pkg/http/middleware/token_middleware.go b/pkg/http/middleware/token_middleware.go index d70d7d36..aa316d26 100644 --- a/pkg/http/middleware/token_middleware.go +++ b/pkg/http/middleware/token_middleware.go @@ -40,7 +40,7 @@ func (t TokenCheckMiddleware) Handle(next http.ApiHandler) http.ApiHandler { return t.getInvalidTokenFormatError(publicToken, err) } - if t.shallReject(accountName, signature) { + if t.shallReject(accountName, publicToken, signature) { return t.getUnauthenticatedError(accountName, publicToken, signature) } @@ -50,13 +50,17 @@ func (t TokenCheckMiddleware) Handle(next http.ApiHandler) http.ApiHandler { } } -func (t TokenCheckMiddleware) shallReject(accountName, signature string) bool { +func (t TokenCheckMiddleware) shallReject(accountName, publicToken, signature string) bool { var item *database.APIKey if item = t.ApiKeys.FindBy(accountName); item == nil { return true } + if strings.TrimSpace(item.PublicKey) != strings.TrimSpace(publicToken) { + return true + } + token := auth.Token{ AccountName: item.AccountName, SecretKey: item.SecretKey, From 01b4b66ec3782aa410664dc6e2162506ad5faacb Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Wed, 16 Jul 2025 16:54:45 +0800 Subject: [PATCH 25/32] encrypt tokens before saving them in db --- .env.example | 1 + boost/factory.go | 5 +- cli/accounts/factory.go | 7 +- cli/accounts/handler.go | 20 ++- cli/main.go | 2 +- database/attrs.go | 4 +- .../infra/migrations/000002_api_keys.up.sql | 4 +- database/model.go | 4 +- env/app.go | 5 +- pkg/auth/encryption.go | 64 ++++++++ pkg/auth/schema.go | 14 +- pkg/auth/token.go | 57 +++++-- pkg/http/middleware/token_middleware.go | 144 +++++++++--------- 13 files changed, 221 insertions(+), 110 deletions(-) create mode 100644 pkg/auth/encryption.go diff --git a/.env.example b/.env.example index aaa50536..8ed420dc 100644 --- a/.env.example +++ b/.env.example @@ -4,6 +4,7 @@ ENV_APP_ENV_TYPE=local ENV_APP_LOG_LEVEL=debug ENV_APP_LOGS_DIR="./storage/logs/logs_%s.log" ENV_APP_LOGS_DATE_FORMAT="2006_02_01" +ENV_APP_MASTER_KEY= # --- DB ENV_DB_USER_NAME="gus" diff --git a/boost/factory.go b/boost/factory.go index 101e1054..7d5dab3f 100644 --- a/boost/factory.go +++ b/boost/factory.go @@ -60,8 +60,9 @@ func MakeEnv(validate *pkg.Validator) *env.Environment { port, _ := strconv.Atoi(env.GetEnvVar("ENV_DB_PORT")) app := env.AppEnvironment{ - Name: env.GetEnvVar("ENV_APP_NAME"), - Type: env.GetEnvVar("ENV_APP_ENV_TYPE"), + Name: env.GetEnvVar("ENV_APP_NAME"), + Type: env.GetEnvVar("ENV_APP_ENV_TYPE"), + MasterKey: env.GetEnvVar("ENV_APP_MASTER_KEY"), } db := env.DBEnvironment{ diff --git a/cli/accounts/factory.go b/cli/accounts/factory.go index 896ad478..2df988a8 100644 --- a/cli/accounts/factory.go +++ b/cli/accounts/factory.go @@ -3,21 +3,24 @@ package accounts import ( "github.com/oullin/database" "github.com/oullin/database/repository" + "github.com/oullin/env" "github.com/oullin/pkg/auth" ) type Handler struct { + Env *env.Environment Tokens *repository.ApiKeys TokenLength int IsDebugging bool } -func MakeHandler(db *database.Connection) Handler { +func MakeHandler(db *database.Connection, env *env.Environment) Handler { tokens := repository.ApiKeys{DB: db} return Handler{ + Env: env, IsDebugging: false, - TokenLength: auth.TokenMinLength, Tokens: &tokens, + TokenLength: auth.TokenMinLength, } } diff --git a/cli/accounts/handler.go b/cli/accounts/handler.go index 3d171592..78faf7a9 100644 --- a/cli/accounts/handler.go +++ b/cli/accounts/handler.go @@ -8,20 +8,30 @@ import ( ) func (h Handler) CreateAccount(accountName string) error { - token, err := auth.SetupNewAccount(accountName, h.TokenLength) + tokenHandler, err := auth.MakeTokenHandler( + []byte(h.Env.App.MasterKey), + auth.AccountNameMinLength, + auth.TokenMinLength, + ) if err != nil { - return fmt.Errorf("failed to create account tokens pair: %v", err) + return fmt.Errorf("error creating the token handler: %v", err) + } + + token, err := tokenHandler.SetupNewAccount(accountName) + + if err != nil { + return fmt.Errorf("failed to create the given account [%s] tokens pair: %v", accountName, err) } _, err = h.Tokens.Create(database.APIKeyAttr{ AccountName: token.AccountName, - SecretKey: token.SecretKey, - PublicKey: token.PublicKey, + SecretKey: token.EncryptedSecretKey, + PublicKey: token.EncryptedPublicKey, }) if err != nil { - return fmt.Errorf("failed to create account: %v", err) + return fmt.Errorf("failed to create account [%s]: %v", accountName, err) } cli.Successln("Account created successfully") diff --git a/cli/main.go b/cli/main.go index 4aed52f5..6ba03bd2 100644 --- a/cli/main.go +++ b/cli/main.go @@ -98,7 +98,7 @@ func createNewAccount(menu panel.Menu) error { return err } - handler := accounts.MakeHandler(dbConn) + handler := accounts.MakeHandler(dbConn, environment) if err = handler.CreateAccount(account); err != nil { return err diff --git a/database/attrs.go b/database/attrs.go index d949da1e..b54168bb 100644 --- a/database/attrs.go +++ b/database/attrs.go @@ -6,8 +6,8 @@ import ( type APIKeyAttr struct { AccountName string - PublicKey string - SecretKey string + PublicKey []byte + SecretKey []byte } type UsersAttrs struct { diff --git a/database/infra/migrations/000002_api_keys.up.sql b/database/infra/migrations/000002_api_keys.up.sql index 46b3afcf..7b48305e 100644 --- a/database/infra/migrations/000002_api_keys.up.sql +++ b/database/infra/migrations/000002_api_keys.up.sql @@ -2,8 +2,8 @@ CREATE TABLE api_keys ( id BIGSERIAL PRIMARY KEY, uuid UUID UNIQUE NOT NULL, account_name VARCHAR(255) UNIQUE NOT NULL, - public_key VARCHAR(255) UNIQUE NOT NULL, - secret_key VARCHAR(255) UNIQUE NOT NULL, + public_key BYTEA NOT NULL, + secret_key BYTEA NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, deleted_at TIMESTAMP DEFAULT NULL, diff --git a/database/model.go b/database/model.go index c8092e60..6e2f2b69 100644 --- a/database/model.go +++ b/database/model.go @@ -27,8 +27,8 @@ type APIKey struct { ID int64 `gorm:"primaryKey"` UUID string `gorm:"type:uuid;unique;not null"` AccountName string `gorm:"column:account_name;not null;unique;uniqueIndex:uq_account_keys"` - PublicKey string `gorm:"column:public_key;not null;unique;index;uniqueIndex:uq_account_keys"` - SecretKey string `gorm:"column:secret_key;not null;unique;index;uniqueIndex:uq_account_keys"` + PublicKey []byte `gorm:"column:public_key;not null;unique;index;uniqueIndex:uq_account_keys"` + SecretKey []byte `gorm:"column:secret_key;not null;unique;index;uniqueIndex:uq_account_keys"` CreatedAt time.Time UpdatedAt time.Time DeletedAt gorm.DeletedAt `gorm:"index"` diff --git a/env/app.go b/env/app.go index 9038eb44..94e8582d 100644 --- a/env/app.go +++ b/env/app.go @@ -5,8 +5,9 @@ const staging = "staging" const production = "production" type AppEnvironment struct { - Name string `validate:"required,min=4"` - Type string `validate:"required,lowercase,oneof=local production staging"` + Name string `validate:"required,min=4"` + Type string `validate:"required,lowercase,oneof=local production staging"` + MasterKey string `validate:"required,min=32"` } func (e AppEnvironment) IsProduction() bool { diff --git a/pkg/auth/encryption.go b/pkg/auth/encryption.go new file mode 100644 index 00000000..cd91d37d --- /dev/null +++ b/pkg/auth/encryption.go @@ -0,0 +1,64 @@ +package auth + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "fmt" + "io" +) + +func Encrypt(plaintext []byte, key []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + // GCM is an authenticated encryption mode that provides confidentiality and integrity. + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + + // A nonce is a "number used once" to ensure that the same plaintext + // encrypts to different ciphertexts each time. It must be unique for each encryption. + nonce := make([]byte, gcm.NonceSize()) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + return nil, err + } + + // Seal will Encrypt the data and append the authentication tag. + // We prepend the nonce to the ciphertext for use during decryption. + ciphertext := gcm.Seal(nonce, nonce, plaintext, nil) + + return ciphertext, nil +} + +func Decrypt(ciphertext []byte, key []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + + if err != nil { + return nil, err + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + + nonceSize := gcm.NonceSize() + if len(ciphertext) < nonceSize { + return nil, fmt.Errorf("ciphertext too short") + } + + // The nonce is prepended to the ciphertext. + nonce, encryptedMessage := ciphertext[:nonceSize], ciphertext[nonceSize:] + + // Open will decrypt and authenticate the message. + plaintext, err := gcm.Open(nil, nonce, encryptedMessage, nil) + if err != nil { + return nil, err + } + + return plaintext, nil +} diff --git a/pkg/auth/schema.go b/pkg/auth/schema.go index 6022718f..ce3cdc9e 100644 --- a/pkg/auth/schema.go +++ b/pkg/auth/schema.go @@ -8,8 +8,14 @@ const ( ) type Token struct { - AccountName string `validate:"required,min=5"` - PublicKey string `validate:"required"` - SecretKey string `validate:"required"` - Length int `validate:"required"` + AccountName string + PublicKey string + EncryptedPublicKey []byte + SecretKey string + EncryptedSecretKey []byte +} + +type SecureToken struct { + PlainText string + EncryptedText []byte } diff --git a/pkg/auth/token.go b/pkg/auth/token.go index 6870997e..5343ba2b 100644 --- a/pkg/auth/token.go +++ b/pkg/auth/token.go @@ -9,40 +9,59 @@ import ( "strings" ) -func SetupNewAccount(accountName string, TokenLength int) (*Token, error) { +type TokenHandler struct { + EncryptionKey []byte + TokenMinLength int + AccountNameMinLength int +} + +func MakeTokenHandler(encryptionKey []byte, accountNameMinLength, tokenMinLength int) (*TokenHandler, error) { + if tokenMinLength < TokenMinLength { + return nil, fmt.Errorf("the token length should be at least %d", TokenMinLength) + } + + if accountNameMinLength < AccountNameMinLength { + return nil, fmt.Errorf("the token length should be at least %d", AccountNameMinLength) + } + + return &TokenHandler{ + EncryptionKey: encryptionKey, + TokenMinLength: tokenMinLength, + AccountNameMinLength: accountNameMinLength, + }, nil +} + +func (t *TokenHandler) SetupNewAccount(accountName string) (*Token, error) { token := Token{} if len(accountName) < AccountNameMinLength { return nil, fmt.Errorf("account name must be at least %d characters", AccountNameMinLength) } - pk, err := generateSecureToken(TokenLength) + pk, err := t.generateSecureToken(PublicKeyPrefix) if err != nil { return nil, fmt.Errorf("failed to generate public key: %w", err) } - sk, err := generateSecureToken(TokenLength) + sk, err := t.generateSecureToken(SecretKeyPrefix) if err != nil { return nil, fmt.Errorf("failed to generate secret key: %w", err) } - token.PublicKey = PublicKeyPrefix + pk - token.SecretKey = SecretKeyPrefix + sk - token.Length = TokenLength token.AccountName = accountName + token.PublicKey = pk.PlainText + token.EncryptedPublicKey = pk.EncryptedText + token.SecretKey = sk.PlainText + token.EncryptedSecretKey = sk.EncryptedText return &token, nil } -func generateSecureToken(length int) (string, error) { - if length < TokenMinLength { - return "", fmt.Errorf("the token length should be >= %d", length) - } - - salt := make([]byte, length) +func (t *TokenHandler) generateSecureToken(prefix string) (*SecureToken, error) { + salt := make([]byte, t.TokenMinLength) if _, err := rand.Read(salt); err != nil { - return "", fmt.Errorf("failed to generate secure tokens salt: %v", err) + return nil, fmt.Errorf("failed to generate secure tokens salt: %v", err) } hasher := sha256.New() @@ -51,7 +70,17 @@ func generateSecureToken(length int) (string, error) { // Get the resulting hash and encode it as a hex string. hashBytes := hasher.Sum(nil) - return hex.EncodeToString(hashBytes), nil + text := prefix + hex.EncodeToString(hashBytes) + encryptedText, err := Encrypt([]byte(text), t.EncryptionKey) + + if err != nil { + return nil, fmt.Errorf("failed to Encrypt: %w", err) + } + + return &SecureToken{ + PlainText: text, + EncryptedText: encryptedText, + }, nil } func ValidateTokenFormat(seed string) error { diff --git a/pkg/http/middleware/token_middleware.go b/pkg/http/middleware/token_middleware.go index aa316d26..71e2871b 100644 --- a/pkg/http/middleware/token_middleware.go +++ b/pkg/http/middleware/token_middleware.go @@ -1,14 +1,10 @@ package middleware import ( - "fmt" - "github.com/oullin/database" "github.com/oullin/database/repository" - "github.com/oullin/pkg/auth" "github.com/oullin/pkg/http" "log/slog" baseHttp "net/http" - "strings" ) const tokenHeader = "X-API-Key" @@ -28,21 +24,21 @@ func MakeTokenMiddleware(apiKeys *repository.ApiKeys) TokenCheckMiddleware { func (t TokenCheckMiddleware) Handle(next http.ApiHandler) http.ApiHandler { return func(w baseHttp.ResponseWriter, r *baseHttp.Request) *http.ApiError { - accountName := strings.TrimSpace(r.Header.Get(usernameHeader)) - publicToken := strings.TrimSpace(r.Header.Get(tokenHeader)) - signature := strings.TrimSpace(r.Header.Get(signatureHeader)) - - if accountName == "" || publicToken == "" || signature == "" { - return t.getInvalidRequestError(accountName, publicToken, signature) - } - - if err := auth.ValidateTokenFormat(publicToken); err != nil { - return t.getInvalidTokenFormatError(publicToken, err) - } - - if t.shallReject(accountName, publicToken, signature) { - return t.getUnauthenticatedError(accountName, publicToken, signature) - } + //accountName := strings.TrimSpace(r.Header.Get(usernameHeader)) + //publicToken := strings.TrimSpace(r.Header.Get(tokenHeader)) + //signature := strings.TrimSpace(r.Header.Get(signatureHeader)) + // + //if accountName == "" || publicToken == "" || signature == "" { + // return t.getInvalidRequestError(accountName, publicToken, signature) + //} + // + //if err := auth.ValidateTokenFormat(publicToken); err != nil { + // return t.getInvalidTokenFormatError(publicToken, err) + //} + // + //if t.shallReject(accountName, publicToken, signature) { + // return t.getUnauthenticatedError(accountName, publicToken, signature) + //} slog.Info("Token validation successful") @@ -50,58 +46,58 @@ func (t TokenCheckMiddleware) Handle(next http.ApiHandler) http.ApiHandler { } } -func (t TokenCheckMiddleware) shallReject(accountName, publicToken, signature string) bool { - var item *database.APIKey - - if item = t.ApiKeys.FindBy(accountName); item == nil { - return true - } - - if strings.TrimSpace(item.PublicKey) != strings.TrimSpace(publicToken) { - return true - } - - token := auth.Token{ - AccountName: item.AccountName, - SecretKey: item.SecretKey, - PublicKey: item.PublicKey, - Length: len(item.PublicKey), - } - - return token.HasInValidSignature(signature) -} - -func (t TokenCheckMiddleware) getInvalidRequestError(accountName, publicToken, signature string) *http.ApiError { - message := fmt.Sprintf( - "invalid request. Please, provide a valid token, signature and accout name headers. [account: %s, public token: %s, signature: %s]", - accountName, - auth.SafeDisplay(publicToken), - auth.SafeDisplay(signature), - ) - - return &http.ApiError{ - Message: message, - Status: baseHttp.StatusForbidden, - } -} - -func (t TokenCheckMiddleware) getInvalidTokenFormatError(publicToken string, err error) *http.ApiError { - return &http.ApiError{ - Message: fmt.Sprintf("invalid token format [token: %s]: %v", auth.SafeDisplay(publicToken), err), - Status: baseHttp.StatusForbidden, - } -} - -func (t TokenCheckMiddleware) getUnauthenticatedError(accountName, publicToken, signature string) *http.ApiError { - message := fmt.Sprintf( - "Unauthenticated, please check your credentials and signature headers: [token: %s, account name: %s, signature: %s]", - auth.SafeDisplay(publicToken), - accountName, - auth.SafeDisplay(signature), - ) - - return &http.ApiError{ - Message: message, - Status: baseHttp.StatusForbidden, - } -} +//func (t TokenCheckMiddleware) shallReject(accountName, publicToken, signature string) bool { +// var item *database.APIKey +// +// if item = t.ApiKeys.FindBy(accountName); item == nil { +// return true +// } +// +// if strings.TrimSpace(item.PublicKey) != strings.TrimSpace(publicToken) { +// return true +// } +// +// token := auth.Token{ +// AccountName: item.AccountName, +// SecretKey: item.SecretKey, +// PublicKey: item.PublicKey, +// Length: len(item.PublicKey), +// } +// +// return token.HasInValidSignature(signature) +//} +// +//func (t TokenCheckMiddleware) getInvalidRequestError(accountName, publicToken, signature string) *http.ApiError { +// message := fmt.Sprintf( +// "invalid request. Please, provide a valid token, signature and accout name headers. [account: %s, public token: %s, signature: %s]", +// accountName, +// auth.SafeDisplay(publicToken), +// auth.SafeDisplay(signature), +// ) +// +// return &http.ApiError{ +// Message: message, +// Status: baseHttp.StatusForbidden, +// } +//} +// +//func (t TokenCheckMiddleware) getInvalidTokenFormatError(publicToken string, err error) *http.ApiError { +// return &http.ApiError{ +// Message: fmt.Sprintf("invalid token format [token: %s]: %v", auth.SafeDisplay(publicToken), err), +// Status: baseHttp.StatusForbidden, +// } +//} +// +//func (t TokenCheckMiddleware) getUnauthenticatedError(accountName, publicToken, signature string) *http.ApiError { +// message := fmt.Sprintf( +// "Unauthenticated, please check your credentials and signature headers: [token: %s, account name: %s, signature: %s]", +// auth.SafeDisplay(publicToken), +// accountName, +// auth.SafeDisplay(signature), +// ) +// +// return &http.ApiError{ +// Message: message, +// Status: baseHttp.StatusForbidden, +// } +//} From 90dfc240679e8662034bac0d3032b922ea18c640 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Thu, 17 Jul 2025 10:36:26 +0800 Subject: [PATCH 26/32] re-work create api account, show api account, and app key generation --- cli/accounts/factory.go | 31 +++++++++++++-------- cli/accounts/handler.go | 43 ++++++++++++++++++++--------- cli/main.go | 60 ++++++++++++++++++++++++++++++++++++++--- cli/panel/menu.go | 14 +++++----- pkg/auth/encryption.go | 10 +++++++ pkg/auth/schema.go | 1 + pkg/auth/token.go | 52 +++++++++++++++++++++++++---------- 7 files changed, 164 insertions(+), 47 deletions(-) diff --git a/cli/accounts/factory.go b/cli/accounts/factory.go index 2df988a8..b9946c12 100644 --- a/cli/accounts/factory.go +++ b/cli/accounts/factory.go @@ -1,6 +1,7 @@ package accounts import ( + "fmt" "github.com/oullin/database" "github.com/oullin/database/repository" "github.com/oullin/env" @@ -8,19 +9,27 @@ import ( ) type Handler struct { - Env *env.Environment - Tokens *repository.ApiKeys - TokenLength int - IsDebugging bool + IsDebugging bool + Env *env.Environment + Tokens *repository.ApiKeys + TokenHandler *auth.TokenHandler } -func MakeHandler(db *database.Connection, env *env.Environment) Handler { - tokens := repository.ApiKeys{DB: db} +func MakeHandler(db *database.Connection, env *env.Environment) (*Handler, error) { + tokenHandler, err := auth.MakeTokensHandler( + []byte(env.App.MasterKey), + auth.AccountNameMinLength, + auth.TokenMinLength, + ) - return Handler{ - Env: env, - IsDebugging: false, - Tokens: &tokens, - TokenLength: auth.TokenMinLength, + if err != nil { + return nil, fmt.Errorf("failed to make token handler: %v", err) } + + return &Handler{ + Env: env, + IsDebugging: false, + Tokens: &repository.ApiKeys{DB: db}, + TokenHandler: tokenHandler, + }, nil } diff --git a/cli/accounts/handler.go b/cli/accounts/handler.go index 78faf7a9..308fdae7 100644 --- a/cli/accounts/handler.go +++ b/cli/accounts/handler.go @@ -8,17 +8,7 @@ import ( ) func (h Handler) CreateAccount(accountName string) error { - tokenHandler, err := auth.MakeTokenHandler( - []byte(h.Env.App.MasterKey), - auth.AccountNameMinLength, - auth.TokenMinLength, - ) - - if err != nil { - return fmt.Errorf("error creating the token handler: %v", err) - } - - token, err := tokenHandler.SetupNewAccount(accountName) + token, err := h.TokenHandler.SetupNewAccount(accountName) if err != nil { return fmt.Errorf("failed to create the given account [%s] tokens pair: %v", accountName, err) @@ -34,7 +24,36 @@ func (h Handler) CreateAccount(accountName string) error { return fmt.Errorf("failed to create account [%s]: %v", accountName, err) } - cli.Successln("Account created successfully") + cli.Successln("Account created successfully.\n") + + return nil +} + +func (h Handler) ReadAccount(accountName string) error { + item := h.Tokens.FindBy(accountName) + + if item == nil { + return fmt.Errorf("the given account [%s] was not found", accountName) + } + + token, err := h.TokenHandler.DecodeTokensFor( + item.AccountName, + item.SecretKey, + item.PublicKey, + ) + + if err != nil { + return fmt.Errorf("could not decode the given account [%s] keys: %v", item.AccountName, err) + } + + cli.Successln("\nThe given account has been found successfully!\n") + cli.Blueln(" > " + fmt.Sprintf("Account name: %s", token.AccountName)) + cli.Blueln(" > " + fmt.Sprintf("Public Key: %s", auth.SafeDisplay(token.PublicKey))) + cli.Blueln(" > " + fmt.Sprintf("Secret Key: %s", auth.SafeDisplay(token.SecretKey))) + cli.Warningln("----- Encrypted Values -----") + cli.Blueln(" > " + fmt.Sprintf("Public Key: %x", token.EncryptedPublicKey)) + cli.Blueln(" > " + fmt.Sprintf("Secret Key: %x", token.EncryptedSecretKey)) + fmt.Println(" ") return nil } diff --git a/cli/main.go b/cli/main.go index 6ba03bd2..bb73a8fa 100644 --- a/cli/main.go +++ b/cli/main.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "github.com/oullin/boost" "github.com/oullin/cli/accounts" "github.com/oullin/cli/panel" @@ -45,13 +46,21 @@ func main() { return case 2: - if err = createNewAccount(menu); err != nil { + if err = createNewApiAccount(menu); err != nil { cli.Errorln(err.Error()) continue } return case 3: + if err = showApiAccount(menu); err != nil { + cli.Errorln(err.Error()) + continue + } + + return + + case 4: signature := auth.CreateSignatureFrom( os.Getenv("ENV_LOCAL_TOKEN_ACCOUNT"), os.Getenv("ENV_LOCAL_TOKEN_SECRET"), @@ -59,6 +68,13 @@ func main() { cli.Successln("Signature: " + signature) + return + case 5: + if err = generateAppEncryptionKey(); err != nil { + cli.Errorln(err.Error()) + continue + } + return case 0: cli.Successln("Goodbye!") @@ -90,15 +106,18 @@ func createBlogPost(menu panel.Menu) error { return nil } -func createNewAccount(menu panel.Menu) error { +func createNewApiAccount(menu panel.Menu) error { var err error var account string + var handler *accounts.Handler if account, err = menu.CaptureAccountName(); err != nil { return err } - handler := accounts.MakeHandler(dbConn, environment) + if handler, err = accounts.MakeHandler(dbConn, environment); err != nil { + return err + } if err = handler.CreateAccount(account); err != nil { return err @@ -106,3 +125,38 @@ func createNewAccount(menu panel.Menu) error { return nil } + +func showApiAccount(menu panel.Menu) error { + var err error + var account string + var handler *accounts.Handler + + if account, err = menu.CaptureAccountName(); err != nil { + return err + } + + if handler, err = accounts.MakeHandler(dbConn, environment); err != nil { + return err + } + + if handler.ReadAccount(account) != nil { + return err + } + + return nil +} + +func generateAppEncryptionKey() error { + var err error + var key []byte + + if key, err = auth.GenerateAESKey(); err != nil { + return err + } + + cli.Successln("\n The key was generated successfully.") + cli.Magentaln(fmt.Sprintf(" > key: %x", key)) + fmt.Println(" ") + + return nil +} diff --git a/cli/panel/menu.go b/cli/panel/menu.go index f6a5848a..f794b15c 100644 --- a/cli/panel/menu.go +++ b/cli/panel/menu.go @@ -85,13 +85,13 @@ func (p *Menu) Print() { fmt.Println(title) fmt.Println(divider) - p.PrintOption("1) Parse Blog Posts", inner) - p.PrintOption(" ", inner) - p.PrintOption("2) Create new account", inner) - p.PrintOption(" ", inner) - p.PrintOption("3) Create account HTTP signature", inner) - p.PrintOption(" ", inner) - p.PrintOption("0) Exit", inner) + p.PrintOption("1) Parse Blog Posts.", inner) + p.PrintOption("2) Create new API account.", inner) + p.PrintOption("3) Show API accounts.", inner) + p.PrintOption("4) Create accounts HTTP signature.", inner) + p.PrintOption("5) Generate app encryption key.", inner) + p.PrintOption(" ", inner) + p.PrintOption("0) Exit.", inner) fmt.Println(footer + cli.Reset) } diff --git a/pkg/auth/encryption.go b/pkg/auth/encryption.go index cd91d37d..6730c422 100644 --- a/pkg/auth/encryption.go +++ b/pkg/auth/encryption.go @@ -8,6 +8,16 @@ import ( "io" ) +func GenerateAESKey() ([]byte, error) { + key := make([]byte, EncryptionKeyLength) + + if _, err := rand.Read(key); err != nil { + return nil, fmt.Errorf("failed to generate random key: %w", err) + } + + return key, nil +} + func Encrypt(plaintext []byte, key []byte) ([]byte, error) { block, err := aes.NewCipher(key) if err != nil { diff --git a/pkg/auth/schema.go b/pkg/auth/schema.go index ce3cdc9e..e6f584d8 100644 --- a/pkg/auth/schema.go +++ b/pkg/auth/schema.go @@ -5,6 +5,7 @@ const ( SecretKeyPrefix = "sk_" TokenMinLength = 16 AccountNameMinLength = 5 + EncryptionKeyLength = 32 ) type Token struct { diff --git a/pkg/auth/token.go b/pkg/auth/token.go index 5343ba2b..acdf2422 100644 --- a/pkg/auth/token.go +++ b/pkg/auth/token.go @@ -15,7 +15,11 @@ type TokenHandler struct { AccountNameMinLength int } -func MakeTokenHandler(encryptionKey []byte, accountNameMinLength, tokenMinLength int) (*TokenHandler, error) { +func MakeTokensHandler(encryptionKey []byte, accountNameMinLength, tokenMinLength int) (*TokenHandler, error) { + if len(encryptionKey) != EncryptionKeyLength { + return nil, fmt.Errorf("encryption key length must be equal to %d bytes", EncryptionKeyLength) + } + if tokenMinLength < TokenMinLength { return nil, fmt.Errorf("the token length should be at least %d", TokenMinLength) } @@ -31,30 +35,50 @@ func MakeTokenHandler(encryptionKey []byte, accountNameMinLength, tokenMinLength }, nil } +func (t *TokenHandler) DecodeTokensFor(accountName string, secret, public []byte) (*Token, error) { + var err error + var publicKey, secretKey []byte + + if publicKey, err = Decrypt(public, t.EncryptionKey); err != nil { + return nil, fmt.Errorf("unable to decrypt public key: %w", err) + } + + if secretKey, err = Decrypt(secret, t.EncryptionKey); err != nil { + return nil, fmt.Errorf("unable to decrypt secret key: %w", err) + } + + return &Token{ + AccountName: accountName, + PublicKey: string(publicKey), + EncryptedPublicKey: public, + SecretKey: string(secretKey), + EncryptedSecretKey: secret, + }, nil +} + func (t *TokenHandler) SetupNewAccount(accountName string) (*Token, error) { - token := Token{} + var err error + var pk, sk *SecureToken if len(accountName) < AccountNameMinLength { return nil, fmt.Errorf("account name must be at least %d characters", AccountNameMinLength) } - pk, err := t.generateSecureToken(PublicKeyPrefix) - if err != nil { + if pk, err = t.generateSecureToken(PublicKeyPrefix); err != nil { return nil, fmt.Errorf("failed to generate public key: %w", err) } - sk, err := t.generateSecureToken(SecretKeyPrefix) - if err != nil { + if sk, err = t.generateSecureToken(SecretKeyPrefix); err != nil { return nil, fmt.Errorf("failed to generate secret key: %w", err) } - token.AccountName = accountName - token.PublicKey = pk.PlainText - token.EncryptedPublicKey = pk.EncryptedText - token.SecretKey = sk.PlainText - token.EncryptedSecretKey = sk.EncryptedText - - return &token, nil + return &Token{ + AccountName: accountName, + PublicKey: pk.PlainText, + EncryptedPublicKey: pk.EncryptedText, + SecretKey: sk.PlainText, + EncryptedSecretKey: sk.EncryptedText, + }, nil } func (t *TokenHandler) generateSecureToken(prefix string) (*SecureToken, error) { @@ -106,7 +130,7 @@ func CreateSignatureFrom(message, secretKey string) string { func SafeDisplay(secret string) string { var prefixLen int - visibleChars := 8 + visibleChars := 10 if strings.HasPrefix(secret, PublicKeyPrefix) { prefixLen = len(PublicKeyPrefix) From 7119e0824930723455543f0f60b545703d73dd3d Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Thu, 17 Jul 2025 10:45:19 +0800 Subject: [PATCH 27/32] fix --- cli/main.go | 2 +- pkg/auth/token.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/main.go b/cli/main.go index bb73a8fa..f9fb9a4f 100644 --- a/cli/main.go +++ b/cli/main.go @@ -139,7 +139,7 @@ func showApiAccount(menu panel.Menu) error { return err } - if handler.ReadAccount(account) != nil { + if err = handler.ReadAccount(account); err != nil { return err } diff --git a/pkg/auth/token.go b/pkg/auth/token.go index acdf2422..6abe1417 100644 --- a/pkg/auth/token.go +++ b/pkg/auth/token.go @@ -25,7 +25,7 @@ func MakeTokensHandler(encryptionKey []byte, accountNameMinLength, tokenMinLengt } if accountNameMinLength < AccountNameMinLength { - return nil, fmt.Errorf("the token length should be at least %d", AccountNameMinLength) + return nil, fmt.Errorf("the account name length should be at least %d", AccountNameMinLength) } return &TokenHandler{ From 8d0120cd46e6734f4bc0bf65e9ba234837676ac8 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Thu, 17 Jul 2025 11:06:08 +0800 Subject: [PATCH 28/32] http signature --- cli/accounts/handler.go | 34 +++++++++++++++++++++++++-- cli/main.go | 32 ++++++++++++++++++------- cli/panel/menu.go | 2 +- pkg/auth/encryption.go | 25 ++++++++++++++++++++ pkg/auth/{token.go => handler.go} | 39 ------------------------------- pkg/auth/render.go | 20 ++++++++++++++++ 6 files changed, 102 insertions(+), 50 deletions(-) rename pkg/auth/{token.go => handler.go} (78%) create mode 100644 pkg/auth/render.go diff --git a/cli/accounts/handler.go b/cli/accounts/handler.go index 308fdae7..8a581b58 100644 --- a/cli/accounts/handler.go +++ b/cli/accounts/handler.go @@ -51,8 +51,38 @@ func (h Handler) ReadAccount(accountName string) error { cli.Blueln(" > " + fmt.Sprintf("Public Key: %s", auth.SafeDisplay(token.PublicKey))) cli.Blueln(" > " + fmt.Sprintf("Secret Key: %s", auth.SafeDisplay(token.SecretKey))) cli.Warningln("----- Encrypted Values -----") - cli.Blueln(" > " + fmt.Sprintf("Public Key: %x", token.EncryptedPublicKey)) - cli.Blueln(" > " + fmt.Sprintf("Secret Key: %x", token.EncryptedSecretKey)) + cli.Magentaln(" > " + fmt.Sprintf("Public Key: %x", token.EncryptedPublicKey)) + cli.Magentaln(" > " + fmt.Sprintf("Secret Key: %x", token.EncryptedSecretKey)) + fmt.Println(" ") + + return nil +} + +func (h Handler) CreateSignature(accountName string) error { + item := h.Tokens.FindBy(accountName) + + if item == nil { + return fmt.Errorf("the given account [%s] was not found", accountName) + } + + token, err := h.TokenHandler.DecodeTokensFor( + item.AccountName, + item.SecretKey, + item.PublicKey, + ) + + if err != nil { + return fmt.Errorf("could not decode the given account [%s] keys: %v", item.AccountName, err) + } + + signature := auth.CreateSignatureFrom(token.PublicKey, token.SecretKey) + + cli.Successln("\nThe given account has been found successfully!\n") + cli.Blueln(" > " + fmt.Sprintf("Account name: %s", token.AccountName)) + cli.Blueln(" > " + fmt.Sprintf("Public Key: %s", auth.SafeDisplay(token.PublicKey))) + cli.Blueln(" > " + fmt.Sprintf("Secret Key: %s", auth.SafeDisplay(token.SecretKey))) + cli.Warningln("----- Encrypted Values -----") + cli.Magentaln(" > " + fmt.Sprintf("Signature: %s", signature)) fmt.Println(" ") return nil diff --git a/cli/main.go b/cli/main.go index f9fb9a4f..60719c80 100644 --- a/cli/main.go +++ b/cli/main.go @@ -11,7 +11,6 @@ import ( "github.com/oullin/pkg" "github.com/oullin/pkg/auth" "github.com/oullin/pkg/cli" - "os" ) var environment *env.Environment @@ -59,14 +58,11 @@ func main() { } return - case 4: - signature := auth.CreateSignatureFrom( - os.Getenv("ENV_LOCAL_TOKEN_ACCOUNT"), - os.Getenv("ENV_LOCAL_TOKEN_SECRET"), - ) - - cli.Successln("Signature: " + signature) + if err = generateApiAccountsHTTPSignature(menu); err != nil { + cli.Errorln(err.Error()) + continue + } return case 5: @@ -146,6 +142,26 @@ func showApiAccount(menu panel.Menu) error { return nil } +func generateApiAccountsHTTPSignature(menu panel.Menu) error { + var err error + var account string + var handler *accounts.Handler + + if account, err = menu.CaptureAccountName(); err != nil { + return err + } + + if handler, err = accounts.MakeHandler(dbConn, environment); err != nil { + return err + } + + if err = handler.CreateSignature(account); err != nil { + return err + } + + return nil +} + func generateAppEncryptionKey() error { var err error var key []byte diff --git a/cli/panel/menu.go b/cli/panel/menu.go index f794b15c..b0090237 100644 --- a/cli/panel/menu.go +++ b/cli/panel/menu.go @@ -88,7 +88,7 @@ func (p *Menu) Print() { p.PrintOption("1) Parse Blog Posts.", inner) p.PrintOption("2) Create new API account.", inner) p.PrintOption("3) Show API accounts.", inner) - p.PrintOption("4) Create accounts HTTP signature.", inner) + p.PrintOption("4) Generate API accounts HTTP signature.", inner) p.PrintOption("5) Generate app encryption key.", inner) p.PrintOption(" ", inner) p.PrintOption("0) Exit.", inner) diff --git a/pkg/auth/encryption.go b/pkg/auth/encryption.go index 6730c422..4210f9b9 100644 --- a/pkg/auth/encryption.go +++ b/pkg/auth/encryption.go @@ -3,9 +3,13 @@ package auth import ( "crypto/aes" "crypto/cipher" + "crypto/hmac" "crypto/rand" + "crypto/sha256" + "encoding/hex" "fmt" "io" + "strings" ) func GenerateAESKey() ([]byte, error) { @@ -72,3 +76,24 @@ func Decrypt(ciphertext []byte, key []byte) ([]byte, error) { return plaintext, nil } + +func CreateSignatureFrom(message, secretKey string) string { + mac := hmac.New(sha256.New, []byte(secretKey)) + mac.Write([]byte(message)) + + return hex.EncodeToString(mac.Sum(nil)) +} + +func ValidateTokenFormat(seed string) error { + token := strings.TrimSpace(seed) + + if token == "" || len(token) < TokenMinLength { + return fmt.Errorf("token not found or invalid") + } + + if strings.HasPrefix(token, PublicKeyPrefix) || strings.HasPrefix(token, SecretKeyPrefix) { + return nil + } + + return fmt.Errorf("the given token [%s] is not valid", token) +} diff --git a/pkg/auth/token.go b/pkg/auth/handler.go similarity index 78% rename from pkg/auth/token.go rename to pkg/auth/handler.go index 6abe1417..dbcf6824 100644 --- a/pkg/auth/token.go +++ b/pkg/auth/handler.go @@ -6,7 +6,6 @@ import ( "crypto/sha256" "encoding/hex" "fmt" - "strings" ) type TokenHandler struct { @@ -107,44 +106,6 @@ func (t *TokenHandler) generateSecureToken(prefix string) (*SecureToken, error) }, nil } -func ValidateTokenFormat(seed string) error { - token := strings.TrimSpace(seed) - - if token == "" || len(token) < TokenMinLength { - return fmt.Errorf("token not found or invalid") - } - - if strings.HasPrefix(token, PublicKeyPrefix) || strings.HasPrefix(token, SecretKeyPrefix) { - return nil - } - - return fmt.Errorf("the given token [%s] is not valid", token) -} - -func CreateSignatureFrom(message, secretKey string) string { - mac := hmac.New(sha256.New, []byte(secretKey)) - mac.Write([]byte(message)) - - return hex.EncodeToString(mac.Sum(nil)) -} - -func SafeDisplay(secret string) string { - var prefixLen int - visibleChars := 10 - - if strings.HasPrefix(secret, PublicKeyPrefix) { - prefixLen = len(PublicKeyPrefix) - } else { - prefixLen = len(SecretKeyPrefix) - } - - if len(secret) <= prefixLen+visibleChars { - return secret - } - - return secret[:prefixLen+visibleChars] + "..." -} - func (t Token) HasInValidSignature(receivedSignature string) bool { return !t.HasValidSignature(receivedSignature) } diff --git a/pkg/auth/render.go b/pkg/auth/render.go new file mode 100644 index 00000000..855b3680 --- /dev/null +++ b/pkg/auth/render.go @@ -0,0 +1,20 @@ +package auth + +import "strings" + +func SafeDisplay(secret string) string { + var prefixLen int + visibleChars := 10 + + if strings.HasPrefix(secret, PublicKeyPrefix) { + prefixLen = len(PublicKeyPrefix) + } else { + prefixLen = len(SecretKeyPrefix) + } + + if len(secret) <= prefixLen+visibleChars { + return secret + } + + return secret[:prefixLen+visibleChars] + "..." +} From 4bcb3234ec2a23b315e863a34606909eec77c7a6 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Thu, 17 Jul 2025 11:35:51 +0800 Subject: [PATCH 29/32] wire middleware --- boost/app.go | 23 +++- boost/router.go | 1 + cli/accounts/factory.go | 2 - main.go | 9 +- pkg/auth/handler.go | 14 +-- pkg/http/middleware/pipeline.go | 6 +- pkg/http/middleware/token_middleware.go | 151 ++++++++++++------------ 7 files changed, 110 insertions(+), 96 deletions(-) diff --git a/boost/app.go b/boost/app.go index b8d1d757..485e40c8 100644 --- a/boost/app.go +++ b/boost/app.go @@ -1,10 +1,12 @@ package boost import ( + "fmt" "github.com/oullin/database" "github.com/oullin/database/repository" "github.com/oullin/env" "github.com/oullin/pkg" + "github.com/oullin/pkg/auth" "github.com/oullin/pkg/http/middleware" "github.com/oullin/pkg/llogs" baseHttp "net/http" @@ -19,7 +21,15 @@ type App struct { db *database.Connection } -func MakeApp(env *env.Environment, validator *pkg.Validator) *App { +func MakeApp(env *env.Environment, validator *pkg.Validator) (*App, error) { + tokenHandler, err := auth.MakeTokensHandler( + []byte(env.App.MasterKey), + ) + + if err != nil { + return nil, fmt.Errorf("bootstrapping error > could not create a token handler: %w", err) + } + db := MakeDbConnection(env) app := App{ @@ -34,19 +44,20 @@ func MakeApp(env *env.Environment, validator *pkg.Validator) *App { Env: env, Mux: baseHttp.NewServeMux(), Pipeline: middleware.Pipeline{ - Env: env, - ApiKeys: &repository.ApiKeys{DB: db}, + Env: env, + ApiKeys: &repository.ApiKeys{DB: db}, + TokenHandler: tokenHandler, }, } app.SetRouter(router) - return &app + return &app, nil } func (a *App) Boot() { - if a.router == nil { - panic("Router is not set") + if a == nil || a.router == nil { + panic("bootstrapping error > Invalid setup") } router := *a.router diff --git a/boost/router.go b/boost/router.go index 4061e485..8433d7fb 100644 --- a/boost/router.go +++ b/boost/router.go @@ -16,6 +16,7 @@ type Router struct { func (r *Router) PipelineFor(apiHandler http.ApiHandler) baseHttp.HandlerFunc { tokenMiddleware := middleware.MakeTokenMiddleware( + r.Pipeline.TokenHandler, r.Pipeline.ApiKeys, ) diff --git a/cli/accounts/factory.go b/cli/accounts/factory.go index b9946c12..8f9752fb 100644 --- a/cli/accounts/factory.go +++ b/cli/accounts/factory.go @@ -18,8 +18,6 @@ type Handler struct { func MakeHandler(db *database.Connection, env *env.Environment) (*Handler, error) { tokenHandler, err := auth.MakeTokensHandler( []byte(env.App.MasterKey), - auth.AccountNameMinLength, - auth.TokenMinLength, ) if err != nil { diff --git a/main.go b/main.go index 046b47c1..0549f9c5 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "fmt" _ "github.com/lib/pq" "github.com/oullin/boost" "github.com/oullin/pkg" @@ -12,10 +13,14 @@ var app *boost.App func init() { validate := pkg.GetDefaultValidator() - secrets := boost.Ignite("./.env", validate) + application, err := boost.MakeApp(secrets, validate) + + if err != nil { + panic(fmt.Sprintf("init: Error creating application: %s", err)) + } - app = boost.MakeApp(secrets, validate) + app = application } func main() { diff --git a/pkg/auth/handler.go b/pkg/auth/handler.go index dbcf6824..e987b866 100644 --- a/pkg/auth/handler.go +++ b/pkg/auth/handler.go @@ -14,23 +14,15 @@ type TokenHandler struct { AccountNameMinLength int } -func MakeTokensHandler(encryptionKey []byte, accountNameMinLength, tokenMinLength int) (*TokenHandler, error) { +func MakeTokensHandler(encryptionKey []byte) (*TokenHandler, error) { if len(encryptionKey) != EncryptionKeyLength { return nil, fmt.Errorf("encryption key length must be equal to %d bytes", EncryptionKeyLength) } - if tokenMinLength < TokenMinLength { - return nil, fmt.Errorf("the token length should be at least %d", TokenMinLength) - } - - if accountNameMinLength < AccountNameMinLength { - return nil, fmt.Errorf("the account name length should be at least %d", AccountNameMinLength) - } - return &TokenHandler{ EncryptionKey: encryptionKey, - TokenMinLength: tokenMinLength, - AccountNameMinLength: accountNameMinLength, + TokenMinLength: TokenMinLength, + AccountNameMinLength: AccountNameMinLength, }, nil } diff --git a/pkg/http/middleware/pipeline.go b/pkg/http/middleware/pipeline.go index b3895cc0..505a9a1b 100644 --- a/pkg/http/middleware/pipeline.go +++ b/pkg/http/middleware/pipeline.go @@ -3,12 +3,14 @@ package middleware import ( "github.com/oullin/database/repository" "github.com/oullin/env" + "github.com/oullin/pkg/auth" "github.com/oullin/pkg/http" ) type Pipeline struct { - Env *env.Environment - ApiKeys *repository.ApiKeys + Env *env.Environment + ApiKeys *repository.ApiKeys + TokenHandler *auth.TokenHandler } // Chain applies a list of middleware handlers to a final ApiHandler. diff --git a/pkg/http/middleware/token_middleware.go b/pkg/http/middleware/token_middleware.go index 71e2871b..fe5f0c03 100644 --- a/pkg/http/middleware/token_middleware.go +++ b/pkg/http/middleware/token_middleware.go @@ -1,10 +1,13 @@ package middleware import ( + "fmt" "github.com/oullin/database/repository" + "github.com/oullin/pkg/auth" "github.com/oullin/pkg/http" "log/slog" baseHttp "net/http" + "strings" ) const tokenHeader = "X-API-Key" @@ -12,33 +15,35 @@ const usernameHeader = "X-API-Username" const signatureHeader = "X-API-Signature" type TokenCheckMiddleware struct { - ApiKeys *repository.ApiKeys + ApiKeys *repository.ApiKeys + TokenHandler *auth.TokenHandler } -func MakeTokenMiddleware(apiKeys *repository.ApiKeys) TokenCheckMiddleware { +func MakeTokenMiddleware(tokenHandler *auth.TokenHandler, apiKeys *repository.ApiKeys) TokenCheckMiddleware { return TokenCheckMiddleware{ - ApiKeys: apiKeys, + ApiKeys: apiKeys, + TokenHandler: tokenHandler, } } func (t TokenCheckMiddleware) Handle(next http.ApiHandler) http.ApiHandler { return func(w baseHttp.ResponseWriter, r *baseHttp.Request) *http.ApiError { - //accountName := strings.TrimSpace(r.Header.Get(usernameHeader)) - //publicToken := strings.TrimSpace(r.Header.Get(tokenHeader)) - //signature := strings.TrimSpace(r.Header.Get(signatureHeader)) - // - //if accountName == "" || publicToken == "" || signature == "" { - // return t.getInvalidRequestError(accountName, publicToken, signature) - //} - // - //if err := auth.ValidateTokenFormat(publicToken); err != nil { - // return t.getInvalidTokenFormatError(publicToken, err) - //} - // - //if t.shallReject(accountName, publicToken, signature) { - // return t.getUnauthenticatedError(accountName, publicToken, signature) - //} + accountName := strings.TrimSpace(r.Header.Get(usernameHeader)) + publicToken := strings.TrimSpace(r.Header.Get(tokenHeader)) + signature := strings.TrimSpace(r.Header.Get(signatureHeader)) + + if accountName == "" || publicToken == "" || signature == "" { + return t.getInvalidRequestError(accountName, publicToken, signature) + } + + if err := auth.ValidateTokenFormat(publicToken); err != nil { + return t.getInvalidTokenFormatError(publicToken, err) + } + + if t.shallReject(accountName, publicToken, signature) { + return t.getUnauthenticatedError(accountName, publicToken, signature) + } slog.Info("Token validation successful") @@ -46,58 +51,58 @@ func (t TokenCheckMiddleware) Handle(next http.ApiHandler) http.ApiHandler { } } -//func (t TokenCheckMiddleware) shallReject(accountName, publicToken, signature string) bool { -// var item *database.APIKey -// -// if item = t.ApiKeys.FindBy(accountName); item == nil { -// return true -// } -// -// if strings.TrimSpace(item.PublicKey) != strings.TrimSpace(publicToken) { -// return true -// } -// -// token := auth.Token{ -// AccountName: item.AccountName, -// SecretKey: item.SecretKey, -// PublicKey: item.PublicKey, -// Length: len(item.PublicKey), -// } -// -// return token.HasInValidSignature(signature) -//} -// -//func (t TokenCheckMiddleware) getInvalidRequestError(accountName, publicToken, signature string) *http.ApiError { -// message := fmt.Sprintf( -// "invalid request. Please, provide a valid token, signature and accout name headers. [account: %s, public token: %s, signature: %s]", -// accountName, -// auth.SafeDisplay(publicToken), -// auth.SafeDisplay(signature), -// ) -// -// return &http.ApiError{ -// Message: message, -// Status: baseHttp.StatusForbidden, -// } -//} -// -//func (t TokenCheckMiddleware) getInvalidTokenFormatError(publicToken string, err error) *http.ApiError { -// return &http.ApiError{ -// Message: fmt.Sprintf("invalid token format [token: %s]: %v", auth.SafeDisplay(publicToken), err), -// Status: baseHttp.StatusForbidden, -// } -//} -// -//func (t TokenCheckMiddleware) getUnauthenticatedError(accountName, publicToken, signature string) *http.ApiError { -// message := fmt.Sprintf( -// "Unauthenticated, please check your credentials and signature headers: [token: %s, account name: %s, signature: %s]", -// auth.SafeDisplay(publicToken), -// accountName, -// auth.SafeDisplay(signature), -// ) -// -// return &http.ApiError{ -// Message: message, -// Status: baseHttp.StatusForbidden, -// } -//} +func (t TokenCheckMiddleware) shallReject(accountName, publicToken, signature string) bool { + return false + //var item *database.APIKey + // + //if item = t.ApiKeys.FindBy(accountName); item == nil { + // return true + //} + // + //if strings.TrimSpace(item.PublicKey) != strings.TrimSpace(publicToken) { + // return true + //} + // + //token := auth.Token{ + // AccountName: item.AccountName, + // SecretKey: item.SecretKey, + // PublicKey: item.PublicKey, + // Length: len(item.PublicKey), + //} + // + //return token.HasInValidSignature(signature) +} +func (t TokenCheckMiddleware) getInvalidRequestError(accountName, publicToken, signature string) *http.ApiError { + message := fmt.Sprintf( + "invalid request. Please, provide a valid token, signature and accout name headers. [account: %s, public token: %s, signature: %s]", + accountName, + auth.SafeDisplay(publicToken), + auth.SafeDisplay(signature), + ) + + return &http.ApiError{ + Message: message, + Status: baseHttp.StatusForbidden, + } +} + +func (t TokenCheckMiddleware) getInvalidTokenFormatError(publicToken string, err error) *http.ApiError { + return &http.ApiError{ + Message: fmt.Sprintf("invalid token format [token: %s]: %v", auth.SafeDisplay(publicToken), err), + Status: baseHttp.StatusForbidden, + } +} + +func (t TokenCheckMiddleware) getUnauthenticatedError(accountName, publicToken, signature string) *http.ApiError { + message := fmt.Sprintf( + "Unauthenticated, please check your credentials and signature headers: [token: %s, account name: %s, signature: %s]", + auth.SafeDisplay(publicToken), + accountName, + auth.SafeDisplay(signature), + ) + + return &http.ApiError{ + Message: message, + Status: baseHttp.StatusForbidden, + } +} From fcff2196298e65856691f5891860bef189be8a9a Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Thu, 17 Jul 2025 11:46:34 +0800 Subject: [PATCH 30/32] add middleware logic --- cli/accounts/handler.go | 3 +- pkg/http/middleware/token_middleware.go | 50 +++++++++++++++---------- 2 files changed, 32 insertions(+), 21 deletions(-) diff --git a/cli/accounts/handler.go b/cli/accounts/handler.go index 8a581b58..4587ca85 100644 --- a/cli/accounts/handler.go +++ b/cli/accounts/handler.go @@ -50,6 +50,7 @@ func (h Handler) ReadAccount(accountName string) error { cli.Blueln(" > " + fmt.Sprintf("Account name: %s", token.AccountName)) cli.Blueln(" > " + fmt.Sprintf("Public Key: %s", auth.SafeDisplay(token.PublicKey))) cli.Blueln(" > " + fmt.Sprintf("Secret Key: %s", auth.SafeDisplay(token.SecretKey))) + cli.Blueln(" > " + fmt.Sprintf("API Signature: %s", auth.CreateSignatureFrom(token.AccountName, token.SecretKey))) cli.Warningln("----- Encrypted Values -----") cli.Magentaln(" > " + fmt.Sprintf("Public Key: %x", token.EncryptedPublicKey)) cli.Magentaln(" > " + fmt.Sprintf("Secret Key: %x", token.EncryptedSecretKey)) @@ -75,7 +76,7 @@ func (h Handler) CreateSignature(accountName string) error { return fmt.Errorf("could not decode the given account [%s] keys: %v", item.AccountName, err) } - signature := auth.CreateSignatureFrom(token.PublicKey, token.SecretKey) + signature := auth.CreateSignatureFrom(token.AccountName, token.SecretKey) cli.Successln("\nThe given account has been found successfully!\n") cli.Blueln(" > " + fmt.Sprintf("Account name: %s", token.AccountName)) diff --git a/pkg/http/middleware/token_middleware.go b/pkg/http/middleware/token_middleware.go index fe5f0c03..0ea87165 100644 --- a/pkg/http/middleware/token_middleware.go +++ b/pkg/http/middleware/token_middleware.go @@ -2,6 +2,7 @@ package middleware import ( "fmt" + "github.com/oullin/database" "github.com/oullin/database/repository" "github.com/oullin/pkg/auth" "github.com/oullin/pkg/http" @@ -52,26 +53,35 @@ func (t TokenCheckMiddleware) Handle(next http.ApiHandler) http.ApiHandler { } func (t TokenCheckMiddleware) shallReject(accountName, publicToken, signature string) bool { - return false - //var item *database.APIKey - // - //if item = t.ApiKeys.FindBy(accountName); item == nil { - // return true - //} - // - //if strings.TrimSpace(item.PublicKey) != strings.TrimSpace(publicToken) { - // return true - //} - // - //token := auth.Token{ - // AccountName: item.AccountName, - // SecretKey: item.SecretKey, - // PublicKey: item.PublicKey, - // Length: len(item.PublicKey), - //} - // - //return token.HasInValidSignature(signature) + var item *database.APIKey + + if item = t.ApiKeys.FindBy(accountName); item == nil { + return true + } + + token, err := t.TokenHandler.DecodeTokensFor( + item.AccountName, + item.SecretKey, + item.PublicKey, + ) + + if err != nil { + slog.Error(fmt.Sprintf("could not decode the given account [%s] keys: %v", item.AccountName, err)) + + return true + } + + if strings.TrimSpace(token.PublicKey) != strings.TrimSpace(publicToken) { + slog.Error(fmt.Sprintf("the given public token does not match tour records [%s]: %v", item.AccountName, err)) + + return true + } + + localSignature := auth.CreateSignatureFrom(token.AccountName, token.SecretKey) + + return signature != localSignature } + func (t TokenCheckMiddleware) getInvalidRequestError(accountName, publicToken, signature string) *http.ApiError { message := fmt.Sprintf( "invalid request. Please, provide a valid token, signature and accout name headers. [account: %s, public token: %s, signature: %s]", @@ -98,7 +108,7 @@ func (t TokenCheckMiddleware) getUnauthenticatedError(accountName, publicToken, "Unauthenticated, please check your credentials and signature headers: [token: %s, account name: %s, signature: %s]", auth.SafeDisplay(publicToken), accountName, - auth.SafeDisplay(signature), + signature, ) return &http.ApiError{ From 67822cd9db04b4bca3f3ba0b206c7bccc8b40289 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Thu, 17 Jul 2025 11:49:42 +0800 Subject: [PATCH 31/32] format --- pkg/auth/handler.go | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/pkg/auth/handler.go b/pkg/auth/handler.go index e987b866..cbc90c93 100644 --- a/pkg/auth/handler.go +++ b/pkg/auth/handler.go @@ -1,7 +1,6 @@ package auth import ( - "crypto/hmac" "crypto/rand" "crypto/sha256" "encoding/hex" @@ -97,19 +96,3 @@ func (t *TokenHandler) generateSecureToken(prefix string) (*SecureToken, error) EncryptedText: encryptedText, }, nil } - -func (t Token) HasInValidSignature(receivedSignature string) bool { - return !t.HasValidSignature(receivedSignature) -} - -func (t Token) HasValidSignature(receivedSignature string) bool { - signature := CreateSignatureFrom( - t.AccountName, - t.SecretKey, - ) - - return hmac.Equal( - []byte(signature), - []byte(receivedSignature), - ) -} From a8d7fa46cd33f038d57adaf5ddab6320c4621e54 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Thu, 17 Jul 2025 11:59:43 +0800 Subject: [PATCH 32/32] this is ok --- cli/accounts/handler.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/accounts/handler.go b/cli/accounts/handler.go index 4587ca85..cdd2e4f3 100644 --- a/cli/accounts/handler.go +++ b/cli/accounts/handler.go @@ -48,8 +48,8 @@ func (h Handler) ReadAccount(accountName string) error { cli.Successln("\nThe given account has been found successfully!\n") cli.Blueln(" > " + fmt.Sprintf("Account name: %s", token.AccountName)) - cli.Blueln(" > " + fmt.Sprintf("Public Key: %s", auth.SafeDisplay(token.PublicKey))) - cli.Blueln(" > " + fmt.Sprintf("Secret Key: %s", auth.SafeDisplay(token.SecretKey))) + cli.Blueln(" > " + fmt.Sprintf("Public Key: %s", token.PublicKey)) + cli.Blueln(" > " + fmt.Sprintf("Secret Key: %s", token.SecretKey)) cli.Blueln(" > " + fmt.Sprintf("API Signature: %s", auth.CreateSignatureFrom(token.AccountName, token.SecretKey))) cli.Warningln("----- Encrypted Values -----") cli.Magentaln(" > " + fmt.Sprintf("Public Key: %x", token.EncryptedPublicKey))