diff --git a/README.md b/README.md index 6232d26..3ecdfd6 100644 --- a/README.md +++ b/README.md @@ -185,9 +185,9 @@ Issue a GET request to `/services/about` Shown below is a fully specified configuration section for Sitecore.Ship: - + - + @@ -201,6 +201,7 @@ Default configuration: * allowPackageStreaming = false * recordInstallationHistory = false * IP address whitelisting is disabled if no elements are specified below the `` element or if the element is omited. +* access token is not specified and service can be used without `Authorization` HTTP header When `recordInstallationHistory` has been set to true packages should follow the naming conventions set out below: @@ -219,6 +220,17 @@ For example: 02-HomePage.zip +### Token based security + +Service can be protected by secure access token. Every request without correct access token in its header will get `401 Unauthorized` HTTP error code. + +**Important:** Token cannot protect your service without transport level security. Do not forget to use `https` when you call APIs. + +Token transmition: + + curl -i -H "Authorization: Bearer some-key" https://mysite/services/about + + ### Tools POSTMAN is a powerful HTTP client that runs as a Chrome browser extension and greatly helps with testing test REST web services. Find out more diff --git a/build/Build.proj b/build/Build.proj index aac8805..afb0ae1 100644 --- a/build/Build.proj +++ b/build/Build.proj @@ -45,6 +45,9 @@ + + + diff --git a/build/build.props b/build/build.props index 088369b..dd2c699 100644 --- a/build/build.props +++ b/build/build.props @@ -16,7 +16,7 @@ $(TestResultsPath)\TestResults.html $(TestResultsPath)\TestResults.xml $(ProjectRoot)\tools - $(ProjectRoot)\packages\xunit.1.9.2\lib\net20 + $(ProjectRoot)\packages\xunit.runner.msbuild.2.0.0\build\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS $(ToolsPath)\curl $(CurlPath)\curl.exe diff --git a/src/Sitecore.Ship.AspNet/BaseHttpHandler.cs b/src/Sitecore.Ship.AspNet/BaseHttpHandler.cs index 0401913..06d553c 100644 --- a/src/Sitecore.Ship.AspNet/BaseHttpHandler.cs +++ b/src/Sitecore.Ship.AspNet/BaseHttpHandler.cs @@ -37,6 +37,7 @@ public void ProcessRequest(HttpContext context) if (!_authoriser.IsAllowed()) { context.Response.StatusCode = (int) HttpStatusCode.Unauthorized; + return; } context.Items.Add(StartTime, DateTime.UtcNow); diff --git a/src/Sitecore.Ship.Core/AccessTokenAuthoriser.cs b/src/Sitecore.Ship.Core/AccessTokenAuthoriser.cs new file mode 100644 index 0000000..bd873f5 --- /dev/null +++ b/src/Sitecore.Ship.Core/AccessTokenAuthoriser.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using Sitecore.Ship.Core.Contracts; +using Sitecore.Ship.Core.Domain; + +namespace Sitecore.Ship.Core +{ + public class AccessTokenAuthoriser// : IAuthoriser - do not inherit from Interface to make sure that IoC dependencies resolution is not affected + { + private readonly ICheckRequests _checkRequests; + private readonly PackageInstallationSettings _packageInstallationSettings; + private readonly ILog _logger; + + private const string AuthorizationHeaderKey = "Authorization"; + + public AccessTokenAuthoriser(ICheckRequests checkRequests, + PackageInstallationSettings packageInstallationSettings, ILog logger) + { + if (checkRequests == null) + throw new ArgumentNullException("checkRequests"); + if (packageInstallationSettings == null) + throw new ArgumentNullException("packageInstallationSettings"); + + _checkRequests = checkRequests; + _packageInstallationSettings = packageInstallationSettings; + _logger = logger; + } + + public bool IsAllowed() + { + if (!_packageInstallationSettings.TokenRequired) + return true; + + if (_checkRequests.Headers == null || !_checkRequests.Headers.AllKeys.Contains(AuthorizationHeaderKey)) + { + LogAccessDenial(AuthorizationHeaderKey + " header missing"); + return false; + } + + var allAuthorizationHeadervalues = _checkRequests.Headers.GetValues(AuthorizationHeaderKey); + +// ReSharper disable once AssignNullToNotNullAttribute + string bearerHeader = allAuthorizationHeadervalues.FirstOrDefault(authorizationHeader => authorizationHeader.StartsWith("bearer", StringComparison.InvariantCultureIgnoreCase)); + + if (bearerHeader == null) + { + LogAccessDenial("Bearer authentication scheme required"); + return false; + } + + var token = bearerHeader.Substring(6).Trim(); + + var success = token == _packageInstallationSettings.AccessToken; + + if (!success) + { + LogAccessDenial("Wrong Authentication Token"); + } + + return success; + } + + private void LogAccessDenial(string diagnostic) + { + if (!_packageInstallationSettings.MuteAuthorisationFailureLogging) + { + _logger.Write(string.Format("Sitecore.Ship access denied: {0}", diagnostic)); + } + } + } +} diff --git a/src/Sitecore.Ship.Core/Contracts/ICheckRequests.cs b/src/Sitecore.Ship.Core/Contracts/ICheckRequests.cs index e5742f1..b338f88 100644 --- a/src/Sitecore.Ship.Core/Contracts/ICheckRequests.cs +++ b/src/Sitecore.Ship.Core/Contracts/ICheckRequests.cs @@ -1,9 +1,13 @@ -namespace Sitecore.Ship.Core.Contracts +using System.Collections.Specialized; + +namespace Sitecore.Ship.Core.Contracts { public interface ICheckRequests { bool IsLocal { get; } string UserHostAddress { get; } + + NameValueCollection Headers { get; } } } \ No newline at end of file diff --git a/src/Sitecore.Ship.Core/Domain/PackageInstallationSettings.cs b/src/Sitecore.Ship.Core/Domain/PackageInstallationSettings.cs index f2bcc33..e774d12 100644 --- a/src/Sitecore.Ship.Core/Domain/PackageInstallationSettings.cs +++ b/src/Sitecore.Ship.Core/Domain/PackageInstallationSettings.cs @@ -22,5 +22,8 @@ public PackageInstallationSettings() public bool MuteAuthorisationFailureLogging { get; set; } public bool HasAddressWhitelist { get { return AddressWhitelist.Count > 0; } } + + public string AccessToken { get; set; } + public bool TokenRequired { get { return !string.IsNullOrWhiteSpace(AccessToken); } } } } \ No newline at end of file diff --git a/src/Sitecore.Ship.Core/HttpRequestAuthoriser.cs b/src/Sitecore.Ship.Core/HttpRequestAuthoriser.cs index 44121a6..81cea3b 100644 --- a/src/Sitecore.Ship.Core/HttpRequestAuthoriser.cs +++ b/src/Sitecore.Ship.Core/HttpRequestAuthoriser.cs @@ -48,6 +48,14 @@ public bool IsAllowed() } } + if (_packageInstallationSettings.TokenRequired) + { + var tokenAuthorizer = new AccessTokenAuthoriser(_checkRequests, _packageInstallationSettings, _logger); + var authorized = tokenAuthorizer.IsAllowed(); + if (!authorized) + return false; + } + return true; } diff --git a/src/Sitecore.Ship.Core/Sitecore.Ship.Core.csproj b/src/Sitecore.Ship.Core/Sitecore.Ship.Core.csproj index 6b86010..39fdc68 100644 --- a/src/Sitecore.Ship.Core/Sitecore.Ship.Core.csproj +++ b/src/Sitecore.Ship.Core/Sitecore.Ship.Core.csproj @@ -48,6 +48,7 @@ Properties\CommonVersionInfo.cs + diff --git a/src/Sitecore.Ship.Infrastructure/Configuration/PackageInstallationConfiguration.cs b/src/Sitecore.Ship.Infrastructure/Configuration/PackageInstallationConfiguration.cs index ffb5f01..352624d 100644 --- a/src/Sitecore.Ship.Infrastructure/Configuration/PackageInstallationConfiguration.cs +++ b/src/Sitecore.Ship.Infrastructure/Configuration/PackageInstallationConfiguration.cs @@ -11,6 +11,7 @@ public class PackageInstallationConfiguration : ConfigurationSection const string RecordInstallationHistoryKey = "recordInstallationHistory"; const string WhitelistElementName = "Whitelist"; const string MuteAuthorisationFailureLoggingKey = "muteAuthorisationFailureLogging"; + const string AccessTokenKey = "accessToken"; public static PackageInstallationConfiguration GetConfiguration() { @@ -38,6 +39,9 @@ public WhitelistCollection Whitelist [ConfigurationProperty(MuteAuthorisationFailureLoggingKey, IsRequired = false, DefaultValue = false)] public bool MuteAuthorisationFailureLogging { get { return (bool)this[MuteAuthorisationFailureLoggingKey]; } } + + [ConfigurationProperty(AccessTokenKey, IsRequired = false, DefaultValue = "")] + public string AccessToken { get { return (string)this[AccessTokenKey]; } } } [ConfigurationCollection(typeof(WhitelistElement))] diff --git a/src/Sitecore.Ship.Infrastructure/Configuration/PackageInstallationConfigurationProvider.cs b/src/Sitecore.Ship.Infrastructure/Configuration/PackageInstallationConfigurationProvider.cs index ccd94a8..c58a57b 100644 --- a/src/Sitecore.Ship.Infrastructure/Configuration/PackageInstallationConfigurationProvider.cs +++ b/src/Sitecore.Ship.Infrastructure/Configuration/PackageInstallationConfigurationProvider.cs @@ -15,7 +15,8 @@ public PackageInstallationConfigurationProvider() AllowRemoteAccess = config.AllowRemoteAccess, AllowPackageStreaming = config.AllowPackageStreaming, RecordInstallationHistory = config.RecordInstallationHistory, - MuteAuthorisationFailureLogging = config.MuteAuthorisationFailureLogging + MuteAuthorisationFailureLogging = config.MuteAuthorisationFailureLogging, + AccessToken = config.AccessToken }; if (config.Whitelist.Count > 0) diff --git a/src/Sitecore.Ship.Infrastructure/Web/HttpRequestChecker.cs b/src/Sitecore.Ship.Infrastructure/Web/HttpRequestChecker.cs index 87937fc..eea5f1f 100644 --- a/src/Sitecore.Ship.Infrastructure/Web/HttpRequestChecker.cs +++ b/src/Sitecore.Ship.Infrastructure/Web/HttpRequestChecker.cs @@ -1,4 +1,5 @@ -using System.Web; +using System.Collections.Specialized; +using System.Web; using Sitecore.Ship.Core.Contracts; namespace Sitecore.Ship.Infrastructure.Web @@ -14,5 +15,10 @@ public string UserHostAddress { get { return HttpContext.Current.Request.UserHostAddress; } } + + public NameValueCollection Headers + { + get { return HttpContext.Current.Request.Headers; } + } } } \ No newline at end of file diff --git a/tests/TestUtils/TestUtils.csproj b/tests/TestUtils/TestUtils.csproj index b3eecde..6af3464 100644 --- a/tests/TestUtils/TestUtils.csproj +++ b/tests/TestUtils/TestUtils.csproj @@ -1,5 +1,6 @@  + Debug @@ -12,6 +13,8 @@ v4.5 512 + ..\..\ + true true @@ -46,6 +49,14 @@ + + + + This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + +