Mixing Mobile Friendly Forms based auth with NTLM Windows auth in SharePoint

One of the complaints I hear often is when users on mobile devices, mainly iOS, attempt to login to SharePoint.  Typically they are presented with a popup box asking for their username and password but there isn’t an option to save the password.  This is because our SharePoint web applications are using NTLM windows authentication because this is the most compatible with desktop PCs and provides the most functionality in SharePoint.  Apple is decidedly “nerfing” their safari browser to force more apps to be sold which makes them more money.  Part of this is them not providing an option to save passwords for NTLM authenticated sites and thus requiring apps to be made to support browsing SharePoint.

I’ve been working on this problem for a while.  Because of BI and some other features we couldn’t move away from Windows authentication for SharePoint.  So last year a coworker and I worked on creating our own browser app in iOS which would save user’s credentials and allow access to our SharePoint sites.  The problem with this approach is now we are developing a commodity, a web browser, which could end up taking a lot of time and effort for not much benefit.  After a long year last year working on many projects I got some breathing room at the beginning of this year and my brain decided to work on this problem and I came up with an idea.

Since I know SharePoint takes a windows principal and converts it into a windows claim, what if I could inject myself right BEFORE it converts the principal into a claim and replace the principal with a user of my choice.  I reflectored the code in Microsoft.SharePoint.IdentityModel.SPWindowsClaimsAuthenticationHttpModule and found the right spot and after building my own httpmodule and configuring it to run before SharePoint’s (see web.config example below), I was able to change the currently logged in user to be one of my choice.  This was great because this meant that I could potentially create a forms based experience for mobile devices and then tell SharePoint to use that user instead of thinking we weren’t authenticated.  The reason forms based authentication is needed is because iOS supports remembering passwords for forms based authentication.

SharePoint web.config

<system.webServer>
...
 <modules runAllManagedModulesForAllRequests="true">
 ...
 <add name="WindowsFormLogin" preCondition="integratedMode" type="SharePoint.HttpModules.WindowsFormLogin, SharePoint.HttpModules, Version=1.0.0.0, Culture=neutral, PublicKeyToken=***********" />
 <add name="SPWindowsClaimsAuthentication" type="Microsoft.SharePoint.IdentityModel.SPWindowsClaimsAuthenticationHttpModule, Microsoft.SharePoint.IdentityModel, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
 ...
 </modules>
...
</system.webServer>

Notice in the httpmodule below that I am checking to see if the useragent of the device is a mobile device on line 21.  Then I am checking if the status being returned is a 401 (unauthorized) on line 39.  Originally those were the only rules I had in place before I redirected to my forms login page.  But then I ran into some issues with the SharePlus app, which uses the same useragent as safari, trying to use webservices and requesting files and it didn’t understand our new authentication.  So i placed some other checks in there to determine whether it was a request from a browser or from an app.  Apps never have referrers set, so I check that first (line 59), and then I have a few other checks in place including looking at the querystring for web=1 (line 69) which is what SP uses to open files in the office web apps (office online).  So if i think this request is coming from a mobile device and it’s not an app, then I redirect to my special login page (line 85), otherwise I add some headers (line 90) which may not be needed but potentially apps such as office might be interested in.

HttpModule – PreSendRequestHeaders

public class WindowsFormLogin : IHttpModule
{
	public void Init(HttpApplication context)
	{
		if (context == null)
		{
			return;
		}
		context.AuthenticateRequest += context_AuthenticateRequest;
		context.PreSendRequestHeaders += context_PreSendRequestHeaders;
	}

	void context_PreSendRequestHeaders(object sender, EventArgs e)
	{
		HttpApplication httpApplication = sender as HttpApplication;
		HttpContext context = httpApplication.Context;

		if (context == null) return;
		if (context.Request == null) return;
		if (context.Request.Browser == null) return;
		if (false == context.Request.Browser.IsMobileDevice) return;
		

		if (context.Request.Cookies[Constants.MOBILE_LOGIN_EXCLUDE_COOKIE_NAME] != null)
		{
			string cookieValue = context.Request.Cookies[Constants.MOBILE_LOGIN_EXCLUDE_COOKIE_NAME].Value;
			bool exclude;
			if (false == bool.TryParse(cookieValue, out exclude))
			{
				exclude = false;
			}

			if (exclude) return;
		}

		string userAgent = context.Request.ServerVariables["HTTP_USER_AGENT"];
		if (userAgent.Contains("AppleWebKit") &&
		    false == userAgent.Contains("Safari")) return;

		int status = context.Response.StatusCode;
		switch (status)
		{
			case 401:
				if (IsPageRequest(context))
		                    {
		                        HandlePageRequest(context);
		                    }
		                    else
		                    {
		                        HandleNonPageRequest(context);
		                    }
				
				break;
		}
	}

private bool IsPageRequest(HttpContext context)
        {
            if (context.Request.Url == null) return false;
            if (string.IsNullOrEmpty(context.Request.Url.AbsolutePath)) return false;

            if (context.Request.UrlReferrer != null &&
                false == string.IsNullOrEmpty(context.Request.UrlReferrer.ToString()))
            {
                return true;
            }

            if (context.Request.Url.AbsolutePath.ToLower().EndsWith(".aspx")) return true;
            if (context.Request.Url.AbsolutePath.Contains("/_layouts/")) return true;
            if (context.Request.Url.AbsolutePath.Contains("/_")) return false;

            if (context.Request.QueryString["web"] != null &&
                context.Request.QueryString["web"] == "1")
            {
                return true;
            }

            try
            {
                if (SPContext.Current.Web.ServerRelativeUrl.ToLower() == context.Request.Url.AbsolutePath.ToLower()) return true;
            }
            catch { }

            return false;
        }

