Skip to content

Commit

Permalink
Add support for ntlm authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
Sam Hanes committed Aug 24, 2017
1 parent b7e31e3 commit e012329
Show file tree
Hide file tree
Showing 16 changed files with 163 additions and 67 deletions.
4 changes: 2 additions & 2 deletions docs/content/commands/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
paket config add-credentials <source URL>
```

Paket will then ask you for the username and password that will be used for the
specified `<source URL>`.
Paket will then ask you for the username, password, and authentication type that
will be used for the specified `<source URL>`.

The credentials you enter here will then be used for `source`s in the
[`paket.dependencies` file](nuget-dependencies.html) that match `<source URL>`
Expand Down
8 changes: 6 additions & 2 deletions docs/content/nuget-dependencies.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,14 @@ source http://myserver/nuget/api/v2 // Custom feed.
It's also possible to provide login information for private NuGet feeds:

```paket
source http://example.com/nuget/api/v2 username: "user name" password: "the password"
source http://example.com/nuget/api/v2 username: "user name" password: "the password" authtype: "basic"
```

If you don't want to check your username and password into source control, you
can use environment variables instead:

```paket
source http://myserver/nuget/api/v2 username: "%PRIVATE_FEED_USER%" password: "%PRIVATE_FEED_PASS%"
source http://myserver/nuget/api/v2 username: "%PRIVATE_FEED_USER%" password: "%PRIVATE_FEED_PASS%" authtype: "ntlm"
```

`%PRIVATE_FEED_USER%` and `%PRIVATE_FEED_PASS%` will be expanded with the
Expand All @@ -52,6 +52,10 @@ variables.

The [`paket.lock` file](lock-file.html) will also reflect these settings.

`authtype` is an optional parameter to specify the authentication scheme. Allowed
values are `basic` and `ntlm`. If no authentication type is specified, basic
authentication will be used.

**Note:** If [`paket.dependencies` file](dependencies-file.html) exists while
running the [`convert-from-nuget` command](paket-convert-from-nuget.html), the
`PRIVATE_FEED_USER` and `PRIVATE_FEED_PASS` will *not* be expanded. Please see
Expand Down
19 changes: 16 additions & 3 deletions src/Paket.Core/Common/Utils.fs
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,17 @@ let internal memoizeAsync f =
fun (x: 'a) -> // task.Result serialization to sync after done.
cache.GetOrAdd(x, fun x -> f(x) |> Async.StartAsTask) |> Async.AwaitTask

type AuthType = | Basic | NTLM

type Auth =
| Credentials of Username : string * Password : string
| Credentials of Username : string * Password : string * Type : AuthType
| Token of string

let internal parseAuthTypeString (str:string) =
match str.Trim().ToLowerInvariant() with
| "ntlm" -> AuthType.NTLM
| _ -> AuthType.Basic

let TimeSpanToReadableString(span:TimeSpan) =
let pluralize x = if x = 1 then String.Empty else "s"
let notZero x y = if x > 0 then y else String.Empty
Expand Down Expand Up @@ -582,7 +589,7 @@ let createHttpClient (url,auth:Auth option) =
let client = new HttpClient(handler)
match auth with
| None -> handler.UseDefaultCredentials <- true
| Some(Credentials(username, password)) ->
| Some(Credentials(username, password, AuthType.Basic)) ->
// htttp://stackoverflow.com/questions/16044313/webclient-httpwebrequest-with-basic-authentication-returns-404-not-found-for-v/26016919#26016919
//this works ONLY if the server returns 401 first
//client DOES NOT send credentials on first request
Expand All @@ -593,6 +600,9 @@ let createHttpClient (url,auth:Auth option) =
let credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes(username + ":" + password))
client.DefaultRequestHeaders.Authorization <-
new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", credentials)
| Some(Credentials(username, password, AuthType.NTLM)) ->
let cred = System.Net.NetworkCredential(username,password)
handler.Credentials <- cred.GetCredential(new Uri(url), "NTLM")
| Some(Token token) ->
client.DefaultRequestHeaders.Authorization <-
new System.Net.Http.Headers.AuthenticationHeaderValue("token", token)
Expand All @@ -616,7 +626,7 @@ let createWebClient (url,auth:Auth option) =
let githubToken = Environment.GetEnvironmentVariable "PAKET_GITHUB_API_TOKEN"

match auth with
| Some (Credentials(username, password)) ->
| Some (Credentials(username, password, AuthType.Basic)) ->
// htttp://stackoverflow.com/questions/16044313/webclient-httpwebrequest-with-basic-authentication-returns-404-not-found-for-v/26016919#26016919
//this works ONLY if the server returns 401 first
//client DOES NOT send credentials on first request
Expand All @@ -627,6 +637,9 @@ let createWebClient (url,auth:Auth option) =
let credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes(username + ":" + password))
client.Headers.[HttpRequestHeader.Authorization] <- sprintf "Basic %s" credentials
client.Credentials <- new NetworkCredential(username,password)
| Some (Credentials(username, password, AuthType.NTLM)) ->
let cred = NetworkCredential(username,password)
client.Credentials <- cred.GetCredential(new Uri(url), "NTLM")
| Some (Token token) ->
client.Headers.[HttpRequestHeader.Authorization] <- sprintf "token %s" token
| None when not (isNull githubToken) ->
Expand Down
13 changes: 8 additions & 5 deletions src/Paket.Core/Dependencies/NuGet.fs
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,7 @@ let GetVersions force alternativeProjectRoot root (sources, packageName:PackageN
if (not force) && errorFileExists then return [] else
match nugetSource with
| NuGetV2 source ->
let auth = source.Authentication |> Option.map toBasicAuth
let auth = source.Authentication |> Option.map toCredentials
if String.containsIgnoreCase "artifactory" source.Url then
return [getVersionsCached "ODataNewestFirst" NuGetV2.tryGetAllVersionsFromNugetODataFindByIdNewestFirst (nugetSource, auth, source.Url, packageName) ]
else
Expand All @@ -449,7 +449,7 @@ let GetVersions force alternativeProjectRoot root (sources, packageName:PackageN
return v2Feeds
| NuGetV3 source ->
let! versionsAPI = PackageSources.getNuGetV3Resource source AllVersionsAPI
let auth = source.Authentication |> Option.map toBasicAuth
let auth = source.Authentication |> Option.map toCredentials
return [ getVersionsCached "V3" tryNuGetV3 (nugetSource, auth, versionsAPI, packageName) ]
| LocalNuGet(path,Some _) ->
return [ NuGetLocal.getAllVersionsFromLocalPath (true, path, packageName, alternativeProjectRoot, root) ]
Expand Down Expand Up @@ -695,9 +695,9 @@ let DownloadPackage(alternativeProjectRoot, root, (source : PackageSource), cach
#endif

if authenticated then
match source.Auth |> Option.map toBasicAuth with
match source.Auth |> Option.map toCredentials with
| None | Some(Token _) -> request.UseDefaultCredentials <- true
| Some(Credentials(username, password)) ->
| Some(Credentials(username, password, AuthType.Basic)) ->
// htttp://stackoverflow.com/questions/16044313/webclient-httpwebrequest-with-basic-authentication-returns-404-not-found-for-v/26016919#26016919
//this works ONLY if the server returns 401 first
//client DOES NOT send credentials on first request
Expand All @@ -707,6 +707,9 @@ let DownloadPackage(alternativeProjectRoot, root, (source : PackageSource), cach
//so use THIS instead to send credentials RIGHT AWAY
let credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes(username + ":" + password))
request.Headers.[HttpRequestHeader.Authorization] <- String.Format("Basic {0}", credentials)
| Some(Credentials(username, password, AuthType.NTLM)) ->
let cred = NetworkCredential(username,password)
request.Credentials <- cred.GetCredential(downloadUri, "NTLM")
else
request.UseDefaultCredentials <- true

Expand Down Expand Up @@ -743,7 +746,7 @@ let DownloadPackage(alternativeProjectRoot, root, (source : PackageSource), cach
| :? System.Net.WebException as exn when
attempt < 5 &&
exn.Status = WebExceptionStatus.ProtocolError &&
(match source.Auth |> Option.map toBasicAuth with
(match source.Auth |> Option.map toCredentials with
| Some(Credentials(_)) -> true
| _ -> false)
-> do! download false (attempt + 1)
Expand Down
2 changes: 1 addition & 1 deletion src/Paket.Core/Dependencies/NuGetV2.fs
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,7 @@ let FindPackages(auth, nugetURL, packageNamePrefix, maxResults) =
let url = sprintf "%s/Packages()?$filter=IsLatestVersion and IsAbsoluteLatestVersion and substringof('%s',tolower(Id))" nugetURL ((packageNamePrefix:string).ToLowerInvariant())
async {
try
let! raw = getFromUrl(auth |> Option.map toBasicAuth,url,acceptXml)
let! raw = getFromUrl(auth |> Option.map toCredentials,url,acceptXml)
let doc = XmlDocument()
doc.LoadXml raw
return
Expand Down
6 changes: 3 additions & 3 deletions src/Paket.Core/Dependencies/NuGetV3.fs
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ let private getPackages(auth, nugetURL, packageNamePrefix, maxResults) = async {
match apiRes with
| Some url ->
let query = sprintf "%s?q=%s&take=%d" url packageNamePrefix maxResults
let! response = safeGetFromUrl(auth |> Option.map toBasicAuth,query,acceptJson)
let! response = safeGetFromUrl(auth |> Option.map toCredentials,query,acceptJson)
match SafeWebResult.asResult response with
| Result.Ok text -> return Result.Ok (extractPackages text)
| Result.Error err -> return Result.Error err
Expand Down Expand Up @@ -208,7 +208,7 @@ let getRegistration (source : NugetV3Source) (packageName:PackageName) (version:
async {
let! registrationUrl = PackageSources.getNuGetV3Resource source Registration
let url = sprintf "%s%s/%s.json" registrationUrl (packageName.ToString().ToLower()) (version.Normalize())
let! rawData = safeGetFromUrl (source.Authentication |> Option.map toBasicAuth, url, acceptJson)
let! rawData = safeGetFromUrl (source.Authentication |> Option.map toCredentials, url, acceptJson)
return
match rawData with
| NotFound -> None //raise <| System.Exception(sprintf "could not get registration data (404) from '%s'" url)
Expand All @@ -235,7 +235,7 @@ let getPackageDetails (source:NugetV3Source) (packageName:PackageName) (version:
match registrationData with
| None -> return EmptyResult
| Some registrationData ->
let! catalogData = getCatalog registrationData.CatalogEntry (source.Authentication |> Option.map toBasicAuth)
let! catalogData = getCatalog registrationData.CatalogEntry (source.Authentication |> Option.map toCredentials)

let dependencies =
if catalogData.DependencyGroups = null then
Expand Down
2 changes: 1 addition & 1 deletion src/Paket.Core/Dependencies/RemoteUpload.fs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ let Push maxTrials url apiKey clientVersion packageFileName =
try
let authOpt = ConfigFile.GetAuthentication(url)
match authOpt with
| Some (Auth.Credentials (u,_)) ->
| Some (Auth.Credentials (u,_,_)) ->
tracefnVerbose "Authorizing using credentials for user %s" u
| Some (Auth.Token _) ->
tracefnVerbose "Authorizing using token"
Expand Down
18 changes: 9 additions & 9 deletions src/Paket.Core/PackageManagement/NugetConvert.fs
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,19 @@ type CredsMigrationMode =

static member ToAuthentication mode sourceName auth =
match mode, auth with
| Encrypt, Credentials(username,password) ->
ConfigAuthentication(username, password)
| Plaintext, Credentials(username,password) ->
PlainTextAuthentication(username, password)
| Selective, Credentials(username,password) ->
| Encrypt, Credentials(username, password, authType) ->
ConfigAuthentication(username, password, authType)
| Plaintext, Credentials(username, password, authType) ->
PlainTextAuthentication(username, password, authType)
| Selective, Credentials(username, password, authType) ->
let question =
sprintf "Credentials for source '%s': " sourceName +
"[encrypt and save in config (Yes) " +
sprintf "| save as plaintext in %s (No)]" Constants.DependenciesFileName

match Utils.askYesNo question with
| true -> ConfigAuthentication(username, password)
| false -> PlainTextAuthentication(username, password)
| true -> ConfigAuthentication(username, password, authType)
| false -> PlainTextAuthentication(username, password, authType)
| _ -> failwith "invalid auth"

/// Represents type of NuGet packages.config file
Expand Down Expand Up @@ -96,8 +96,8 @@ type NugetConfig =
let encryptedPass = authNode |> tryGetValue "Password"

match userName, encryptedPass, clearTextPass with
| Some userName, Some encryptedPass, _ -> Some(Credentials(userName, ConfigFile.DecryptNuget encryptedPass))
| Some userName, _, Some clearTextPass -> Some(Credentials(userName,clearTextPass))
| Some userName, Some encryptedPass, _ -> Some(Credentials(userName, ConfigFile.DecryptNuget encryptedPass, AuthType.Basic))
| Some userName, _, Some clearTextPass -> Some(Credentials(userName,clearTextPass, AuthType.Basic))
| _ -> None

configNode
Expand Down
4 changes: 2 additions & 2 deletions src/Paket.Core/PublicAPI.fs
Original file line number Diff line number Diff line change
Expand Up @@ -161,10 +161,10 @@ type Dependencies(dependenciesFileName: string) =
projectName, installAfter))

/// Adds credentials for a Nuget feed
member this.AddCredentials(source: string, username: string, password : string) : unit =
member this.AddCredentials(source: string, username: string, password : string, authType : string) : unit =
RunInLockedAccessMode(
this.RootPath,
fun () -> ConfigFile.askAndAddAuth source username password |> returnOrFail )
fun () -> ConfigFile.askAndAddAuth source username password authType |> returnOrFail )

/// Adds a token for a source
member this.AddToken(source : string, token : string) : unit =
Expand Down
29 changes: 21 additions & 8 deletions src/Paket.Core/Versioning/ConfigFile.fs
Original file line number Diff line number Diff line change
Expand Up @@ -94,16 +94,21 @@ let private readPassword (message : string) : string =
Console.Write "\b \b"
password.Substring(0, (password.Length - 1))
else ""
else Console.Write "\r"
else Console.WriteLine()
password

let getAuthFromNode (node : XmlNode) =
match node.Name.ToLowerInvariant() with
| "credential" ->
let username = node.Attributes.["username"].Value
let password = node.Attributes.["password"].Value
let authType =
match node.Attributes.["authType"] with
| null -> AuthType.Basic
| n -> n.Value |> Utils.parseAuthTypeString

let salt = node.Attributes.["salt"].Value
Credentials (username, Decrypt salt password)
Credentials (username, Decrypt salt password, authType)
| "token" -> Token node.Attributes.["value"].Value
| _ -> failwith "unknown node"

Expand All @@ -113,10 +118,11 @@ let private createSourceNode (credentialsNode : XmlNode) source nodeName =
credentialsNode.AppendChild node |> ignore
node

let private setCredentials (username : string) (password : string) (node : XmlElement) =
let private setCredentials (username : string) (password : string) (authType : string) (node : XmlElement) =
let salt, encrypedPassword = Encrypt password
node.SetAttribute ("username", username)
node.SetAttribute ("password", encrypedPassword)
node.SetAttribute ("authType", authType)
node.SetAttribute ("salt", salt)
node

Expand Down Expand Up @@ -167,19 +173,19 @@ let GetAuthenticationForUrl =
let GetAuthentication (source : string) =
GetAuthenticationForUrl(source,source)

let AddCredentials (source, username, password) =
let AddCredentials (source, username, password, authType) =
trial {
let! credentialsNode = getConfigNode "credentials"
let newCredentials =
match getSourceNodes credentialsNode source "credential" |> List.tryHead with
| None -> createSourceNode credentialsNode source "credential" |> Some
| Some existingNode ->
match getAuthFromNode existingNode with
| Credentials (_, existingPassword) ->
| Credentials (_, existingPassword, _) ->
if existingPassword <> password then existingNode |> Some
else None
| _ -> None
|> Option.map (setCredentials username password)
|> Option.map (setCredentials username password authType)
match newCredentials with
| Some credentials -> do! saveConfigNode credentials
| None -> ()
Expand All @@ -203,7 +209,7 @@ let AddToken (source, token) =
| None -> ()
}

let askAndAddAuth (source : string) (username : string) (password : string) =
let askAndAddAuth (source : string) (username : string) (password : string) (authType : string) =
let username =
if username = "" then
Console.Write "Username: "
Expand All @@ -216,4 +222,11 @@ let askAndAddAuth (source : string) (username : string) (password : string) =
readPassword "Password: "
else
password
AddCredentials (source.TrimEnd [|'/'|], username, password)
let authType =
if authType = "" then
Console.Write "Authentication type (basic|ntlm, default = basic): "
let input = Console.ReadLine().Trim()
if input = "" then "basic" else input
else
authType
AddCredentials (source.TrimEnd [|'/'|], username, password, authType)
Loading

0 comments on commit e012329

Please sign in to comment.