Closed dhubler closed 3 years ago
@dhubler FYI, When a client first requests a protected page, the server returns a 401 status code along with a challenge in the WWW-Authenticate header. At this point, most browsers will present a dialog box to the user prompting them to log in. then the browser will keep sending the same nonce to the server as long as it's valid on the server-side, once the nonce expired then the whole process repeated. same as well when the server restarts. unless nonce persistent.
it's not recommended to persist the nonce and it may cause a security vulnerability.
I've tested the digest with FF 85, Chrome 88, Safari 14. and it keeps authenticating the user and once the server restarted it present a dialog box to the user prompting them to log in and it continues from there.
code:
package main
import (
"crypto"
_ "crypto/md5"
"fmt"
"log"
"net/http"
"time"
"github.com/gorilla/mux"
"github.com/shaj13/libcache"
_ "github.com/shaj13/libcache/fifo"
"github.com/shaj13/go-guardian/v2/auth"
"github.com/shaj13/go-guardian/v2/auth/strategies/digest"
)
var strategy *digest.Digest
func init() {
var c libcache.Cache
c = libcache.FIFO.New(10)
c.SetTTL(time.Minute * 3)
c.RegisterOnExpired(func(key, _ interface{}) {
c.Delete(key)
})
opt := digest.SetHash(crypto.MD5, crypto.MD5.String())
strategy = digest.New(validateUser, c, opt)
}
func main() {
router := mux.NewRouter()
router.HandleFunc("/v1/book/{id}", middleware(http.HandlerFunc(getBookAuthor))).Methods("GET")
log.Println("server started and listening on http://127.0.0.1:8080")
http.ListenAndServe("0.0.0.0:8080", router)
}
func getBookAuthor(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
books := map[string]string{
"1449311601": "Ryan Boyd",
"148425094X": "Yvonne Wilson",
"1484220498": "Prabath Siriwarden",
}
body := fmt.Sprintf("Author: %s \n", books[id])
w.Write([]byte(body))
}
func validateUser(userName string) (string, auth.Info, error) {
// here connect to db or any other service to fetch user and validate it.
if userName == "admin" {
return "admin", auth.NewDefaultUser("admin", "1", nil, nil), nil
}
return "", nil, fmt.Errorf("Invalid credentials")
}
func middleware(next http.Handler) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Println("Executing Auth Middleware")
log.Println("Debug Authorization Header:", r.Header.Get("Authorization"))
user, err := strategy.Authenticate(r.Context(), r)
if err != nil {
code := http.StatusUnauthorized
w.Header().Add("WWW-Authenticate", strategy.GetChallenge())
http.Error(w, http.StatusText(code), code)
fmt.Println("send error", err)
return
}
log.Printf("User %s Authenticated\n", user.GetUserName())
next.ServeHTTP(w, r)
})
}
Thanks for the response.
Is nginx digest is insecure because it survives restarts? Do you have any recommendations for what I want to achieve?
@dhubler
nginx digest store nonce in /dev/shm so when the process restarted it all the nonce can be accessible again but once VM or bare-metal server rebooted all the nonce will be dropped.
it depends on your requirement and what security level you want to achieve, but basically, you can stick with the re-login, the server must be highly available, and restart should only happen occasionally. for scalability, you can use jwt token alongside digest so the first request will use user credentials and then switch to token once token expired ask the user to re-login using digest and so on. so if the server restarted and the token still active use it to prevent the user login.
make sure your server is listening over an HTTPS socket to mitigate MITM attacks.
sample code
package main
import (
"crypto"
_ "crypto/md5"
"fmt"
"log"
"net/http"
"time"
"github.com/gorilla/mux"
"github.com/shaj13/libcache"
_ "github.com/shaj13/libcache/fifo"
"github.com/shaj13/go-guardian/v2/auth"
"github.com/shaj13/go-guardian/v2/auth/strategies/digest"
"github.com/shaj13/go-guardian/v2/auth/strategies/jwt"
"github.com/shaj13/go-guardian/v2/auth/strategies/token"
"github.com/shaj13/go-guardian/v2/auth/strategies/union"
)
// Usage:
// curl --digest --user admin:admin http://127.0.0.1:8080/v1/book/1449311601
var strategy union.Union
var digestStrategy *digest.Digest
var secret jwt.SecretsKeeper
func init() {
var c libcache.Cache
c = libcache.FIFO.New(10)
c.SetTTL(time.Minute * 3)
c.RegisterOnExpired(func(key, _ interface{}) {
c.Delete(key)
})
secret = jwt.StaticSecret{
Algorithm: jwt.HS256,
ID: "static-kid",
Secret: []byte("generated-secrets"),
}
parser := token.SetParser(token.CookieParser("token"))
jwtStrategy := jwt.New(c, secret, parser)
opt := digest.SetHash(crypto.MD5, crypto.MD5.String())
digestStrategy = digest.New(validateUser, c, opt)
strategy = union.New(jwtStrategy, digestStrategy)
}
func main() {
router := mux.NewRouter()
router.HandleFunc("/v1/book/{id}", middleware(http.HandlerFunc(getBookAuthor))).Methods("GET")
log.Println("server started and listening on http://127.0.0.1:8080")
http.ListenAndServe("0.0.0.0:8080", router)
}
func getBookAuthor(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
books := map[string]string{
"1449311601": "Ryan Boyd",
"148425094X": "Yvonne Wilson",
"1484220498": "Prabath Siriwarden",
}
body := fmt.Sprintf("Author: %s \n", books[id])
w.Write([]byte(body))
}
func validateUser(userName string) (string, auth.Info, error) {
// here connect to db or any other service to fetch user and validate it.
if userName == "admin" {
return "admin", auth.NewDefaultUser("admin", "1", nil, nil), nil
}
return "", nil, fmt.Errorf("Invalid credentials")
}
func middleware(next http.Handler) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Println("Executing Auth Middleware")
target, user, err := strategy.AuthenticateRequest(r)
if err != nil {
code := http.StatusUnauthorized
w.Header().Add("WWW-Authenticate", digestStrategy.GetChallenge())
http.Error(w, http.StatusText(code), code)
fmt.Println("send error", err)
return
}
log.Printf("User %s Authenticated\n", user.GetUserName())
if _, ok := target.(*digest.Digest); ok {
opt := jwt.SetExpDuration(time.Second * 3)
token, _ := jwt.IssueAccessToken(user, secret, opt)
cookie := http.Cookie{Name: "token", Value: token}
http.SetCookie(w, &cookie)
}
next.ServeHTTP(w, r)
})
}
feel free to reach out or reopen.
I'm using Digest strategy and works fine until I restart my service. Chrome re-challenges user to give creds again. Strategy returns: userInfo = nil, err = Invalid Response to all web requests. I'm using FIFO lib cache FWIW.
Do I need to implement my own cache implementation that stores values on disk to survive restarts?
I'm trying to replace nginx reverse proxy and this doesn't happen w/nginx so this is a loss in functionality as well and user issue.