        private void HandlePageRequest(HttpContext context)
        {
            context.Response.Redirect("/_mobilelogin/Home/Login?ReturnUrl=" + HttpUtility.UrlEncode(context.Request.Url.ToString()));
        }

        private void HandleNonPageRequest(HttpContext context)
        {
            if (context.Response.Headers["X-Forms_Based_Auth_Required"] == null)
            {
                context.Response.Headers.Add("X-Forms_Based_Auth_Required", "/_mobilelogin/Home/Login?ReturnUrl=" + HttpUtility.UrlEncode(context.Request.Url.ToString()));
            }
            if (context.Response.Headers["X-Forms_Based_Auth_Return_Url"] == null)
            {
                Uri relativeUri;
                Uri.TryCreate("/" + SPUtility.ContextLayoutsFolder + "/error.aspx", UriKind.Relative, out relativeUri);
                string absoluteUri2 = new Uri(SPAlternateUrl.ContextUri, relativeUri).AbsoluteUri;
                context.Response.Headers.Add("X-Forms_Based_Auth_Return_Url", absoluteUri2);
            }

            if (context.Response.Headers["X-MSDAVEXT_Error"] == null)
            {
                string value = "917656; " + HttpUtility.HtmlAttributeEncode(SPResource.GetString("FormsAuthenticationNotBrowser", new object[0]));
                context.Response.Headers.Add("X-MSDAVEXT_Error", value);
            }
        }


}

I decided to create a ASP.NET MVC web application for my login page.  For things to work as expected, this needs to be hosted in your SharePoint farm.  I decided to add it as a virtual directory called _mobilelogin in IIS under my SharePoint web applications.  Because I need a windows principal object later, I need to store the person’s username and password somewhere I can get to it later.  There are a few different ways of handling this, each with their own pros and cons but to keep things simple for this blog post, I will use the FormsAuthenticationTicket (line 70) that normal ASP.NET Forms based authentication uses to store both the userid and password.  This ticket gets encrypted (line 78) and stored as a cookie in the browser of the mobile device (line 84).  Since the password is sensitive, it will be doubly encrypted because it gets encrypted once (line 75) and then put into the ticket which gets encrypted again.

MVC Model and Controller

public class LoginModel
{
	public string UserName { get; set; }
	public string Password { get; set; }
	public bool CookieSet { get; set; }
	public bool RememberMe { get; set; }
}
	
public class HomeController : Controller
{
	[HttpGet]
	[OutputCache(NoStore = true, Duration = 0, VaryByParam = "None")] 
	public ActionResult Login()
	{
		LoginModel mdl = new LoginModel();
		mdl.RememberMe = true;
		try
		{
			if (Request.Cookies[Constants.MOBILE_LOGIN_COOKIE_NAME] != null)
			{

				string cookieValue = Request.Cookies[Constants.MOBILE_LOGIN_COOKIE_NAME].Value;

				if (false == string.IsNullOrEmpty(cookieValue))
				{
					FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(cookieValue);
					if (ticket.Expired)
					{
						Utility.LogoutHelper(Response);
					}
					else
					{
						mdl.UserName = ticket.Name;
						mdl.CookieSet = true;
					}
				}
			}
		}
		catch (Exception e)
		{
			ViewBag.ErrorMsg = e.Message;
		}

		return View(mdl);
	}

	[HttpPost]
	[OutputCache(NoStore = true, Duration = 0, VaryByParam = "None")] 
	public ActionResult Login(LoginModel mdl)
	{
		try
		{
			if (false == string.IsNullOrEmpty(mdl.UserName) &&
				false == string.IsNullOrEmpty(mdl.Password))
			{
				string username = mdl.UserName;
				if (false == Utility.IsAnEmailAddress(username) &&
					string.IsNullOrEmpty(Utility.ExtractDomain(username)))
				{
					username = ConfigurationManager.AppSettings["DefaultDomain"] + "\\" + username;
				}
				WindowsIdentity identity = Utility.Logon(username, mdl.Password);

				DateTime expiration = DateTime.Now.AddMinutes(Constants.MOBILE_LOGIN_TTL_SHORT_MIN);
				if (mdl.RememberMe)
				{
					expiration = DateTime.Now.AddDays(Constants.MOBILE_LOGIN_TTL_LONG_DAYS);
				}

				FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(1,
					username,
					DateTime.Now,
					expiration,
					mdl.RememberMe,
					Utility.Protect(mdl.Password, Constants.MOBILE_LOGIN_SALT),
					FormsAuthentication.FormsCookiePath);

				HttpCookie cookie = new HttpCookie(Constants.MOBILE_LOGIN_COOKIE_NAME, FormsAuthentication.Encrypt(ticket));
				if (mdl.RememberMe)
				{
					cookie.Expires = ticket.Expiration;
				}
				Utility.SetAttributes(cookie);
				Response.Cookies.Add(cookie);

				HttpCookie cookieExclude = new HttpCookie(Constants.MOBILE_LOGIN_EXCLUDE_COOKIE_NAME, false.ToString());
				Utility.SetAttributes(cookieExclude);
				cookieExclude.Expires = DateTime.Now.AddDays(-1);
				Response.Cookies.Add(cookieExclude);
				
				Redirect();
			}
			else
			{
				ViewBag.ErrorMsg = "Please fill in both the UserName and Password fields.";
			}
		}
		catch (Exception e)
		{
			ViewBag.ErrorMsg = e.Message;
		}

		return View(mdl);
	}

