Constructing an Account SAS (Shared Access Signature) - azure

I am trying to generate Account SAS token:
MSDN DOC
When I am trying to use generated token, I get following:
AuthenticationFailed
Server failed to authenticate the request. Make sure the value of Authorization header is formed correctly including the signature.
RequestId:89959111-0001-00c8-24d1-e0515b000000
Time:2016-07-18T08:49:00.8383767Z
Signature did not match. String to sign used was [accountName]
rl
b
sc
2017-01-01
2015-04-05
Here are my code:
var signedVersion = "2015-04-05";
var signedServices = "b";
var signedResourceTypes = "sc";
var signedPermission = "rl";
var signedExpiry = "2017-01-01";
var stringToSign =
accountName + "\n" +
signedPermission + "\n" +
signedServices + "\n" +
signedResourceTypes + "\n" +
signedExpiry + "\n" +
signedVersion + "\n"
;
var keyBytes = Encoding.UTF8.GetBytes(accountKey);
byte[] hash;
using (var mac = new HMACSHA256(keyBytes))
{
var stringToSignBytes = Encoding.UTF8.GetBytes(stringToSign);
hash = mac.ComputeHash(stringToSignBytes);
}
var str = Convert.ToBase64String(hash);
var sig = HttpUtility.UrlEncode(str);
var url = $"https://{accountName}.blob.core.windows.net/?comp=list&sv={signedVersion}&ss={signedServices}&srt={signedResourceTypes}&sp={signedPermission}&se={signedExpiry}&sig={sig}";
What am I doing wrong?

I noticed a few issues with the code:
First, to convert account key into byte array you would need to use Convert.FromBase64String(accountKey) instead of Encoding.UTF8.GetBytes(accountKey);.
Next, even if you're not using start time, signed protocol and signed IP addresses, you would need to include them in your stringToSign.
Once you do these things, the code should work. Based on these, I have included the modified code below. I tested it for listing containers in my storage account and it works.
static void AccountSas()
{
var signedVersion = "2015-04-05";
var signedServices = "b";
var signedResourceTypes = "sc";
var signedPermission = "rl";
var signedExpiry = "2017-01-01";
var signedStart = "";
var signedIP = "";
var signedProtocol = "";
var stringToSign =
accountName + "\n" +
signedPermission + "\n" +
signedServices + "\n" +
signedResourceTypes + "\n" +
signedStart + "\n" +
signedExpiry + "\n" +
signedIP + "\n" +
signedProtocol + "\n" +
signedVersion + "\n"
;
var keyBytes = Convert.FromBase64String(accountKey);
byte[] hash;
using (var mac = new HMACSHA256(keyBytes))
{
var stringToSignBytes = Encoding.UTF8.GetBytes(stringToSign);
hash = mac.ComputeHash(stringToSignBytes);
}
var str = Convert.ToBase64String(hash);
var sig = HttpUtility.UrlEncode(str);
var url = string.Format("https://{0}.blob.core.windows.net/?comp=list&sv={1}&ss={2}&srt={3}&sp={4}&se={5}&sig={6}", accountName, signedVersion, signedServices, signedResourceTypes, signedPermission, signedExpiry, sig);
}

Related

Call Create File on Azure File Service REST Api

