-
Notifications
You must be signed in to change notification settings - Fork 0
feat(portal): API token (Bearer) auth + Helm ui.token #38
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -230,3 +230,9 @@ ui: | |||||||||||||||||||||||||
| enabled: false | ||||||||||||||||||||||||||
| host: localhost | ||||||||||||||||||||||||||
| issuer: letsencrypt | ||||||||||||||||||||||||||
| # When enabled, portal accepts Authorization: Bearer <token> as the given DB user (same as UI login). | ||||||||||||||||||||||||||
| # TODO: load API_TOKEN from a Kubernetes Secret instead of plain values. | ||||||||||||||||||||||||||
| token: | ||||||||||||||||||||||||||
| enabled: true | ||||||||||||||||||||||||||
| user: system | ||||||||||||||||||||||||||
| value: litefunctionsxxxtoken | ||||||||||||||||||||||||||
|
Comment on lines
+233
to
+238
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do not ship API auth enabled with a known token. These defaults enable bearer auth as 🔒 Proposed safer defaults # When enabled, portal accepts Authorization: Bearer <token> as the given DB user (same as UI login).
# TODO: load API_TOKEN from a Kubernetes Secret instead of plain values.
token:
- enabled: true
+ enabled: false
user: system
- value: litefunctionsxxxtoken
+ value: ""📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,39 +1,102 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| package middleware | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "crypto/subtle" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "log" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "net/http" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "strings" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "github.com/ashupednekar/litefunctions/portal/internal/auth" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "github.com/ashupednekar/litefunctions/portal/pkg" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "github.com/gin-gonic/gin" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func AuthMiddleware(sessionStore auth.SessionStore) gin.HandlerFunc { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func constantTimeEqual(a, b string) bool { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if len(a) != len(b) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return false | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func parseBearerToken(h string) (string, bool) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| h = strings.TrimSpace(h) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const prefix = "Bearer " | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if len(h) < len(prefix) || !strings.EqualFold(h[:len(prefix)], prefix) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return "", false | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tok := strings.TrimSpace(h[len(prefix):]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if tok == "" { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return "", false | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return tok, true | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func authRequiredFailure(c *gin.Context) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if strings.HasPrefix(c.Request.URL.Path, "/api/") { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| c.Redirect(http.StatusFound, "/?redirect="+c.Request.URL.Path) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+34
to
+39
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
rg -n -C2 'redirect=\+|Redirect\(http\.StatusFound' portalRepository: ashupednekar/litefunctions Length of output: 915 🏁 Script executed: head -20 portal/pkg/server/middleware/auth.goRepository: ashupednekar/litefunctions Length of output: 440 🏁 Script executed: sed -n '30,45n' portal/pkg/server/middleware/auth.goRepository: ashupednekar/litefunctions Length of output: 52 🏁 Script executed: sed -n '30,45p' portal/pkg/server/middleware/auth.goRepository: ashupednekar/litefunctions Length of output: 564 🏁 Script executed: rg -n "Redirect.*\+.*Request" portal/pkg/server/middleware/auth.goRepository: ashupednekar/litefunctions Length of output: 137 🏁 Script executed: cat -n portal/pkg/server/middleware/auth.go | head -50Repository: ashupednekar/litefunctions Length of output: 1718 Escape the redirect query value. Line 38 concatenates Proposed fix import (
"crypto/subtle"
"log"
"net/http"
+ "net/url"
"strings"
"github.com/ashupednekar/litefunctions/portal/internal/auth"
"github.com/ashupednekar/litefunctions/portal/pkg"
"github.com/gin-gonic/gin"
) func authRequiredFailure(c *gin.Context) {
if strings.HasPrefix(c.Request.URL.Path, "/api/") {
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
} else {
- c.Redirect(http.StatusFound, "/?redirect="+c.Request.URL.Path)
+ c.Redirect(http.StatusFound, "/?redirect="+url.QueryEscape(c.Request.URL.Path))
}
c.Abort()
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| c.Abort() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // tryAPIBearerAuth handles Authorization: Bearer when API_TOKEN_ENABLED is set. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Returns true if the request was fully handled (Next or Abort). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func tryAPIBearerAuth(c *gin.Context, store auth.PasskeyStore) bool { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if !pkg.Cfg.APITokenEnabled || pkg.Cfg.APIToken == "" { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return false | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tok, hasBearer := parseBearerToken(c.GetHeader("Authorization")) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if !hasBearer { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return false | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if !constantTimeEqual(tok, pkg.Cfg.APIToken) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log.Printf("[WARN] invalid API token attempt for %s", c.Request.URL.Path) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| c.Abort() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return true | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| userName := pkg.Cfg.APITokenUser | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if userName == "" { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| userName = "system" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name, userID, err := store.GetUserByName(userName) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log.Printf("[ERROR] API token user %q not found: %v", userName, err) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| c.JSON(http.StatusUnauthorized, gin.H{"error": "api token user not found"}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| c.Abort() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return true | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| c.Set("userID", userID) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| c.Set("userName", name) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| c.Next() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return true | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func AuthMiddleware(store auth.PasskeyStore) gin.HandlerFunc { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return func(c *gin.Context) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if tryAPIBearerAuth(c, store) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sessionID, err := c.Cookie(auth.SessionCookieName) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log.Printf("[DEBUG] No session cookie found: %v", err) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| c.Redirect(http.StatusFound, "/?redirect="+c.Request.URL.Path) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| c.Abort() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| authRequiredFailure(c) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| userName, userID, found, err := sessionStore.GetUserSession(sessionID) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| userName, userID, found, err := store.GetUserSession(sessionID) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log.Printf("[ERROR] Error retrieving session: %v", err) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| c.Redirect(http.StatusFound, "/?redirect="+c.Request.URL.Path) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| c.Abort() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| authRequiredFailure(c) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if !found { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log.Printf("[DEBUG] Session not found or expired") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| c.SetCookie(auth.SessionCookieName, "", -1, "/", "", false, true) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| c.Redirect(http.StatusFound, "/?redirect="+c.Request.URL.Path) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| c.Abort() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| authRequiredFailure(c) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -43,14 +106,30 @@ func AuthMiddleware(sessionStore auth.SessionStore) gin.HandlerFunc { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func OptionalAuthMiddleware(sessionStore auth.SessionStore) gin.HandlerFunc { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func OptionalAuthMiddleware(store auth.PasskeyStore) gin.HandlerFunc { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return func(c *gin.Context) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if pkg.Cfg.APITokenEnabled && pkg.Cfg.APIToken != "" { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tok, ok := parseBearerToken(c.GetHeader("Authorization")) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if ok && constantTimeEqual(tok, pkg.Cfg.APIToken) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| userName := pkg.Cfg.APITokenUser | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if userName == "" { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| userName = "system" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if name, userID, err := store.GetUserByName(userName); err == nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| c.Set("userID", userID) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| c.Set("userName", name) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| c.Set("authenticated", true) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| c.Next() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+111
to
+125
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do not skip session fallback when token user lookup fails. With a matching bearer token, 🐛 Proposed fix if ok && constantTimeEqual(tok, pkg.Cfg.APIToken) {
userName := pkg.Cfg.APITokenUser
if userName == "" {
userName = "system"
}
if name, userID, err := store.GetUserByName(userName); err == nil {
c.Set("userID", userID)
c.Set("userName", name)
c.Set("authenticated", true)
+ c.Next()
+ return
+ } else {
+ log.Printf("[ERROR] API token user %q not found: %v", userName, err)
}
- c.Next()
- return
}
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sessionID, err := c.Cookie(auth.SessionCookieName) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err == nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| userName, userID, found, err := sessionStore.GetUserSession(sessionID) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| userName, userID, found, err := store.GetUserSession(sessionID) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err == nil && found { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| c.Set("userID", userID) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| c.Set("userName", userName) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| c.Set("userName", userName) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| c.Set("authenticated", true) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: ashupednekar/litefunctions
Length of output: 6790
🏁 Script executed:
Repository: ashupednekar/litefunctions
Length of output: 221
Read
API_TOKENfrom a Secret, not a literal value.Line 55 renders the bearer token into the Deployment manifest and Helm release metadata. The value is currently stored as plain text in
chart/values.yaml(litefunctionsxxxtoken), which should never be committed to version control. UsevalueFrom.secretKeyRefwith chart values for the Secret name/key, following the pattern already established forVCS_TOKEN,DATABASE_URL, andNGROK_AUTHTOKENelsewhere in the chart.🔒 Proposed template direction
Update
chart/values.yaml:Remove the
valuefield fromchart/values.yamland ensure the Secret is created separately (outside the chart or via external secret management).📝 Committable suggestion
🤖 Prompt for AI Agents