	public ActionResult Logout()
	{
		Utility.LogoutHelper(Response);

		return View();
	}

	[OutputCache(NoStore = true, Duration = 0, VaryByParam = "None")] 
	public ActionResult Exclude()
	{
		HttpCookie cookieExclude = new HttpCookie(Constants.MOBILE_LOGIN_EXCLUDE_COOKIE_NAME, true.ToString());
		Utility.SetAttributes(cookieExclude);
		Response.Cookies.Add(cookieExclude);

		Utility.LogoutHelper(Response);
		Redirect();

		return View();
	}

	private void Redirect()
	{
		string returnUrl = "/";
		if (false == string.IsNullOrEmpty(Request.QueryString["ReturnUrl"]))
		{
			returnUrl = HttpUtility.UrlDecode(Request.QueryString["ReturnUrl"]);
		}

		Response.Redirect(returnUrl);
	}
}

MVC View

@model LoginModel

@{
    ViewBag.Title = "Login";
}


<div class="container">
    <div class="row">
        <div class="col-sm-12">
            @using (Html.BeginForm())
            {
                <div class="form-horizontal" style="margin-top:90px;width:90%">
                    <div class="form-group">
                        <div class="col-md-offset-3 col-sm-9">
                            <h4>This is a special login page just for mobile devices.  If you feel you reached this page in error, @Html.ActionLink("click here to restore normal login functionality", "Exclude", "Home").</h4>
                        </div>
                    </div>
                    <div class="form-group">
                        <div class="col-md-offset-3 col-sm-9">
                            @if (false == string.IsNullOrEmpty(ViewBag.ErrorMsg))
                            {
                                <p class="error">@ViewBag.ErrorMsg</p>
                            }
                        </div>
                    </div>
                    @if (Model.CookieSet)
                    {
                        <div class="form-group">
                            <div class="col-md-offset-3 col-sm-9">
                                <h4>
                                    You are currently logged in as: @Model.UserName <br />
                                    @Html.ActionLink("Click here to logout", "Logout", "Home", new { ReturnUrl = Request.QueryString["ReturnUrl"] }, new { } ).
                                </h4>
                            </div>
                        </div>
                        <div class="form-group">
                            <div class="col-md-offset-3 col-sm-9">
                                <h4>
                                    @if (string.IsNullOrEmpty(Request.QueryString["ReturnUrl"]))
                                    {
                                        <a href="/">Return to site</a>
                                    }else{
                                        <a href="@HttpUtility.UrlDecode(Request.QueryString["ReturnUrl"])">Return to site</a>
                                    }
                                </h4>
                            </div>
                        </div>
                    }
                    else
                    {
                    <div class="form-group">
                        @Html.Label("User Name", new { @class = "col-sm-3 control-label" })
                        <div class="col-md-9">
                            @Html.TextBoxFor(m => m.UserName, new { @class = "form-control" })
                            @Html.ValidationMessageFor(m => m.UserName, null, new { @class = "help-inline" })
                        </div>
                    </div>
                    <div class="form-group">
                        @Html.LabelFor(m => m.Password, new { @class = "col-sm-3 control-label" })
                        <div class="col-md-9">
                            @Html.PasswordFor(m => m.Password, new { @class = "form-control" })
                            @Html.ValidationMessageFor(m => m.Password, null, new { @class = "help-inline" })
                        </div>
                    </div>
                    <div class="form-group">
                        <div class="col-md-offset-3 col-sm-9">
                            <input type="submit" value="Log in" class="btn btn-default" />
                            @Html.Label("Remember Me")
                            @Html.CheckBoxFor(m => m.RememberMe, new { @checked = "checked" })
                        </div>
                    </div>
                    <div class="form-group">
                        <div class="col-md-offset-3 col-sm-9">
                            <h4>If you have previously saved your password in your browser, select the username field above and then select autofill password at the top of the onscreen keyboard.</h4>
                        </div>
                    </div>
                    }
                    <br />
                </div>
            }
        </div>
    </div>
</div>

After we set the cookie in our MVC application, we need to redirect back to where we came from.  Then the HttpModule from earlier kicks in but on a different event: AuthenticateRequest.  Notice on line 26 where our Ticket gets unencrypted and I check to make sure it is still valid and not expired.  After that I get a WindowsIdentity object by logging in as the user with their username and password (line 33).  I then create a WindowsPrincipal object from the identity and assign it to the user property of the HttpContext (line 36).  This HttpContext.User property is what the SPWindowsClaimsAuthentication HttpModule will look at when creating the Windows Claim which SharePoint then uses everywhere.  I also hijack the signout page on line 38 to redirect to our forms logout page when we are logged in using our forms method.