I am trying to create a file in Azure File Service:
https://learn.microsoft.com/en-us/rest/api/storageservices/create-file
I've seen and read multiple threads about this issue, but I can't figure out what might be the problem in my case... Most likely I am missing some small thing in the request which I cannot find.
The error I am getting: The MAC signature found in the HTTP request (...) is not the same as any computed signature.
The signature string I've used:
PUT
\n
\n
\n
\n
text/plain // Content-Type
\n
\n
\n
\n
\n
\n
x-ms-content-length:1000
x-ms-date:Tue, 20 Apr 2021 19:23:30 GMT
x-ms-type:file
x-ms-version:2015-02-21
/account/share/test
Authorization header:
SharedKey account:XXXXXXXXXX
I've used the HMACSHA256 for hashing the authorization header and signature string, it all works when I use other endpoint (List Shares: https://learn.microsoft.com/en-us/rest/api/storageservices/list-shares), but I can't make it work for the file. I've seen this topic AZURE File Service - Upload PDF through REST API and I believe I am using very similar request, but with no success...
I appreciate any help : )
Edit:
I am not sure if I correctly set the content headers. For example, does the x-ms-content-length should be placed in the CanonicalizedHeaders string?
Edit2:
With regard to what Ivan Yang wrote, I made the code to work, but only when my CanonicalizedHeaders are built like that:
CanonicalizedHeaders := 'x-ms-content-length:1200'
+ LF + 'x-ms-date:' + UTCDateTimeText
+ LF + 'x-ms-file-attributes:Archive' + LF + 'x-ms-file-creation-time:Now' + LF + 'x-ms-file-last-write-time:Now' + LF + 'x-ms-file-permission:Inherit'
+ LF + 'x-ms-type:file' + LF + 'x-ms-version:2019-02-02';
If I have them in different order, then it crashes:
CanonicalizedHeaders := 'x-ms-date:' + UTCDateTimeText + LF +
'x-ms-content-length:1200' + LF +
'x-ms-version:2019-02-02' + LF +
'x-ms-file-attributes:Archive' + LF +
'x-ms-file-creation-time:Now' + LF +
'x-ms-file-last-write-time:Now' + LF +
'x-ms-file-permission:Inherit' + LF +
'x-ms-type:file';
How come does this make a difference?
Your signature string is incorrect(you're missing some "\n"). I'm using the x-ms-version:2019-02-02 instead of the older one x-ms-version:2015-02-21, and the correct signature string should look like this one:
"PUT\n"
+ "\n" // content encoding
+ "\n" // content language
+ "\n" // content length
+ "\n" // content md5
+ content_type + "\n" // content type
+ "\n" // date
+ "\n" // if modified since
+ "\n" // if match
+ "\n" // if none match
+ "\n" // if unmodified since
+ "\n" // range
+ "x-ms-content-length:" + content_length
+ "\nx-ms-date:" + dt.ToString("R")
+ "\nx-ms-file-attributes:Archive" + "\nx-ms-file-creation-time:Now" + "\nx-ms-file-last-write-time:Now" + "\nx-ms-file-permission:Inherit"
+ "\nx-ms-type:file" + "\nx-ms-version:" + apiversion + "\n" // headers
+ "/{0}/{1}/{2}", Account, FileShare, FileName);
Here is the c# code using create file api:
using System;
using System.Globalization;
using System.Net;
using System.Security.Cryptography;
namespace ConsoleApp25
{
class Program
{
static void Main(string[] args)
{
string Account = "storage_account_name";
string Key = "storage_account_key";
string FileShare = "file_share_name";
string FileName = "test555.txt";
string apiversion = "2019-02-02";
int content_length = 1200;
string content_type = "text/plain";
DateTime dt = DateTime.UtcNow;
string StringToSign = String.Format("PUT\n"
+ "\n" // content encoding
+ "\n" // content language
+ "\n" // content length
+ "\n" // content md5
+ content_type + "\n" // content type
+ "\n" // date
+ "\n" // if modified since
+ "\n" // if match
+ "\n" // if none match
+ "\n" // if unmodified since
+ "\n" // range
+ "x-ms-content-length:" + content_length
+ "\nx-ms-date:" + dt.ToString("R")
+ "\nx-ms-file-attributes:Archive" + "\nx-ms-file-creation-time:Now" + "\nx-ms-file-last-write-time:Now" + "\nx-ms-file-permission:Inherit"
+ "\nx-ms-type:file" + "\nx-ms-version:" + apiversion + "\n" // headers
+ "/{0}/{1}/{2}", Account, FileShare, FileName);
string auth = SignThis(StringToSign, Key, Account);
string method = "PUT";
string urlPath = string.Format("https://{0}.file.core.windows.net/{1}/{2}", Account, FileShare, FileName);
Uri uri = new Uri(urlPath);
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(uri);
request.Method = method;
request.ContentLength = 0;
request.Headers.Add("x-ms-content-length", $"{content_length}");
request.Headers.Add("Content-Type", content_type);
request.Headers.Add("x-ms-type", "file");
request.Headers.Add("x-ms-date", dt.ToString("R"));
request.Headers.Add("x-ms-version", apiversion);
request.Headers.Add("x-ms-file-attributes", "Archive"); //note it is case-sensitive.
request.Headers.Add("x-ms-file-permission", "Inherit");
request.Headers.Add("x-ms-file-creation-time", "Now");
request.Headers.Add("x-ms-file-last-write-time", "Now");
request.Headers.Add("Authorization", auth);
using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
{
//read the response code
Console.WriteLine("the response is:" + response.StatusCode);
}
Console.WriteLine("**completed**");
Console.ReadLine();
}
private static String SignThis(String StringToSign, string Key, string Account)
{
String signature = string.Empty;
byte[] unicodeKey = Convert.FromBase64String(Key);
using (HMACSHA256 hmacSha256 = new HMACSHA256(unicodeKey))
{
Byte[] dataToHmac = System.Text.Encoding.UTF8.GetBytes(StringToSign);
signature = Convert.ToBase64String(hmacSha256.ComputeHash(dataToHmac));
}
String authorizationHeader = String.Format(
CultureInfo.InvariantCulture,
"{0} {1}:{2}",
"SharedKey",
Account,
signature);
return authorizationHeader;
}
}
}

