├── CONTRIBUTING.md ├── GetSasToken-Net ├── function.json └── run.csx ├── LICENSE ├── README.md ├── azuredeploy.json └── deploy.cmd /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Azure samples 2 | 3 | Thank you for your interest in contributing to Azure samples! 4 | 5 | ## Ways to contribute 6 | 7 | You can contribute to [Azure samples](https://azure.microsoft.com/documentation/samples/) in a few different ways: 8 | 9 | - Submit feedback on [this sample page](https://azure.microsoft.com/documentation/samples/functions-dotnet-sas-token/) whether it was helpful or not. 10 | - Submit issues through [issue tracker](https://github.com/Azure-Samples/functions-dotnet-sas-token/issues) on GitHub. We are actively monitoring the issues and improving our samples. 11 | - If you wish to make code changes to samples, or contribute something new, please follow the [GitHub Forks / Pull requests model](https://help.github.com/articles/fork-a-repo/): Fork the sample repo, make the change and propose it back by submitting a pull request. -------------------------------------------------------------------------------- /GetSasToken-Net/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "disabled": false, 3 | "bindings": [ 4 | { 5 | "authLevel": "function", 6 | "name": "req", 7 | "type": "httpTrigger", 8 | "direction": "in", 9 | "methods": [ "post" ] 10 | }, 11 | { 12 | "name": "res", 13 | "type": "http", 14 | "direction": "out" 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /GetSasToken-Net/run.csx: -------------------------------------------------------------------------------- 1 | // An HTTP trigger Azure Function that returns a SAS token for Azure Storage for the specified container. 2 | // You can also optionally specify a particular blob name and access permissions. 3 | // To learn more, see https://github.com/Azure-Samples/functions-dotnet-sas-token/blob/master/README.md 4 | 5 | #r "Microsoft.WindowsAzure.Storage" 6 | 7 | using System.Net; 8 | using System.Configuration; 9 | using Microsoft.WindowsAzure.Storage; 10 | using Microsoft.WindowsAzure.Storage.Blob; 11 | 12 | // Request body format: 13 | // - `container` - *required*. Name of container in storage account 14 | // - `blobName` - *optional*. Used to scope permissions to a particular blob 15 | // - `permissions` - *optional*. Default value is read permissions. The format matches the enum values of SharedAccessBlobPermissions. 16 | // Possible values are "Read", "Write", "Delete", "List", "Add", "Create". Comma-separate multiple permissions, such as "Read, Write, Create". 17 | 18 | public static async Task Run(HttpRequestMessage req, TraceWriter log) 19 | { 20 | dynamic data = await req.Content.ReadAsAsync(); 21 | 22 | if (data.container == null) { 23 | return req.CreateResponse(HttpStatusCode.BadRequest, new { 24 | error = "Specify value for 'container'" 25 | }); 26 | } 27 | 28 | var permissions = SharedAccessBlobPermissions.Read; // default to read permissions 29 | bool success = Enum.TryParse(data.permissions.ToString(), out permissions); 30 | 31 | if (!success) { 32 | return req.CreateResponse(HttpStatusCode.BadRequest, new { 33 | error = "Invalid value for 'permissions'" 34 | }); 35 | } 36 | 37 | var storageAccount = CloudStorageAccount.Parse(ConfigurationManager.AppSettings["AzureWebJobsStorage"]); 38 | var blobClient = storageAccount.CreateCloudBlobClient(); 39 | var container = blobClient.GetContainerReference(data.container.ToString()); 40 | 41 | var sasToken = 42 | data.blobName != null ? 43 | GetBlobSasToken(container, data.blobName.ToString(), permissions) : 44 | GetContainerSasToken(container, permissions); 45 | 46 | return req.CreateResponse(HttpStatusCode.OK, new { 47 | token = sasToken, 48 | uri = container.Uri + sasToken 49 | }); 50 | } 51 | 52 | public static string GetBlobSasToken(CloudBlobContainer container, string blobName, SharedAccessBlobPermissions permissions, string policyName = null) 53 | { 54 | string sasBlobToken; 55 | 56 | // Get a reference to a blob within the container. 57 | // Note that the blob may not exist yet, but a SAS can still be created for it. 58 | CloudBlockBlob blob = container.GetBlockBlobReference(blobName); 59 | 60 | if (policyName == null) { 61 | var adHocSas = CreateAdHocSasPolicy(permissions); 62 | 63 | // Generate the shared access signature on the blob, setting the constraints directly on the signature. 64 | sasBlobToken = blob.GetSharedAccessSignature(adHocSas); 65 | } 66 | else { 67 | // Generate the shared access signature on the blob. In this case, all of the constraints for the 68 | // shared access signature are specified on the container's stored access policy. 69 | sasBlobToken = blob.GetSharedAccessSignature(null, policyName); 70 | } 71 | 72 | return sasBlobToken; 73 | } 74 | 75 | public static string GetContainerSasToken(CloudBlobContainer container, SharedAccessBlobPermissions permissions, string storedPolicyName = null) 76 | { 77 | string sasContainerToken; 78 | 79 | // If no stored policy is specified, create a new access policy and define its constraints. 80 | if (storedPolicyName == null) { 81 | var adHocSas = CreateAdHocSasPolicy(permissions); 82 | 83 | // Generate the shared access signature on the container, setting the constraints directly on the signature. 84 | sasContainerToken = container.GetSharedAccessSignature(adHocSas, null); 85 | } 86 | else { 87 | // Generate the shared access signature on the container. In this case, all of the constraints for the 88 | // shared access signature are specified on the stored access policy, which is provided by name. 89 | // It is also possible to specify some constraints on an ad-hoc SAS and others on the stored access policy. 90 | // However, a constraint must be specified on one or the other; it cannot be specified on both. 91 | sasContainerToken = container.GetSharedAccessSignature(null, storedPolicyName); 92 | } 93 | 94 | return sasContainerToken; 95 | } 96 | 97 | private static SharedAccessBlobPolicy CreateAdHocSasPolicy(SharedAccessBlobPermissions permissions) 98 | { 99 | // Create a new access policy and define its constraints. 100 | // Note that the SharedAccessBlobPolicy class is used both to define the parameters of an ad-hoc SAS, and 101 | // to construct a shared access policy that is saved to the container's shared access policies. 102 | 103 | return new SharedAccessBlobPolicy() { 104 | // Set start time to five minutes before now to avoid clock skew. 105 | SharedAccessStartTime = DateTime.UtcNow.AddMinutes(-5), 106 | SharedAccessExpiryTime = DateTime.UtcNow.AddHours(1), 107 | Permissions = permissions 108 | }; 109 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Microsoft Corporation 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | services: app-service, functions 3 | platforms: dotnet 4 | author: lindydonna 5 | --- 6 | 7 | # C# Azure Function for generating SAS tokens 8 | 9 | This is a sample HTTP trigger Azure Function that returns a SAS token for Azure Storage for the specified container, blob, and permissions. A SAS token provides a secure way for client apps to access particular storage account resources, without giving them the full control of the storage access key. 10 | 11 | ##Deploy to Azure 12 | 13 | The automated deployment provisions an Azure Storage account and an Azure Function in a Dynamic compute plan and sets up deployment from source control. 14 | 15 | The deployment template has a parameter `manualIntegration` which controls whether or not a deployment trigger is registered with GitHub. Use `true` if you are deploying from the main Azure-Samples repo (does not register hook), `false` otherwise (registers hook). Since a value of `false` registers the deployment hook with GitHub, deployment will fail if you don't have write permissions to the repo. 16 | 17 | ## How it works 18 | 19 | When you create a storage account, you get two storage access keys, which provide full control over the storage account contents. Since these keys are admin credentials, they should **never** be distributed with a client app. 20 | 21 | Instead, clients should use a shared access signature (SAS) for delegated access to storage resources. A SAS token, which is appended to a storage resource URI, provides access to only a particular resource for a limited period of time. A SAS token can be scoped to a blob or a container and specifies access permissions (such as read or write). 22 | 23 | A SAS token is usually generated server-side, using the account access key and the Azure Storage SDK. This sample shows how to use an Azure Function as a SAS token service. Web and mobile clients can call this function to request access to a particular container or blob. By default, the sample creates a token that expires after an hour, but this can be customized. 24 | 25 | If the function will be called from a mobile client or a JavaScript web app, we recommend that you add authentication to your Function using [App Service Authentication/Authorization](https://azure.microsoft.com/en-us/documentation/articles/app-service-authentication-overview/). The API key is usually insufficent for security purposes since it can be discovered by sniffing traffic or decompiling the client app. 26 | 27 | ##Calling the function 28 | 29 | To request a SAS token, send an HTTP POST to your function URI, including the API key if you've specified one. The request body format is: 30 | 31 | - `container` - *required*. Name of container in storage account 32 | - `blobName` - *optional*. Used to scope permissions to a particular blob 33 | - `permissions` - *optional*. Default value is read permissions. The format matches the enum values of SharedAccessBlobPermissions. Possible values are "Read", "Write", "Delete", "List", "Add", "Create". Comma-separate multiple permissions, such as "Read, Write, Create". 34 | 35 | Response: 36 | 37 | - `token` - SAS token, including a leading "?" 38 | - `uri` - Resource URI with token appended as query string 39 | 40 | ## Learn more 41 | 42 | - [Authentication and authorization in Azure App Service](https://azure.microsoft.com/en-us/documentation/articles/app-service-authentication-overview/) 43 | - [Shared Access Signatures: Understanding the SAS Model](https://azure.microsoft.com/documentation/articles/storage-dotnet-shared-access-signature-part-1/) 44 | - [Create and use a SAS with Blob storage](https://azure.microsoft.com/documentation/articles/storage-dotnet-shared-access-signature-part-2/) 45 | - [Delegating Access with a Shared Access Signature](https://msdn.microsoft.com/library/ee395415.aspx) 46 | - [Establishing a Stored Access Policy](https://msdn.microsoft.com/library/dn140257.aspx) 47 | -------------------------------------------------------------------------------- /azuredeploy.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://schemas.management.azure.com/schemas/2015-01-01-preview/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "appName": { 6 | "type": "string", 7 | "metadata": { 8 | "description": "The name of the function app that you wish to create." 9 | } 10 | }, 11 | "storageAccountType": { 12 | "type": "string", 13 | "defaultValue": "Standard_LRS", 14 | "allowedValues": [ 15 | "Standard_LRS", 16 | "Standard_GRS", 17 | "Standard_ZRS", 18 | "Premium_LRS" 19 | ], 20 | "metadata": { 21 | "description": "Storage Account type" 22 | } 23 | }, 24 | "repoURL": { 25 | "type": "string", 26 | "defaultValue": "https://github.com/Azure-Samples/functions-dotnet-sas-token.git", 27 | "metadata": { 28 | "description": "Repo URL" 29 | } 30 | }, 31 | "branch": { 32 | "type": "string", 33 | "defaultValue": "master", 34 | "metadata": { 35 | "description": "Repo branch" 36 | } 37 | }, 38 | "manualIntegration": { 39 | "type": "bool", 40 | "defaultValue": true, 41 | "metadata": { 42 | "description": "Use 'true' if you are deploying from the base repo, 'false' if you are deploying from your own fork. If you're using 'false', make sure you have admin permissions to the repo." 43 | } 44 | } 45 | }, 46 | "variables": { 47 | "functionAppName": "[parameters('appName')]", 48 | "hostingPlanName": "[parameters('appName')]", 49 | "storageAccountName": "[concat(uniquestring(resourceGroup().id), 'azfunctions')]" 50 | }, 51 | "resources": [ 52 | { 53 | "type": "Microsoft.Storage/storageAccounts", 54 | "name": "[variables('storageAccountName')]", 55 | "apiVersion": "2015-06-15", 56 | "location": "[resourceGroup().location]", 57 | "properties": { 58 | "accountType": "[parameters('storageAccountType')]" 59 | } 60 | }, 61 | { 62 | "type": "Microsoft.Web/serverfarms", 63 | "apiVersion": "2015-04-01", 64 | "name": "[variables('hostingPlanName')]", 65 | "location": "[resourceGroup().location]", 66 | "properties": { 67 | "name": "[variables('hostingPlanName')]", 68 | "computeMode": "Dynamic", 69 | "sku": "Dynamic" 70 | } 71 | }, 72 | { 73 | "apiVersion": "2015-08-01", 74 | "type": "Microsoft.Web/sites", 75 | "name": "[variables('functionAppName')]", 76 | "location": "[resourceGroup().location]", 77 | "kind": "functionapp", 78 | "properties": { 79 | "name": "[variables('functionAppName')]", 80 | "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]" 81 | }, 82 | "dependsOn": [ 83 | "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]", 84 | "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]" 85 | ], 86 | "resources": [ 87 | { 88 | "apiVersion": "2016-03-01", 89 | "name": "appsettings", 90 | "type": "config", 91 | "dependsOn": [ 92 | "[resourceId('Microsoft.Web/sites', variables('functionAppName'))]", 93 | "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]" 94 | ], 95 | "properties": { 96 | "AzureWebJobsStorage": "[concat('DefaultEndpointsProtocol=https;AccountName=',variables('storageAccountName'),';AccountKey=',listkeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2015-05-01-preview').key1,';')]", 97 | "AzureWebJobsDashboard": "[concat('DefaultEndpointsProtocol=https;AccountName=',variables('storageAccountName'),';AccountKey=',listkeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2015-05-01-preview').key1,';')]", 98 | "FUNCTIONS_EXTENSION_VERSION": "latest", 99 | "command": "deploy.cmd" 100 | } 101 | }, 102 | { 103 | "apiVersion": "2015-08-01", 104 | "name": "web", 105 | "type": "sourcecontrols", 106 | "dependsOn": [ 107 | "[resourceId('Microsoft.Web/sites/', parameters('appName'))]", 108 | "[resourceId('Microsoft.Web/Sites/config', parameters('appName'), 'appsettings')]" 109 | ], 110 | "properties": { 111 | "RepoUrl": "[parameters('repoUrl')]", 112 | "branch": "[parameters('branch')]", 113 | "IsManualIntegration": "[parameters('manualIntegration')]" 114 | } 115 | } 116 | ] 117 | } 118 | ] 119 | } 120 | -------------------------------------------------------------------------------- /deploy.cmd: -------------------------------------------------------------------------------- 1 | @if "%SCM_TRACE_LEVEL%" NEQ "4" @echo off 2 | 3 | :: ---------------------- 4 | :: KUDU Deployment Script with Functions Package restore 5 | :: Version: 1.0.6 6 | :: ---------------------- 7 | 8 | :: Prerequisites 9 | :: ------------- 10 | 11 | :: Verify node.js installed 12 | where node 2>nul >nul 13 | IF %ERRORLEVEL% NEQ 0 ( 14 | echo Missing node.js executable, please install node.js, if already installed make sure it can be reached from current environment. 15 | goto error 16 | ) 17 | 18 | :: Setup 19 | :: ----- 20 | 21 | setlocal enabledelayedexpansion 22 | 23 | SET ARTIFACTS=%~dp0%..\artifacts 24 | 25 | IF NOT DEFINED DEPLOYMENT_SOURCE ( 26 | SET DEPLOYMENT_SOURCE=%~dp0%. 27 | ) 28 | 29 | IF NOT DEFINED DEPLOYMENT_TARGET ( 30 | SET DEPLOYMENT_TARGET=%ARTIFACTS%\wwwroot 31 | ) 32 | 33 | IF NOT DEFINED NEXT_MANIFEST_PATH ( 34 | SET NEXT_MANIFEST_PATH=%ARTIFACTS%\manifest 35 | 36 | IF NOT DEFINED PREVIOUS_MANIFEST_PATH ( 37 | SET PREVIOUS_MANIFEST_PATH=%ARTIFACTS%\manifest 38 | ) 39 | ) 40 | 41 | IF NOT DEFINED KUDU_SYNC_CMD ( 42 | :: Install kudu sync 43 | echo Installing Kudu Sync 44 | call npm install kudusync -g --silent 45 | IF !ERRORLEVEL! NEQ 0 goto error 46 | 47 | :: Locally just running "kuduSync" would also work 48 | SET KUDU_SYNC_CMD=%appdata%\npm\kuduSync.cmd 49 | ) 50 | 51 | :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 52 | :: Deployment 53 | :: ---------- 54 | 55 | :: NuGet package restore 56 | echo "Restoring function packages" 57 | 58 | FOR /F %%d in ('DIR "Project.json" /S /B') DO ( 59 | call nuget restore %%d -PackagesDirectory %home%\data\Functions\packages\nuget 60 | ) 61 | 62 | echo Handling Basic Web Site deployment. 63 | 64 | :: KuduSync 65 | IF /I "%IN_PLACE_DEPLOYMENT%" NEQ "1" ( 66 | call :ExecuteCmd "%KUDU_SYNC_CMD%" -v 50 -f "%DEPLOYMENT_SOURCE%" -t "%DEPLOYMENT_TARGET%" -n "%NEXT_MANIFEST_PATH%" -p "%PREVIOUS_MANIFEST_PATH%" -i ".git;.hg;.deployment;deploy.cmd;Project.json" 67 | IF !ERRORLEVEL! NEQ 0 goto error 68 | ) 69 | 70 | :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 71 | goto end 72 | 73 | :: Execute command routine that will echo out when error 74 | :ExecuteCmd 75 | setlocal 76 | set _CMD_=%* 77 | call %_CMD_% 78 | if "%ERRORLEVEL%" NEQ "0" echo Failed exitCode=%ERRORLEVEL%, command=%_CMD_% 79 | exit /b %ERRORLEVEL% 80 | 81 | :error 82 | endlocal 83 | echo An error has occurred during web site deployment. 84 | call :exitSetErrorLevel 85 | call :exitFromFunction 2>nul 86 | 87 | :exitSetErrorLevel 88 | exit /b 1 89 | 90 | :exitFromFunction 91 | () 92 | 93 | :end 94 | endlocal 95 | echo Finished successfully. 96 | --------------------------------------------------------------------------------