HttpModule – AuthenticateRequest

public class WindowsFormLogin : IHttpModule
{
	public void Init(HttpApplication context)
	{
		if (context == null)
		{
			return;
		}
		context.AuthenticateRequest += context_AuthenticateRequest;
		context.PreSendRequestHeaders += context_PreSendRequestHeaders;
	}

	void context_AuthenticateRequest(object sender, EventArgs e)
	{
		HttpApplication httpApplication = sender as HttpApplication;
		HttpContext context = httpApplication.Context;

		try
		{
			if (context.Request.Cookies[Constants.MOBILE_LOGIN_COOKIE_NAME] == null) return;

			string cookieValue = context.Request.Cookies[Constants.MOBILE_LOGIN_COOKIE_NAME].Value;

			if (false == string.IsNullOrEmpty(cookieValue))
			{
				FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(cookieValue);
				if (ticket.Expired)
				{
					throw new ApplicationException("Mobile Login Expired");
				}

				string user = ticket.Name;
				WindowsIdentity identity = Utility.Logon(user, Utility.Unprotect(ticket.UserData, Constants.MOBILE_LOGIN_SALT));

				context.Request.ServerVariables["LOGON_USER"] = user;
				context.User = new WindowsPrincipal(identity);

				int index = context.Request.RawUrl.ToLower().IndexOf("/_layouts/15/signout.aspx");
				if (index >= 0)
				{
					string returnUrl = "/";
					if (index > 0)
					{
						returnUrl = context.Request.RawUrl.ToLower().Substring(0, index);
					}
					if (context.Request.UrlReferrer != null &&
						false == string.IsNullOrEmpty(context.Request.UrlReferrer.ToString()))
					{
						returnUrl = context.Request.UrlReferrer.ToString();
					}
					context.Response.Redirect("/_mobilelogin/Home/Logout?ReturnUrl=" + HttpUtility.UrlEncode(returnUrl));
				}
			}
		}
		catch (Exception ex)
		{
			//MobileLogin Login Error
			Utility.LogoutHelper(context);
		}
	}
}

I have used a few helper methods in my code above so below is this code which should enable you to put this solution together in your own environment.  Also, the MVC login app pool service account and your SharePoint app pool service account need special permissions on the server in order for them to login as another user on line 61.  This permission is called “Act as part of the operating system” which can be found in user rights assignment in local security policies.

Utility Class

public class Constants{
	public const string MOBILE_LOGIN_COOKIE_NAME = "MobileLogin";
	public const string MOBILE_LOGIN_EXCLUDE_COOKIE_NAME = "MobileLoginExcluded";
	public const int MOBILE_LOGIN_TTL_LONG_DAYS = 90;
	public const int MOBILE_LOGIN_TTL_SHORT_MIN = 90;
	public const string MOBILE_LOGIN_SALT = "TEST 123";
}

public class Utility
{
	public static string Protect(string text, params string[] purpose)
	{
		if (string.IsNullOrEmpty(text))
			return null;

		byte[] stream = Encoding.UTF8.GetBytes(text);
		byte[] encodedValue = MachineKey.Protect(stream, purpose);
		return HttpServerUtility.UrlTokenEncode(encodedValue);
	}
	public static string Unprotect(string text, params string[] purpose)
	{
		if (string.IsNullOrEmpty(text))
			return null;

		byte[] stream = HttpServerUtility.UrlTokenDecode(text);
		byte[] decodedValue = MachineKey.Unprotect(stream, purpose);
		return Encoding.UTF8.GetString(decodedValue);
	}

	public static WindowsIdentity Logon(string username, string password)
	{
		string domain = Utility.ExtractDomain(username);
		username = Utility.ExtractName(username);

		IntPtr handle = new IntPtr(0);
		handle = IntPtr.Zero;

		const int LOGON32_LOGON_NETWORK = 3;
		const int LOGON32_PROVIDER_DEFAULT = 0;

		// attempt to authenticate domain user account
		bool logonSucceeded = LogonUser(username, domain, password, LOGON32_LOGON_NETWORK, LOGON32_PROVIDER_DEFAULT, ref handle);

		if (!logonSucceeded)
		{
			// if the logon failed, get the error code and throw an exception
			int errorCode = Marshal.GetLastWin32Error();
			throw new Exception("User logon failed. Error Number: " + errorCode);
		}

		// if logon succeeds, create a WindowsIdentity instance
		WindowsIdentity winIdentity = new WindowsIdentity(handle, "NTLM");

		// close the open handle to the authenticated account
		CloseHandle(handle);

		return winIdentity;
	}

	[DllImport("advapi32.dll", SetLastError = true)]
	private static extern bool LogonUser(string lpszUsername,
										string lpszDomain,
										string lpszPassword,
										int dwLogonType,
										int dwLogonProvider,
										ref IntPtr phToken);

	[DllImport("kernel32.dll", CharSet = CharSet.Auto)]
	private static extern bool CloseHandle(IntPtr handle);
	