How to upload a binary file (PDF) to Azure using File Services

I'm trying to upload a binary file (PDF or Word) to Azure using the Services from C#. I'm restricted to using the REST API's and therefore I cannot use the SDK.
I've managed to create the file but which REST API should I use to upload the content. As far as I can see the PUT RANGE is only for text (not for binary content).
Which File Services REST API should i use? Or is the File Services API not meant for this and should i use the Blob API?
Best regards,
Michel
string endpoint_putrange = "https://" + ssStorageAccount + ".file.core.windows.net/" + ssShareNaam + "/" + ssBestandsNaam + "?comp=range";
Uri _endpoint_putrange = new Uri(endpoint_putrange);
HttpRequestMessage _requestMessage = new HttpRequestMessage(HttpMethod.Put, _endpoint_putrange);
HttpClient httpClient = new HttpClient();
httpClient.Timeout = new TimeSpan(20000 * 10000); // 20sec in ticks
httpClient.BaseAddress = _endpoint_putrange;
string RequestDateString = DateTime.UtcNow.ToString("R", CultureInfo.InvariantCulture);
// First set the header and construct the canonicalHeader string. See
// These must be in alpabetical order !!!!
string canonicalHeaders = string.Empty;
canonicalHeaders += setHeader(httpClient, "Content-Length", ssBestandsBinary.Length.ToString());
canonicalHeaders += setHeader(httpClient, "x-ms-date", RequestDateString);
canonicalHeaders += setHeader(httpClient, "x-ms-range", "bytes=0-"); // https://learn.microsoft.com/en-us/rest/api/storageservices/specifying-the-range-header-for-file-service-operations
canonicalHeaders += setHeader(httpClient, "x-ms-version", "2020-04-08"); // https://learn.microsoft.com/nl-nl/rest/api/storageservices/versioning-for-the-azure-storage-services
canonicalHeaders += setHeader(httpClient, "x-ms-write", "Update");
// Construct the canonicalResource string
string canonicalResource = "/" + ssStorageAccount + "/" + ssShareNaam + "/" + ssBestandsNaam;
// Construct the string to Sign
string stingToSign = "PUT\n" + /*HTTP Verb*/
"\n" + /*Content-Encoding*/
"\n" + /*Content-Language*/
ssBestandsBinary.Length.ToString() + "\n" + /*Content-Length (empty string when zero)*/
"\n" + /*Content-MD5*/
"\n" + /*Content-Type*/
"\n" + /*Date*/
"\n" + /*If-Modified-Since */
"\n" + /*If-Match*/
"\n" + /*If-None-Match*/
"\n" + /*If-Unmodified-Since*/
"bytes=0-" + "\n" + /*Range*/
canonicalHeaders + /*CanonicalizedHeaders*/
canonicalResource; /*CanonicalizedResource*/
// Sign it
HMACSHA256 hmac = new HMACSHA256();
byte[] dataToHmac = Encoding.UTF8.GetBytes(stingToSign);
string signature = Convert.ToBase64String(hmac.ComputeHash(dataToHmac));
// Add the authorization header
string authorizationHeader = ssStorageAccount + ":" + signature;
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("SharedKey", authorizationHeader);
// How to specify the binary content of the _requestmessage/httpClient
/// ?????????????????????????
// Do the (synchronous) call and collect the response
HttpResponseMessage httpResponse = httpClient.SendAsync(_requestMessage, HttpCompletionOption.ResponseContentRead, CancellationToken.None).GetAwaiter().GetResult();
..... etc etc
Please try by setting the Content property of your _requestMessage. I am assuming that variable ssBestandsBinary is a byte array so you can set the Content property as ByteArrayContent.
Your code could be something like (not tested though):
_requestMessage.Content = new ByteArrayContent(ssBestandsBinary);
HttpResponseMessage httpResponse = httpClient.SendAsync(_requestMessage, HttpCompletionOption.ResponseContentRead, CancellationToken.None).GetAwaiter().GetResult();

