From cb9c89a9f2455468983ae40f8267860dd0917767 Mon Sep 17 00:00:00 2001 From: Eugene Tolmachev Date: Thu, 28 May 2020 16:29:45 -0400 Subject: [PATCH] Initial OSS --- .config/dotnet-tools.json | 12 + .editorconfig | 3 + .gitattributes | 7 + .gitignore | 359 +---------- .vscode/tasks.json | 29 + AAD.Giraffe/AAD.Giraffe.fsproj | 19 + AAD.Giraffe/Noop.fs | 24 + AAD.Giraffe/PartProtector.fs | 102 ++++ AAD.Giraffe/ReadersAndWriters.fs | 27 + AAD.Suave/AAD.Suave.fsproj | 18 + AAD.Suave/Noop.fs | 24 + AAD.Suave/PartProtector.fs | 100 ++++ AAD.Suave/Readers.fs | 20 + AAD.Test/AAD.Test.fsproj | 43 ++ AAD.Test/DomainTests.fs | 48 ++ AAD.Test/JsonWebKeySet.json | 15 + AAD.Test/Logging.fs | 33 ++ AAD.Test/OpenIdConnectMetadata.json | 32 + AAD.Test/ResourceOwnerTests.fs | 178 ++++++ AAD.Test/ResourceProxy.fs | 96 +++ AAD.Test/ResourceServers.fs | 46 ++ AAD.Test/Roles.json | 30 + AAD.Test/Settings.fs | 37 ++ AAD.Test/TestsCommon.fs | 56 ++ AAD.fs.sln | 116 ++++ AAD.fs.tasks/AAD.fs.tasks.fsproj | 22 + AAD.fs/AAD.fs.fsproj | 22 + AAD.fs/Awaitable.fs | 35 ++ AAD.fs/Domain.fs | 86 +++ AAD.fs/Requestor.fs | 82 +++ AAD.fs/ResourceOwner.fs | 128 ++++ AAD.fs/YoLo.fs | 850 +++++++++++++++++++++++++++ AAD.tasks.Test/AAD.tasks.Test.fsproj | 41 ++ AAD.tasks.Test/ResourceOwnerTests.fs | 96 +++ AAD.tasks.Test/ResourceProxy.fs | 74 +++ AAD.tasks.Test/ResourceServers.fs | 46 ++ AAD.tasks.Test/TestsCommon.fs | 84 +++ README.md | 49 +- RELEASE_NOTES.md | 11 + azure-pipelines.yml | 32 + build.fsx | 203 +++++++ build.fsx.lock | 697 ++++++++++++++++++++++ 42 files changed, 3684 insertions(+), 348 deletions(-) create mode 100644 .config/dotnet-tools.json create mode 100755 .editorconfig create mode 100755 .gitattributes create mode 100755 .vscode/tasks.json create mode 100644 AAD.Giraffe/AAD.Giraffe.fsproj create mode 100644 AAD.Giraffe/Noop.fs create mode 100644 AAD.Giraffe/PartProtector.fs create mode 100644 AAD.Giraffe/ReadersAndWriters.fs create mode 100644 AAD.Suave/AAD.Suave.fsproj create mode 100644 AAD.Suave/Noop.fs create mode 100644 AAD.Suave/PartProtector.fs create mode 100644 AAD.Suave/Readers.fs create mode 100644 AAD.Test/AAD.Test.fsproj create mode 100644 AAD.Test/DomainTests.fs create mode 100644 AAD.Test/JsonWebKeySet.json create mode 100644 AAD.Test/Logging.fs create mode 100644 AAD.Test/OpenIdConnectMetadata.json create mode 100644 AAD.Test/ResourceOwnerTests.fs create mode 100644 AAD.Test/ResourceProxy.fs create mode 100644 AAD.Test/ResourceServers.fs create mode 100644 AAD.Test/Roles.json create mode 100644 AAD.Test/Settings.fs create mode 100644 AAD.Test/TestsCommon.fs create mode 100644 AAD.fs.sln create mode 100644 AAD.fs.tasks/AAD.fs.tasks.fsproj create mode 100644 AAD.fs/AAD.fs.fsproj create mode 100644 AAD.fs/Awaitable.fs create mode 100644 AAD.fs/Domain.fs create mode 100644 AAD.fs/Requestor.fs create mode 100644 AAD.fs/ResourceOwner.fs create mode 100644 AAD.fs/YoLo.fs create mode 100644 AAD.tasks.Test/AAD.tasks.Test.fsproj create mode 100644 AAD.tasks.Test/ResourceOwnerTests.fs create mode 100644 AAD.tasks.Test/ResourceProxy.fs create mode 100644 AAD.tasks.Test/ResourceServers.fs create mode 100644 AAD.tasks.Test/TestsCommon.fs create mode 100644 RELEASE_NOTES.md create mode 100644 azure-pipelines.yml create mode 100644 build.fsx create mode 100644 build.fsx.lock diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 0000000..3aac42e --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,12 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "fake-cli": { + "version": "5.18.3", + "commands": [ + "fake" + ] + } + } +} \ No newline at end of file diff --git a/.editorconfig b/.editorconfig new file mode 100755 index 0000000..1eb6fb3 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,3 @@ +end_of_line = lf +indent_size = 4 + diff --git a/.gitattributes b/.gitattributes new file mode 100755 index 0000000..5d70bc1 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,7 @@ +* text=auto +*.yml text eol=lf +*.yaml text eol=lf +*.wsd text eol=lf +*.fs text eol=lf +*.fsx text eol=lf +*.sh text eol=lf \ No newline at end of file diff --git a/.gitignore b/.gitignore index dfcfd56..44ee8b8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,350 +1,15 @@ -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. -## -## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore - -# User-specific files -*.rsuser -*.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Mono auto generated files -mono_crash.* - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -[Aa][Rr][Mm]/ -[Aa][Rr][Mm]64/ -bld/ -[Bb]in/ -[Oo]bj/ -[Ll]og/ -[Ll]ogs/ - -# Visual Studio 2015/2017 cache/options directory .vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# Visual Studio 2017 auto generated files -Generated\ Files/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUnit -*.VisualState.xml -TestResult.xml -nunit-*.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# Benchmark Results -BenchmarkDotNet.Artifacts/ - -# .NET Core -project.lock.json -project.fragment.lock.json -artifacts/ - -# StyleCop -StyleCopReport.xml - -# Files built by Visual Studio -*_i.c -*_p.c -*_h.h -*.ilk -*.meta -*.obj -*.iobj -*.pch -*.pdb -*.ipdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*_wpftmp.csproj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -*.VC.db -*.VC.VC.opendb - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# Visual Studio Trace Files -*.e2e - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# AxoCover is a Code Coverage Tool -.axoCover/* -!.axoCover/settings.json - -# Visual Studio code coverage results -*.coverage -*.coveragexml - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# Note: Comment the next line if you want to checkin your web deploy settings, -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ - -# NuGet Packages -*.nupkg -# NuGet Symbol Packages -*.snupkg -# The packages folder can be ignored because of Package Restore -**/[Pp]ackages/* -# except build/, which is used as an MSBuild target. -!**/[Pp]ackages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/[Pp]ackages/repositories.config -# NuGet v3's project.json files produces more ignorable files -*.nuget.props -*.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Windows Store app package directories and files -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt -*.appx -*.appxbundle -*.appxupload - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!?*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.jfm -*.pfx -*.publishsettings -orleans.codegen.cs - -# Including strong name files can present a security risk -# (https://github.com/github/gitignore/pull/2483#issue-259490424) -#*.snk - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm -ServiceFabricBackup/ -*.rptproj.bak - -# SQL Server files -*.mdf -*.ldf -*.ndf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings -*.rptproj.rsuser -*- [Bb]ackup.rdl -*- [Bb]ackup ([0-9]).rdl -*- [Bb]ackup ([0-9][0-9]).rdl - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat -node_modules/ - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe -paket-files/ - -# FAKE - F# Make -.fake/ - -# CodeRush personal settings -.cr/personal - -# Python Tools for Visual Studio (PTVS) -__pycache__/ -*.pyc - -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config - -# Tabs Studio -*.tss - -# Telerik's JustMock configuration file -*.jmconfig - -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs - -# OpenCover UI analysis results -OpenCover/ - -# Azure Stream Analytics local run output -ASALocalRun/ - -# MSBuild Binary and Structured Log -*.binlog - -# NVidia Nsight GPU debugger configuration file -*.nvuser - -# MFractors (Xamarin productivity tool) working folder -.mfractor/ - -# Local History for Visual Studio -.localhistory/ - -# BeatPulse healthcheck temp database -healthchecksdb +.idea/ +.ionide/ -# Backup folder for Package Reference Convert tool in Visual Studio 2017 -MigrationBackup/ +.dotnet/ +.local/ +.nuget/ +nugets/ +bin/ +obj/ +packages/ -# Ionide (cross platform F# VS Code tools) working folder -.ionide/ +.fake +symbolCache.db* +*.user diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100755 index 0000000..9721d50 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,29 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "windows": { + "options": { + "shell": { + "executable": "cmd.exe", + "args": [ "/d", "/c" ] + } + } + }, + "tasks": [ + { + "label": "build", + "command": "dotnet build", + "type": "shell", + "group": "build", + "problemMatcher": "$msCompile" + }, + { + "label": "test", + "command": "dotnet test --no-restore .", + "type": "shell", + "problemMatcher": "$msCompile" + } + + ] +} \ No newline at end of file diff --git a/AAD.Giraffe/AAD.Giraffe.fsproj b/AAD.Giraffe/AAD.Giraffe.fsproj new file mode 100644 index 0000000..e08d79d --- /dev/null +++ b/AAD.Giraffe/AAD.Giraffe.fsproj @@ -0,0 +1,19 @@ + + + netstandard2.0 + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/AAD.Giraffe/Noop.fs b/AAD.Giraffe/Noop.fs new file mode 100644 index 0000000..5e990ac --- /dev/null +++ b/AAD.Giraffe/Noop.fs @@ -0,0 +1,24 @@ +namespace AAD.Noop + +open Giraffe +open Microsoft.AspNetCore.Http +open System.Threading.Tasks +open System.IdentityModel.Tokens.Jwt +open AAD + +/// PartProtector implements no-op verification (it always succeeds) for PartProtector interface. +[] +module PartProtector = + let token = JwtSecurityToken() + /// Creates PartProtector instance. + let mkNew () = + { new PartProtector with + member __.Verify (getDemand: HttpContext -> Task) + (onSuccess: JwtSecurityToken -> HttpHandler) = + onSuccess token + + member __.VerifyWith (getDemand: HttpContext -> Task) + (onSuccess: JwtSecurityToken -> HttpHandler) + (onError: JwtSecurityToken option -> WWWAuthenticate -> HttpHandler) = + onSuccess token + } diff --git a/AAD.Giraffe/PartProtector.fs b/AAD.Giraffe/PartProtector.fs new file mode 100644 index 0000000..fb7ca1c --- /dev/null +++ b/AAD.Giraffe/PartProtector.fs @@ -0,0 +1,102 @@ +namespace AAD + +open Microsoft.AspNetCore.Http +open System.Threading.Tasks +open Giraffe +open FSharp.Control.Tasks.V2.ContextInsensitive +open System.IdentityModel.Tokens.Jwt +open Microsoft.IdentityModel.Protocols.OpenIdConnect + +/// PartProtector is the interface for a stateful protector instance. +/// Use PartProtector module to create the instances implementing this interface. +type PartProtector = + /// Wraps the verify call + abstract Verify: getDemand: (HttpContext -> Task) -> + onSuccess: (JwtSecurityToken -> HttpHandler) -> + HttpHandler + abstract VerifyWith: getDemand: (HttpContext -> Task) -> + onSuccess: (JwtSecurityToken -> HttpHandler) -> + onError: (JwtSecurityToken option -> WWWAuthenticate -> HttpHandler) -> + HttpHandler + +/// PartProtector module for working with stateful instances of PartProtector interface. +[] +module PartProtector = + open System.Net.Http + + module internal ResultHandler = + let mkNew onError onSuccess = + let mutable result = ServerErrors.INTERNAL_ERROR "Shouldn't happen" + let handleSuccess token = + result <- onSuccess token + let handleMissing (token: _ option) (authenticate:WWWAuthenticate) = + result <- onError token authenticate + handleSuccess,handleMissing,fun ctx -> result ctx + + let mkDefault onSuccess = + mkNew (fun _ (WWWAuthenticate authenticate) -> + Writers.setWWWAuthenticate authenticate + >=> Writers.forbidden "Missing required demand") + onSuccess + + /// Creates PartProtector instance using the client credentials provided. + let mkNew (introspect: TokenString -> Task>) + (validate: Demand -> JwtSecurityToken -> Result) + (audiences: #seq) + (oidcConfig: OpenIdConnectConfiguration) = + let resourceOwner = + ResourceOwner.mkNew introspect + validate + audiences + oidcConfig + + { new PartProtector with + member __.Verify (getDemand: HttpContext -> Task) + (onSuccess: JwtSecurityToken -> HttpHandler) = + fun next (ctx:HttpContext) -> + task { + let handleSuccess,handleMissing,result = + ResultHandler.mkDefault onSuccess + let! demand = getDemand ctx + do! resourceOwner.Validate demand + handleSuccess + handleMissing + (Readers.bearerTokenString ctx) + return! result next ctx + } + member __.VerifyWith (getDemand: HttpContext -> Task) + (onSuccess: JwtSecurityToken -> HttpHandler) + (onError: JwtSecurityToken option -> WWWAuthenticate -> HttpHandler) = + fun next (ctx:HttpContext) -> + task { + let handleSuccess,handleMissing,result = + ResultHandler.mkNew onError onSuccess + let! demand = getDemand ctx + do! resourceOwner.Validate demand + handleSuccess + handleMissing + (Readers.bearerTokenString ctx) + return! result next ctx + } + } + + let mkDefault (httpClient: HttpClient) + (audiences: #seq) + (authority: System.Uri) = + task { + let! conf = OpenIdConnectConfigurationRetriever + .GetAsync(sprintf "%O/.well-known/openid-configuration" authority, httpClient, System.Threading.CancellationToken.None) + .ConfigureAwait(false) + + let introspect = + (TokenCache.mkDefault(), audiences, conf) |||> Introspector.mkNew + let inline filter claim = + ResourceOwner.ClaimFilters.isAppRole claim + || ResourceOwner.ClaimFilters.isRole claim + || ResourceOwner.ClaimFilters.isScope claim + + return mkNew introspect + (ResourceOwner.validate '/' filter) + audiences + conf + } \ No newline at end of file diff --git a/AAD.Giraffe/ReadersAndWriters.fs b/AAD.Giraffe/ReadersAndWriters.fs new file mode 100644 index 0000000..9e5ee37 --- /dev/null +++ b/AAD.Giraffe/ReadersAndWriters.fs @@ -0,0 +1,27 @@ +namespace AAD +open Giraffe + +module Readers = + open Microsoft.AspNetCore.Http + open AAD.Domain + + let bearer (ctx: HttpContext) = + ctx.TryGetRequestHeader "Authorization" + |> Option.map (String.split ' ') + |> Option.bind (function ["Bearer"; token] -> Some token | _ -> None) + + let bearerTokenString : HttpContext -> TokenString = + bearer >> Option.map TokenString >> Option.defaultValue (TokenString "") + +module Writers = + let inline setMimeType mimeType: HttpHandler = + setHttpHeader "Content-Type" mimeType + + let inline setWWWAuthenticate value: HttpHandler = + setHttpHeader "WWW-Authenticate" value + + let unauthorized body: HttpHandler = + setStatusCode 401 >=> setBodyFromString body + + let forbidden body: HttpHandler = + setStatusCode 403 >=> setBodyFromString body \ No newline at end of file diff --git a/AAD.Suave/AAD.Suave.fsproj b/AAD.Suave/AAD.Suave.fsproj new file mode 100644 index 0000000..f61a3fb --- /dev/null +++ b/AAD.Suave/AAD.Suave.fsproj @@ -0,0 +1,18 @@ + + + netstandard2.0 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/AAD.Suave/Noop.fs b/AAD.Suave/Noop.fs new file mode 100644 index 0000000..eacc7ef --- /dev/null +++ b/AAD.Suave/Noop.fs @@ -0,0 +1,24 @@ +namespace AAD.Noop + + +open Suave +open Suave.Operators +open AAD +open System.IdentityModel.Tokens.Jwt + +/// PartProtector implements no-op verification (it always succeeds) for PartProtector interface. +[] +module PartProtector = + let token = JwtSecurityToken() + /// Creates PartProtector instance. + let mkNew () = + { new PartProtector with + member __.Verify (getDemand: HttpContext -> Async) + (onSuccess: JwtSecurityToken -> WebPart) = + onSuccess token + + member __.VerifyWith (getDemand: HttpContext -> Async) + (onSuccess: JwtSecurityToken -> WebPart) + (onError: JwtSecurityToken option -> WWWAuthenticate -> WebPart) = + onSuccess token + } diff --git a/AAD.Suave/PartProtector.fs b/AAD.Suave/PartProtector.fs new file mode 100644 index 0000000..ba21f47 --- /dev/null +++ b/AAD.Suave/PartProtector.fs @@ -0,0 +1,100 @@ +namespace AAD + +open Suave +open Suave.Operators +open System.IdentityModel.Tokens.Jwt +open Microsoft.IdentityModel.Protocols.OpenIdConnect + +/// PartProtector is the interface for a stateful protector instance. +/// Use PartProtector module to create the instances implementing this interface. +type PartProtector = + /// Wraps the verify call + abstract Verify: getDemand: (HttpContext -> Async) -> + onSuccess: (JwtSecurityToken -> WebPart) -> + WebPart + abstract VerifyWith: getDemand: (HttpContext -> Async) -> + onSuccess: (JwtSecurityToken -> WebPart) -> + onError: (JwtSecurityToken option -> WWWAuthenticate -> WebPart) -> + WebPart + +/// PartProtector module for working with stateful instances of PartProtector interface. +[] +module PartProtector = + open System.Net.Http + + module internal ResultHandler = + let mkNew onError onSuccess = + let mutable result = ServerErrors.INTERNAL_ERROR "Shouldn't happen" + let handleSuccess token = + result <- onSuccess token + let handleMissing (token: _ option) (authenticate:WWWAuthenticate) = + result <- onError token authenticate + handleSuccess,handleMissing,fun ctx -> result ctx + + let mkDefault onSuccess = + mkNew (fun _ (WWWAuthenticate authenticate) -> + RequestErrors.FORBIDDEN "Missing required claims" + >=> Writers.setHeader "WWW-Authenticate" authenticate) + onSuccess + + /// Creates PartProtector instance using the client credentials provided. + let mkNew (introspect: TokenString -> Async>) + (validate: Demand -> JwtSecurityToken -> Result) + (audiences: #seq) + (oidcConfig: OpenIdConnectConfiguration) = + let resourceOwner = + ResourceOwner.mkNew introspect + validate + audiences + oidcConfig + + { new PartProtector with + member __.Verify (getDemand: HttpContext -> Async) + (onSuccess: JwtSecurityToken -> WebPart) = + fun (ctx:HttpContext) -> + async { + let handleSuccess,handleMissing,result = + ResultHandler.mkDefault onSuccess + let! claims = getDemand ctx + do! resourceOwner.Validate claims + handleSuccess + handleMissing + (Readers.bearerTokenString ctx) + return! result ctx + } + member __.VerifyWith (getDemand: HttpContext -> Async) + (onSuccess: JwtSecurityToken -> WebPart) + (onError: JwtSecurityToken option -> WWWAuthenticate -> WebPart) = + fun (ctx:HttpContext) -> + async { + let handleSuccess,handleMissing,result = + ResultHandler.mkNew onError onSuccess + let! claims = getDemand ctx + do! resourceOwner.Validate claims + handleSuccess + handleMissing + (Readers.bearerTokenString ctx) + return! result ctx + } + } + + let mkDefault (httpClient: HttpClient) + (audiences: #seq) + (authority: System.Uri) = + async { + let! conf = OpenIdConnectConfigurationRetriever + .GetAsync(sprintf "%O/.well-known/openid-configuration" authority, httpClient, System.Threading.CancellationToken.None) + |> Async.AwaitTask + + let introspect = + (TokenCache.mkDefault(), audiences, conf) |||> Introspector.mkNew + let inline filter claim = + ResourceOwner.ClaimFilters.isAppRole claim + || ResourceOwner.ClaimFilters.isRole claim + || ResourceOwner.ClaimFilters.isScope claim + + return mkNew introspect + (ResourceOwner.validate '/' filter) + audiences + conf + } \ No newline at end of file diff --git a/AAD.Suave/Readers.fs b/AAD.Suave/Readers.fs new file mode 100644 index 0000000..8d8168a --- /dev/null +++ b/AAD.Suave/Readers.fs @@ -0,0 +1,20 @@ +namespace AAD + +module Readers = + open Suave + open AAD.Domain + + module internal TokenString = + let inline ofChoice choice = + match choice with + | Choice2Of2 s + | Choice1Of2 s -> TokenString s + + let bearer ctx = + ctx.request.header "Authorization" + |> Choice.map (String.split ' ') + |> Choice.bind (function ["Bearer"; token] -> Choice1Of2 token | _ -> Choice2Of2 "") + |> Choice.bindSnd (fun _ -> Choice2Of2 "") + + let bearerTokenString = + bearer >> TokenString.ofChoice diff --git a/AAD.Test/AAD.Test.fsproj b/AAD.Test/AAD.Test.fsproj new file mode 100644 index 0000000..d4f20d1 --- /dev/null +++ b/AAD.Test/AAD.Test.fsproj @@ -0,0 +1,43 @@ + + + netcoreapp3.0 + 79a3edd0-2092-40a2-a04d-dcb46d5ca9ed + + + + PreserveNewest + + + PreserveNewest + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + + + + + + diff --git a/AAD.Test/DomainTests.fs b/AAD.Test/DomainTests.fs new file mode 100644 index 0000000..059696e --- /dev/null +++ b/AAD.Test/DomainTests.fs @@ -0,0 +1,48 @@ +namespace AADTests + +open Xunit +open Swensen.Unquote +open AAD + +module DemandTests = + + [] + let ``All when present`` () = + All [Pattern ["A";"1"]; Pattern ["B";"2"]] + |> Demand.eval [["A";"1"]; ["B";"2"]] =! true + + [] + let ``All when absent`` () = + All [Pattern ["A";"1"]; Pattern ["B";"2"]] + |> Demand.eval [["A";"1"]] =! false + + [] + let ``Any when present`` () = + Any [Pattern ["A";"1"]; Pattern ["B";"2"]] + |> Demand.eval [["A";"1"]] =! true + + [] + let ``Any when absent`` () = + Any [Pattern ["A";"1"]; Pattern ["B";"2"]] + |> Demand.eval [] =! false + + [] + let ``Pattern no match`` () = + Pattern ["A";"1"] + |> Demand.eval [["";""]; ["A"]; ["1"]] =! false + + + [] + let ``Pattern exact match`` () = + Pattern ["A";"1"] + |> Demand.eval [["A";"1"]] =! true + + [] + let ``Pattern whildcard match`` () = + Pattern ["A";"1"] + |> Demand.eval [["*";"*"]] =! true + + [] + let ``Partial whildcard match`` () = + Pattern ["A";"1"] + |> Demand.eval [["*"; "*"; "*"]] =! true diff --git a/AAD.Test/JsonWebKeySet.json b/AAD.Test/JsonWebKeySet.json new file mode 100644 index 0000000..e87a0bf --- /dev/null +++ b/AAD.Test/JsonWebKeySet.json @@ -0,0 +1,15 @@ +{ + "keys": [ + {"kty":"RSA", + "n":"0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw", + "e":"AQAB", + "d":"X4cTteJY_gn4FYPsXB8rdXix5vwsg1FLN5E3EaG6RJoVH-HLLKD9M7dx5oo7GURknchnrRweUkC7hT5fJLM0WbFAKNLWY2vv7B6NqXSzUvxT0_YSfqijwp3RTzlBaCxWp4doFk5N2o8Gy_nHNKroADIkJ46pRUohsXywbReAdYaMwFs9tv8d_cPVY3i07a3t8MN6TNwm0dSawm9v47UiCl3Sk5ZiG7xojPLu4sbg1U2jx4IBTNBznbJSzFHK66jT8bgkuqsk0GjskDJk19Z4qwjwbsnn4j2WBii3RL-Us2lGVkY8fkFzme1z0HbIkfz0Y6mqnOYtqc0X4jfcKoAC8Q", + "p":"83i-7IvMGXoMXCskv73TKr8637FiO7Z27zv8oj6pbWUQyLPQBQxtPVnwD20R-60eTDmD2ujnMt5PoqMrm8RfmNhVWDtjjMmCMjOpSXicFHj7XOuVIYQyqVWlWEh6dN36GVZYk93N8Bc9vY41xy8B9RzzOGVQzXvNEvn7O0nVbfs", + "q":"3dfOR9cuYq-0S-mkFLzgItgMEfFzB2q3hWehMuG0oCuqnb3vobLyumqjVZQO1dIrdwgTnCdpYzBcOfW5r370AFXjiWft_NGEiovonizhKpo9VVS78TzFgxkIdrecRezsZ-1kYd_s1qDbxtkDEgfAITAG9LUnADun4vIcb6yelxk", + "dp":"G4sPXkc6Ya9y8oJW9_ILj4xuppu0lzi_H7VTkS8xj5SdX3coE0oimYwxIi2emTAue0UOa5dpgFGyBJ4c8tQ2VF402XRugKDTP8akYhFo5tAA77Qe_NmtuYZc3C3m3I24G2GvR5sSDxUyAN2zq8Lfn9EUms6rY3Ob8YeiKkTiBj0", + "dq":"s9lAH9fggBsoFR8Oac2R_E2gw282rT2kGOAhvIllETE1efrA6huUUvMfBcMpn8lqeW6vzznYY5SSQF7pMdC_agI3nG8Ibp1BUb0JUiraRNqUfLhcQb_d9GF4Dh7e74WbRsobRonujTYN1xCaP6TO61jvWrX-L18txXw494Q_cgk", + "qi":"GyM_p6JrXySiz1toFgKbWV-JdI3jQ4ypu9rbMWx3rQJBfmt0FoYzgUIZEVFEcOqwemRN81zoDAaa-Bk0KWNGDjJHZDdDmFhW3AN7lI-puxk_mHZGJ11rxyR8O55XLSe3SPmRfKwZI6yU24ZxvQKFYItdldUKGzO6Ia6zTKhAVRU", + "alg":"RS256", + "kid":"test-key"} + ] + } \ No newline at end of file diff --git a/AAD.Test/Logging.fs b/AAD.Test/Logging.fs new file mode 100644 index 0000000..60ac47c --- /dev/null +++ b/AAD.Test/Logging.fs @@ -0,0 +1,33 @@ +module AADTests.Logging + +open System.Threading +open System.Threading.Tasks +open System.Net.Http +open FSharp.Control.Tasks + +type Log = string -> (string*obj) list -> unit + +type HttpClientLogger(innerHandler, log:Log) = + inherit DelegatingHandler(innerHandler) + member private __.baseImpl (request, cancellationToken) = base.SendAsync (request, cancellationToken) + override this.SendAsync(request:HttpRequestMessage, cancellationToken:CancellationToken):Task = + + task { + if isNull request.Content then + log "Request: {req}" ["req", box request] + else + let! content = request.Content.ReadAsStringAsync() + log "Request: {req}, Content: {content}" ["content",box content + "req", box request] + + let! response = this.baseImpl (request, cancellationToken) + + if isNull response.Content then + log "Response: {resp}" ["resp",box response] + else + let! content = response.Content.ReadAsStringAsync() + log "Response: {resp}, Content: {content}" ["resp", box response + "content", box content] + + return response + } diff --git a/AAD.Test/OpenIdConnectMetadata.json b/AAD.Test/OpenIdConnectMetadata.json new file mode 100644 index 0000000..8b2bd41 --- /dev/null +++ b/AAD.Test/OpenIdConnectMetadata.json @@ -0,0 +1,32 @@ +{ + "issuer": "https://sts.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/", + "authorization_endpoint": "https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/oauth2/authorize", + "token_endpoint": "https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/oauth2/token", + "token_endpoint_auth_methods_supported": [ + "client_secret_post", + "private_key_jwt" + ], + "jwks_uri": "JsonWebKeySet.json", + "response_types_supported": [ + "code", + "id_token", + "code id_token" + ], + "response_modes_supported": [ + "query", + "fragment", + "form_post" + ], + "subject_types_supported": [ + "pairwise" + ], + "scopes_supported": [ + "openid" + ], + "id_token_signing_alg_values_supported": [ + "RS256" + ], + "microsoft_multi_refresh_token": true, + "check_session_iframe": "https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/oauth2/checksession", + "end_session_endpoint": "https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/oauth2/logout" +} \ No newline at end of file diff --git a/AAD.Test/ResourceOwnerTests.fs b/AAD.Test/ResourceOwnerTests.fs new file mode 100644 index 0000000..4daada0 --- /dev/null +++ b/AAD.Test/ResourceOwnerTests.fs @@ -0,0 +1,178 @@ +namespace AADTests + +open System +open System.Threading +open Xunit +open Swensen.Unquote +open System.IdentityModel.Tokens.Jwt +open AAD +open AADTests.TestsCommon +open System.Net + +module Internals = + open Microsoft.IdentityModel.Tokens + open System.Security.Claims + + module Introspection = + let audience = ".default" + let introspect = + (TokenCache.mkDefault(), [Audience audience], oidcConfig) + |||> Introspector.mkNew + + [] + let Introspects () = + let tokenHandler = JwtSecurityTokenHandler() + let jwtToken = + tokenHandler.CreateJwtSecurityToken( + oidcConfig.Issuer, + audience, + ClaimsIdentity([Claim(ClaimTypes.Role, "Test/read/*")]), + Nullable DateTime.UtcNow, + Nullable (DateTime.UtcNow + (TimeSpan.FromHours 1.)), + Nullable (DateTime.UtcNow + (TimeSpan.FromHours 1.)), + SigningCredentials(oidcConfig.SigningKeys |> Seq.head, SecurityAlgorithms.RsaSha256)) + |> tokenHandler.WriteToken + + async { + let! introspected = introspect (TokenString jwtToken) + true =! Result.isOk introspected + return introspected + } |> Async.RunSynchronously + + [] + let Expired () = + let tokenHandler = JwtSecurityTokenHandler() + let jwtToken = + let yesterday = DateTime.Today - (TimeSpan.FromDays 1.) + tokenHandler.CreateJwtSecurityToken( + oidcConfig.Issuer, + audience, + ClaimsIdentity([]), + Nullable yesterday, + Nullable (yesterday + (TimeSpan.FromHours 1.)), + Nullable (yesterday + (TimeSpan.FromHours 1.)), + SigningCredentials(oidcConfig.SigningKeys |> Seq.head, SecurityAlgorithms.RsaSha256)) + |> tokenHandler.WriteToken + + async { + let! introspected = introspect (TokenString jwtToken) + true =! Result.isError introspected + } |> Async.RunSynchronously + + [] + let ``Invalid Audience`` () = + let tokenHandler = JwtSecurityTokenHandler() + let jwtToken = + tokenHandler.CreateJwtSecurityToken( + oidcConfig.Issuer, + "audience", + ClaimsIdentity([]), + Nullable DateTime.UtcNow, + Nullable (DateTime.UtcNow + (TimeSpan.FromHours 1.)), + Nullable (DateTime.UtcNow + (TimeSpan.FromHours 1.)), + SigningCredentials(oidcConfig.SigningKeys |> Seq.head, SecurityAlgorithms.RsaSha256)) + |> tokenHandler.WriteToken + + async { + let! introspected = introspect (TokenString jwtToken) + true =! Result.isError introspected + } |> Async.RunSynchronously + + module Validation = + [] + let ``Role demand satisfied`` () = + let token = Introspection.Introspects() + let result = + token + |> Result.bind (ResourceOwner.validate '/' ResourceOwner.ClaimFilters.isRole (Pattern ["Test"; "read"; "A"])) + true =! Result.isOk result + + [] + let ``Scope demand is not satisfied`` () = + let token = Introspection.Introspects() + let result = + token + |> Result.bind (ResourceOwner.validate '/' ResourceOwner.ClaimFilters.isScope (Pattern ["Test"; "read"; "A"])) + true =! Result.isError result + + +/// Fixture to share initialization across all 3BodyProblem tests +type Parties() = + let mutable last = None + let init httpClient = + async { + match last with + | Some args -> return args + | _ -> + let cts = new CancellationTokenSource() + let! address = Sample.start cts + httpClient + [settings.Audience] + settings.Authority + let proxy = ResourceProxy.mkDefault (Uri address) + httpClient + + last <- Some(cts, proxy) + return last.Value + } + + interface IDisposable with + member __.Dispose() = + match last with + | Some(cts,_) -> cts.Cancel() + | _ -> () + + member __.Init httpClient = init httpClient |> Async.RunSynchronously + + +[] +type ThreeBodyProblem(output: Xunit.Abstractions.ITestOutputHelper, fixture: Parties) = + let httpClient = mkHttpClient output + + interface IClassFixture + + [] + member __.``Admin can write and read`` () = + async { + let _, proxy = fixture.Init httpClient + let requestor = + proxy |> AsyncRequestor.mkNew (ResourceProxy.authenticate ([settings.Scope], ClientId settings.AdminAppId, Secret settings.AdminSecret, settings.Authority)) + let! response = requestor.Call (ProxyResult.bindAsync (fun p -> p.read())) + response =! Success "Read!" + let! response = requestor.Call (ProxyResult.bindAsync (fun p -> p.write())) + response =! Success "Written!" + } |> Async.RunSynchronously + + [] + member __.``Writer can write but not read`` () = + async { + let _, proxy = fixture.Init httpClient + let requestor = + proxy |> AsyncRequestor.mkNew (ResourceProxy.authenticate ([settings.Scope], ClientId settings.WriterAppId, Secret settings.WriterSecret, settings.Authority)) + let! response = requestor.Call (ProxyResult.bindAsync (fun p -> p.read())) + response =! AuthorizationError HttpStatusCode.Forbidden + let! response = requestor.Call (ProxyResult.bindAsync (fun p -> p.write())) + response =! Success "Written!" + } |> Async.RunSynchronously + + [] + member __.``Reader can read`` () = + async { + let _, proxy = fixture.Init httpClient + let requestor = + proxy |> AsyncRequestor.mkNew (ResourceProxy.authenticate ([settings.Scope], ClientId settings.ReaderAppId, Secret settings.ReaderSecret, settings.Authority)) + let! response = requestor.Call (ProxyResult.bindAsync (fun p -> p.read())) + response =! Success "Read!" + } |> Async.RunSynchronously + + [] + member __.``Forbidden`` () = + async { + let _, proxy = fixture.Init httpClient + + let! response = proxy.provision() + response =! Success () + + let! response = proxy.read() + response =! AuthorizationError HttpStatusCode.Forbidden + } |> Async.RunSynchronously \ No newline at end of file diff --git a/AAD.Test/ResourceProxy.fs b/AAD.Test/ResourceProxy.fs new file mode 100644 index 0000000..74cda24 --- /dev/null +++ b/AAD.Test/ResourceProxy.fs @@ -0,0 +1,96 @@ +namespace AADTests + +open System +open System.Net +open System.Net.Http +open Microsoft.Identity.Client +open AAD + +type ProxyResult<'r> = + | Success of 'r + | AuthenticationError of exn + | AuthorizationError of HttpStatusCode + +module Async = + let catchResult comp = + async { + match! comp |> Async.Catch with + | Choice1Of2 r -> return Success r + | Choice2Of2 err -> return AuthenticationError err + } + +module ProxyResult = + let bindAsync comp r = + async { + match r with + | Success r -> return! comp r + | AuthenticationError err -> return AuthenticationError err + | AuthorizationError code -> return AuthorizationError code + } + +/// Async Result-based proxy +type ResourceProxy = + abstract provision: unit->Async> + abstract read: unit->Async> + abstract write: unit->Async> + abstract httpClient: HttpClient + abstract address: Uri + +module ResourceProxy = + + let internal mkNew (address:Uri) (httpClient:HttpClient) withHeaders = + { new ResourceProxy with + member __.httpClient = httpClient + member __.address = address + member __.provision() = + async { + use r = new HttpRequestMessage(Method = HttpMethod.Head, + RequestUri = address) + withHeaders r.Headers + let! response = httpClient.SendAsync r |> Async.AwaitTask + if int response.StatusCode > 400 then + return AuthorizationError response.StatusCode + else + return Success () + } + member __.read() = + async { + use r = new HttpRequestMessage(Method = HttpMethod.Get, + RequestUri = address) + withHeaders r.Headers + let! response = httpClient.SendAsync r |> Async.AwaitTask + let! content = response.Content.ReadAsStringAsync() + if int response.StatusCode > 400 then + return AuthorizationError response.StatusCode + else + return Success content + } + member __.write() = + async { + use r = new HttpRequestMessage(Method = HttpMethod.Put, + RequestUri = address) + withHeaders r.Headers + let! response = httpClient.SendAsync r |> Async.AwaitTask + let! content = response.Content.ReadAsStringAsync() + if int response.StatusCode > 400 then + return AuthorizationError response.StatusCode + else + return Success content + } + } + + let mkDefault (address:Uri) (httpClient:HttpClient) = + mkNew address httpClient ignore + + let authenticate = + Dictionary.memoize (fun (scopes:seq, clientId, Secret secret, authority: Uri) -> + let app = ConfidentialClientApplicationBuilder.Create(ClientId.toString clientId) + .WithClientSecret(secret) + .WithAuthority(authority) + .Build() + fun (proxy:ResourceProxy) -> + ProxyAuthenticator.ofConfidentialClient (HeaderSetter.bearerAuthorization >> mkNew proxy.address proxy.httpClient) + scopes + app + |> Async.catchResult + ) diff --git a/AAD.Test/ResourceServers.fs b/AAD.Test/ResourceServers.fs new file mode 100644 index 0000000..e46a4b1 --- /dev/null +++ b/AAD.Test/ResourceServers.fs @@ -0,0 +1,46 @@ +[] +module AADTests.ResourceServers + +open System +open System.Threading +open System.Net +open Suave +open Suave.Filters +open Suave.Operators +open AAD +open AADTests.TestsCommon + +let rnd = Random() +module Sample = + + let start (cts:CancellationTokenSource) httpClient audience authority = + let testPort = uint16 (rnd.Next(1,1000)+52767) + let conf = { defaultConfig with + cancellationToken = cts.Token + bindings = [HttpBinding.create HTTP IPAddress.Loopback testPort] } + async { + let! protector = + PartProtector.mkDefault httpClient audience authority + + let read : WebPart = + protector.Verify (fun ctx -> Async.result <| Pattern ["items"; "r"]) + (fun token -> Successful.OK "Read!") + let write : WebPart = + protector.Verify (fun ctx -> Async.result <| Pattern ["items"; "w"]) + (fun token -> Successful.OK "Written!") + + let app = + choose [ + HEAD >=> path "/" >=> Successful.NO_CONTENT + GET >=> path "/" >=> read + PUT >=> path "/" >=> write + RequestErrors.NOT_FOUND "" + ] + let listening, server = startWebServerAsync conf app + + Async.Start(server, cts.Token) + let address = sprintf "http://localhost:%d" testPort + do! address |> Http.waitFor "HEAD" 10_000 + return address + } + diff --git a/AAD.Test/Roles.json b/AAD.Test/Roles.json new file mode 100644 index 0000000..883fc2f --- /dev/null +++ b/AAD.Test/Roles.json @@ -0,0 +1,30 @@ +[{ + "allowedMemberTypes": [ + "User", + "Application" + ], + "description": "Read items", + "displayName": "Reader", + "isEnabled": "true", + "value": "items/r" +}, +{ + "allowedMemberTypes": [ + "User", + "Application" + ], + "description": "Write items", + "displayName": "Writer", + "isEnabled": "true", + "value": "items/w" +}, +{ + "allowedMemberTypes": [ + "User", + "Application" + ], + "description": "All access", + "displayName": "Admin", + "isEnabled": "true", + "value": "*/*" +}] \ No newline at end of file diff --git a/AAD.Test/Settings.fs b/AAD.Test/Settings.fs new file mode 100644 index 0000000..d3fe164 --- /dev/null +++ b/AAD.Test/Settings.fs @@ -0,0 +1,37 @@ +[] +module AADTests.Environment +open System +open Microsoft.Extensions.Configuration +open AAD + +[] +type Settings = + { AppId: Guid + ReaderAppId: Guid + ReaderSecret: string + WriterAppId: Guid + WriterSecret: string + AdminAppId: Guid + AdminSecret: string + TenantId: string } +with + member x.Audience = sprintf "api://%O" x.AppId |> Audience + member x.Scope = sprintf "api://%O/.default" x.AppId |> Scope + member x.Authority = sprintf "https://login.microsoftonline.com/%s" x.TenantId |> Uri + static member Default = + { AppId = Guid.Empty + ReaderAppId = Guid.Empty + ReaderSecret = "" + WriterAppId = Guid.Empty + WriterSecret = "" + AdminAppId = Guid.Empty + AdminSecret = "" + TenantId = "" } + + static member Load() = + let config = ConfigurationBuilder() + .AddUserSecrets() + .Build() + let settings = Settings.Default + config.Bind settings + settings diff --git a/AAD.Test/TestsCommon.fs b/AAD.Test/TestsCommon.fs new file mode 100644 index 0000000..24c1809 --- /dev/null +++ b/AAD.Test/TestsCommon.fs @@ -0,0 +1,56 @@ +[] +module AADTests.TestsCommon + +open Serilog +open System.Net.Http +open Microsoft.IdentityModel.Protocols.OpenIdConnect +open Microsoft.IdentityModel.Protocols + +let mkNonRedirectingHandler () = + new HttpClientHandler(AllowAutoRedirect = false) + +let mkHttpClientWith<'test> (output:Xunit.Abstractions.ITestOutputHelper) handler = + let logger = LoggerConfiguration() + .MinimumLevel.Verbose() + .WriteTo.TestOutput(output, Events.LogEventLevel.Verbose) + .CreateLogger() + .ForContext<'test>() + + let log template args = + logger.Verbose(template, args |> Seq.map snd |> Array.ofSeq) + + new HttpClient(new Logging.HttpClientLogger(handler,log) :> HttpMessageHandler) + +let mkHttpClient<'test> (output:Xunit.Abstractions.ITestOutputHelper) = + mkHttpClientWith output (new HttpClientHandler()) + +let settings = Settings.Load() +let oidcConfig = + OpenIdConnectConfigurationRetriever + .GetAsync("OpenIdConnectMetadata.json", + FileDocumentRetriever(), + System.Threading.CancellationToken.None) + .Result + + +module Http = + open System.Net + + let waitFor method period url = + async { + use client = new HttpClient() + let rec poll sleep = + async { + if sleep then do! Async.Sleep period + use request = new HttpRequestMessage(Method = HttpMethod(method), + RequestUri = System.Uri(url, System.UriKind.Absolute)) + let! response = client.SendAsync request |> Async.AwaitTask |> Async.Catch + match response with + | Choice1Of2 r when r.StatusCode = HttpStatusCode.NoContent -> () + | Choice1Of2 r -> + return! poll true + | Choice2Of2 ex -> + return! poll true + } + return! poll false + } diff --git a/AAD.fs.sln b/AAD.fs.sln new file mode 100644 index 0000000..3eeee82 --- /dev/null +++ b/AAD.fs.sln @@ -0,0 +1,116 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26124.0 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "AAD.fs", "AAD.fs\AAD.fs.fsproj", "{8D639525-3BA9-44AE-B33E-4FE8DE050498}" +EndProject +Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "AAD.Test", "AAD.Test\AAD.Test.fsproj", "{5458D182-9EF7-49EF-AF61-B62B3220E063}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution", "Solution", "{91CAD23C-686B-4AE1-8075-40CE069A6ED9}" +ProjectSection(SolutionItems) = preProject + README.md = README.md + docker-compose.yml = docker-compose.yml + build.fsx = build.fsx + swagger.yaml = swagger.yaml +EndProjectSection +EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "AAD.Suave", "AAD.Suave\AAD.Suave.fsproj", "{04B1BA1E-E9E9-4666-8811-4DB2B2204675}" +EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "AAD.fs.tasks", "AAD.fs.tasks\AAD.fs.tasks.fsproj", "{4E3780B7-8A07-4881-9A0A-6E0F83677B10}" +EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "AAD.Giraffe", "AAD.Giraffe\AAD.Giraffe.fsproj", "{EB4E9886-78C0-4225-8259-623ABA0BDD41}" +EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "AAD.tasks.Test", "AAD.tasks.Test\AAD.tasks.Test.fsproj", "{8B6670C1-BFFB-424F-8ABC-EDB36E81EEC6}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {8D639525-3BA9-44AE-B33E-4FE8DE050498}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8D639525-3BA9-44AE-B33E-4FE8DE050498}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8D639525-3BA9-44AE-B33E-4FE8DE050498}.Debug|x64.ActiveCfg = Debug|Any CPU + {8D639525-3BA9-44AE-B33E-4FE8DE050498}.Debug|x64.Build.0 = Debug|Any CPU + {8D639525-3BA9-44AE-B33E-4FE8DE050498}.Debug|x86.ActiveCfg = Debug|Any CPU + {8D639525-3BA9-44AE-B33E-4FE8DE050498}.Debug|x86.Build.0 = Debug|Any CPU + {8D639525-3BA9-44AE-B33E-4FE8DE050498}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8D639525-3BA9-44AE-B33E-4FE8DE050498}.Release|Any CPU.Build.0 = Release|Any CPU + {8D639525-3BA9-44AE-B33E-4FE8DE050498}.Release|x64.ActiveCfg = Release|Any CPU + {8D639525-3BA9-44AE-B33E-4FE8DE050498}.Release|x64.Build.0 = Release|Any CPU + {8D639525-3BA9-44AE-B33E-4FE8DE050498}.Release|x86.ActiveCfg = Release|Any CPU + {8D639525-3BA9-44AE-B33E-4FE8DE050498}.Release|x86.Build.0 = Release|Any CPU + {5458D182-9EF7-49EF-AF61-B62B3220E063}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5458D182-9EF7-49EF-AF61-B62B3220E063}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5458D182-9EF7-49EF-AF61-B62B3220E063}.Debug|x64.ActiveCfg = Debug|Any CPU + {5458D182-9EF7-49EF-AF61-B62B3220E063}.Debug|x64.Build.0 = Debug|Any CPU + {5458D182-9EF7-49EF-AF61-B62B3220E063}.Debug|x86.ActiveCfg = Debug|Any CPU + {5458D182-9EF7-49EF-AF61-B62B3220E063}.Debug|x86.Build.0 = Debug|Any CPU + {5458D182-9EF7-49EF-AF61-B62B3220E063}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5458D182-9EF7-49EF-AF61-B62B3220E063}.Release|Any CPU.Build.0 = Release|Any CPU + {5458D182-9EF7-49EF-AF61-B62B3220E063}.Release|x64.ActiveCfg = Release|Any CPU + {5458D182-9EF7-49EF-AF61-B62B3220E063}.Release|x64.Build.0 = Release|Any CPU + {5458D182-9EF7-49EF-AF61-B62B3220E063}.Release|x86.ActiveCfg = Release|Any CPU + {5458D182-9EF7-49EF-AF61-B62B3220E063}.Release|x86.Build.0 = Release|Any CPU + {04B1BA1E-E9E9-4666-8811-4DB2B2204675}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {04B1BA1E-E9E9-4666-8811-4DB2B2204675}.Debug|Any CPU.Build.0 = Debug|Any CPU + {04B1BA1E-E9E9-4666-8811-4DB2B2204675}.Debug|x64.ActiveCfg = Debug|Any CPU + {04B1BA1E-E9E9-4666-8811-4DB2B2204675}.Debug|x64.Build.0 = Debug|Any CPU + {04B1BA1E-E9E9-4666-8811-4DB2B2204675}.Debug|x86.ActiveCfg = Debug|Any CPU + {04B1BA1E-E9E9-4666-8811-4DB2B2204675}.Debug|x86.Build.0 = Debug|Any CPU + {04B1BA1E-E9E9-4666-8811-4DB2B2204675}.Release|Any CPU.ActiveCfg = Release|Any CPU + {04B1BA1E-E9E9-4666-8811-4DB2B2204675}.Release|Any CPU.Build.0 = Release|Any CPU + {04B1BA1E-E9E9-4666-8811-4DB2B2204675}.Release|x64.ActiveCfg = Release|Any CPU + {04B1BA1E-E9E9-4666-8811-4DB2B2204675}.Release|x64.Build.0 = Release|Any CPU + {04B1BA1E-E9E9-4666-8811-4DB2B2204675}.Release|x86.ActiveCfg = Release|Any CPU + {04B1BA1E-E9E9-4666-8811-4DB2B2204675}.Release|x86.Build.0 = Release|Any CPU + {4E3780B7-8A07-4881-9A0A-6E0F83677B10}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4E3780B7-8A07-4881-9A0A-6E0F83677B10}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4E3780B7-8A07-4881-9A0A-6E0F83677B10}.Debug|x64.ActiveCfg = Debug|Any CPU + {4E3780B7-8A07-4881-9A0A-6E0F83677B10}.Debug|x64.Build.0 = Debug|Any CPU + {4E3780B7-8A07-4881-9A0A-6E0F83677B10}.Debug|x86.ActiveCfg = Debug|Any CPU + {4E3780B7-8A07-4881-9A0A-6E0F83677B10}.Debug|x86.Build.0 = Debug|Any CPU + {4E3780B7-8A07-4881-9A0A-6E0F83677B10}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4E3780B7-8A07-4881-9A0A-6E0F83677B10}.Release|Any CPU.Build.0 = Release|Any CPU + {4E3780B7-8A07-4881-9A0A-6E0F83677B10}.Release|x64.ActiveCfg = Release|Any CPU + {4E3780B7-8A07-4881-9A0A-6E0F83677B10}.Release|x64.Build.0 = Release|Any CPU + {4E3780B7-8A07-4881-9A0A-6E0F83677B10}.Release|x86.ActiveCfg = Release|Any CPU + {4E3780B7-8A07-4881-9A0A-6E0F83677B10}.Release|x86.Build.0 = Release|Any CPU + {EB4E9886-78C0-4225-8259-623ABA0BDD41}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EB4E9886-78C0-4225-8259-623ABA0BDD41}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EB4E9886-78C0-4225-8259-623ABA0BDD41}.Debug|x64.ActiveCfg = Debug|Any CPU + {EB4E9886-78C0-4225-8259-623ABA0BDD41}.Debug|x64.Build.0 = Debug|Any CPU + {EB4E9886-78C0-4225-8259-623ABA0BDD41}.Debug|x86.ActiveCfg = Debug|Any CPU + {EB4E9886-78C0-4225-8259-623ABA0BDD41}.Debug|x86.Build.0 = Debug|Any CPU + {EB4E9886-78C0-4225-8259-623ABA0BDD41}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EB4E9886-78C0-4225-8259-623ABA0BDD41}.Release|Any CPU.Build.0 = Release|Any CPU + {EB4E9886-78C0-4225-8259-623ABA0BDD41}.Release|x64.ActiveCfg = Release|Any CPU + {EB4E9886-78C0-4225-8259-623ABA0BDD41}.Release|x64.Build.0 = Release|Any CPU + {EB4E9886-78C0-4225-8259-623ABA0BDD41}.Release|x86.ActiveCfg = Release|Any CPU + {EB4E9886-78C0-4225-8259-623ABA0BDD41}.Release|x86.Build.0 = Release|Any CPU + {8B6670C1-BFFB-424F-8ABC-EDB36E81EEC6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8B6670C1-BFFB-424F-8ABC-EDB36E81EEC6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8B6670C1-BFFB-424F-8ABC-EDB36E81EEC6}.Debug|x64.ActiveCfg = Debug|Any CPU + {8B6670C1-BFFB-424F-8ABC-EDB36E81EEC6}.Debug|x64.Build.0 = Debug|Any CPU + {8B6670C1-BFFB-424F-8ABC-EDB36E81EEC6}.Debug|x86.ActiveCfg = Debug|Any CPU + {8B6670C1-BFFB-424F-8ABC-EDB36E81EEC6}.Debug|x86.Build.0 = Debug|Any CPU + {8B6670C1-BFFB-424F-8ABC-EDB36E81EEC6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8B6670C1-BFFB-424F-8ABC-EDB36E81EEC6}.Release|Any CPU.Build.0 = Release|Any CPU + {8B6670C1-BFFB-424F-8ABC-EDB36E81EEC6}.Release|x64.ActiveCfg = Release|Any CPU + {8B6670C1-BFFB-424F-8ABC-EDB36E81EEC6}.Release|x64.Build.0 = Release|Any CPU + {8B6670C1-BFFB-424F-8ABC-EDB36E81EEC6}.Release|x86.ActiveCfg = Release|Any CPU + {8B6670C1-BFFB-424F-8ABC-EDB36E81EEC6}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {EA50EB5C-3A7E-40EE-8EAE-8BC70BFDC380} + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + EndGlobalSection +EndGlobal diff --git a/AAD.fs.tasks/AAD.fs.tasks.fsproj b/AAD.fs.tasks/AAD.fs.tasks.fsproj new file mode 100644 index 0000000..d468303 --- /dev/null +++ b/AAD.fs.tasks/AAD.fs.tasks.fsproj @@ -0,0 +1,22 @@ + + + netstandard2.0 + true + TASKS + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/AAD.fs/AAD.fs.fsproj b/AAD.fs/AAD.fs.fsproj new file mode 100644 index 0000000..9f2fe65 --- /dev/null +++ b/AAD.fs/AAD.fs.fsproj @@ -0,0 +1,22 @@ + + + netstandard2.0 + true + $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/AAD.fs/Awaitable.fs b/AAD.fs/Awaitable.fs new file mode 100644 index 0000000..26c9fd8 --- /dev/null +++ b/AAD.fs/Awaitable.fs @@ -0,0 +1,35 @@ +namespace AAD + +open System + +[] +module internal AwaitableBuilder = + open System.Threading.Tasks +#if TASKS + open FSharp.Control.Tasks + let awaitable = task + type Awaitable<'r> = Task<'r> + + [] + module Awaitable = + let inline result x = Task.FromResult x + let inline awaitTask x = x + let inline awaitUnitTask (x:Task) = x.ContinueWith(fun _ -> ()) + let inline awaitAsync x = Async.StartAsTask x + let inline map f (x:Task<_>) = task { let! v = x in return f v } + let inline bind f (x:Task<_>) = task { let! v = x in return! f v } + let inline whenAll (xs:#seq>) = Task.WhenAll<'t> (Array.ofSeq xs) +#else + let awaitable = async + type Awaitable<'r> = Async<'r> + + [] + module Awaitable = + let inline result x = async.Return x + let inline awaitTask x = Async.AwaitTask x + let inline awaitUnitTask (x:Task) = Async.AwaitTask x + let inline awaitAsync x = x + let inline map f x = async { let! v = x in return f v } + let inline bind f x = async { let! v = x in return! f v } + let inline whenAll x = Async.Parallel x +#endif diff --git a/AAD.fs/Domain.fs b/AAD.fs/Domain.fs new file mode 100644 index 0000000..8ad1998 --- /dev/null +++ b/AAD.fs/Domain.fs @@ -0,0 +1,86 @@ +[] +module AAD.Domain + +[] +type ClientName = ClientName of string + +[] +type Scope = Scope of string + +[] +type Audience = Audience of string + +[] +type ClientId = ClientId of System.Guid + +[] +type Secret = Secret of string + +[] +type UserName = UserName of string + +[] +type Password = Password of string + +type Demand = + | Pattern of string list + | Any of Demand list + | All of Demand list + +[] +type TokenString = TokenString of string + +[] +type WWWAuthenticate = WWWAuthenticate of string + +[] +module Secret = + let inline toString (Secret s) = s + +[] +module ClientId = + let inline toGuid (ClientId id) = id + let inline toString (ClientId id) = string id + +[] +module UserName = + let inline format domain uid = + sprintf "%s/%s" domain uid |> UserName + +[] +module TokenString = + let inline toString (TokenString s) = s + +[] +module Scope = + let inline toString (Scope scope) = scope + +[] +module Audience = + let inline toString (Audience audience) = audience + +[] +module Demand = + let rec private map = + function + | Pattern pattern -> + let xs claim = seq { yield! claim; yield! Seq.initInfinite (fun _ -> "") } |> Seq.zip pattern + Seq.exists (xs >> Seq.fold (fun acc (p,s) -> acc && (s = "*" || String.equalsCaseInsensitive p s)) true) + | All demands -> + fun claims -> (true,demands) ||> List.fold (fun acc demand -> acc && map demand claims) + | Any demands -> + fun claims -> (false,demands) ||> List.fold (fun acc demand -> acc || map demand claims) + + let eval (claims: #seq) (demand: Demand) = + let mapped = map demand + claims |> Seq.cache |> mapped + + +module Assembly = + open System.Runtime.CompilerServices + + [] + [] + [] + [] + () \ No newline at end of file diff --git a/AAD.fs/Requestor.fs b/AAD.fs/Requestor.fs new file mode 100644 index 0000000..9ad926e --- /dev/null +++ b/AAD.fs/Requestor.fs @@ -0,0 +1,82 @@ +namespace AAD + +open System +open Microsoft.Identity.Client + +/// Requestor interface for transparent authentication of task-based proxies. +type TaskRequestor<'proxy> = + /// Call a task that returns a value. + abstract member Call<'r> : ('proxy -> Threading.Tasks.Task<'r>) -> Awaitable<'r> + /// Call a task that doesn't return anything. + abstract member Do : ('proxy -> Threading.Tasks.Task) -> Awaitable + +/// Requestor interface for transparent authentication of async-based clients. +type AsyncRequestor<'proxy> = + /// Call an async function that returns a value. + abstract member Call<'r> : ('proxy -> Async<'r>) -> Awaitable<'r> + /// Call an async function that doesn't return anything. + abstract member Do : ('proxy -> Async) -> Awaitable + +/// Requestor for task-based proxies. +[] +module TaskRequestor = + + /// Creates new instance of a Task requestor. + let mkNew (mkAuthenticated: 'arg -> Threading.Tasks.Task<'proxy>) + (arg: 'arg) = + { new TaskRequestor<'proxy> with + member __.Call(call: 'proxy -> Threading.Tasks.Task<'r>) = + awaitable { + let! proxy = mkAuthenticated arg |> Awaitable.awaitTask + return! call proxy |> Awaitable.awaitTask + } + + member __.Do(call: 'proxy -> Threading.Tasks.Task) = + awaitable { + let! proxy = mkAuthenticated arg |> Awaitable.awaitTask + return! call proxy |> Awaitable.awaitUnitTask + } + } + +/// Requestor for async-based proxies. +[] +module AsyncRequestor = + /// Creates new instance of an Async requestor. + let mkNew (mkAuthenticated: 'arg -> Async<'proxy>) + (arg: 'arg) = + { new AsyncRequestor<'proxy> with + member __.Call(call: 'proxy -> Async<'r>) = + awaitable { + let! proxy = mkAuthenticated arg |> Awaitable.awaitAsync + return! call proxy |> Awaitable.awaitAsync + } + + member __.Do(call: 'proxy -> Async) = + awaitable { + let! proxy = mkAuthenticated arg |> Awaitable.awaitAsync + return! call proxy |> Awaitable.awaitAsync + } + } + +/// Authenticator uses MSAL to obtain a token and create a proxy with it +[] +module ProxyAuthenticator = + let ofConfidentialClient (mkAuthenticated: TokenString -> 'proxy) + (scopes: #seq) + (clientApp: IConfidentialClientApplication) = + awaitable { + let! token = + (scopes |> Seq.map Scope.toString |> clientApp.AcquireTokenForClient).ExecuteAsync() + |> Awaitable.awaitTask + return mkAuthenticated (TokenString token.AccessToken) + } + +/// Helpers to set request headers +[] +module HeaderSetter = + open System.Net.Http + + let bearerAuthorization (TokenString token) = + let header = Headers.AuthenticationHeaderValue("Bearer", token) + fun (headers:Headers.HttpRequestHeaders) -> + headers.Authorization <- header \ No newline at end of file diff --git a/AAD.fs/ResourceOwner.fs b/AAD.fs/ResourceOwner.fs new file mode 100644 index 0000000..737fff2 --- /dev/null +++ b/AAD.fs/ResourceOwner.fs @@ -0,0 +1,128 @@ +namespace AAD + +open System +open System.IdentityModel.Tokens.Jwt + + +[] +module internal TokenCache = + open System.Threading.Tasks + open Microsoft.Extensions.Caching.Memory + open FSharp.Control.Tasks + + let mkNew options = + let cache = new MemoryCache(options) + let getOrAdd key (mkEntry: string -> Task>) = + cache.GetOrCreateAsync(key, fun e -> task { + let! r = mkEntry key + match r with + | Ok entry -> + e.SetAbsoluteExpiration (DateTimeOffset entry.ValidTo) |> ignore + | _ -> + e.SetSlidingExpiration (TimeSpan.FromMinutes 1.) |> ignore + e.SetPriority CacheItemPriority.Low |> ignore + e.SetSize 1L |> ignore + return r + }) + getOrAdd + + let mkDefault () = + MemoryCacheOptions(CompactionPercentage = 0.10, SizeLimit = Nullable 100L) + |> mkNew + + +[] +module internal Introspector = + open System.Threading.Tasks + open YoLo + open Microsoft.IdentityModel.Tokens + open Microsoft.IdentityModel.Protocols.OpenIdConnect + + let mkNew (cache:string -> (string -> Task<_>) -> Task<_>) + (audiences: #seq) + (oidcConfig: OpenIdConnectConfiguration)= + let vparams = TokenValidationParameters + (ValidIssuer = oidcConfig.Issuer, + ValidAudiences = Seq.map Audience.toString audiences, + IssuerSigningKeys = oidcConfig.SigningKeys) + let handler = JwtSecurityTokenHandler() + + let local (jwtEncodedString: string) = + try + let _,token = handler.ValidateToken(jwtEncodedString,vparams) + Ok (token :?> JwtSecurityToken) + with err -> + Error err.Message + + let parse s = + s + |> String.split '.' + |> function | [_;_;_] -> local s + | [""] -> Error "No token" + | _ -> Error "Unsupported token" + + fun (TokenString s) -> + awaitable { + let! r = cache s (parse >> Task.FromResult) + return r + } + +type ResourceOwner = + abstract Validate : demand: Demand -> + onSuccess: (JwtSecurityToken -> unit) -> + onUnauthorized: (JwtSecurityToken option -> WWWAuthenticate -> unit) -> + tokenString: TokenString -> + Awaitable + +[] +module ResourceOwner = + open Microsoft.IdentityModel.Protocols.OpenIdConnect + open System.Security.Claims + + module ClaimFilters = + let isScope (claim: Claim) = + claim.Type = "scp" + || claim.Type = "http://schemas.microsoft.com/identity/claims/scope" + + let isRole (claim: Claim) = + claim.Type = "role" + || claim.Type = ClaimTypes.Role + + let isAppRole (claim: Claim) = + claim.Type = "roles" + + let mkNew (introspect: TokenString -> Awaitable>) + (validate: Demand -> JwtSecurityToken -> Result) + (audiences: #seq) + (oidcConfig: OpenIdConnectConfiguration) = + { new ResourceOwner with + member __.Validate (demand: Demand) + (onSuccess: JwtSecurityToken -> unit) + (onUnauthorized: JwtSecurityToken option -> WWWAuthenticate -> unit) + (tokenString: TokenString) = + awaitable { + let! introspected = introspect tokenString + let validated = introspected |> Result.bind (validate demand) + + let wwwAuthenticate err = + sprintf "Bearer realm=\"%s\", audience=\"%A\", error_description=\"%s\"" + oidcConfig.Issuer + (audiences |> Seq.map Audience.toString |> String.concat ",") + err + match validated, introspected with + | Ok t,_ -> onSuccess t + | Error err, Ok t -> + onUnauthorized (Some t) (wwwAuthenticate err |> WWWAuthenticate) + | Error err, _ -> + onUnauthorized None (wwwAuthenticate err |> WWWAuthenticate) + } + } + + let validate (splitChar: char) (claimsFilter: Claim -> bool) (demand: Demand) (t: JwtSecurityToken) = + let claims = + t.Claims + |> Seq.filter claimsFilter + |> Seq.map (fun c -> c.Value |> String.split splitChar) + demand + |> Demand.eval claims + |> function true -> Ok t | _ -> Error (sprintf "Demand not met: %A" demand) diff --git a/AAD.fs/YoLo.fs b/AAD.fs/YoLo.fs new file mode 100644 index 0000000..38419d8 --- /dev/null +++ b/AAD.fs/YoLo.fs @@ -0,0 +1,850 @@ +// Most of the YoLo comes from https://github.com/haf/YoLo. +[] +module internal YoLo + +#nowarn "64" + +open System +open System.Threading.Tasks + +let curry f a b = f (a, b) + +let uncurry f (a, b) = f a b + +let flip f a b = f b a + +let ct x = fun _ -> x + +module AsyncOption = + let lift f (v:Option<'a>) = + async { + match v with + | Some a -> return! f a + | _ -> return None + } + +module Choice = + + let create v = Choice1Of2 v + + let createSnd v = Choice2Of2 v + + let map f = function + | Choice1Of2 v -> Choice1Of2 (f v) + | Choice2Of2 msg -> Choice2Of2 msg + + let mapSnd f = function + | Choice1Of2 v -> Choice1Of2 v + | Choice2Of2 v -> Choice2Of2 (f v) + + let map2 f1 f2: Choice<'a, 'b> -> Choice<'c, 'd> = function + | Choice1Of2 v -> Choice1Of2 (f1 v) + | Choice2Of2 v -> Choice2Of2 (f2 v) + + let bind (f: 'a -> Choice<'b, 'c>) (v: Choice<'a, 'c>) = + match v with + | Choice1Of2 v -> f v + | Choice2Of2 c -> Choice2Of2 c + + let bindSnd (f: 'a -> Choice<'c, 'b>) (v: Choice<'c, 'a>) = + match v with + | Choice1Of2 x -> Choice1Of2 x + | Choice2Of2 x -> f x + + let fold f g = + function + | Choice1Of2 x -> f x + | Choice2Of2 y -> g y + + let apply f v = + bind (fun f' -> + bind (fun v' -> + create (f' v')) v) f + + let applySnd f v = + bind (fun f' -> + bindSnd (fun v' -> + createSnd (f' v')) v) f + + let lift2 f v1 v2 = + apply (apply (create f) v1) v2 + + let lift3 f v1 v2 v3 = + apply (apply (apply (create f) v1) v2) v3 + + let lift4 f v1 v2 v3 v4 = + apply (apply (apply (apply (create f) v1) v2) v3) v4 + + let lift5 f v1 v2 v3 v4 v5 = + apply (apply (apply (apply (apply (create f) v1) v2) v3) v4) v5 + + let ofOption onMissing = function + | Some x -> Choice1Of2 x + | None -> Choice2Of2 (onMissing ()) + + let toOption = function + | Choice1Of2 x -> Some x + | Choice2Of2 _ -> None + + let ofResult = function + | Ok x -> Choice1Of2 x + | Error x -> Choice2Of2 x + + let toResult = function + | Choice1Of2 x -> Ok x + | Choice2Of2 x -> Error x + + let orDefault value = function + | Choice1Of2 v -> v + | _ -> value () + + let inject f = function + | Choice1Of2 x -> f x; Choice1Of2 x + | Choice2Of2 x -> Choice2Of2 x + + let injectSnd f = function + | Choice1Of2 x -> Choice1Of2 x + | Choice2Of2 x -> f x; Choice2Of2 x + + module Operators = + + let inline (>>=) m f = + bind f m + + let inline (>>-) m f = // snd + bindSnd f m + + let inline (=<<) f m = + bind f m + + let inline (-<<) f m = // snd + bindSnd f m + + let inline (>>*) m f = + inject f m + + let inline (>>@) m f = // snd + injectSnd f m + + let inline (<*>) f m = + apply f m + + let inline () f m = + map f m + + let inline (>!>) m f = + map f m + + let inline (<@>) f m = // snd + mapSnd f m + + let inline (>@>) m f = // snd + mapSnd f m + + let inline ( *>) m1 m2 = + lift2 (fun _ x -> x) m1 m2 + + let inline ( <*) m1 m2 = + lift2 (fun x _ -> x) m1 m2 + + +module Result = + + let map2 f1 f2: Result<'a, 'b> -> Result<'c, 'd> = function + | Ok v -> Ok (f1 v) + | Error v -> Error (f2 v) + + let bindError (f: 'a -> Result<'c, 'b>) (v: Result<'c, 'a>) = + match v with + | Ok x -> Ok x + | Error x -> f x + + let fold f g = + function + | Ok x -> f x + | Error y -> g y + + let apply f v = + Result.bind (fun f' -> + Result.bind (fun v' -> + Ok (f' v')) v) f + + let applyError f v = + Result.bind (fun f' -> + bindError (fun v' -> + Error (f' v')) v) f + + let lift2 f v1 v2 = + apply (apply (Ok f) v1) v2 + + let lift3 f v1 v2 v3 = + apply (apply (apply (Ok f) v1) v2) v3 + + let lift4 f v1 v2 v3 v4 = + apply (apply (apply (apply (Ok f) v1) v2) v3) v4 + + let lift5 f v1 v2 v3 v4 v5 = + apply (apply (apply (apply (apply (Ok f) v1) v2) v3) v4) v5 + + let ofOption onMissing = function + | Some x -> Ok x + | None -> Error onMissing + + let toChoice = function + | Ok x -> Choice1Of2 x + | Error x -> Choice2Of2 x + + let ofChoice = function + | Choice1Of2 x -> Ok x + | Choice2Of2 x -> Error x + + let inject f = function + | Ok x -> f x; Ok x + | Error x -> Error x + + let injectError f = function + | Ok x -> Ok x + | Error x -> f x; Error x + + let isOk r = + match r with | Ok _ -> true | _ -> false + + let isError r = + match r with | Error _ -> true | _ -> false + + module Operators = + + let inline (>>=) m f = + Result.bind f m + + let inline (>>-) m f = // snd + bindError f m + + let inline (=<<) f m = + Result.bind f m + + let inline (-<<) f m = // snd + bindError f m + + let inline (>>*) m f = + inject f m + + let inline (>>@) m f = // snd + injectError f m + + let inline (<*>) f m = + apply f m + + let inline () f m = + Result.map f m + + let inline (>!>) m f = + Result.map f m + + let inline (<@>) f m = // snd + Result.mapError f m + + let inline (>@>) m f = // snd + Result.mapError f m + + let inline ( *>) m1 m2 = + lift2 (fun _ x -> x) m1 m2 + + let inline ( <*) m1 m2 = + lift2 (fun x _ -> x) m1 m2 + +module Option = + + let create x = Some x + + let apply (f : ('a -> 'b) option) (v: 'a option) = + Option.bind (fun f' -> + Option.bind (fun v' -> + create (f' v')) v) f + + let lift2 f v1 v2 = + apply (apply (create f) v1) v2 + + let lift3 f v1 v2 v3 = + apply (apply (apply (create f) v1) v2) v3 + + let lift4 f v1 v2 v3 v4 = + apply (apply (apply (apply (create f) v1) v2) v3) v4 + + let lift5 f v1 v2 v3 v4 v5 = + apply (apply (apply (apply (apply (create f) v1) v2) v3) v4) v5 + + let ofChoice = function + | Choice1Of2 x -> Some x + | _ -> None + + let toChoice case2 = function + | Some x -> Choice1Of2 x + | None -> Choice2Of2 (case2 ()) + + let ofNullable nullable: 'a option = + match box nullable with + | null -> None // CLR null + | :? Nullable<_> as n when not n.HasValue -> None // CLR struct + | :? Nullable<_> as n when n.HasValue -> Some (n.Value) // CLR struct + | x when x.Equals (DBNull.Value) -> None // useful when reading from the db into F# + | x -> Some (unbox x) // anything else + + let toNullable = function + | Some item -> new Nullable<_>(item) + | None -> new Nullable<_>() + + let orDefault x = function + | None -> x () + | Some y -> y + + let inject f = function + | Some x -> f x; Some x + | None -> None + + module Operators = + + let inline (>>=) m f = + Option.bind f m + + let inline (=<<) f m = + Option.bind f m + + let inline (>>*) m f = + inject f m + + let inline (<*>) f m = + apply f m + + let inline () f m = + Option.map f m + + let inline ( *>) m1 m2 = + lift2 (fun _ x -> x) m1 m2 + + let inline ( <*) m1 m2 = + lift2 (fun x _ -> x) m1 m2 + +type Base64String = string + +module String = + open System.Globalization // needed when using DNXCORE50 + open System.IO + open System.Security.Cryptography + + /// Also, invariant culture + let equals (a: string) (b: string) = +#if DNXCORE50 + (CultureInfo.InvariantCulture.CompareInfo.GetStringComparer(CompareOptions.None)).Equals(a, b) +#else + a.Equals(b, StringComparison.InvariantCulture) +#endif + + /// Also, invariant culture + let equalsCaseInsensitive (a: string) (b: string) = +#if DNXCORE50 + (CultureInfo.InvariantCulture.CompareInfo.GetStringComparer(CompareOptions.IgnoreCase)).Equals(a, b) +#else + a.Equals(b, StringComparison.InvariantCultureIgnoreCase) +#endif + + /// Compare ordinally with ignore case. + let equalsOrdinalCI (str1: string) (str2: string) = + String.Equals(str1, str2, StringComparison.OrdinalIgnoreCase) + + /// Ordinally compare two strings in constant time, bounded by the length of the + /// longest string. + let equalsConstantTime (str1: string) (str2: string) = + let mutable xx = uint32 str1.Length ^^^ uint32 str2.Length + let mutable i = 0 + while i < str1.Length && i < str2.Length do + xx <- xx ||| uint32 (int str1.[i] ^^^ int str2.[i]) + i <- i + 1 + xx = 0u + + let toLowerInvariant (str: string) = + str.ToLowerInvariant() + + let replace (find: string) (replacement: string) (str: string) = + str.Replace(find, replacement) + + let isEmpty (s: string) = + s.Length = 0 + + let trim (s: string) = + s.Trim() + + let trimc (toTrim: char) (s: string) = + s.Trim toTrim + + let trimStart (s: string) = + s.TrimStart() + + let split (c: char) (s: string) = + s.Split c |> Array.toList + + let splita (c: char) (s: string) = + s.Split c + + let startsWith (substring: string) (s: string) = + s.StartsWith substring + + let contains (substring: string) (s: string) = + s.Contains substring + + let substring index (s: string) = + s.Substring index + +module Bytes = + open System.IO + open System.Linq + open System.Security.Cryptography + + let hash (algo: unit -> #HashAlgorithm) (bs: byte[]) = + use ms = new MemoryStream() + ms.Write(bs, 0, bs.Length) + ms.Seek(0L, SeekOrigin.Begin) |> ignore + use sha = algo () + sha.ComputeHash ms + + let sha1 = +#if DNXCORE50 + hash (fun () -> SHA1.Create()) +#else + hash (fun () -> new SHA1Managed()) +#endif + + let sha256 = +#if DNXCORE50 + hash (fun () -> SHA256.Create()) +#else + hash (fun () -> new SHA256Managed()) +#endif + + let sha512 = +#if DNXCORE50 + hash (fun () -> SHA512.Create()) +#else + hash (fun () -> new SHA512Managed()) +#endif + + let toHex (bs: byte[]) = + BitConverter.ToString bs + |> String.replace "-" "" + |> String.toLowerInvariant + + let ofHex (digestString: string) = + Enumerable.Range(0, digestString.Length) + .Where(fun x -> x % 2 = 0) + .Select(fun x -> Convert.ToByte(digestString.Substring(x, 2), 16)) + .ToArray() + + /// Compare two byte arrays in constant time, bounded by the length of the + /// longest byte array. + let equalsConstantTime (bits: byte []) (bobs: byte []) = + let mutable xx = uint32 bits.Length ^^^ uint32 bobs.Length + let mutable i = 0 + while i < bits.Length && i < bobs.Length do + xx <- xx ||| uint32 (bits.[i] ^^^ bobs.[i]) + i <- i + 1 + xx = 0u + +[] +module Culture = + open System.Globalization + + let invariant = CultureInfo.InvariantCulture + +module UTF8 = + open System.Text + + let private utf8 = Encoding.UTF8 + + /// Convert the full buffer `b` filled with UTF8-encoded strings into a CLR + /// string. + let toString (bs: byte []) = + utf8.GetString bs + + /// Convert the byte array to a string, by indexing into the passed buffer `b` + /// and taking `count` bytes from it. + let toStringAtOffset (b: byte []) (index: int) (count: int) = + utf8.GetString(b, index, count) + + /// Get the UTF8-encoding of the string. + let bytes (s: string) = + utf8.GetBytes s + + /// Convert the passed string `s` to UTF8 and then encode the buffer with + /// base64. + let encodeBase64: string -> Base64String = + bytes >> Convert.ToBase64String + + /// Convert the passed string `s`, assumed to be a valid Base64 encoding, to a + /// CLR string, going through UTF8. + let decodeBase64: Base64String -> string = + Convert.FromBase64String >> toString + + let sha1 = + bytes >> Bytes.sha1 + + let sha1Hex = + bytes >> Bytes.sha1 >> Bytes.toHex + + let sha256 = + bytes >> Bytes.sha256 + + let sha256Hex = + bytes >> Bytes.sha256 >> Bytes.toHex + + let sha512 = + bytes >> Bytes.sha512 + + let sha512Hex = + bytes >> Bytes.sha512 >> Bytes.toHex + +module Comparisons = + + /// compare x to yobj mapped on selected value from function f + let compareOn f x (yobj: obj) = + match yobj with + | :? 'T as y -> compare (f x) (f y) + | _ -> invalidArg "yobj" "cannot compare values of different types" + + /// check equality on x and y mapped on selected value from function f + let equalsOn f x (yobj:obj) = + match yobj with + | :? 'T as y -> (f x = f y) + | _ -> false + + /// hash x on the selected value from f + let hashOn f x = hash (f x) + +type Random with + /// generate a new random ulong64 value + member x.NextUInt64() = + let buffer = Array.zeroCreate sizeof + x.NextBytes buffer + BitConverter.ToUInt64(buffer, 0) + +module Array = + + /// Ordinally compare two arrays in constant time, bounded by the length of the + /// longest array. This function uses the F# language equality. + let equalsConstantTime (arr1: 'a []) (arr2: 'a []) = + if arr1.Length <> arr2.Length then false else + let mutable b = true + for i in 0 .. arr1.Length - 1 do + b <- b && (arr1.[i] = arr2.[i]) + b + + /// Returns a sequence that yields chunks of length n. + /// Each chunk is returned as an array. + /// Thanks to + /// https://nbevans.wordpress.com/2014/03/13/really-simple-way-to-split-a-f-sequence-into-chunks-partitions/ + let chunk (n: uint32) (s: seq<'t>) = seq { + let n = int n + let pos = ref 0 + let buffer = Array.zeroCreate<'t> n + + for x in s do + buffer.[!pos] <- x + if !pos = n - 1 then + yield buffer |> Array.copy + pos := 0 + else + incr pos + + if !pos > 0 then + yield Array.sub buffer 0 !pos + } + +module Regex = + open System.Text.RegularExpressions + + type RegexMatch = Match + + let escape input = + Regex.Escape input + + let split pattern input = + Regex.Split(input, pattern) + |> List.ofArray + + let replace pattern replacement input = + Regex.Replace(input, pattern, (replacement: string)) + + let replaceWithFunction pattern (replaceFunc: RegexMatch -> string) input = + Regex.Replace(input, pattern, replaceFunc) + + /// Match the `input` against the regex `pattern`. You can do a + /// `Seq.cast` on the result to get it as a sequence + /// and also index with `.["name"]` into the result if you have + /// named capture groups. + let ``match`` pattern input = + match Regex.Matches(input, pattern) with + | x when x.Count > 0 -> + x + |> Seq.cast + |> Seq.head + |> fun x -> x.Groups + |> Some + | _ -> None + +type Microsoft.FSharp.Control.Async with + /// Raise an exception on the async computation/workflow. + static member AsyncRaise (e: exn) = + Async.FromContinuations(fun (_,econt,_) -> econt e) + + /// Await a task asynchronously + static member AwaitTask (t: Task) = + let flattenExns (e: AggregateException) = e.Flatten().InnerExceptions.[0] + let rewrapAsyncExn (it: Async) = + async { try do! it with :? AggregateException as ae -> do! Async.AsyncRaise (flattenExns ae) } + let tcs = new TaskCompletionSource(TaskCreationOptions.None) + t.ContinueWith((fun t' -> + if t.IsFaulted then tcs.SetException(t.Exception |> flattenExns) + elif t.IsCanceled then tcs.SetCanceled () + else tcs.SetResult(())), TaskContinuationOptions.ExecuteSynchronously) + |> ignore + tcs.Task |> Async.AwaitTask |> rewrapAsyncExn + +type Microsoft.FSharp.Control.AsyncBuilder with + /// An extension method that overloads the standard 'Bind' of the 'async' builder. The new overload awaits on + /// a standard .NET task + member x.Bind(t: Task<'T>, f:'T -> Async<'R>): Async<'R> = + async.Bind(Async.AwaitTask t, f) + +module Async = + + let result = async.Return + + let map f value = async { + let! v = value + return f v + } + + let bind f xAsync = async { + let! x = xAsync + return! f x + } + + let withTimeout timeoutMillis operation = + async { + let! child = Async.StartChild(operation, timeoutMillis) + try + let! result = child + return Some result + with :? TimeoutException -> + return None + } + + let apply fAsync xAsync = async { + // start the two asyncs in parallel + let! fChild = Async.StartChild fAsync + let! xChild = Async.StartChild xAsync + + // wait for the results + let! f = fChild + let! x = xChild + + // apply the function to the results + return f x + } + + let lift2 f x y = + apply (apply (result f) x) y + + let lift3 f x y z = + apply (apply (apply (result f) x) y) z + + let lift4 f x y z a = + apply (apply (apply (apply (result f) x) y) z) a + + let lift5 f x y z a b = + apply (apply (apply (apply (apply (result f) x) y) z) a) b + + module Operators = + + let inline (>>=) m f = + bind f m + + let inline (=<<) f m = + bind f m + + let inline (<*>) f m = + apply f m + + let inline () f m = + map f m + + let inline ( *>) m1 m2 = + lift2 (fun _ x -> x) m1 m2 + + let inline ( <*) m1 m2 = + lift2 (fun x _ -> x) m1 m2 + +module List = + + /// Split xs at n, into two lists, or where xs ends if xs.Length < n. + let split n xs = + let rec splitUtil n xs acc = + match xs with + | [] -> List.rev acc, [] + | _ when n = 0u -> List.rev acc, xs + | x::xs' -> splitUtil (n - 1u) xs' (x::acc) + splitUtil n xs [] + + /// Chunk a list into pageSize large chunks + let chunk pageSize = function + | [] -> None + | l -> let h, t = l |> split pageSize in Some(h, t) + + let first = function + | [] -> None + | x :: _ -> Some x + + // Description of the below functions: + // http://fsharpforfunandprofit.com/posts/elevated-world-5/#asynclist + + /// Map a Async producing function over a list to get a new Async using + /// applicative style. ('a -> Async<'b>) -> 'a list -> Async<'b list> + let rec traverseAsyncA f list = + let (<*>) = Async.apply + let cons head tail = head :: tail + let initState = Async.result [] + let folder head tail = + Async.result cons <*> (f head) <*> tail + + List.foldBack folder list initState + + /// Transform a "list" into a "Async" and collect the results + /// using apply. + let sequenceAsyncA x = traverseAsyncA id x + + /// Map a Choice-producing function over a list to get a new Choice using + /// applicative style. ('a -> Choice<'b, 'c>) -> 'a list -> Choice<'b list, 'c> + let rec traverseChoiceA f list = + let (<*>) = Choice.apply + let cons head tail = head :: tail + + // right fold over the list + let initState = Choice.create [] + let folder head tail = + Choice.create cons <*> (f head) <*> tail + + List.foldBack folder list initState + + /// Transform a "list" into a "Choice" and collect the results + /// using apply. + let sequenceChoiceA x = traverseChoiceA id x + + /// Map a Result-producing function over a list to get a new Result using + /// applicative style. ('a -> Result<'b, 'c>) -> 'a list -> Result<'b list, 'c> + let rec traverseResultA f list = + let (<*>) = Result.apply + let cons head tail = head :: tail + + // right fold over the list + let initState = Result.Ok [] + let folder head tail = + Result.Ok cons <*> (f head) <*> tail + + List.foldBack folder list initState + + /// Transform a "list" into a "Result" and collect the results + /// using apply. + let sequenceResultA x = traverseResultA id x + +module Seq = + + let combinations size set = + let rec combinations' acc size set = + seq { + match size, set with + | n, x::xs -> + if n > 0 then yield! combinations' (x::acc) (n - 1) xs + if n >= 0 then yield! combinations' acc n xs + | 0, [] -> yield acc + | _, [] -> () + } + combinations' [] size set + + let first (xs: _ seq): _ option = + if Seq.isEmpty xs then None else Seq.head xs |> Some + +module Env = + + let var (k: string) = + let v = Environment.GetEnvironmentVariable k + if isNull v then None else Some v + + let varParse parse (k: string) = + var k |> Option.map parse + + let varDefault (key: String) (getDefault: unit -> string) = + match var key with + | Some v -> v + | None -> getDefault () + + let varDefaultParse parse (key: string) getDefault = + varDefault key getDefault |> parse + + let varRequired (k: String) = + match var k with + | Some v -> v + | None -> failwithf "The environment variable '%s' is missing." k + +module App = + + open System.IO + open System.Reflection + + /// Gets the calling assembly's informational version number as a string + let getVersion () = +#if DNXCORE50 + (typeof.GetTypeInfo().Assembly) +#else + Assembly.GetCallingAssembly() +#endif + .GetCustomAttribute() + .InformationalVersion + + /// Get the assembly resource + let resourceIn (assembly: Assembly) name = + use stream = assembly.GetManifestResourceStream name + if stream = null then + assembly.GetManifestResourceNames() + |> Array.fold (fun s t -> sprintf "%s\n - %s" s t) "" + |> sprintf "couldn't find resource named '%s', from: %s" name + |> Choice2Of2 + else + use reader = new StreamReader(stream) + reader.ReadToEnd () + |> Choice1Of2 + + /// Get the current assembly resource + let resource = +#if DNXCORE50 + let assembly = typeof.GetTypeInfo().Assembly +#else + let assembly = Assembly.GetExecutingAssembly () +#endif + resourceIn assembly + +module Dictionary = + open System.Collections.Generic + + /// Attempts to retrieve a value as an option from a dictionary using the provided key + let tryFind key (dict: Dictionary<_, _>) = + match dict.TryGetValue key with + | true, value -> Some value + | _ -> None + + let memoize fn = + let cache = new Dictionary<_,_>() + (fun x -> + match cache.TryGetValue x with + | true, v -> v + | false, _ -> let v = fn (x) + cache.[x] <- v + v) \ No newline at end of file diff --git a/AAD.tasks.Test/AAD.tasks.Test.fsproj b/AAD.tasks.Test/AAD.tasks.Test.fsproj new file mode 100644 index 0000000..8ad169d --- /dev/null +++ b/AAD.tasks.Test/AAD.tasks.Test.fsproj @@ -0,0 +1,41 @@ + + + netcoreapp3.0 + 79a3edd0-2092-40a2-a04d-dcb46d5ca9ed + + + + PreserveNewest + + + PreserveNewest + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + + + + + diff --git a/AAD.tasks.Test/ResourceOwnerTests.fs b/AAD.tasks.Test/ResourceOwnerTests.fs new file mode 100644 index 0000000..c63b711 --- /dev/null +++ b/AAD.tasks.Test/ResourceOwnerTests.fs @@ -0,0 +1,96 @@ +namespace AADTests + +open System +open System.Threading +open System.Threading.Tasks +open Xunit +open Swensen.Unquote +open FSharp.Control.Tasks.V2.ContextInsensitive +open AAD +open AADTests.TestsCommon + + +/// Fixture to share initialization across all 3BodyProblem tests +type Parties() = + let mutable last = None + let init httpClient = + task { + match last with + | Some args -> return args + | _ -> + let cts = new CancellationTokenSource() + let! address = Sample.start cts + httpClient + [settings.Audience] + settings.Authority + let proxy = ResourceProxy.mkDefault (Uri address) + httpClient + + last <- Some(cts, proxy) + return last.Value + } + + interface IDisposable with + member __.Dispose() = + match last with + | Some(cts,_) -> cts.Cancel() + | _ -> () + + member __.Init httpClient = (init httpClient).Result + + +[] +type ThreeBodyProblem(output: Xunit.Abstractions.ITestOutputHelper, fixture: Parties) = + let httpClient = mkHttpClient output + + interface IClassFixture + + [] + member __.``Admin can write and read`` () = + task { + let _, proxy = fixture.Init httpClient + let requestor = + proxy |> TaskRequestor.mkNew (ResourceProxy.authenticate ([settings.Scope], ClientId settings.AdminAppId, Secret settings.AdminSecret, settings.Authority)) + let! response = requestor.Call (fun p -> p.read()) + response =! "Read!" + let! response = requestor.Call (fun p -> p.write()) + response =! "Written!" + } + + [] + member __.``Writer can write but not read`` () = + task { + let _, proxy = fixture.Init httpClient + let requestor = + proxy |> TaskRequestor.mkNew (ResourceProxy.authenticate ([settings.Scope], ClientId settings.WriterAppId, Secret settings.WriterSecret, settings.Authority)) + try + let! _ = proxy.read() + () + with + :? ProxyException as e when e.Denied -> () + let! response = requestor.Call (fun p -> p.write()) + response =! "Written!" + } + + [] + member __.``Reader can read`` () = + task { + let _, proxy = fixture.Init httpClient + let requestor = + proxy |> TaskRequestor.mkNew (ResourceProxy.authenticate ([settings.Scope], ClientId settings.ReaderAppId, Secret settings.ReaderSecret, settings.Authority)) + let! response = requestor.Call (fun p -> p.read()) + response =! "Read!" + } + + [] + member __.``Forbidden`` () = + task { + let _, proxy = fixture.Init httpClient + let! response = proxy.provision() // should always succeed + + try + let! _ = proxy.read() + () + with + :? ProxyException as e when e.Denied -> () + } \ No newline at end of file diff --git a/AAD.tasks.Test/ResourceProxy.fs b/AAD.tasks.Test/ResourceProxy.fs new file mode 100644 index 0000000..adfcaeb --- /dev/null +++ b/AAD.tasks.Test/ResourceProxy.fs @@ -0,0 +1,74 @@ +namespace AADTests + +open System +open System.Threading.Tasks +open System.Net.Http +open FSharp.Control.Tasks +open AAD +open Microsoft.Identity.Client + +/// An NSwag-like service proxy +type ResourceProxy = + abstract provision: unit->Task + abstract read: unit->Task + abstract write: unit->Task + abstract httpClient: HttpClient + abstract address: Uri + +type ProxyException(status) = + inherit exn() + member __.Denied = status = System.Net.HttpStatusCode.Forbidden + +module ResourceProxy = + + let internal mkNew (address:Uri) (httpClient:HttpClient) withHeaders = + { new ResourceProxy with + member __.httpClient = httpClient + member __.address = address + member __.provision() = + task { + use r = new HttpRequestMessage(Method = HttpMethod.Head, + RequestUri = address) + withHeaders r.Headers + let! response = httpClient.SendAsync r + if int response.StatusCode > 400 then + raise (ProxyException response.StatusCode) + } :> Task + member __.read() = + task { + use r = new HttpRequestMessage(Method = HttpMethod.Get, + RequestUri = address) + withHeaders r.Headers + let! response = httpClient.SendAsync r + let! content = response.Content.ReadAsStringAsync() + if int response.StatusCode > 400 then + raise (ProxyException response.StatusCode) + return content + } + member __.write() = + task { + use r = new HttpRequestMessage(Method = HttpMethod.Put, + RequestUri = address) + withHeaders r.Headers + let! response = httpClient.SendAsync r + let! content = response.Content.ReadAsStringAsync() + if int response.StatusCode > 400 then + raise (ProxyException response.StatusCode) + return content + } + } + + let mkDefault (address:Uri) (httpClient:HttpClient) = + mkNew address httpClient ignore + + let authenticate = + Dictionary.memoize (fun (scopes:seq, clientId, Secret secret, authority: Uri) -> + let app = ConfidentialClientApplicationBuilder.Create(ClientId.toString clientId) + .WithClientSecret(secret) + .WithAuthority(authority) + .Build() + fun (proxy:ResourceProxy) -> + ProxyAuthenticator.ofConfidentialClient (HeaderSetter.bearerAuthorization >> mkNew proxy.address proxy.httpClient) + scopes + app + ) diff --git a/AAD.tasks.Test/ResourceServers.fs b/AAD.tasks.Test/ResourceServers.fs new file mode 100644 index 0000000..7dfea26 --- /dev/null +++ b/AAD.tasks.Test/ResourceServers.fs @@ -0,0 +1,46 @@ +[] +module AADTests.ResourceServers + +open System +open System.Threading +open System.Threading.Tasks +open System.Net +open Giraffe +open FSharp.Control.Tasks.V2.ContextInsensitive +open AAD +open AADTests.TestsCommon + +let rnd = Random() +module Sample = + + let start (cts:CancellationTokenSource) httpClient audience authority = + task { + let testPort = uint16 (rnd.Next(1,1000)+52767) + let conf = (cts.Token,IPAddress.Loopback,testPort) + + let! protector = + PartProtector.mkDefault httpClient audience authority + + let read : HttpHandler = + protector.Verify (fun ctx -> Task.FromResult <| Pattern ["items"; "r"]) + (fun token -> text "Read!") + + let write : HttpHandler = + protector.Verify (fun ctx -> Task.FromResult <| Pattern ["items"; "w"]) + (fun token -> text "Written!") + + let app = + choose [ + HEAD >=> route "/" >=> Successful.NO_CONTENT + GET >=> route "/" >=> read + PUT >=> route "/" >=> write + RequestErrors.NOT_FOUND "" + ] + let _ = Http.startServer conf app + + let address = sprintf "http://localhost:%d" testPort + do! address |> Http.waitFor "HEAD" 10_000 + return address + } + + diff --git a/AAD.tasks.Test/TestsCommon.fs b/AAD.tasks.Test/TestsCommon.fs new file mode 100644 index 0000000..4c8fe5d --- /dev/null +++ b/AAD.tasks.Test/TestsCommon.fs @@ -0,0 +1,84 @@ +module AADTests.TestsCommon + +open System +open Serilog +open System.Net.Http +open System.Threading.Tasks +open Microsoft.IdentityModel.Protocols.OpenIdConnect +open Microsoft.IdentityModel.Protocols + +let mkNonRedirectingHandler () = + new HttpClientHandler(AllowAutoRedirect = false) + +let mkHttpClientWith<'test> (output:Xunit.Abstractions.ITestOutputHelper) handler = + let logger = LoggerConfiguration() + .MinimumLevel.Verbose() + .WriteTo.TestOutput(output, Events.LogEventLevel.Verbose) + .CreateLogger() + .ForContext<'test>() + + let log template args = + logger.Verbose(template, args |> Seq.map snd |> Array.ofSeq) + + new HttpClient(new Logging.HttpClientLogger(handler,log) :> HttpMessageHandler) + +let mkHttpClient<'test> (output:Xunit.Abstractions.ITestOutputHelper) = + mkHttpClientWith output (new HttpClientHandler()) + +let settings = Settings.Load() + +let oidcConfig = + OpenIdConnectConfigurationRetriever + .GetAsync("OpenIdConnectMetadata.json", + FileDocumentRetriever(), + System.Threading.CancellationToken.None) + .Result + + +module Http = + open System.Net + open FSharp.Control.Tasks.V2.ContextInsensitive + + let waitFor method period url = + task { + use client = new HttpClient() + let rec poll sleep = + task { + if sleep then do! Async.Sleep period + use request = new HttpRequestMessage(Method = HttpMethod(method), + RequestUri = Uri(url, UriKind.Absolute)) + try + let! r = client.SendAsync request + if r.StatusCode <> HttpStatusCode.NoContent then return! poll true + with _ -> + return! poll true + } + return! poll false + } + + open Microsoft.AspNetCore.Hosting + open Giraffe + + let startServer (cl: System.Threading.CancellationToken, ip, port:uint16) router = + let configureApp (app : Microsoft.AspNetCore.Builder.IApplicationBuilder) = + // Add Giraffe to the ASP.NET Core pipeline + app.UseGiraffe router + + let configureServices (services : Microsoft.Extensions.DependencyInjection.IServiceCollection) = + // Add Giraffe dependencies + services.AddGiraffe() |> ignore + + Task.Factory.StartNew(fun _ -> + WebHostBuilder() + .UseKestrel() + .UseUrls(sprintf "http://%O:%d" ip (int port)) + .Configure(Action configureApp) + .ConfigureServices(configureServices) + .Build() + .Run() + ,cl) + +module Task = + let inline wait (t: Task<_>) = + t.Wait() + t.Result \ No newline at end of file diff --git a/README.md b/README.md index 8eeee9c..e96f6ac 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,52 @@ +## Azure AD API protection based on OIDC protocols -# Contributing + +#### Consuming the library + +The library ships as following packages: + +- AAD.fs: F# abstractions with `Async` public interfaces +- AAD.fs.tasks: F# abstractions with `Task` public interfaces +- AAD.Suave: Suave-specific wrappers +- AAD.Giraffe: Giraffe-specific wrappers + +##### For resource server +- Use Suave or Giraffe package and `PartProtector` abstraction, alternatively build on the base AAD.fs `ResourceOwner` primitives +- Use `Noop.PartProtector` to bypass the verification of demands (for example to implement feature switch) + +##### For requesting party +- Use `AsyncRequestor` or `TaskRequestor` from AAD.fs package or [MSAL](https://github.com/AzureAD/microsoft-authentication-library-for-dotnet) library directly + +#### Building + +##### Prerequisites +The build requires at least .NET Core SDK 3 installed. +When building for the first time restore the local tools, in this directory run: + +* `dotnet tool restore` to install [FAKE](https://fake.build/fake-gettingstarted.html), then +* `dotnet fake build` or try `fake build --list` to see the available targets. + +#### Test scenario +The test scenario implements authorization using [Azure Application Roles](https://docs.microsoft.com/en-us/azure/active-directory/develop/howto-add-app-roles-in-azure-ad-apps). The sample application can be found in your Azure Active Directory once provisioned: +- Search Enterprise Applications for user and group role assignments +- See Applications for the manfest of the registered application and the information about associated URI and the service principal. + +##### Running integration tests +* Make sure you are logged in: `az login` +* Only once: Register the application and service principals: `dotnet fake build -t registerSample` +* `dotnet fake build -t integration` + +The registrated application and principals are kept in your Azure subscription and information about them - in your `dotnet user-secrets`, +when you no longer need them, you can delete them with `dotnet fake build -t unregisterSample`. + +> Note: +> Integration tests demonstrate a couple approaches in requestor error handling: +> * Async-based implementation uses custom result type to avoid throwing exceptions +> * Task-based implementation depends on the consumer code to handle the exceptions +> +> Either approach can be used with either version of the requestor. + +## Contributing This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md new file mode 100644 index 0000000..a32d193 --- /dev/null +++ b/RELEASE_NOTES.md @@ -0,0 +1,11 @@ +### 1.1.0 + +* Pass invalid token along into error handler if present + +### 1.0.0 + +* Resize the claim to match the demand pattern + +### 1.0.0-beta-1 + +* Initial release \ No newline at end of file diff --git a/azure-pipelines.yml b/azure-pipelines.yml new file mode 100644 index 0000000..2d5592b --- /dev/null +++ b/azure-pipelines.yml @@ -0,0 +1,32 @@ +pool: + vmImage: 'ubuntu-16.04' +variables: + BUILD_NUMBER: $[counter('buildCounter',1)] + NUGET_REPO_URL: https://api.nuget.org/v3/index.json +trigger: + branches: + include: + - master + - refs/tags/* +steps: + - task: UseDotNet@2 + inputs: + version: '3.1.x' + + - task: DotNetCoreCLI@2 + displayName: "Restore tools" + inputs: + command: custom + custom: tool + arguments: restore + + - task: DotNetCoreCLI@2 + displayName: "Build and publish" + inputs: + command: custom + custom: fake + ${{ if startsWith(variables['Build.SourceBranch'], 'refs/tags') }}: + arguments: 'build -t publish' + ${{ if not(startsWith(variables['Build.SourceBranch'], 'refs/tags')) }}: + arguments: build + \ No newline at end of file diff --git a/build.fsx b/build.fsx new file mode 100644 index 0000000..cef3f20 --- /dev/null +++ b/build.fsx @@ -0,0 +1,203 @@ +#r "paket: +nuget FSharp.Core ~> 4.6.0 +nuget Fake.DotNet.Cli +nuget Fake.IO.FileSystem +nuget Fake.Core.ReleaseNotes +nuget Fake.Core.Target //" +#load "./.fake/build.fsx/intellisense.fsx" +#if !FAKE + #r "Facades/netstandard" +#endif + +open Fake.Core +open Fake.Core.TargetOperators +open Fake.DotNet +open Fake.IO +open Fake.IO.Globbing.Operators +open System +open System.Text.RegularExpressions +let release = ReleaseNotes.load "RELEASE_NOTES.md" +let ver = + match Environment.environVarOrNone "BUILD_NUMBER" with + | Some n -> { release.SemVer with Patch = uint32 n; Original = None } + | _ -> SemVer.parse "0.0.0" + +[] +module Shell = + let sh cmd args cwd parse = + CreateProcess.fromRawCommandLine cmd args + |> CreateProcess.withWorkingDirectory cwd + |> CreateProcess.redirectOutput + |> CreateProcess.ensureExitCode + |> CreateProcess.map parse + |> Proc.run + let inline az arg = sh "az" arg + let aza args cwd parse = + async { return az args cwd parse } + let parsePlain r = String.trimChars [|' '; '\n'|] r.Result.Output + let mapOfTsv s = + Regex.Matches(s, @"(\w+)\s+([-\w]+)\s*") + |> Seq.cast + |> Seq.map (fun m -> m.Groups.[1].Value,m.Groups.[2].Value) + |> Map.ofSeq + +module Async = + let tuple cont comp = + async { + let! x = comp + let! y = cont + return x,y + } + +module Graph = + let assign roleId principalId appObjId tenantId = + let body = sprintf """{\"id\":\"%s\",\"principalId\":\"%s\",\"resourceId\":\"%s\"}""" + roleId + principalId + appObjId + let args = + sprintf "rest --method post --uri \"https://graph.windows.net/%s/servicePrincipals/%s/appRoleAssignments?api-version=1.6\" --body \"%s\" --headers \"Content-type=application/json\"" + tenantId + principalId + body + az args "." ignore + +module Secret = + let set name value = + sh "dotnet" (sprintf "user-secrets set \"%s\" \"%s\"" name value) "AAD.Test" ignore + let private parseList r = + Regex.Matches(r.Result.Output, @"(\w+)\s+=\s+([-\w]+)\n*") + |> Seq.cast + |> Seq.map (fun m -> m.Groups.[1].Value,m.Groups.[2].Value) + let list project = + sh "dotnet" "user-secrets list" project parseList + +let packages = + ["AAD.fs" + "AAD.fs.tasks" + "AAD.Suave" + "AAD.Giraffe"] + +Target.create "clean" (fun _ -> + !! "./**/bin" + ++ "./**/obj" + |> Seq.iter Shell.cleanDir +) + +Target.create "restore" (fun _ -> + DotNet.restore id "." +) + +Target.create "build" (fun _ -> + let args = sprintf "/p:Version=%s --no-restore" ver.AsString + DotNet.build (fun a -> a.WithCommon (fun c -> { c with CustomParams = Some args})) "." +) + +Target.create "test" (fun _ -> + let args = "--no-restore --filter \"(Category!=integration & Category!=interactive)\"" + DotNet.test (fun a -> a.WithCommon (fun c -> { c with CustomParams = Some args})) "." +) + +Target.create "registerSample" (fun _ -> + let parseApp = + parsePlain >> String.split '\n' >> List.filter String.isNotNullOrEmpty >> function + | [l1;l2] -> l1, mapOfTsv l2 + | x -> failwithf "Unexpected output:%A" x + let parseClient = + parsePlain >> String.split '\n' >> List.filter String.isNotNullOrEmpty >> function + | [l1;l2] -> l1, l2 + | x -> failwithf "Unexpected output:%A" x + + let tenantId = az "account show --query tenantId --output tsv" "." parsePlain + let rnd = Random().Next(1,1000) + let appName = sprintf "aad-fs-sample%d" rnd + printfn "Registring: %s" appName + + // resource server app + let appId,roles = + az (sprintf "ad app create --display-name %s --app-roles @Roles.json --query \"[appId,appRoles[].[displayName,id][]]\" --output tsv" appName) + "AAD.Test" + parseApp + az (sprintf "ad app update --id %s --identifier-uris \"api://%s\"" appId appId) "." ignore + az (sprintf "ad sp create --id %s" appId) "." ignore + az (sprintf "ad sp update --id %s --add tags WindowsAzureActiveDirectoryIntegratedApp" appId) "." ignore + let appSPId = + az (sprintf "ad sp show --id %s --query objectId --output tsv" appId) "." parsePlain + // client principals + let (readerPwd,(readerId,readerAppId)),(writerPwd,(writerId,writerAppId)),(adminPwd,(adminId,adminAppId)) = + [ sprintf "http://aad-sample-reader%d" rnd + sprintf "http://aad-sample-writer%d" rnd + sprintf "http://aad-sample-admin%d" rnd ] + |> List.map (fun n -> + aza (sprintf "ad sp create-for-rbac -n \"%s\" --query password --output tsv" n) "." parsePlain + |> Async.tuple (aza (sprintf "ad sp show --id \"%s\" --query \"[objectId,appId]\" --output tsv" n) "." parseClient)) + |> Async.Parallel + |> Async.RunSynchronously + |> function [|r1; r2; r3|] -> r1, r2, r3 | _ -> failwith "Arity mismatch" + // role assignment + Graph.assign (roles |> Map.find "Reader") readerId appSPId tenantId + Graph.assign (roles |> Map.find "Writer") writerId appSPId tenantId + Graph.assign (roles |> Map.find "Admin") adminId appSPId tenantId + + Secret.set "AppId" appId + Secret.set "ReaderAppId" readerAppId + Secret.set "ReaderSecret" readerPwd + Secret.set "WriterAppId" writerAppId + Secret.set "WriterSecret" writerPwd + Secret.set "AdminAppId" adminAppId + Secret.set "AdminSecret" adminPwd + Secret.set "TenantId" tenantId +) + +Target.create "unregisterSample" (fun _ -> + let secrets = Secret.list "AAD.Test" |> Map.ofSeq + [ secrets.["AppId"] + secrets.["AdminAppId"] + secrets.["ReaderAppId"] + secrets.["WriterAppId"] ] + |> List.map (fun id -> aza (sprintf "ad sp delete --id %s" id) "." ignore) + |> Async.Parallel + |> Async.RunSynchronously + |> ignore +) + +Target.create "integration" (fun _ -> + let args = "--no-restore --filter \"Category = integration\"" + DotNet.test (fun a -> a.WithCommon (fun c -> { c with CustomParams = Some args})) "." +) + +Target.create "package" (fun _ -> + let args = sprintf "/p:Version=%s --no-restore" ver.AsString + packages + |> List.iter (DotNet.pack (fun a -> a.WithCommon (fun c -> { c with CustomParams = Some args }))) +) + +Target.create "publish" (fun _ -> + let exec dir = + DotNet.exec (fun a -> a.WithCommon (fun c -> { c with WorkingDirectory=dir })) + packages + |> List.iter (fun folder -> + let args = sprintf "push %s.%s.nupkg -s %s -k %s" + folder ver.AsString + (Environment.environVar "NUGET_REPO_URL") + (Environment.environVar "NUGET_REPO_KEY") + let result = exec (folder + "/bin/Release") "nuget" args + if (not result.OK) then failwithf "%A" result.Errors) +) + +Target.create "release" ignore + +"clean" + ==> "restore" + ==> "build" + ==> "test" + ==> "package" + ==> "publish" + +"integration" + <== [ "test" ] + +"release" + <== [ "publish" ] + +Target.runOrDefault "test" \ No newline at end of file diff --git a/build.fsx.lock b/build.fsx.lock new file mode 100644 index 0000000..085b78c --- /dev/null +++ b/build.fsx.lock @@ -0,0 +1,697 @@ +STORAGE: NONE +RESTRICTION: == netstandard2.0 +NUGET + remote: https://api.nuget.org/v3/index.json + BlackFox.VsWhere (1.0) + FSharp.Core (>= 4.2.3) + Fake.Core.CommandLineParsing (5.15.4) + FParsec (>= 1.0.3) + FSharp.Core (>= 4.3.4) + Fake.Core.Context (5.15.4) + FSharp.Core (>= 4.3.4) + Fake.Core.Environment (5.15.4) + FSharp.Core (>= 4.3.4) + Fake.Core.FakeVar (5.15.4) + Fake.Core.Context (>= 5.15.4) + FSharp.Core (>= 4.3.4) + Fake.Core.Process (5.15.4) + Fake.Core.Environment (>= 5.15.4) + Fake.Core.FakeVar (>= 5.15.4) + Fake.Core.String (>= 5.15.4) + Fake.Core.Trace (>= 5.15.4) + Fake.IO.FileSystem (>= 5.15.4) + FSharp.Core (>= 4.3.4) + System.Diagnostics.Process (>= 4.3) + Fake.Core.ReleaseNotes (5.15.4) + Fake.Core.SemVer (>= 5.15.4) + Fake.Core.String (>= 5.15.4) + FSharp.Core (>= 4.3.4) + Fake.Core.SemVer (5.15.4) + FSharp.Core (>= 4.3.4) + System.Runtime.Numerics (>= 4.3) + Fake.Core.String (5.15.4) + FSharp.Core (>= 4.3.4) + Fake.Core.Target (5.15.4) + Fake.Core.CommandLineParsing (>= 5.15.4) + Fake.Core.Context (>= 5.15.4) + Fake.Core.Environment (>= 5.15.4) + Fake.Core.FakeVar (>= 5.15.4) + Fake.Core.Process (>= 5.15.4) + Fake.Core.String (>= 5.15.4) + Fake.Core.Trace (>= 5.15.4) + FSharp.Control.Reactive (>= 4.2) + FSharp.Core (>= 4.3.4) + System.Reactive.Compatibility (>= 4.1.5) + Fake.Core.Tasks (5.15.4) + Fake.Core.Trace (>= 5.15.4) + FSharp.Core (>= 4.3.4) + Fake.Core.Trace (5.15.4) + Fake.Core.Environment (>= 5.15.4) + Fake.Core.FakeVar (>= 5.15.4) + FSharp.Core (>= 4.3.4) + Fake.Core.Xml (5.15.4) + Fake.Core.String (>= 5.15.4) + FSharp.Core (>= 4.3.4) + System.Xml.ReaderWriter (>= 4.3.1) + System.Xml.XDocument (>= 4.3) + System.Xml.XPath (>= 4.3) + System.Xml.XPath.XDocument (>= 4.3) + System.Xml.XPath.XmlDocument (>= 4.3) + Fake.DotNet.Cli (5.15.4) + Fake.Core.Environment (>= 5.15.4) + Fake.Core.Process (>= 5.15.4) + Fake.Core.String (>= 5.15.4) + Fake.Core.Trace (>= 5.15.4) + Fake.DotNet.MSBuild (>= 5.15.4) + Fake.DotNet.NuGet (>= 5.15.4) + Fake.IO.FileSystem (>= 5.15.4) + FSharp.Core (>= 4.3.4) + Newtonsoft.Json (>= 12.0.2) + Fake.DotNet.MSBuild (5.15.4) + BlackFox.VsWhere (>= 1.0) + Fake.Core.Environment (>= 5.15.4) + Fake.Core.Process (>= 5.15.4) + Fake.Core.String (>= 5.15.4) + Fake.Core.Trace (>= 5.15.4) + Fake.IO.FileSystem (>= 5.15.4) + FSharp.Core (>= 4.3.4) + MSBuild.StructuredLogger (>= 2.0.94) + Fake.DotNet.NuGet (5.15.4) + Fake.Core.Environment (>= 5.15.4) + Fake.Core.Process (>= 5.15.4) + Fake.Core.SemVer (>= 5.15.4) + Fake.Core.String (>= 5.15.4) + Fake.Core.Tasks (>= 5.15.4) + Fake.Core.Trace (>= 5.15.4) + Fake.Core.Xml (>= 5.15.4) + Fake.IO.FileSystem (>= 5.15.4) + Fake.Net.Http (>= 5.15.4) + FSharp.Core (>= 4.3.4) + Newtonsoft.Json (>= 12.0.2) + NuGet.Protocol (>= 4.9.4) + System.Net.Http (>= 4.3.4) + Fake.IO.FileSystem (5.15.4) + Fake.Core.String (>= 5.15.4) + FSharp.Core (>= 4.3.4) + System.Diagnostics.FileVersionInfo (>= 4.3) + System.IO.FileSystem.Watcher (>= 4.3) + Fake.Net.Http (5.15.4) + Fake.Core.Trace (>= 5.15.4) + FSharp.Core (>= 4.3.4) + System.Net.Http (>= 4.3.4) + FParsec (1.0.3) + FSharp.Core (>= 4.2.3) + NETStandard.Library (>= 1.6.1) + FSharp.Control.Reactive (4.2) + FSharp.Core (>= 4.2.3) + System.Reactive (>= 4.0) + FSharp.Core (4.6.2) + Microsoft.Build (16.3) + Microsoft.Build.Framework (16.3) + System.Runtime.Serialization.Primitives (>= 4.1.1) + System.Threading.Thread (>= 4.0) + Microsoft.Build.Tasks.Core (16.3) + Microsoft.Build.Framework (>= 16.3) + Microsoft.Build.Utilities.Core (>= 16.3) + Microsoft.Win32.Registry (>= 4.3) + System.CodeDom (>= 4.4) + System.Collections.Immutable (>= 1.5) + System.Linq.Parallel (>= 4.0.1) + System.Net.Http (>= 4.3.4) + System.Reflection.Metadata (>= 1.6) + System.Reflection.TypeExtensions (>= 4.1) + System.Resources.Extensions (>= 4.6) + System.Resources.Writer (>= 4.0) + System.Threading.Tasks.Dataflow (>= 4.9) + Microsoft.Build.Utilities.Core (16.3) + Microsoft.Build.Framework (>= 16.3) + Microsoft.Win32.Registry (>= 4.3) + System.Collections.Immutable (>= 1.5) + System.Text.Encoding.CodePages (>= 4.0.1) + Microsoft.NETCore.Platforms (3.0) + Microsoft.NETCore.Targets (3.0) + Microsoft.Win32.Primitives (4.3) + Microsoft.NETCore.Platforms (>= 1.1) + Microsoft.NETCore.Targets (>= 1.1) + System.Runtime (>= 4.3) + Microsoft.Win32.Registry (4.6) + System.Buffers (>= 4.5) + System.Memory (>= 4.5.3) + System.Security.AccessControl (>= 4.6) + System.Security.Principal.Windows (>= 4.6) + MSBuild.StructuredLogger (2.0.152) + Microsoft.Build (>= 15.8.166) + Microsoft.Build.Framework (>= 15.8.166) + Microsoft.Build.Tasks.Core (>= 15.8.166) + Microsoft.Build.Utilities.Core (>= 15.8.166) + NETStandard.Library (2.0.3) + Microsoft.NETCore.Platforms (>= 1.1) + Newtonsoft.Json (12.0.3) + NuGet.Common (5.3.1) + NuGet.Frameworks (>= 5.3.1) + System.Diagnostics.Process (>= 4.3) + System.Threading.Thread (>= 4.3) + NuGet.Configuration (5.3.1) + NuGet.Common (>= 5.3.1) + System.Security.Cryptography.ProtectedData (>= 4.3) + NuGet.Frameworks (5.3.1) + NuGet.Packaging (5.3.1) + Newtonsoft.Json (>= 9.0.1) + NuGet.Configuration (>= 5.3.1) + NuGet.Versioning (>= 5.3.1) + System.Dynamic.Runtime (>= 4.3) + NuGet.Protocol (5.3.1) + NuGet.Packaging (>= 5.3.1) + System.Dynamic.Runtime (>= 4.3) + NuGet.Versioning (5.3.1) + runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl (4.3.3) + runtime.debian.9-x64.runtime.native.System.Security.Cryptography.OpenSsl (4.3.3) + runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl (4.3.3) + runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl (4.3.3) + runtime.fedora.27-x64.runtime.native.System.Security.Cryptography.OpenSsl (4.3.3) + runtime.fedora.28-x64.runtime.native.System.Security.Cryptography.OpenSsl (4.3.3) + runtime.native.System (4.3.1) + Microsoft.NETCore.Platforms (>= 1.1.1) + Microsoft.NETCore.Targets (>= 1.1.3) + runtime.native.System.Net.Http (4.3.1) + Microsoft.NETCore.Platforms (>= 1.1.1) + Microsoft.NETCore.Targets (>= 1.1.3) + runtime.native.System.Security.Cryptography.Apple (4.3.1) + runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple (>= 4.3.1) + runtime.native.System.Security.Cryptography.OpenSsl (4.3.3) + runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl (>= 4.3.3) + runtime.debian.9-x64.runtime.native.System.Security.Cryptography.OpenSsl (>= 4.3.3) + runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl (>= 4.3.3) + runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl (>= 4.3.3) + runtime.fedora.27-x64.runtime.native.System.Security.Cryptography.OpenSsl (>= 4.3.3) + runtime.fedora.28-x64.runtime.native.System.Security.Cryptography.OpenSsl (>= 4.3.3) + runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl (>= 4.3.3) + runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl (>= 4.3.3) + runtime.opensuse.42.3-x64.runtime.native.System.Security.Cryptography.OpenSsl (>= 4.3.3) + runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl (>= 4.3.3) + runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl (>= 4.3.3) + runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl (>= 4.3.3) + runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl (>= 4.3.3) + runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl (>= 4.3.3) + runtime.ubuntu.18.04-x64.runtime.native.System.Security.Cryptography.OpenSsl (>= 4.3.3) + runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl (4.3.3) + runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl (4.3.3) + runtime.opensuse.42.3-x64.runtime.native.System.Security.Cryptography.OpenSsl (4.3.3) + runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple (4.3.1) + runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl (4.3.3) + runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl (4.3.3) + runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl (4.3.3) + runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl (4.3.3) + runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl (4.3.3) + runtime.ubuntu.18.04-x64.runtime.native.System.Security.Cryptography.OpenSsl (4.3.3) + System.Buffers (4.5) + System.CodeDom (4.6) + System.Collections (4.3) + Microsoft.NETCore.Platforms (>= 1.1) + Microsoft.NETCore.Targets (>= 1.1) + System.Runtime (>= 4.3) + System.Collections.Concurrent (4.3) + System.Collections (>= 4.3) + System.Diagnostics.Debug (>= 4.3) + System.Diagnostics.Tracing (>= 4.3) + System.Globalization (>= 4.3) + System.Reflection (>= 4.3) + System.Resources.ResourceManager (>= 4.3) + System.Runtime (>= 4.3) + System.Runtime.Extensions (>= 4.3) + System.Threading (>= 4.3) + System.Threading.Tasks (>= 4.3) + System.Collections.Immutable (1.6) + System.Memory (>= 4.5.3) + System.Diagnostics.Debug (4.3) + Microsoft.NETCore.Platforms (>= 1.1) + Microsoft.NETCore.Targets (>= 1.1) + System.Runtime (>= 4.3) + System.Diagnostics.DiagnosticSource (4.6) + System.Memory (>= 4.5.3) + System.Diagnostics.FileVersionInfo (4.3) + Microsoft.NETCore.Platforms (>= 1.1) + System.Globalization (>= 4.3) + System.IO (>= 4.3) + System.IO.FileSystem (>= 4.3) + System.IO.FileSystem.Primitives (>= 4.3) + System.Reflection.Metadata (>= 1.4.1) + System.Runtime (>= 4.3) + System.Runtime.Extensions (>= 4.3) + System.Runtime.InteropServices (>= 4.3) + System.Diagnostics.Process (4.3) + Microsoft.NETCore.Platforms (>= 1.1) + Microsoft.Win32.Primitives (>= 4.3) + Microsoft.Win32.Registry (>= 4.3) + runtime.native.System (>= 4.3) + System.Collections (>= 4.3) + System.Diagnostics.Debug (>= 4.3) + System.Globalization (>= 4.3) + System.IO (>= 4.3) + System.IO.FileSystem (>= 4.3) + System.IO.FileSystem.Primitives (>= 4.3) + System.Resources.ResourceManager (>= 4.3) + System.Runtime (>= 4.3) + System.Runtime.Extensions (>= 4.3) + System.Runtime.Handles (>= 4.3) + System.Runtime.InteropServices (>= 4.3) + System.Text.Encoding (>= 4.3) + System.Text.Encoding.Extensions (>= 4.3) + System.Threading (>= 4.3) + System.Threading.Tasks (>= 4.3) + System.Threading.Thread (>= 4.3) + System.Threading.ThreadPool (>= 4.3) + System.Diagnostics.Tools (4.3) + Microsoft.NETCore.Platforms (>= 1.1) + Microsoft.NETCore.Targets (>= 1.1) + System.Runtime (>= 4.3) + System.Diagnostics.Tracing (4.3) + Microsoft.NETCore.Platforms (>= 1.1) + Microsoft.NETCore.Targets (>= 1.1) + System.Runtime (>= 4.3) + System.Dynamic.Runtime (4.3) + System.Collections (>= 4.3) + System.Diagnostics.Debug (>= 4.3) + System.Linq (>= 4.3) + System.Linq.Expressions (>= 4.3) + System.ObjectModel (>= 4.3) + System.Reflection (>= 4.3) + System.Reflection.Emit (>= 4.3) + System.Reflection.Emit.ILGeneration (>= 4.3) + System.Reflection.Primitives (>= 4.3) + System.Reflection.TypeExtensions (>= 4.3) + System.Resources.ResourceManager (>= 4.3) + System.Runtime (>= 4.3) + System.Runtime.Extensions (>= 4.3) + System.Threading (>= 4.3) + System.Globalization (4.3) + Microsoft.NETCore.Platforms (>= 1.1) + Microsoft.NETCore.Targets (>= 1.1) + System.Runtime (>= 4.3) + System.Globalization.Calendars (4.3) + Microsoft.NETCore.Platforms (>= 1.1) + Microsoft.NETCore.Targets (>= 1.1) + System.Globalization (>= 4.3) + System.Runtime (>= 4.3) + System.Globalization.Extensions (4.3) + Microsoft.NETCore.Platforms (>= 1.1) + System.Globalization (>= 4.3) + System.Resources.ResourceManager (>= 4.3) + System.Runtime (>= 4.3) + System.Runtime.Extensions (>= 4.3) + System.Runtime.InteropServices (>= 4.3) + System.IO (4.3) + Microsoft.NETCore.Platforms (>= 1.1) + Microsoft.NETCore.Targets (>= 1.1) + System.Runtime (>= 4.3) + System.Text.Encoding (>= 4.3) + System.Threading.Tasks (>= 4.3) + System.IO.FileSystem (4.3) + Microsoft.NETCore.Platforms (>= 1.1) + Microsoft.NETCore.Targets (>= 1.1) + System.IO (>= 4.3) + System.IO.FileSystem.Primitives (>= 4.3) + System.Runtime (>= 4.3) + System.Runtime.Handles (>= 4.3) + System.Text.Encoding (>= 4.3) + System.Threading.Tasks (>= 4.3) + System.IO.FileSystem.Primitives (4.3) + System.Runtime (>= 4.3) + System.IO.FileSystem.Watcher (4.3) + Microsoft.NETCore.Platforms (>= 1.1) + Microsoft.Win32.Primitives (>= 4.3) + runtime.native.System (>= 4.3) + System.Collections (>= 4.3) + System.IO.FileSystem (>= 4.3) + System.IO.FileSystem.Primitives (>= 4.3) + System.Resources.ResourceManager (>= 4.3) + System.Runtime (>= 4.3) + System.Runtime.Extensions (>= 4.3) + System.Runtime.Handles (>= 4.3) + System.Runtime.InteropServices (>= 4.3) + System.Text.Encoding (>= 4.3) + System.Threading (>= 4.3) + System.Threading.Overlapped (>= 4.3) + System.Threading.Tasks (>= 4.3) + System.Threading.Thread (>= 4.3) + System.Linq (4.3) + System.Collections (>= 4.3) + System.Diagnostics.Debug (>= 4.3) + System.Resources.ResourceManager (>= 4.3) + System.Runtime (>= 4.3) + System.Runtime.Extensions (>= 4.3) + System.Linq.Expressions (4.3) + System.Collections (>= 4.3) + System.Diagnostics.Debug (>= 4.3) + System.Globalization (>= 4.3) + System.IO (>= 4.3) + System.Linq (>= 4.3) + System.ObjectModel (>= 4.3) + System.Reflection (>= 4.3) + System.Reflection.Emit (>= 4.3) + System.Reflection.Emit.ILGeneration (>= 4.3) + System.Reflection.Emit.Lightweight (>= 4.3) + System.Reflection.Extensions (>= 4.3) + System.Reflection.Primitives (>= 4.3) + System.Reflection.TypeExtensions (>= 4.3) + System.Resources.ResourceManager (>= 4.3) + System.Runtime (>= 4.3) + System.Runtime.Extensions (>= 4.3) + System.Threading (>= 4.3) + System.Linq.Parallel (4.3) + System.Collections (>= 4.3) + System.Collections.Concurrent (>= 4.3) + System.Diagnostics.Debug (>= 4.3) + System.Diagnostics.Tracing (>= 4.3) + System.Linq (>= 4.3) + System.Resources.ResourceManager (>= 4.3) + System.Runtime (>= 4.3) + System.Runtime.Extensions (>= 4.3) + System.Threading (>= 4.3) + System.Threading.Tasks (>= 4.3) + System.Memory (4.5.3) + System.Buffers (>= 4.4) + System.Numerics.Vectors (>= 4.4) + System.Runtime.CompilerServices.Unsafe (>= 4.5.2) + System.Net.Http (4.3.4) + Microsoft.NETCore.Platforms (>= 1.1.1) + runtime.native.System (>= 4.3) + runtime.native.System.Net.Http (>= 4.3) + runtime.native.System.Security.Cryptography.OpenSsl (>= 4.3.2) + System.Collections (>= 4.3) + System.Diagnostics.Debug (>= 4.3) + System.Diagnostics.DiagnosticSource (>= 4.3) + System.Diagnostics.Tracing (>= 4.3) + System.Globalization (>= 4.3) + System.Globalization.Extensions (>= 4.3) + System.IO (>= 4.3) + System.IO.FileSystem (>= 4.3) + System.Net.Primitives (>= 4.3) + System.Resources.ResourceManager (>= 4.3) + System.Runtime (>= 4.3) + System.Runtime.Extensions (>= 4.3) + System.Runtime.Handles (>= 4.3) + System.Runtime.InteropServices (>= 4.3) + System.Security.Cryptography.Algorithms (>= 4.3) + System.Security.Cryptography.Encoding (>= 4.3) + System.Security.Cryptography.OpenSsl (>= 4.3) + System.Security.Cryptography.Primitives (>= 4.3) + System.Security.Cryptography.X509Certificates (>= 4.3) + System.Text.Encoding (>= 4.3) + System.Threading (>= 4.3) + System.Threading.Tasks (>= 4.3) + System.Net.Primitives (4.3.1) + Microsoft.NETCore.Platforms (>= 1.1.1) + Microsoft.NETCore.Targets (>= 1.1.3) + System.Runtime (>= 4.3.1) + System.Runtime.Handles (>= 4.3) + System.Numerics.Vectors (4.5) + System.ObjectModel (4.3) + System.Collections (>= 4.3) + System.Diagnostics.Debug (>= 4.3) + System.Resources.ResourceManager (>= 4.3) + System.Runtime (>= 4.3) + System.Threading (>= 4.3) + System.Reactive (4.2) + System.Runtime.InteropServices.WindowsRuntime (>= 4.3) + System.Threading.Tasks.Extensions (>= 4.5.3) + System.Reactive.Compatibility (4.2) + System.Reactive.Core (>= 4.2) + System.Reactive.Interfaces (>= 4.2) + System.Reactive.Linq (>= 4.2) + System.Reactive.PlatformServices (>= 4.2) + System.Reactive.Providers (>= 4.2) + System.Reactive.Core (4.2) + System.Reactive (>= 4.2) + System.Threading.Tasks.Extensions (>= 4.5.3) + System.Reactive.Interfaces (4.2) + System.Reactive (>= 4.2) + System.Threading.Tasks.Extensions (>= 4.5.3) + System.Reactive.Linq (4.2) + System.Reactive (>= 4.2) + System.Threading.Tasks.Extensions (>= 4.5.3) + System.Reactive.PlatformServices (4.2) + System.Reactive (>= 4.2) + System.Threading.Tasks.Extensions (>= 4.5.3) + System.Reactive.Providers (4.2) + System.Reactive (>= 4.2) + System.Threading.Tasks.Extensions (>= 4.5.3) + System.Reflection (4.3) + Microsoft.NETCore.Platforms (>= 1.1) + Microsoft.NETCore.Targets (>= 1.1) + System.IO (>= 4.3) + System.Reflection.Primitives (>= 4.3) + System.Runtime (>= 4.3) + System.Reflection.Emit (4.6) + System.Reflection.Emit.ILGeneration (>= 4.6) + System.Reflection.Emit.ILGeneration (4.6) + System.Reflection.Emit.Lightweight (4.6) + System.Reflection.Emit.ILGeneration (>= 4.6) + System.Reflection.Extensions (4.3) + Microsoft.NETCore.Platforms (>= 1.1) + Microsoft.NETCore.Targets (>= 1.1) + System.Reflection (>= 4.3) + System.Runtime (>= 4.3) + System.Reflection.Metadata (1.7) + System.Collections.Immutable (>= 1.6) + System.Reflection.Primitives (4.3) + Microsoft.NETCore.Platforms (>= 1.1) + Microsoft.NETCore.Targets (>= 1.1) + System.Runtime (>= 4.3) + System.Reflection.TypeExtensions (4.6) + System.Resources.Extensions (4.6) + System.Memory (>= 4.5.3) + System.Resources.ResourceManager (4.3) + Microsoft.NETCore.Platforms (>= 1.1) + Microsoft.NETCore.Targets (>= 1.1) + System.Globalization (>= 4.3) + System.Reflection (>= 4.3) + System.Runtime (>= 4.3) + System.Resources.Writer (4.3) + System.Collections (>= 4.3) + System.IO (>= 4.3) + System.Resources.ResourceManager (>= 4.3) + System.Runtime (>= 4.3) + System.Runtime.Extensions (>= 4.3) + System.Text.Encoding (>= 4.3) + System.Runtime (4.3.1) + Microsoft.NETCore.Platforms (>= 1.1.1) + Microsoft.NETCore.Targets (>= 1.1.3) + System.Runtime.CompilerServices.Unsafe (4.6) + System.Runtime.Extensions (4.3.1) + Microsoft.NETCore.Platforms (>= 1.1.1) + Microsoft.NETCore.Targets (>= 1.1.3) + System.Runtime (>= 4.3.1) + System.Runtime.Handles (4.3) + Microsoft.NETCore.Platforms (>= 1.1) + Microsoft.NETCore.Targets (>= 1.1) + System.Runtime (>= 4.3) + System.Runtime.InteropServices (4.3) + Microsoft.NETCore.Platforms (>= 1.1) + Microsoft.NETCore.Targets (>= 1.1) + System.Reflection (>= 4.3) + System.Reflection.Primitives (>= 4.3) + System.Runtime (>= 4.3) + System.Runtime.Handles (>= 4.3) + System.Runtime.InteropServices.WindowsRuntime (4.3) + System.Runtime (>= 4.3) + System.Runtime.Numerics (4.3) + System.Globalization (>= 4.3) + System.Resources.ResourceManager (>= 4.3) + System.Runtime (>= 4.3) + System.Runtime.Extensions (>= 4.3) + System.Runtime.Serialization.Primitives (4.3) + System.Resources.ResourceManager (>= 4.3) + System.Runtime (>= 4.3) + System.Security.AccessControl (4.6) + System.Security.Principal.Windows (>= 4.6) + System.Security.Cryptography.Algorithms (4.3.1) + Microsoft.NETCore.Platforms (>= 1.1) + runtime.native.System.Security.Cryptography.Apple (>= 4.3.1) + runtime.native.System.Security.Cryptography.OpenSsl (>= 4.3.2) + System.Collections (>= 4.3) + System.IO (>= 4.3) + System.Resources.ResourceManager (>= 4.3) + System.Runtime (>= 4.3) + System.Runtime.Extensions (>= 4.3) + System.Runtime.Handles (>= 4.3) + System.Runtime.InteropServices (>= 4.3) + System.Runtime.Numerics (>= 4.3) + System.Security.Cryptography.Encoding (>= 4.3) + System.Security.Cryptography.Primitives (>= 4.3) + System.Text.Encoding (>= 4.3) + System.Security.Cryptography.Cng (4.6.1) + System.Security.Cryptography.Csp (4.3) + Microsoft.NETCore.Platforms (>= 1.1) + System.IO (>= 4.3) + System.Reflection (>= 4.3) + System.Resources.ResourceManager (>= 4.3) + System.Runtime (>= 4.3) + System.Runtime.Extensions (>= 4.3) + System.Runtime.Handles (>= 4.3) + System.Runtime.InteropServices (>= 4.3) + System.Security.Cryptography.Algorithms (>= 4.3) + System.Security.Cryptography.Encoding (>= 4.3) + System.Security.Cryptography.Primitives (>= 4.3) + System.Text.Encoding (>= 4.3) + System.Threading (>= 4.3) + System.Security.Cryptography.Encoding (4.3) + Microsoft.NETCore.Platforms (>= 1.1) + runtime.native.System.Security.Cryptography.OpenSsl (>= 4.3) + System.Collections (>= 4.3) + System.Collections.Concurrent (>= 4.3) + System.Linq (>= 4.3) + System.Resources.ResourceManager (>= 4.3) + System.Runtime (>= 4.3) + System.Runtime.Extensions (>= 4.3) + System.Runtime.Handles (>= 4.3) + System.Runtime.InteropServices (>= 4.3) + System.Security.Cryptography.Primitives (>= 4.3) + System.Text.Encoding (>= 4.3) + System.Security.Cryptography.OpenSsl (4.6) + System.Security.Cryptography.Primitives (4.3) + System.Diagnostics.Debug (>= 4.3) + System.Globalization (>= 4.3) + System.IO (>= 4.3) + System.Resources.ResourceManager (>= 4.3) + System.Runtime (>= 4.3) + System.Threading (>= 4.3) + System.Threading.Tasks (>= 4.3) + System.Security.Cryptography.ProtectedData (4.6) + System.Memory (>= 4.5.3) + System.Security.Cryptography.X509Certificates (4.3.2) + Microsoft.NETCore.Platforms (>= 1.1) + runtime.native.System (>= 4.3) + runtime.native.System.Net.Http (>= 4.3) + runtime.native.System.Security.Cryptography.OpenSsl (>= 4.3.2) + System.Collections (>= 4.3) + System.Diagnostics.Debug (>= 4.3) + System.Globalization (>= 4.3) + System.Globalization.Calendars (>= 4.3) + System.IO (>= 4.3) + System.IO.FileSystem (>= 4.3) + System.IO.FileSystem.Primitives (>= 4.3) + System.Resources.ResourceManager (>= 4.3) + System.Runtime (>= 4.3) + System.Runtime.Extensions (>= 4.3) + System.Runtime.Handles (>= 4.3) + System.Runtime.InteropServices (>= 4.3) + System.Runtime.Numerics (>= 4.3) + System.Security.Cryptography.Algorithms (>= 4.3) + System.Security.Cryptography.Cng (>= 4.3) + System.Security.Cryptography.Csp (>= 4.3) + System.Security.Cryptography.Encoding (>= 4.3) + System.Security.Cryptography.OpenSsl (>= 4.3) + System.Security.Cryptography.Primitives (>= 4.3) + System.Text.Encoding (>= 4.3) + System.Threading (>= 4.3) + System.Security.Principal.Windows (4.6) + System.Text.Encoding (4.3) + Microsoft.NETCore.Platforms (>= 1.1) + Microsoft.NETCore.Targets (>= 1.1) + System.Runtime (>= 4.3) + System.Text.Encoding.CodePages (4.6) + System.Runtime.CompilerServices.Unsafe (>= 4.6) + System.Text.Encoding.Extensions (4.3) + Microsoft.NETCore.Platforms (>= 1.1) + Microsoft.NETCore.Targets (>= 1.1) + System.Runtime (>= 4.3) + System.Text.Encoding (>= 4.3) + System.Text.RegularExpressions (4.3.1) + System.Collections (>= 4.3) + System.Globalization (>= 4.3) + System.Resources.ResourceManager (>= 4.3) + System.Runtime (>= 4.3.1) + System.Runtime.Extensions (>= 4.3.1) + System.Threading (>= 4.3) + System.Threading (4.3) + System.Runtime (>= 4.3) + System.Threading.Tasks (>= 4.3) + System.Threading.Overlapped (4.3) + Microsoft.NETCore.Platforms (>= 1.1) + System.Resources.ResourceManager (>= 4.3) + System.Runtime (>= 4.3) + System.Runtime.Handles (>= 4.3) + System.Threading.Tasks (4.3) + Microsoft.NETCore.Platforms (>= 1.1) + Microsoft.NETCore.Targets (>= 1.1) + System.Runtime (>= 4.3) + System.Threading.Tasks.Dataflow (4.10) + System.Threading.Tasks.Extensions (4.5.3) + System.Runtime.CompilerServices.Unsafe (>= 4.5.2) + System.Threading.Thread (4.3) + System.Runtime (>= 4.3) + System.Threading.ThreadPool (4.3) + System.Runtime (>= 4.3) + System.Runtime.Handles (>= 4.3) + System.Xml.ReaderWriter (4.3.1) + System.Collections (>= 4.3) + System.Diagnostics.Debug (>= 4.3) + System.Globalization (>= 4.3) + System.IO (>= 4.3) + System.IO.FileSystem (>= 4.3) + System.IO.FileSystem.Primitives (>= 4.3) + System.Resources.ResourceManager (>= 4.3) + System.Runtime (>= 4.3) + System.Runtime.Extensions (>= 4.3) + System.Runtime.InteropServices (>= 4.3) + System.Text.Encoding (>= 4.3) + System.Text.Encoding.Extensions (>= 4.3) + System.Text.RegularExpressions (>= 4.3) + System.Threading.Tasks (>= 4.3) + System.Threading.Tasks.Extensions (>= 4.3) + System.Xml.XDocument (4.3) + System.Collections (>= 4.3) + System.Diagnostics.Debug (>= 4.3) + System.Diagnostics.Tools (>= 4.3) + System.Globalization (>= 4.3) + System.IO (>= 4.3) + System.Reflection (>= 4.3) + System.Resources.ResourceManager (>= 4.3) + System.Runtime (>= 4.3) + System.Runtime.Extensions (>= 4.3) + System.Text.Encoding (>= 4.3) + System.Threading (>= 4.3) + System.Xml.ReaderWriter (>= 4.3) + System.Xml.XmlDocument (4.3) + System.Collections (>= 4.3) + System.Diagnostics.Debug (>= 4.3) + System.Globalization (>= 4.3) + System.IO (>= 4.3) + System.Resources.ResourceManager (>= 4.3) + System.Runtime (>= 4.3) + System.Runtime.Extensions (>= 4.3) + System.Text.Encoding (>= 4.3) + System.Threading (>= 4.3) + System.Xml.ReaderWriter (>= 4.3) + System.Xml.XPath (4.3) + System.Collections (>= 4.3) + System.Diagnostics.Debug (>= 4.3) + System.Globalization (>= 4.3) + System.IO (>= 4.3) + System.Resources.ResourceManager (>= 4.3) + System.Runtime (>= 4.3) + System.Runtime.Extensions (>= 4.3) + System.Threading (>= 4.3) + System.Xml.ReaderWriter (>= 4.3) + System.Xml.XPath.XDocument (4.3) + System.Diagnostics.Debug (>= 4.3) + System.Linq (>= 4.3) + System.Resources.ResourceManager (>= 4.3) + System.Runtime (>= 4.3) + System.Runtime.Extensions (>= 4.3) + System.Threading (>= 4.3) + System.Xml.ReaderWriter (>= 4.3) + System.Xml.XDocument (>= 4.3) + System.Xml.XPath (>= 4.3) + System.Xml.XPath.XmlDocument (4.3) + System.Collections (>= 4.3) + System.Globalization (>= 4.3) + System.IO (>= 4.3) + System.Resources.ResourceManager (>= 4.3) + System.Runtime (>= 4.3) + System.Runtime.Extensions (>= 4.3) + System.Threading (>= 4.3) + System.Xml.ReaderWriter (>= 4.3) + System.Xml.XmlDocument (>= 4.3) + System.Xml.XPath (>= 4.3)