	public static bool IsAnEmailAddress(string text)
	{
		if (text == null) return false;
		if (text.Length <= 0) return false;
		if (text.IndexOf(" ") >= 0) return false;

		string patternLenient = @"\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*";
		Regex reLenient = new Regex(patternLenient);
		string patternStrict = @"^(([^<>()[\]\\.,;:\s@\""]+"
			+ @"(\.[^<>()[\]\\.,;:\s@\""]+)*)|(\"".+\""))@"
			+ @"((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}"
			+ @"\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+"
			+ @"[a-zA-Z]{2,}))$";
		Regex reStrict = new Regex(patternStrict);
		return reStrict.IsMatch(text);
	}

	public static string ExtractDomain(string UserName)
	{
		if (UserName.IndexOf(Convert.ToChar("\\")) > 0)
		{
			string[] fullDomainUserName = UserName.Split(new char[] { '\\' }, 2);
			return fullDomainUserName[0];
		}
		else
		{
			return string.Empty;
		}
	}

	public static string ExtractName(string name)
	{
		if (name.IndexOf(Convert.ToChar("\\")) > 0)
		{
			string[] fullDomainName = name.Split(new char[] { '\\' }, 2);
			return fullDomainName[fullDomainName.Length - 1];
		}
		else
		{
			return name;
		}
	}
	
	public static void SetAttributes(HttpCookie cookie, HttpContext context)
	{
		cookie.HttpOnly = true;
		string domain = GetRootCookieDomain(GetBasePath(context));
		if (false == string.IsNullOrEmpty(domain) &&
			domain.Contains('.'))
		{
			cookie.Domain = domain;
		}
	}

	public static void LogoutHelper(HttpContext context)
	{
		try
		{
			HttpCookie cookie = new HttpCookie(Constants.MOBILE_LOGIN_COOKIE_NAME, string.Empty);
			cookie.Expires = DateTime.Now.AddDays(-1);
			SetAttributes(cookie, context);

			context.Response.Cookies.Add(cookie);
		}
		catch (Exception err)
		{
			//MobileLogin Logout Cookie Error
		}
	}
	
	public static string GetRootCookieDomain(string url)
	{
		url = GetBasePathFromUrl(url);
		if (string.IsNullOrEmpty(url)) return url;
		Uri uri = new Uri(url);
		string host = uri.Host;
		if (false == host.Contains('.')) return uri.Host;

		string[] hostParts = host.Split('.');
		string domain = "." + hostParts[hostParts.Length - 2] + "." + hostParts[hostParts.Length - 1];

		return domain;
	}
	
	public static string GetBasePathFromUrl(string url)
	{
		string newUrl = string.Empty;
		if (url.Contains("://"))
		{
			newUrl = url.Substring(0, url.IndexOf("://") + 3);
			url = url.Substring(url.IndexOf("://") + 3, url.Length - url.IndexOf("://") - 3);
			if (url.Contains("/"))
			{
				newUrl += url.Substring(0, url.IndexOf("/"));
			}
			else
			{
				newUrl += url;
			}
		}
		return newUrl;
	}
	
	public static string GetBasePath(HttpContext context)
	{
		string basepath = "";
		if (context.Request.ServerVariables["HTTPS"].ToString().ToUpper().Equals("ON"))
		{
			basepath = "https://";
		}
		else
		{
			basepath = "http://";
		}
		basepath += context.Request.ServerVariables["SERVER_NAME"].ToString();
		if (!context.Request.ServerVariables["SERVER_PORT"].ToString().Equals("80") &&
			!context.Request.ServerVariables["SERVER_PORT"].ToString().Equals("443"))
		{
			basepath += ":" + context.Request.ServerVariables["SERVER_PORT"].ToString();
		}
		return basepath;
	}
}

Edit 05/18/2016:

Added a useragent check before the 401 status check to help with apps like the SharePoint iOS app and SharePlus which already know how to connect to SharePoint using NTLM.

SharePoint Cross Site Word Document Properties

Recently I had a project where I was using SharePoint columns as document property fields in MS Word.  This is pretty straight forward when you have one library and some files but when you have a central repository of master files that are then pushed out to different sites across your farm it makes it a little more interesting.  We built our content types and columns in Visual Studio which were deployed on all sites and thus had the same contenttypeid’s and column guids.  One thing I noticed was that in the master library the document field would work correctly but once the file was copied to another site the document field would no longer work.  I was unable to find anything on the web about this scenario which is why I’m posting my findings here.

First thing I did was to open the copied file in one of the non-master sites and fix the field.  I just deleted the old one and re-added it and everything was fine.  Next I renamed both the fixed document and the master document to have a .zip extension.  This enables me to extract the contents of the word file so I could examine the differences.  What I found was that in the word\document.xml file where the document fields were inserted they had different GUIDs for the ns3 namespace attribute (see on line 21).  Also check out the xpath statement on line 23 and then notice how the namespace (ns3) for our document field (ProjectName) is the namespace where we have different GUIDs between the two documents.

<w:sdt>
	<w:sdtPr>
		<w:rPr>
			<w:b/>
			<w:smallCaps/>
			<w:snapToGrid w:val="0"/>
			<w:sz w:val="18"/>
		</w:rPr>
		<w:alias w:val="Project Name"/>
		<w:tag w:val="ProjectName"/>
		<w:id w:val="-1780325992"/>
		<w:lock w:val="sdtContentLocked"/>
		<w:placeholder>
			<w:docPart w:val="1BC5893A50C54E06B3DBC32132B17E14"/>
		</w:placeholder>
		<w:showingPlcHdr/>
		<w:dataBinding 
			w:prefixMappings="xmlns:ns0='http://schemas.microsoft.com/office/2006/metadata/properties' 
			xmlns:ns1='http://www.w3.org/2001/XMLSchema-instance' 
			xmlns:ns2='http://schemas.microsoft.com/office/infopath/2007/PartnerControls' 
			xmlns:ns3='b5fc2735-42dd-40f0-a521-255c535f6692' 
			xmlns:ns4='cb6c538f-4b29-4910-832a-74df7c62e784' " 
			w:xpath="/ns0:properties[1]/documentManagement[1]/ns3:ProjectName[1]" 
			w:storeItemID="{59580BC9-FAD5-4EB7-BF24-59B00B2B217C}"/>
		<w:text/>
	</w:sdtPr>
	<w:sdtEndPr/>
	<w:sdtContent>
		<w:r w:rsidR="00556E42" w:rsidRPr="00920EC6">
			<w:rPr>
				<w:rStyle w:val="PlaceholderText"/>
			</w:rPr>
			<w:t>[Project Name]</w:t>
		</w:r>
	</w:sdtContent>
</w:sdt>

So following a hunch I looked up the SPWeb.ID and sure enough, it was the same GUID as ns3.  So, when copying documents from one site to another, the document field xml data from SharePoint must be tied to the GUID of the site it came from.  I was able to verify this by looking at the customXml\item1.xml file (it’s always makes things a lot easier to be able to view your xml data when working with xpath queries) which showed all of my document fields coming from SharePoint and they all had a namespace of the SPWeb.ID GUID.

Since the namespace will be changing every time I copy the file I decided to update the xpath query to ignore the namespace.  Changing it from:

w:xpath="/ns0:properties[1]/documentManagement[1]/ns3:ProjectName[1]"

to:

w:xpath="/ns0:properties[1]/documentManagement[1]/*[local-name() = 'ProjectName'][1]"

This xpath trick will query only based on the name of the element and exclude any namespaces.  Sure enough, once I updated the file in my master site and did another copy down to a non-master site, the field updated correctly in word and everything was happy.  You can update the document.xml file using any text editor you’d like, and then just zip all of your files back up, and then rename the file to be a .docx and it should open correctly in word.

Microsoft Band 2 vs Fitbit Surge

Microsoft Band 2
Microsoft Band 2

Last year I was debating between the Microsoft Band or the upcoming Fitbit Surge.  After trying on the band at the Microsoft Store, I was not happy with how it felt on my wrist so I decided to go with the Surge.  I’ve been using a Fitbit Surge everyday for most of the past year.  For a fitness tracker with GPS, the Surge has been fantastic but I wanted some smartwatch features as well.

Fitbit Surge
Fitbit Surge

After trying on the new Microsoft Band 2 at the Microsoft Store, I decided to get that one to replace my Surge.  Here are my current thoughts on the Band 2 compared to the Surge.

Phone Compatibility

The Band 2 works with Android, iOS and Windows Phone but the Cortana (voice) integration isn’t available on Android and iOS,only on the Windows Phone and the onscreen keyboard isn’t available on iOS.

View Time and steps (secondary display item is configurable)

I have been using it with my Lumia 950 Windows Phone (see post here).  The Fitbit Surge also works on Android, iOS and Windows Phone but some things didn’t work on the Windows Phone like phone/text notifications and music controls.  I used my Surge mainly on a Windows Phone but my wife has been using it on her iPhone recently.

 

Same features as Fitbit Surge

Heart rate monitor

 

  • Displays the time and date
  • Tracks steps, calories burned, distance, floors, heartrate
  • GPS tracks runs
  • Tracks other workouts with heartrate like the elliptical
  • Alarms, timer, stopwatch

