diff --git a/internal/api/auth.go b/internal/api/auth.go
index 4944f7f065e6b75e9dda341de154ebcfeecdd40b..850b9198597c3b587e348ef9bd960fafd123c693 100644
--- a/internal/api/auth.go
+++ b/internal/api/auth.go
@@ -46,9 +46,10 @@ type AuthCacheEntry struct {
 
 // Entries are keyed by project + credentials
 type AuthCacheKey struct {
-	project string
-	query   string // e.g. with passing in private_token=...
-	header  string // request header url-encoded, e.g. PRIVATE-TOKEN=...
+	project  string
+	userinfo string // user[:password] or ""
+	query    string // e.g. with passing in private_token=...
+	header   string // request header url-encoded, e.g. PRIVATE-TOKEN=...
 }
 
 // Authorization reply cache
@@ -67,7 +68,13 @@ func NewAuthCache(a *API) *AuthCache {
 // Verify that download access is ok or not.
 // first we try to use the cache; if information is not there -> ask auth backend
 // download is ok if AuthReply.RepoPath != ""
-func (c *AuthCache) VerifyDownloadAccess(project string, query string, header http.Header) AuthReply {
+func (c *AuthCache) VerifyDownloadAccess(project string, userinfo *url.Userinfo, query string, header http.Header) AuthReply {
+	// In addition to userinfo:
+	u := ""
+	if userinfo != nil {
+		u = userinfo.String()
+	}
+
 	// Use only tokens from query/header and selected cookies to minimize cache and avoid
 	// creating redundant cache entries because of e.g. unrelated headers.
 	queryValues, _ := url.ParseQuery(query) // this is what URL.Query() does
@@ -99,7 +106,7 @@ func (c *AuthCache) VerifyDownloadAccess(project string, query string, header ht
 		h["Cookie"] = []string{hc}
 	}
 
-	key := AuthCacheKey{project, q.Encode(), h.Encode()}
+	key := AuthCacheKey{project, u, q.Encode(), h.Encode()}
 	return c.verifyDownloadAccess(key)
 }
 
@@ -198,6 +205,20 @@ func (c *AuthCache) refreshEntry(auth *AuthCacheEntry, key AuthCacheKey) {
 
 // Ask auth backend about cache key
 func (c *AuthCache) askAuthBackend(key AuthCacheKey) AuthReply {
+	// key.userinfo -> url.Userinfo
+	var user *url.Userinfo
+	if key.userinfo != "" {
+		u, err := url.Parse("http://" + key.userinfo + "@/")
+		// url prepared-to-parse userinfo must be valid
+		if err != nil {
+			panic(err)
+		}
+		if u.User == nil {
+			panic(fmt.Errorf("userinfo parse: `%s` -> empty", key.userinfo))
+		}
+		user = u.User
+	}
+
 	// key.header -> url.Values -> http.Header
 	hv, err := url.ParseQuery(key.header)
 	if err != nil {
@@ -210,7 +231,7 @@ func (c *AuthCache) askAuthBackend(key AuthCacheKey) AuthReply {
 		header[k] = v
 	}
 
-	return c.a.verifyDownloadAccess(key.project, key.query, header)
+	return c.a.verifyDownloadAccess(key.project, user, key.query, header)
 }
 
 // for detecting whether archive download is ok via senddata mechanism
@@ -237,11 +258,17 @@ func (aok *testDownloadOkViaSendArchive) Inject(w http.ResponseWriter, r *http.R
 //
 // Replies from authentication backend are cached for 30 seconds as each
 // request to Rails code is heavy and slow.
-func (a *API) VerifyDownloadAccess(project, query string, header http.Header) AuthReply {
-	return a.authCache.VerifyDownloadAccess(project, query, header)
+func (a *API) VerifyDownloadAccess(project string, user *url.Userinfo, query string, header http.Header) AuthReply {
+	return a.authCache.VerifyDownloadAccess(project, user, query, header)
+}
+
+// like Userinfo.Password(), "" if unset
+func xpassword(user *url.Userinfo) string {
+	password, _ := user.Password()
+	return password
 }
 
-func (a *API) verifyDownloadAccess(project, query string, header http.Header) AuthReply {
+func (a *API) verifyDownloadAccess(project string, user *url.Userinfo, query string, header http.Header) AuthReply {
 	authReply := AuthReply{
 		RawReply: httptest.NewRecorder(),
 	}
@@ -251,7 +278,6 @@ func (a *API) verifyDownloadAccess(project, query string, header http.Header) Au
 	//   side this supports only basic auth, not private token.
 	// - that's why we auth backend to authenticate as if it was request to
 	//   get repo archive and propagate request query and header.
-	// url := project + ".git/info/refs?service=git-upload-pack"
 	url := project + "/repository/archive.zip"
 	if query != "" {
 		url += "?" + query
@@ -261,6 +287,10 @@ func (a *API) verifyDownloadAccess(project, query string, header http.Header) Au
 		helper.Fail500(authReply.RawReply, fmt.Errorf("GET git-upload-pack: %v", err))
 		return authReply
 	}
+	if user != nil {
+		// just in case - Rails does not support HTTP Basic Auth for usual requests
+		reqDownloadAccess.SetBasicAuth(user.Username(), xpassword(user))
+	}
 	for k, v := range header {
 		reqDownloadAccess.Header[k] = v
 	}
@@ -278,5 +308,36 @@ func (a *API) verifyDownloadAccess(project, query string, header http.Header) Au
 	)
 	authProxy.ServeHTTP(authReply.RawReply, reqDownloadAccess)
 
+	// If not successful and userinfo is provided without query, retry
+	// authenticating as `git fetch` would do.
+	//
+	// The reason we want to do this second try is that HTTP auth is
+	// handled by upstream auth backend for git requests only, and we might
+	// want to use e.g. https://gitlab-ci-token:token@/.../raw/...
+	if authReply.RepoPath != "" || user == nil || query != "" {
+		return authReply
+	}
+
+	url = project + ".git/info/refs?service=git-upload-pack"
+	reqFetchAccess, err := http.NewRequest("GET", url, nil)
+	if err != nil {
+		helper.Fail500(authReply.RawReply, fmt.Errorf("GET git-upload-pack: %v", err))
+		return authReply
+	}
+	reqFetchAccess.SetBasicAuth(user.Username(), xpassword(user))
+	for k, v := range header {
+		reqFetchAccess.Header[k] = v
+	}
+
+	// reset RawReply - if failed we will return to client what this latter -
+	// - request to auth backend as `git fetch` - replies.
+	authReply.RawReply = httptest.NewRecorder()
+	a.PreAuthorizeHandler(
+		func(w http.ResponseWriter, req *http.Request, resp *Response) {
+			// if we ever get to this point - auth handler approved
+			// access and thus it is ok to download
+			authReply.Response = *resp
+		}, "").ServeHTTP(authReply.RawReply, reqFetchAccess)
+
 	return authReply
 }
diff --git a/internal/git/xblob.go b/internal/git/xblob.go
index 60386aa3ad45fe6b4340081385ca97014dcf6ca0..80cdb886d582eb91ad7dc3a82f925d8ee3b4fa1b 100644
--- a/internal/git/xblob.go
+++ b/internal/git/xblob.go
@@ -14,6 +14,7 @@ import (
 	"io"
 	"log"
 	"net/http"
+	"net/url"
 	"regexp"
 	"strings"
 )
@@ -39,8 +40,15 @@ func handleGetBlobRaw(a *api.API, w http.ResponseWriter, r *http.Request) {
 	project := u.Path[:rawLoc[0]]
 	refpath := u.Path[rawLoc[1]:]
 
+	// Prepare userinfo
+	var user *url.Userinfo
+	username, password, ok := r.BasicAuth()
+	if ok {
+		user = url.UserPassword(username, password)
+	}
+
 	// Query download access auth for this project
-	authReply := a.VerifyDownloadAccess(project, u.RawQuery, r.Header)
+	authReply := a.VerifyDownloadAccess(project, user, u.RawQuery, r.Header)
 	if authReply.RepoPath == "" {
 		// access denied - copy auth reply to client in full -
 		// there are HTTP code and other headers / body relevant for
diff --git a/main_test.go b/main_test.go
index 0b4c10edf3362e182aa206520df5f39b2ff310aa..24c93ec093e64d7717997accb989a777a754e7a0 100644
--- a/main_test.go
+++ b/main_test.go
@@ -603,11 +603,14 @@ func sha1s(data []byte) string {
 }
 
 // download an URL
-func download(t *testing.T, url string, h http.Header) (*http.Response, []byte) {
+func download(t *testing.T, url, username, password string, h http.Header) (*http.Response, []byte) {
 	req, err := http.NewRequest("GET", url, nil)
 	if err != nil {
 		t.Fatal(err)
 	}
+	if !(username == "" && password == "") {
+		req.SetBasicAuth(username, password)
+	}
 	// copy header to request
 	for k, v := range h {
 		req.Header[k] = v
@@ -630,15 +633,17 @@ type DownloadContext struct {
 	t         *testing.T
 	urlPrefix string
 	Header    http.Header
+	username  string
+	password  string
 }
 
 func NewDownloadContext(t *testing.T, urlPrefix string) *DownloadContext {
 	h := make(http.Header)
-	return &DownloadContext{t, urlPrefix, h}
+	return &DownloadContext{t, urlPrefix, h, "", ""}
 }
 
 func (dl DownloadContext) download(path string) (*http.Response, []byte) {
-	return download(dl.t, dl.urlPrefix+path, dl.Header)
+	return download(dl.t, dl.urlPrefix+path, dl.username, dl.password, dl.Header)
 }
 
 // download `path` and expect content sha1 to be `expectSha1`
@@ -704,7 +709,14 @@ func TestPrivateBlobDownload(t *testing.T) {
 		token_ok2 := r.Header.Get("BBB-TOKEN") == "TOKEN-4BBB"
 		cookie, _ := r.Cookie("_gitlab_session")
 		cookie_ok3 := (cookie != nil && cookie.Value == "COOKIE-CCC")
-		if !(token_ok1 || token_ok2 || cookie_ok3) {
+		username, password, user_ok4 := r.BasicAuth()
+		if user_ok4 {
+			// user:password only accepted for `git fetch` requests
+			user_ok4 = (strings.HasSuffix(r.URL.Path, "/info/refs") &&
+				r.URL.RawQuery == "service=git-upload-pack" &&
+				username == "user-ddd" && password == "password-eee")
+		}
+		if !(token_ok1 || token_ok2 || cookie_ok3 || user_ok4) {
 			w.WriteHeader(403)
 			fmt.Fprintf(w, "Access denied")
 			return
@@ -745,4 +757,15 @@ func TestPrivateBlobDownload(t *testing.T) {
 	dl.Header.Set("Cookie", "alpha=1; _gitlab_session=COOKIE-CCC; beta=2")
 	dl.ExpectCode("/5f923865/README.md", 200)
 	dl.ExpectSha1("/5f923865/README.md", "5f7af35c185a9e5face2f4afb6d7c4f00328d04c")
+
+	dl.Header = make(http.Header) // clear
+	dl.ExpectCode("/5f923865/README.md", 403)
+	dl.username = "user-aaa"
+	dl.password = "password-bbb"
+	dl.ExpectCode("/5f923865/README.md", 403)
+	dl.username = "user-ddd"
+	dl.password = "password-eee"
+	dl.ExpectCode("/5f923865/README.md?qqq_token=1", 403)
+	dl.ExpectCode("/5f923865/README.md", 200)
+	dl.ExpectSha1("/5f923865/README.md", "5f7af35c185a9e5face2f4afb6d7c4f00328d04c")
 }