Generate Azure Blob SAS token with APEX

I want to generate SAS Token, as per the Azure Doc following is the example for BLOB. I am still getting error. Can you please identify what is the mistake I am doing.
StringToSign = signedpermissions + "\n" +
signedstart + "\n" +
signedexpiry + "\n" +
canonicalizedresource + "\n" +
signedidentifier + "\n" +
signedIP + "\n" +
signedProtocol + "\n" +
signedversion + "\n" +
rscc + "\n" +
rscd + "\n" +
rsce + "\n" +
rscl + "\n" +
rsct
URL = https://myaccount.blob.core.windows.net/mycontainer/test.txt
canonicalizedresource = "/blob/myaccount/mycontainer/test.txt
I am getting following error
<?xml version="1.0" encoding="utf-8"?>
<Error>
<Code>AuthenticationFailed</Code>
<Message>Server failed to authenticate the request. Make sure the value of Authorization header is formed correctly including the signature.
RequestId:4b1d1c59-c01e-0052-0197-ffe09f000000
Time:2020-03-21T15:44:34.2757923Z</Message>
<AuthenticationErrorDetail>Signature fields not well formed.</AuthenticationErrorDetail>
</Error>
Following is my code
public void getSasToken() {
this.storageName = 'zaindevtesting';
this.storageContainer = 'zaindevblob';
this.storageKey = 'xxxxxxZFQ==';
this.storageUrl ='https://zaindevtesting.blob.core.windows.net';
Datetime sasExpiry = Datetime.now();
sasExpiry.addMinutes(15);
Datetime sasStart = Datetime.now();
sasStart.addMinutes(-5);
string signedpermissions = 'rwdlac';
String signedstart = sasStart.formatGMT('YYYY-MM-dd\'T\'HH:mm:ss\'Z\'');
string signedexpiry = sasExpiry.formatGMT('YYYY-MM-dd\'T\'HH:mm:ss\'Z\'');
string signedservice = 'b';
String canonicalizedresource = '/blob/zaindevtesting/zaindevblob/test.txt';
string signedidentifier = '';
string signedIP = '';
string signedProtocol = 'https';
string signedversion = '2015-04-05';
string rscc='';
string rscd='';
string rsce='';
string rscl='';
string rsct='';
string stringToSign =signedpermissions + '\n' +
signedstart + '\n' +
signedexpiry + '\n' +
canonicalizedresource + '\n' +
signedidentifier + '\n' +
signedIP + '\n' +
signedProtocol + '\n' +
signedversion + '\n' +
rscc + '\n' +
rscd + '\n' +
rsce + '\n' +
rscl + '\n' +
rsct;
string signedExpiryEncode = EncodingUtil.urlEncode(signedexpiry, 'UTF-8');
string signedStartEncode = EncodingUtil.urlEncode(signedstart, 'UTF-8');
String sasToken = '';
Blob unicodeKey = EncodingUtil.base64Decode(storageKey);
Blob data = Crypto.generateMac('HMACSHA256', Blob.valueOf(stringToSign), unicodeKey);
sasToken = EncodingUtil.base64Encode(data);
String sasTokenString= '?sv=' + signedversion + '&se=' + signedexpiry +'&st='+signedstart+'&sr='+signedservice+'&sp=' + signedpermissions + '&sig=' + sasToken;
string sasURL = 'https://zaindevtesting.blob.core.windows.net/test.txt'+sasTokenString;
System.debug('sasURL--->'+sasURL);
System.debug(sasTokenString);
// return sasToken;
}
This code generate following SAS Link
https://zaindevtesting.blob.core.windows.net/test.txt??sv=2017-11-09&st=2020-03-21T15%3A44%3A07Z&se=2020-03-21T15%3A44%3A07Z&sr=b&sp=rwdc&sig=eJVbGWI4rcyjggOYAE308ilXEA/zAsFYbuNi24IZhX4=
Please try the following code:
public void generateSASToken()
{
string storageName = 'zaindevtesting';
string storageContainer = 'zaindevblob';
string storageKey = 'xxxx==';
string storageUrl ='https://zaindevtesting.blob.core.windows.net';
Datetime sasExpiry = Datetime.now();
sasExpiry = sasExpiry.addMinutes(15);
Datetime sasStart = Datetime.now();
sasStart = sasStart.addMinutes(-5);
string signedpermissions = 'r';
String signedstart = sasStart.formatGMT('YYYY-MM-dd\'T\'HH:mm:ss\'Z\'');
string signedexpiry = sasExpiry.formatGMT('YYYY-MM-dd\'T\'HH:mm:ss\'Z\'');
string signedservice = 'b';
String canonicalizedresource = '/blob/zaindevtesting/zaindevblob/test.txt';
string signedidentifier = '';
string signedIP = '';
string signedProtocol = '';
string signedversion = '2015-04-05';
string rscc='';
string rscd='';
string rsce='';
string rscl='';
string rsct='';
string stringToSign =signedpermissions + '\n' +
signedstart + '\n' +
signedexpiry + '\n' +
canonicalizedresource + '\n' +
signedidentifier + '\n' +
signedIP + '\n' +
signedProtocol + '\n' +
signedversion + '\n' +
rscc + '\n' +
rscd + '\n' +
rsce + '\n' +
rscl + '\n' +
rsct;
System.debug('stringToSign--->'+stringToSign);
string signedExpiryEncode = EncodingUtil.urlEncode(signedexpiry, 'UTF-8');
string signedStartEncode = EncodingUtil.urlEncode(signedstart, 'UTF-8');
String sasToken = '';
Blob unicodeKey = EncodingUtil.base64Decode(storageKey);
Blob data = Crypto.generateMac('HMACSHA256', Blob.valueOf(stringToSign), unicodeKey);
sasToken = EncodingUtil.base64Encode(data);
sasToken = EncodingUtil.urlEncode(sasToken, 'UTF-8');
String sasTokenString= '?sv=' + signedversion + '&se=' + signedexpiry +'&st='+signedstart+'&sr='+signedservice+'&sp=' + signedpermissions + '&sig=' + sasToken;
string sasURL = 'https://zaindevtesting.blob.core.windows.net/zaindevblob/test.txt'+sasTokenString;
System.debug('sasURL--->'+sasURL);
System.debug(sasTokenString);
// return sasToken;
}
I just tried it with your storage account key and I was able to download the blob.
Found the following issues with your code:
Permissions has to be in specific order. You're using string signedpermissions = 'rwdlac'; which is not the correct order. I just specified the read permission.
Signed Protocol was specified in stringToString but was not included in the SAS URL. I omitted that from both places.
SAS Token was not URL encoded.
You generated SAS Token for zaindevtesting.blob.core.windows.net/zaindevblob/test.txt URL but forgot to include the blob container name in the final URL.