Pros over Fitbit Surge

  • More notifications
    See full text messages, email partials, and my upcoming calendar
    • Phone (Improvement for Windows Phone only)
    • Text (Improvement for Windows Phone only)
    • Email
    • Facebook
    • Facebook Messenger
    • Calendar Reminders
  • Control music (Improvement for Windows Phone only)
  • Reply to texts and phone calls
    • Up to 4 quick responses (with automatic detection of a question and showing yes and no as additional quick responses)

      The small keyboard used to reply to texts works surprisingly well
      The small keyboard used to reply to texts works surprisingly well
    • Small keyboard to reply to text messages, which works surprisingly well (Windows Phone and Android only)
    • Cortana voice dictation to reply to text messages (Windows phone only)
  • See weather, news, and calendar
  • Use Cortana to initiate reminders, texts, calls, etc. (Windows phone only)
    • I can even ask questions like “How old is j-lo” and the answer shows up on the band screen.

      Cortana - "How old is J Lo?"
      Cortana – “How old is J Lo?”
  • See last exercise and sleep
    • On the fitbit this was only available to view using the app or the website, not on the watch itself
  • Sleep cycle alarm to wake you up, up to 30 mins before scheduled alarm
  • Prettier design and user interface
    sleep
    A lot better sleep stats compared to Fitbit
    • Lots of people have mentioned that the Band 2 is a MUCH prettier watch than the fitbit surge.
    • The fitbit surge is large and looks big on my wife where the band 2 would look fine on a male or a female
    • The fitbit surge had a basic black and white interface whereas the band 2 is colorful and dynamic.
  • Less bulky
    • Fitbit would get caught when tucking in my shirt, taking off my jacket, or putting on my backpack
  • Auto detected sleep mode or manual
    • Manual is nice because it also turns off watch mode and notifications other than the watch alarms.
    • Tracks how long it takes to go sleep.
  • Watch mode
    • Off
    • Always on
    • Rotate
  • Lots more settings right on the watch, the fitbit had very few settings on the watch itself and was mostly configured using the app or website.
    • Vibrate
    • Dim

      Turning my wrist over turns on the low power watch mode for a few seconds
  • Tile and color settings using phone
    • I can even create my own background pictures or download some to the device using additional apps
  • More info can be seen while during an activity
    • The fitbit displayed a few pieces of data but that data wasn’t customizable.  Any additional data I had to swipe through at the bottom of the screen.
    • The band 2 allows me to configure what data I see on the main screen while during an activity and then it provides a single swipe right to see the rest of the data which I configured to be shown.
  • More analytics on the website and app
    More exercise stats compared to Fitbit
    More exercise stats compared to Fitbit
    • Vo2 max
    • Max and mins (heart rate, elevation, etc)
    • Recovery time
  • UV sensor tells me how long I was exposed to sunlight
    • Considering it’s currently winter, I haven’t had a chance to use this a lot yet.

Cons/current issues

  • Health app
    • No weight tracking – Now available in February Update
    • No food tracking
      • The fitibit app is fantastic at tracking food
      • I used this functionality for a little bit with my fitbit to get an idea but it wasn’t something I did all the time.
    • Not Universal Windows Platform and supporting continuum like the Fitbit app (WTH MS, can’t even utilize your own tech?)
  • Weird how different types of activities are grouped together
  • Best feel is wearing on bottom of wrist which feels kind of weird at first but I’ve gotten used to it.
  • The clasp took a little bit to get used to but it seems pretty solid
  • Battery life
    • Fitbit is 5 days
      • I would put it on the charger while taking a shower, giving the kids a bath, and while at the climbing gym

        View from the top of my wrist shows the clasp with integrated battery and the UV sensor.
      • On climbing trips typically I would charge while driving to the location and that was usually enough to last the whole trip
    • Band 2 is 24-48 hours
      • I’ve been charging mine nightly when I am sitting down to watch TV (it’s usually around 50% when I start charging)
      • Unsure how this is going to work when i am on climbing trips.  I might have to forgo sleep tracking just to maintain a charge.

Conclusion

I’ve really enjoyed the Band 2.  It’s been really handy when playing with my kids. I can put my phone down near by and glace at my watch when receiving texts from my wife or seeing emails come through.  It beats trying to dig my phone out of my pocket only to see the text was from Chick-fil-a telling me about some deal they are running. I’ve also really enjoyed being able to text my wife or reply to texts from my wrist while playing with the kids or during other activities where my phone isn’t easily accessible. I wish the app was better and the battery life was a lot better but compared to the advantages, I can get over those things pretty quickly.

What I like and dislike about Windows 10 Phone

IMG_0062
Lumia 950 – 5.2″ Quad HD (AMOLED, 2560 x 1440), 6-core 1.8 Ghz proc, 3 GB of RAM, 32 GB storage with up to 200 GB expandable with microSD, Dual SIM, 20 MP Camera

So if you are new to Windows Phone, you can refer to my post here on why I like Windows Phone better than iOS.  I recently have been playing around with Windows 10 Mobile on a Lumia 640 and a Lumia 950.  This post is mainly my running list of what has improved in Windows 10 Mobile over Windows Phone 8.1 and listing what current cons and issues I have.

Pros over Windows Phone 8.1

  • Hey Cortana with learn my voice or use any voice
    • Hey Cortana was only on select Windows Phone 8.1 devices
    • Learn my voice is new in Windows 10 Phone
  • Continuum (See video demonstration here)
    • Connect to a TV or monitor as a second larger screen to do work on

      Windows Hello - Iris Recognition
      Windows Hello – Iris Recognition
    • Use phone as mouse and keyboard or connect usb/bluetooth devices
  • Mouse support
  • Windows Hello (Only on some devices)
    • Iris recognition instead of typing a pin number
  • Outlook for email
    • Attach any files on device to emails
    • Attach from onedrive
  • Better organized settings
  • Keyboard joystick
    • Dot near z and x to easily move the cursor around
  • Notification text and facebook inline reply
  • Voice dictation built into keyboard
  • Don’t forget when powering off
    • Displays a reminder of your next appointment when you power off the device

      Notification inline reply, keyboard joystick, voice dictation easily accessible on keyboard
      Notification inline reply, keyboard joystick, voice dictation easily accessible on keyboard
  • Call Recording
  • Fast USB-C charging (only on some devices)
    • 50% in 30 mins

