git-lfs/lfsapi/ntlm.go
Fabrizio Steiner 2a166fd94e Use SSPI ntlm authentication on windows:
This brings support for SingleSignOn on windows using the default credentials of the currently logged in user if
an empty username and empty password is provided from gitcredentials. This plays well with the Git for windows
implementation which stores an empty username and password if it should use the default credentials.
2018-02-27 17:02:57 +01:00

140 lines
3.2 KiB
Go

package lfsapi
import (
"encoding/base64"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"strings"
"github.com/git-lfs/git-lfs/errors"
)
type ntmlCredentials struct {
domain string
username string
password string
}
func (c *Client) doWithNTLM(req *http.Request, credHelper CredentialHelper, creds Creds, credsURL *url.URL) (*http.Response, error) {
res, err := c.do(req)
if err != nil && !errors.IsAuthError(err) {
return res, err
}
if res.StatusCode != 401 {
return res, nil
}
return c.ntlmReAuth(req, credHelper, creds, true)
}
// If the status is 401 then we need to re-authenticate
func (c *Client) ntlmReAuth(req *http.Request, credHelper CredentialHelper, creds Creds, retry bool) (*http.Response, error) {
ntmlCreds, err := ntlmGetCredentials(creds)
if err != nil {
return nil, err
}
res, err := c.ntlmAuthenticateRequest(req, ntmlCreds)
if err != nil && !errors.IsAuthError(err) {
return res, err
}
switch res.StatusCode {
case 401:
credHelper.Reject(creds)
if retry {
return c.ntlmReAuth(req, credHelper, creds, false)
}
case 403:
credHelper.Reject(creds)
default:
if res.StatusCode < 300 && res.StatusCode > 199 {
credHelper.Approve(creds)
}
}
return res, nil
}
func (c *Client) ntlmSendType1Message(req *http.Request, message []byte) (*http.Response, []byte, error) {
res, err := c.ntlmSendMessage(req, message)
if err != nil && !errors.IsAuthError(err) {
return res, nil, err
}
io.Copy(ioutil.Discard, res.Body)
res.Body.Close()
by, err := parseChallengeResponse(res)
return res, by, err
}
func (c *Client) ntlmSendType3Message(req *http.Request, authenticate []byte) (*http.Response, error) {
return c.ntlmSendMessage(req, authenticate)
}
func (c *Client) ntlmSendMessage(req *http.Request, message []byte) (*http.Response, error) {
body, err := rewoundRequestBody(req)
if err != nil {
return nil, err
}
req.Body = body
msg := base64.StdEncoding.EncodeToString(message)
req.Header.Set("Authorization", "NTLM "+msg)
return c.do(req)
}
func parseChallengeResponse(res *http.Response) ([]byte, error) {
header := res.Header.Get("Www-Authenticate")
if len(header) < 6 {
return nil, fmt.Errorf("Invalid NTLM challenge response: %q", header)
}
//parse out the "NTLM " at the beginning of the response
challenge := header[5:]
val, err := base64.StdEncoding.DecodeString(challenge)
if err != nil {
return nil, err
}
return []byte(val), nil
}
func rewoundRequestBody(req *http.Request) (io.ReadCloser, error) {
if req.Body == nil {
return nil, nil
}
body, ok := req.Body.(ReadSeekCloser)
if !ok {
return nil, fmt.Errorf("Request body must implement io.ReadCloser and io.Seeker. Got: %T", body)
}
_, err := body.Seek(0, io.SeekStart)
return body, err
}
func ntlmGetCredentials(creds Creds) (*ntmlCredentials, error) {
username := creds["username"]
password := creds["password"]
if username == "" && password == "" {
return nil, nil
}
splits := strings.Split(username, "\\")
if len(splits) != 2 {
return nil, fmt.Errorf("Your user name must be of the form DOMAIN\\user. It is currently '%s'", username)
}
domain := strings.ToUpper(splits[0])
username = splits[1]
return &ntmlCredentials{domain: domain, username: username, password: password}, nil
}