public static bool IsAuthorized(IHttpRequestWrapper request, RequestHeaders headers, RequestQueryParameters queryParams, bool ignoreRequestAge) { // Quick request age check var authHeader = headers.Value<string>("Authorization"); DateTimeOffset requestVersion = headers.RequestVersion; string requestDateHeader = headers.Value<string>("x-ms-date"); string dateHeader = String.Empty; if (String.IsNullOrWhiteSpace(requestDateHeader)) { requestDateHeader = headers.Value<string>("Date"); dateHeader = requestDateHeader; } if (String.IsNullOrWhiteSpace(requestDateHeader)) { // One of the date headers is mandatory return false; } if (!ignoreRequestAge) { DateTime requestDate; if (!DateTime.TryParse(requestDateHeader, out requestDate)) { return false; } else if (requestDate < DateTime.Now.AddMinutes(-15)) { return false; } } var parts = authHeader.Split(' ', ':'); if (parts.Length != 3) { return false; } else if (parts[0] != SharedKeySignature.AlgorithmSharedKey && parts[0] != SharedKeySignature.AlgorithmSharedKeyLite) { return false; } var account = parts[1]; var signature = parts[2]; if (!String.Equals(account, SharedKeySignature.AccountName, StringComparison.OrdinalIgnoreCase)) { return false; } // We have to deal with multiple encodings (the spec is a bit ambiguous on what an 'encoded' path actually is). // Only run the validation if the encodings result in different strings var requestUriParts = request.UriParts; var pathsToCheck = new List<string>() { requestUriParts.OriginalUriPath }; var unencodedPath = requestUriParts.PublicUriPath; if (unencodedPath != pathsToCheck[0]) { pathsToCheck.Add(unencodedPath); } var alternateEncodingPaths = AlternateEncodeString(pathsToCheck[0]); if (alternateEncodingPaths != null) { pathsToCheck.AddRange(alternateEncodingPaths); } // For some verbs we can't tell if the Content-Length header was specified as 0 or that IIS/UrlRewrite/ASP.NET has constructed // the header value for us. The difference is significant to the signature as content length is included for SharedKey. // The ambiguity has been resolved in version 2015-02-21 bool fullKeyAlgorithm = parts[0] == SharedKeySignature.AlgorithmSharedKey; bool runBlankContentLengthComparison = false; string method = request.HttpMethod.ToUpper(); var contentLength = headers.Value("Content-Length", String.Empty); if (fullKeyAlgorithm) { int length; if (!int.TryParse(contentLength, out length) || length <= 0) { if (requestVersion >= StorageServiceVersions.Version_2015_02_21) { // This version made it explicit what to do with 0 content-length contentLength = String.Empty; runBlankContentLengthComparison = false; } else { // Preserve a Content-Length: 0 header for PUT methods runBlankContentLengthComparison = !method.Equals(WebRequestMethods.Http.Put, StringComparison.OrdinalIgnoreCase); } } } var stringsToCheck = pathsToCheck.SelectMany(uriPath => new[] { runBlankContentLengthComparison ? Tuple.Create(uriPath, String.Empty) : null, Tuple.Create(uriPath, contentLength), }) .Where(check => check != null) .Select(pathWithContentLength => GetStringToSign(!fullKeyAlgorithm, method, pathWithContentLength.Item1, headers, queryParams, dateHeader, pathWithContentLength.Item2)) .SelectMany(stringToSign => new[] { Tuple.Create(true, stringToSign), Tuple.Create(false, stringToSign) }); var evaluationResult = stringsToCheck .FirstOrDefault(validatationCheck => VerifyRequestAuthorization(signature, validatationCheck.Item1, validatationCheck.Item2)); if (evaluationResult != null) { // Remember the Auth Scheme & Key for when we have to sign the response request.AuthenticationScheme = parts[0]; request.AuthenticationKey = evaluationResult.Item1 ? SharedKeySignature.PrimaryAccountKey : SharedKeySignature.SecondaryAccountKey; return true; } DashTrace.TraceWarning("Failed to authenticate SharedKey request: {0}:{1}:{2}:{3}:{4}", parts[0], account, method, request.Url, signature); return false; }
public static async Task<bool> IsAuthorizedAsync(IHttpRequestWrapper request, RequestHeaders headers, RequestQueryParameters queryParams, bool ignoreRequestAge) { var requestUriParts = request.UriParts; string resourceType = String.Empty; DateTimeOffset? start = queryParams.Value(ParamStartTime, DateTimeOffset.UtcNow); DateTimeOffset? expiry = queryParams.Value(ParamExpiryTime, DateTimeOffset.MinValue); DateTimeOffset sasVersion = queryParams.Value(ParamVersion, StorageServiceVersions.Version_2009_09_19); bool accountSas = queryParams.Contains(ParamResourceTypeEx); if (accountSas) { resourceType = queryParams.Value<string>(ParamResourceTypeEx).ToLowerInvariant(); } else { resourceType = queryParams.Value<string>(ParamResourceType).ToLowerInvariant(); } if (expiry == DateTimeOffset.MinValue) { expiry = null; } SharedAccessBlobPermissions permissions = PermissionsFromString(queryParams.Value(ParamPermissions, String.Empty)); // Determine validity of the structure first if (requestUriParts.IsAccountRequest) { if (sasVersion < StorageServiceVersions.Version_2015_04_05 || !resourceType.Contains('s')) { // SAS keys are not valid for account operations before 2015-04-05 return false; } } else if (requestUriParts.IsContainerRequest) { if (resourceType != "c") { return false; } } else if (requestUriParts.IsBlobRequest) { if (resourceType.IndexOfAny(new[] { 'c', 'b', 'o' }) == -1) { return false; } } if (!accountSas) { var storedPolicyId = queryParams.Value<string>(ParamStoredPolicy); if (!String.IsNullOrWhiteSpace(storedPolicyId)) { // Validate that we're not duplicating values for both stored access policy & url // Allow stored policy to the specified from a different container for test purposes - this isn't a security violation as it must come from the same account. var aclContainer = headers.Value("StoredPolicyContainer", String.Empty); if (String.IsNullOrEmpty(aclContainer)) { aclContainer = requestUriParts.Container; } var storedPolicy = await GetStoredPolicyForContainer(aclContainer, storedPolicyId); if (storedPolicy == null) { return false; } if (storedPolicy.SharedAccessStartTime.HasValue) { start = storedPolicy.SharedAccessStartTime; } if (storedPolicy.SharedAccessExpiryTime.HasValue) { if (expiry.HasValue) { return false; } expiry = storedPolicy.SharedAccessExpiryTime; } if (queryParams.Contains(ParamPermissions)) { return false; } permissions = storedPolicy.Permissions; } } else { if (!queryParams.Value<string>(ParamServices, String.Empty).Contains('b')) { // SAS must include blob service return false; } } if (!expiry.HasValue || permissions == SharedAccessBlobPermissions.None) { return false; } else if (!ignoreRequestAge && (start.Value > DateTimeOffset.UtcNow || expiry.Value < DateTimeOffset.UtcNow)) { return false; } else if (!IsProtocolMatched(request, queryParams.Value<string>(ParamProtocol))) { return false; } else if (!IsSourceAddressInRange(request, queryParams.Value<string>(ParamSourceIP))) { return false; } // Verify the assigned permissions line up with the requested operation StorageOperationTypes requestOperation = StorageOperations.GetBlobOperation(request); switch (requestOperation) { case StorageOperationTypes.GetBlob: case StorageOperationTypes.GetBlobMetadata: case StorageOperationTypes.GetBlobProperties: case StorageOperationTypes.GetBlockList: case StorageOperationTypes.GetPageRanges: case StorageOperationTypes.GetBlobServiceProperties: case StorageOperationTypes.GetBlobServiceStats: case StorageOperationTypes.GetContainerProperties: case StorageOperationTypes.GetContainerMetadata: if (!permissions.IsFlagSet(SharedAccessBlobPermissions.Read)) { return false; } break; case StorageOperationTypes.AbortCopyBlob: case StorageOperationTypes.CopyBlob: case StorageOperationTypes.LeaseBlob: case StorageOperationTypes.PutBlob: case StorageOperationTypes.PutBlock: case StorageOperationTypes.PutBlockList: case StorageOperationTypes.PutPage: case StorageOperationTypes.SetBlobMetadata: case StorageOperationTypes.SetBlobProperties: case StorageOperationTypes.SnapshotBlob: case StorageOperationTypes.SetBlobServiceProperties: case StorageOperationTypes.CreateContainer: case StorageOperationTypes.SetContainerMetadata: case StorageOperationTypes.LeaseContainer: if (!permissions.IsFlagSet(SharedAccessBlobPermissions.Write)) { return false; } break; case StorageOperationTypes.DeleteBlob: case StorageOperationTypes.DeleteContainer: if (!permissions.IsFlagSet(SharedAccessBlobPermissions.Delete)) { return false; } break; case StorageOperationTypes.ListBlobs: case StorageOperationTypes.ListContainers: if (!permissions.IsFlagSet(SharedAccessBlobPermissions.List)) { return false; } break; default: // All other operations are not supported by SAS uris return false; } Func<string> stringToSignFactory = null; if (!accountSas) { Func<string> baseStringToSign = () => String.Format("{0}\n{1}\n{2}\n{3}\n{4}", queryParams.Value<string>(ParamPermissions), queryParams.Value<string>(ParamStartTime), queryParams.Value<string>(ParamExpiryTime), GetCanonicalizedResource(requestUriParts, resourceType, sasVersion), queryParams.Value<string>(ParamStoredPolicy)); Func<string> ipAndProtocolSnippet = () => String.Format("{0}\n{1}\n", queryParams.Value<string>(ParamSourceIP), queryParams.Value<string>(ParamProtocol)); Func<string> v2012_02_12StringToSign = () => String.Format("{0}\n{1}{2}", baseStringToSign(), sasVersion >= StorageServiceVersions.Version_2015_04_05 ? ipAndProtocolSnippet() : String.Empty, sasVersion.ToVersionString()); if (sasVersion < StorageServiceVersions.Version_2012_02_12) { stringToSignFactory = baseStringToSign; } else if (sasVersion == StorageServiceVersions.Version_2012_02_12) { stringToSignFactory = v2012_02_12StringToSign; } else { stringToSignFactory = () => String.Format("{0}\n{1}\n{2}\n{3}\n{4}\n{5}", v2012_02_12StringToSign(), queryParams.Value<string>(ParamCacheControl), queryParams.Value<string>(ParamContentDisposition), queryParams.Value<string>(ParamContentEncoding), queryParams.Value<string>(ParamContentLang), queryParams.Value<string>(ParamContentType)); } } else { stringToSignFactory = () => String.Format("{0}\n{1}\n{2}\n{3}\n{4}\n{5}\n{6}\n{7}\n{8}\n", SharedKeySignature.AccountName, queryParams.Value<string>(ParamPermissions), queryParams.Value<string>(ParamServices), queryParams.Value<string>(ParamResourceTypeEx), queryParams.Value<string>(ParamStartTime), queryParams.Value<string>(ParamExpiryTime), queryParams.Value<string>(ParamSourceIP), queryParams.Value<string>(ParamProtocol), sasVersion.ToVersionString()); } string signature = queryParams.Value<string>(ParamSignature); var usingPrimaryKey = new[] { true, false }; string stringToSign = stringToSignFactory(); int matchIndex = Array.FindIndex(usingPrimaryKey, usePrimaryKey => VerifySignature(signature, usePrimaryKey, stringToSign)); if (matchIndex != -1) { // We can't sign the redirection response when the request uses a SAS key - preserve the matching key, however request.AuthenticationScheme = String.Empty; request.AuthenticationKey = usingPrimaryKey[matchIndex] ? SharedKeySignature.PrimaryAccountKey : SharedKeySignature.SecondaryAccountKey; return true; } return false; }
static string GetStringToSign(bool liteAlgorithm, string method, string uriPath, RequestHeaders headers, RequestQueryParameters queryParams, string requestDate, string contentLength) { // Signature scheme is described at: http://msdn.microsoft.com/en-us/library/azure/dd179428.aspx // and the SDK implementation is at: https://github.com/Azure/azure-storage-net/tree/master/Lib/ClassLibraryCommon/Auth/Protocol if (liteAlgorithm) { return String.Format("{0}\n{1}\n{2}\n{3}\n{4}\n{5}", method, headers.Value("Content-MD5", String.Empty), headers.Value("Content-Type", String.Empty), requestDate, SharedKeySignature.GetCanonicalizedHeaders(headers), GetCanonicalizedResource(liteAlgorithm, uriPath, queryParams, SharedKeySignature.AccountName)); } else { return String.Format("{0}\n{1}\n{2}\n{3}\n{4}\n{5}\n{6}\n{7}\n{8}\n{9}\n{10}\n{11}\n{12}\n{13}", method, headers.Value("Content-Encoding", String.Empty), headers.Value("Content-Language", String.Empty), contentLength, headers.Value("Content-MD5", String.Empty), headers.Value("Content-Type", String.Empty), requestDate, headers.Value("If-Modified-Since", String.Empty), headers.Value("If-Match", String.Empty), headers.Value("If-None-Match", String.Empty), headers.Value("If-Unmodified-Since", String.Empty), headers.Value("Range", String.Empty), SharedKeySignature.GetCanonicalizedHeaders(headers), GetCanonicalizedResource(liteAlgorithm, uriPath, queryParams, SharedKeySignature.AccountName)); } }