How to escape characters in Azure Table queries?

I want to query for rows where a column named message starts with: metric="foo"
I tried encoding = and " with percentage and hex codes but it did not work.
Microsoft documentations says special characters must be encoded but does not tell how: https://learn.microsoft.com/en-us/rest/api/storageservices/querying-tables-and-entities#query-string-encoding
What should the query look like when value being compared contains special characters?
If you're using azure sdk, then the sdk already did the tricky for you.
In my test, I'm using the latest azure table storage sdk Microsoft.Azure.Cosmos.Table, version 1.0.4.
The test code:
static void Main(string[] args)
{
string connstr = "xxxx";
var storageAccount = CloudStorageAccount.Parse(connstr);
CloudTableClient tableClient = storageAccount.CreateCloudTableClient();
CloudTable table = tableClient.GetTableReference("myCustomer123");
TableQuery<CustomerEntity> query = new TableQuery<CustomerEntity>();
string myfilter = TableQuery.CombineFilters(TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, "ivan"),
TableOperators.And,
//for metric="foo", like below.
TableQuery.GenerateFilterCondition("PhoneNumber", QueryComparisons.Equal, "metric=\"foo\"")
);
query.FilterString = myfilter;
var items = table.ExecuteQuery(query);
foreach (var item in items)
{
Console.WriteLine(item.RowKey);
Console.WriteLine(item.PhoneNumber);
}
Console.WriteLine("*****end******");
Console.ReadLine();
}
Test result:
If you want to use the parameter to filter results, you can use ?$filter=<your parameter>%20eq%20'<vaule>'. For example
var date = DateTime.Now.ToUniversalTime().AddYears(1).ToString("R");
var CanonicalizedResource = "/" + StorageAccountName + "/people";
var StringToSign = date + "\n" + CanonicalizedResource;
// List the containers in a storage account.
// ListContainersAsyncREST(StorageAccountName, StorageAccountKey, CancellationToken.None).GetAwaiter().GetResult();
var hmacsha = new HMACSHA256();
hmacsha.Key = Convert.FromBase64String(StorageAccountKey);
var sig= hmacsha.ComputeHash(UTF8Encoding.UTF8.GetBytes(StringToSign));
var sig1 = Convert.ToBase64String(sig);
Console.WriteLine(sig1);
String uri = "https://jimtestperfdiag516.table.core.windows.net/people" + "?$filter=PartitionKey%20eq%20'Jim'";
HttpClient client = new HttpClient();
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, uri);
httpRequestMessage.Headers.Add("x-ms-date", date);
var str = "SharedKeyLite " + StorageAccountName + ":" + sig1;
httpRequestMessage.Headers.TryAddWithoutValidation("Authorization", str);
httpRequestMessage.Headers.Add("x-ms-version", "2017-04-17");
httpRequestMessage.Headers.Add("Accept", "application/json;odata=fullmetadata");
var results = client.SendAsync(httpRequestMessage).Result;
var response = results.Content.ReadAsStringAsync().Result;
var objs = JsonConvert.DeserializeObject(response);
Console.WriteLine(objs);