Cons/Current issues

  • NTLM passwords aren’t saved in Edge
  • Can’t connect to NTLM auth OneNote
  • Excel, PowerPoint and Word can’t connect to on-prem NTLM SharePoint
  • Airwatch blocks SD card – Confirmed bug with airwatch
  • Airwatch doesn’t install cert chain – Maybe fixed with a newer build
  • Cannot open EML attachment in email – Fixed in Outlook Mail 17.6868.41032.0
  • Continuum
  • Option to only try Windows Hello when unlock is needed
    • Right now Hello will get locked out from too many attempts because of notifications (email, text, etc)
  • Facebook app needs additional features and bug fixes
    • Live tile not working – Better in 10.2.1.0
    • Pinned pages don’t show notifications on live tile
    • Some types of content don’t display correctly
      Windows Hello tries too much and ends up locking itself out
      Windows Hello tries too much and ends up locking itself out
      • Memories
      • Birthday rollups
  • Battery life
    • Better after installing 10586.63
  • Dual Sim
    • No Visual voicemail on dual sim devices – Fixed in 14267.1004
    • Should be a way to hide 2nd sim when not used in dual sim
    • No vibrate on answer on dual sim devices – Fixed in 14283
  • OneDrive doesn’t connect to on-prem SharePoint
  • Live lock screen app from 8.1 isn’t available
  • Edge does not change to desktop user agent when used on continuum
    • Whatever screen the tab is opened in is the user agent that will be used
  • Random reboots (a lot less after 10586.63)
    • Seemed to happen a lot more at my office.  The following things are potentially different at my office vs at home.
      • Bluetooth devices
      • AT&T Microcell – Often after it reboots it says that my carrier has changed my SIM settings and I need to restart again.
      • Wireless charging
    • Kids corner (Fixed since 10586.71)
      • Since 10586.63, I’ve tried going to kids corner several times from the lock screen and the phone will freeze and eventually reboot.  I have since figured out if I don’t try to hold it up to my face and let windows hello to attempt to sign me in, then I can go into kids corner successfully.  It seems to be the combination of windows hello attempting to read my iris’s while I’m going to kids corner which locks it up.

Why I like Windows Phone better than iOS

Customizable live tiles showing unread counts and information
Customizable live tiles showing unread counts and information

The goal in the post is for me to document my running list of why I like Windows Phone better than iOS (iPhone/iPad).

  • Customizable home screen with three different tile sizes.  This enables users that have a hard time seeing small text or hitting small buttons can make them bigger.
  • Live tiles show more information when not using the small size which shows alerts directly on the home screen whereas iOS only shows the number of notifications.
  • Swipe right enables viewing all installed apps with search and jump by alpha.  Unlike iOS where every app installed has to be in a home page somewhere
  • Can have a combined tile for all email, separate tile for each email address or link several mailboxes together such as Gmail and Hotmail can be linked together as a personal inbox and keep work inbox separate

    Swiping right shows all apps with easy to find by alpha or using search
    Swiping right shows all apps with easy to find by alpha or using search
  • Can combine tiles into a tile folder
  • Driving mode
    • Reads texts to you over bluetooth
    • Can dictate responses over bluetooth
    • Does not popup or sound notifications for anything except phone and text
      • Can also turn off phone and/or text as well
  • Kids corner
    • Swipe right to show a start screen that you configured just for your kids and the apps they are allowed to use
    • Need PIN to get back into normal phone mode.
    • No longer can your child send your boss an email by accident.
  • Gadgets app
    • Run apps when accessories connect
    • Find accessory by seeing on a map where you last disconnected the accessory
  • Near Field Communication (NFC) can be used for more things than just payments like in iOS

    The Glance Screen is like a low powered notification screen.
    The Glance Screen is like a low powered notification screen.
  • Wireless charging in some models
  • Standard USB charging cable (micro or newer type c)
  • Full desktop class browser which can switch from mobile user agent to desktop one
  • Social integration with contacts so when you get a new friend in Facebook or twitter they can show up as a contact
  • Glance screen
    • Low powered screen that shows the time, date, next appointment and notification count.
  • Expand storage with SD cards
  • Battery Saver
    Wi-Fi can automatically turn back on
    Wi-Fi can automatically turn back on
    • Turns off checking for email and other background tasks to help save battery
    • Can automatically turn on at 20% battery
  • Configure apps from within the app instead of needing to leave and go to settings
  • When turning off WiFi, option to turn back on based on a time frame or when at a favorite place such as home
  • Cortana – Personal Digital Assistant with voice recognition
    • Quiet Hours
      • Inner circle can break through
    • Collection of news and other information that is important to you
    • Reminders
      • Place
        • Cortana is your personal digital assistant. Here she is giving me a run down of my interests.
          Cortana is your personal digital assistant. Here she is giving me a run down of my interests.

          Remind me to buy print cartridges next time I’m at the Best Buy off Rea road

        • Remind me to pick up groceries when I leave work
        • Remind me to start laundry when I arrive home
      • Person
        • Remind me to ask my Mom how her cat is doing next time I’m in contact with her (phone, email, text)
      • Time
    • Text message with nicknames
      • Text message my wife on my way
      • Text message my manager I’m running late