Azure Mobile Services support storing the Oauth user credentials token in the PasswordVault of the OS a client app is running in. This is supported in Windows Universal apps, Xamarin, etc.
See here for the details regarding user credentials from the AMS documentation.
And this is a good thing, now the user does not have to supply the Oauth credentials every time the app is started. We can check if the vault contains the credentials we need and this saves a round trip to the server and an extra input screen. If it is not in the vault, just let the user login using Facebook, Google etc. and store the token.
But wait: these stored credential tokens are not valid until the end of times. For example, Google credentials are valid for 30+ days. After that, the token stored in the vault is worthless.
Therefor, in the AMS example above a simple check is done by calling the AMS service with a trivial request:
... try { // Try to return an item now to determine if the cached credential has expired. await App.MobileService.GetTable<TodoItem>().Take(1).ToListAsync(); // <<-- WHY??? } catch (MobileServiceInvalidOperationException ex) { if (ex.Response.StatusCode == System.Net.HttpStatusCode.Unauthorized) { // Remove the credential with the expired token. vault.Remove(credential); credential = null; continue; } } ...
Here a simple GET is done to check the validity of the token. If it is not valid, it is deleted and the user must login again.
But this is a web call I do not like.
This is done every time user logs in / starts the app, even in an offline scenario!
In an offline scenario I am not even interested in the expiration date…
Is there a better way?
Well, what we get from the Oauth providers, when the user logs in successfully, is a Json Web Token (JWT). And this token can be decoded following the JWT specifications.
This will result in something like:
public class Token { [JsonProperty(PropertyName = "ver")] public string Ver { get; set; } [JsonProperty(PropertyName = "uid")] public string Uid { get; set; } [JsonProperty(PropertyName = "exp")] public string Exp { get; set; } [JsonProperty(PropertyName = "nbf")] public string Nbf { get; set; } [JsonProperty(PropertyName = "aud")] public string Aud { get; set; } }
This token is available on the server as:
App.MobileService.CurrentUser.MobileServiceAuthenticationToken
And the token can be decoded using a Nuget package. The only drawback is that this package is not available on the Universal apps client, only on the Azure Mobile Services server, the backend.
So I wrote this extra API controller:
public class DecodeExpirationController : ApiController { public ApiServices Services { get; set; } // GET api/DecodeExpiration [AuthorizeLevel(AuthorizationLevel.User)] public DateTime? Get(string token) { string adminKey; if (!Services.Settings.TryGetValue("ADMIN_KEY", out adminKey)) { return null; } try { var jsonPayload = JWT.JsonWebToken.Decode(token, adminKey, false); var result = JsonConvert.DeserializeObject<Token>(jsonPayload); var startOfUtcCount = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); var expirationDate = startOfUtcCount. AddSeconds(Convert.ToInt32(result.Exp)); return expirationDate; } catch (JWT.SignatureVerificationException) { return null; } catch (Exception ex) { return null; } } }
Note the level of authorization, the user should be logged in by now…
And we can call this WebApi GET on the client:
try { var parameters = new Dictionary<string, string>(); parameters.Add("token", App.MobileService.CurrentUser.MobileServiceAuthenticationToken); var result = await App.MobileService. InvokeApiAsync<DateTime?>("DecodeExpiration", HttpMethod.Get, parameters); message = result.HasValue ? result.Value.ToString() : "Nothing returned."; } catch (Exception ex) { message = "Exception: " + ex.Message; }
This should work using with every provider.
The Admin Key is coming from the portal. I have added this appsetting to the configuration of the AMS.
Using this call we get the UTC time of the token expiration date. So store this on the client and check this value every time (give or take a few hours due to correct daylight savings when applicable).