Add-in persistent settings in OSX/Mac

I am having trouble finding a way to store persistent settings for an office.js add-in on Mac.
On windows localStorage works perfect as it saves settings that persist through closing and opening Word.
On Mac localStorage does not persist closing and opening Word, not even through a refresh or closing and opening of the add-in.
Here is a simple code sample:
var settingString = 'mySetting';
var oldValue = localStorage.getItem(settingString);
write('oldValue: "' + oldValue + '"');
var d = new Date();
var newValue = d.getHours() + ':' + d.getMinutes() + ':' + d.getSeconds();
localStorage.setItem(settingString, newValue);
write('newValue: "' + newValue + '"');
iOS currently has a bug that's preventing us from fixing this localStorage issue yet. In the meantime, you have two potential workarounds:
Cookies
If you want the setting to be persisted across documents, use JavaScript cookies (w3schools doc) until the bug is fixed:
var settingString = 'mySetting';
var oldValue;
var myCookies = document.cookie.split(';');
for(var i = 0;i < myCookies.length;i++){
var myCookie = myCookies[i].trim();
if(myCookie.indexOf(settingString + "=") == 0){
oldValue = myCookie.substring(settingString + 1,myCookie.length);
}
}
write('oldValue: "' + oldValue + '"');
var d = new Date();
var newValue = d.getHours() + ':' + d.getMinutes() + ':' + d.getSeconds();
var expiry = d.setTime(d.getTime() + (14 * 86400000)); // # milliseconds in a day
document.cookie = settingString + "=" + newValue + "; expires=" + expiry.toGMTString();
write('newValue: "' + newValue + '"');
Settings
If it's sufficient for you to persist the value only in the current document, you can use the Office Settings API (Office.js Settings object doc):
var settingString = 'mySetting';
var oldValue = Office.context.Settings.get(settingString);
write('oldValue: "' + oldValue + '"');
var d = new Date();
var newValue = d.getHours() + ':' + d.getMinutes() + ':' + d.getSeconds();
Office.context.Settings.set(settingString, newValue);
Office.context.Settings.saveAsync(function(asyncResult){
write('newValue: "' + newValue + '"');
});
-Michael Saunders, program manager for Office add-ins

Resources