Bootstrap copied BFF and local accounts with public Sendico contracts (#34)
This commit is contained in:
1 parent
67dc5b5b1e
commit
7062672a87
556 files changed
+42291
-777
No files matched your search
@@ -0,0 +1,121 @@
|
||||
package browser
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/merrors"
|
||||
)
|
||||
|
||||
const (
|
||||
TransportHeader = "X-Auth-Transport"
|
||||
TransportCookie = "cookie"
|
||||
CSRFHeader = "X-CSRF-Protection"
|
||||
CSRFValue = "1"
|
||||
RefreshCookie = "sxchnge_refresh"
|
||||
)
|
||||
|
||||
func UsesCookie(r *http.Request) bool {
|
||||
return r != nil && strings.EqualFold(strings.TrimSpace(r.Header.Get(TransportHeader)), TransportCookie)
|
||||
}
|
||||
|
||||
func RefreshCredential(r *http.Request, fallback string) (string, error) {
|
||||
if !UsesCookie(r) {
|
||||
return strings.TrimSpace(fallback), nil
|
||||
}
|
||||
cookie, err := r.Cookie(RefreshCookie)
|
||||
if err != nil || strings.TrimSpace(cookie.Value) == "" {
|
||||
return "", merrors.Unauthorized("browser session is missing")
|
||||
}
|
||||
return cookie.Value, nil
|
||||
}
|
||||
|
||||
func SetRefreshCookie(w http.ResponseWriter, r *http.Request, token string, expiresAt time.Time) {
|
||||
if !UsesCookie(r) {
|
||||
return
|
||||
}
|
||||
maxAge := int(time.Until(expiresAt).Seconds())
|
||||
if maxAge < 1 {
|
||||
maxAge = 1
|
||||
}
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: RefreshCookie,
|
||||
Value: token,
|
||||
Path: cookiePath(r),
|
||||
Expires: expiresAt.UTC(),
|
||||
MaxAge: maxAge,
|
||||
HttpOnly: true,
|
||||
Secure: true,
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
})
|
||||
}
|
||||
|
||||
func ClearRefreshCookie(w http.ResponseWriter, r *http.Request) {
|
||||
if !UsesCookie(r) {
|
||||
return
|
||||
}
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: RefreshCookie,
|
||||
Value: "",
|
||||
Path: cookiePath(r),
|
||||
Expires: time.Unix(1, 0).UTC(),
|
||||
MaxAge: -1,
|
||||
HttpOnly: true,
|
||||
Secure: true,
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
})
|
||||
}
|
||||
|
||||
func ValidateCSRF(r *http.Request, allowedOrigins []string) error {
|
||||
if !UsesCookie(r) || isSafeMethod(r.Method) {
|
||||
return nil
|
||||
}
|
||||
if r.Header.Get(CSRFHeader) != CSRFValue {
|
||||
return merrors.PermissionDenied("browser CSRF protection header is missing")
|
||||
}
|
||||
origin := strings.TrimSpace(r.Header.Get("Origin"))
|
||||
if origin == "" || !originAllowed(origin, allowedOrigins) {
|
||||
return merrors.PermissionDenied("browser request origin is not allowed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func cookiePath(r *http.Request) string {
|
||||
const accountsSegment = "/accounts"
|
||||
if apiEndpoint := strings.TrimSpace(os.Getenv("API_ENDPOINT")); apiEndpoint != "" {
|
||||
return path.Join("/", apiEndpoint, accountsSegment)
|
||||
}
|
||||
if r != nil && r.URL != nil {
|
||||
if index := strings.Index(r.URL.Path, accountsSegment); index >= 0 {
|
||||
return r.URL.Path[:index+len(accountsSegment)]
|
||||
}
|
||||
}
|
||||
return accountsSegment
|
||||
}
|
||||
|
||||
func isSafeMethod(method string) bool {
|
||||
switch method {
|
||||
case http.MethodGet, http.MethodHead, http.MethodOptions, http.MethodTrace:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func originAllowed(origin string, allowedOrigins []string) bool {
|
||||
parsed, err := url.Parse(origin)
|
||||
if err != nil || parsed.Scheme == "" || parsed.Host == "" || parsed.User != nil || parsed.Path != "" {
|
||||
return false
|
||||
}
|
||||
normalized := strings.TrimSuffix(strings.ToLower(origin), "/")
|
||||
for _, allowed := range allowedOrigins {
|
||||
if strings.TrimSuffix(strings.ToLower(strings.TrimSpace(allowed)), "/") == normalized {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in new issue
Block a user