Saturday 7 March 2015

Azure AD Cloud silent Authentication with ADAL and TokenCache encryption

The ADAL library was launched a couple of years ago, luckily people from Microsoft like Vittorio Bertocci, have been working hard to have a library capable of doing authentication between native applications, web applications and web services (Web API). ADAL.js is in beta, it is a full working library but there are some issues need to be solved before we can have a robust library (This is when I am writing my article so March 2015) but Vittorio promised me we will have something working in this side by April 2015.
Unfortunately for me, and due to a project we are releasing before April, there is not choice, so I will have to write a wrapper around my Azure Web Api’s and skip Adal.js until we have v1.0. So you will be asking yourself “what is this guy talking about?”.
Ok, let’s go to start from a "simple" scenario, so you can have an  idea of what I am talking about. I have a Web Application (MVC 5) which is talking with few web services (Web API 2’s). All of them are register in Azure AD, so Azure AD acts as a Black Box, and manage the authentication. In this case Azure AD will take care of the MVC Web Application and the Web API’s Authentication.
So where is the problem? Well if I want to be sure that the Web Application and the Web APis share the same authentication, I will have to use refresh tokens, but because the hard work done by Vittorio’s team, now we can use Silent Tokens, so we don’t have to worry about building a whole system to refresh the tokens, but the only place where we need to worry it is about about where to store the Tokens.
It is quite common to store the tokens in Sessions, but there is a small problem with this, you lose the multi-farm factor plus WebApi’s don’t have sessions, so when you have to talk to them becomes an impossible job.
So what is the solution? store the tokens in a persistent area. If we have a native application we could store the token’s in the file system, but if we have a Web Application we could store them in Blob Storage or Databases.
Let’s go to a scenario, when someone, which is not really a user, and it is trying to get our token to access to our farm or MVC application, manage to access to the database or file system. Well, I think we will be in big trouble here. That person will have at least 20 minutes (Time when our token expires) to hack our system.
So… let’s go to make the things more difficult for this person, let’s go to inject encryption in the tokens, with double key encryption, String and Byte Array.
The following example it is a simple call from a MVC Application to a Web API 2 using silent Authentication.
...//## GETTING THE TOKEN TO BE AUTHORISE string userObjectID = ClaimsPrincipal.Current.FindFirst(userSchema).Value; AuthenticationContext authContext = new AuthenticationContext(Startup.Authority, new TokenEncryptedDatabaseCache(userObjectID, "Iamakey"); ClientCredential credential = new ClientCredential(clientId, appKey); AuthenticationResult result = authContext.AcquireTokenSilent(resourceId, credential, new UserIdentifier(userObjectID, UserIdentifierType.UniqueId));string authnHeader = "Authorization: Bearer " + result.AccessToken; ...


Now… check the parameter we are passing on  AcquireTokenSilent , it is a TokenCache.

This is the model we use to store the data in the database:


public class PerUserWebCache { [Key] public int EntryId { get; set; } public string WebUserUniqueId { get; set; } public byte[] CacheBits { get; set; } public DateTime LastWrite { get; set; } }
And finally this is the beauty! TokenEncryptedDatabaseCache, which encapsulates the encryption. You will need to create a dbcontext to store the model with the data…which I am going to add as well, so you can save some time.

using System;using System.Collections.Generic;using System.Data.Entity;using System.Linq;using System.Web;using System.Web.Caching;using Microsoft.IdentityModel.Clients.ActiveDirectory;using System.Security.Cryptography;using System.IO; public class TokenEncryptedDatabaseCache : TokenCache { private TokenCacheDataContext db = new TokenCacheDataContext(); string User; private PerUserWebCache Cache; private string Key; /// <summary> /// Contsructor for the TokenEncryptedDatabaseCache class /// </summary> /// <param name="user">Current User</param> /// <param name="key">Key for the encription</param> public TokenEncryptedDatabaseCache(string user, string key) { Key = key; User = user; this.AfterAccess = AfterAccessNotification; this.BeforeAccess = BeforeAccessNotification; this.BeforeWrite = BeforeWriteNotification; //## We check if the user is in our database Cache = db.PerUserCacheList.FirstOrDefault(c => c.WebUserUniqueId == User); //## If that is the case we keep it in memory //## We decrypt the token this.Deserialize((Cache == null) ? null : Cache.CacheBits!=null?Decrypt(Cache.CacheBits):null); } /// <summary> /// Method to clean the database /// </summary> public override void Clear() { base.Clear(); foreach (var cacheEntry in db.PerUserCacheList) db.PerUserCacheList.Remove(cacheEntry); db.SaveChanges(); } /// <summary> /// ADAL raise a notification before acces to the cache. /// Notification raised before ADAL accesses the cache. /// This is your chance to update the in-memory copy from the DB, /// if the in-memory version is stale. The token is decrypted. /// </summary> /// <param name="args"></param> void BeforeAccessNotification(TokenCacheNotificationArgs args) { if (Cache == null) { // first time access Cache = db.PerUserCacheList.FirstOrDefault(c => c.WebUserUniqueId == User); } else { // retrieve last write from the DB var status = from e in db.PerUserCacheList where (e.WebUserUniqueId == User) select new { LastWrite = e.LastWrite }; // if the in-memory copy is older than the persistent copy if (status.First().LastWrite > Cache.LastWrite) //// read from from storage, update in-memory copy { Cache = db.PerUserCacheList.FirstOrDefault(c => c.WebUserUniqueId == User); } } this.Deserialize((Cache == null) ? null : Cache.CacheBits!=null?Decrypt(Cache.CacheBits):null); } /// <summary> ///Notification raised after ADAL accessed the cache. ///If the HasStateChanged flag is set, ADAL changed the content of the cache, ///At this time we encrypt the token and save it. /// </summary> /// <param name="args"></param> void AfterAccessNotification(TokenCacheNotificationArgs args) { // if state changed if (this.HasStateChanged) { Cache = new PerUserWebCache { WebUserUniqueId = User, CacheBits = Encrypt(this.Serialize()), LastWrite = DateTime.Now }; //// update the DB and the lastwrite db.Entry(Cache).State = Cache.EntryId == 0 ? EntityState.Added : EntityState.Modified; db.SaveChanges(); this.HasStateChanged = false; } } void BeforeWriteNotification(TokenCacheNotificationArgs args) { // if you want to ensure that no concurrent write take place, use this notification to place a lock on the entry } /// <summary> /// Encription of the token. /// </summary> /// <param name="DataToEncrypt">Data to be encrypted</param> /// <returns>Data encrypted.</returns> private byte[] Encrypt(byte[] DataToEncrypt) { PasswordDeriveBytes passwordDeriveBytes = new PasswordDeriveBytes(Key, new byte[] { 0x43, 0x87, 0x23, 0x72 }); MemoryStream memoryStream = new MemoryStream(); Aes aes = new AesManaged(); aes.Key = passwordDeriveBytes.GetBytes(aes.KeySize / 8); aes.IV = passwordDeriveBytes.GetBytes(aes.BlockSize / 8); CryptoStream cryptoStream = new CryptoStream(memoryStream,aes.CreateEncryptor(), CryptoStreamMode.Write); cryptoStream.Write(DataToEncrypt, 0, DataToEncrypt.Length); cryptoStream.Close(); return memoryStream.ToArray(); } /// <summary> /// Decryption of the token. /// </summary> /// <param name="DataToDecrypt">Data to be decrypted</param> /// <returns>Data decrypted</returns> private byte[] Decrypt(byte[] DataToDecrypt) { PasswordDeriveBytes passwordDeriveBytes = new PasswordDeriveBytes(Key, new byte[] { 0x43, 0x87, 0x23, 0x72 }); MemoryStream memoryStream = new MemoryStream(); Aes aes = new AesManaged(); aes.Key = passwordDeriveBytes.GetBytes(aes.KeySize / 8); aes.IV = passwordDeriveBytes.GetBytes(aes.BlockSize / 8); CryptoStream cryptoStream = new CryptoStream(memoryStream, aes.CreateDecryptor(), CryptoStreamMode.Write); cryptoStream.Write(DataToDecrypt, 0, DataToDecrypt.Length); cryptoStream.Close(); return memoryStream.ToArray(); } }

This is the class where we have our DBContext


using System;using System.Collections.Generic;using System.ComponentModel.DataAnnotations;using System.Data.Entity;using System.Data.Entity.ModelConfiguration.Conventions;using System.Linq;using System.Text;using System.Threading.Tasks; public class TokenCacheDataContext : DbContext { public TokenCacheDataContext() : base("TokenCacheDataContext") { } public DbSet<PerUserWebCache> PerUserCacheList { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Conventions.Remove<PluralizingTableNameConvention>(); } }


Initializer…

using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Threading.Tasks; public class TokenCacheInitializer : System.Data.Entity.DropCreateDatabaseIfModelChanges<TokenCacheDataContext> { }

Well, I hope you enjoy, any questions let me know.

Thursday 19 February 2015

Creating Your Own Helper for the Azure AD Graph API

If you want to implement Azure AD in an Enterprise Solution, you will probably looking for of adding extensions (attributes) to users, so you can add things like Company Name or Twitter Account. I agree you could save that into a nice table in a Database, but why do you need to do that if you can use Azure AD as a database to store all these details.

This technique will eliminate dependencies and will make the application more scalable. At the end of the day, Azure AD is a black box we can’t “invade” but we can can manipulate.

In this post you will see few things which you will need in order to make the whole Azure AD something useful, so things like:

  • Adding/Removing Extensions.
  • Override the claims so we can extract the extensions.

The first thing we are going to do it is to Create an out-of-the-box MVC application which will include our UX to perform the actions. Below there we will create our helper with the actions and models.

So let’s go to start… Add a new MVC project, and click OK. Then click on Change Authentication:
image 

Select Organizational Accounts and in the Domain box enter your tenant, it could be somekindofcompany.onmicrosoft.com (If you don’t have a domain you will need to create one on Azure, this is a nice post if you don’t have a clue). Select “Sign on read and write …” at the end of the day you are going to be manipulating the Azure AD tenant, so you need permissions. You the will be promtend to login, just login with the admin account, and go for it!, click ok.
 image

Now! if you run the application you will be prompted with the login screen, just login and you will see the default page. At this stage, if you manage to login, you will realize that you have a nice little token to perform RESTFul operations with you Application. This is what you should get after doing the Login.
image

Ok! So far so good…

Let’s go to add a project in the solution. This project will be the one in charge of keeping the Models for our Application and doing the Restful operations. It will not care about the tenant, clientid etc… we will be passing that. So Ok, Add a new project to the solution and call it AADGraphAPIManager, when you finish remove Class1.cs. This is how our solution should look like:
image

Now we are going to create two folders in the AADGraphAPIManager project, one it will be called Models and the other called Services.
Under Models we are going to add a class called AADGraphApiModel.cs
image

Inside this class let’s go to copy the model from the Graph API version 1.5, I assume that in few months time we will have a new version. Okp, don’t you worry apparently Microsoft is going to be supporting all the versions for a while. (the code is below)

 

using Newtonsoft.Json;
using System.Collections.Generic;
using System.ComponentModel;

namespace AADGraphAPIManager.Models
{
public class UserContext
{
[JsonProperty(
"odata.metadata")]
public string userContext;

[JsonProperty(
"value")]
public List<UserDetails> value;
}

public class UserDetails
{
[JsonProperty(
"objectId")]
public string objectId { get; set; }

[DisplayName(
"Display Name")]
public string displayName { get; set; }
[DisplayName(
"Given Name")]
public string givenName { get; set; }
[DisplayName(
"Surname")]
public string surname { get; set; }
[DisplayName(
"Job Title")]
public string jobTitle { get; set; }
[DisplayName(
"Department")]
public string department { get; set; }
[DisplayName(
"Mobile")]
public string mobile { get; set; }
[DisplayName(
"City")]
public string city { get; set; }
[DisplayName(
"Street Address")]
public string streetAddress { get; set; }
[DisplayName(
"Country")]
public string country { get; set; }
[DisplayName(
"Postal Code")]
public string postalCode { get; set; }
[DisplayName(
"Phone Number")]
public string telephoneNumber { get; set; }
[DisplayName(
"Email Address")]
public string mail { get; set; }
[DisplayName(
"UPN")]
public string userPrincipalName { get; set; }
[DisplayName(
"Last DirSync")]
public string lastDirSyncTime { get; set; }

//## THIS ONES ARE NEW ONES ... SO EXTENSIONS
[DisplayName("Client Id")]
public string ClientId { get; set; }
[DisplayName(
"Client Name")]
public string ClientName { get; set; }
[DisplayName(
"Client Address")]
public string ClientAddress { get; set; }
[DisplayName(
"Client Post Code")]
public string ClientPostCode { get; set; }
[DisplayName(
"Client Country")]
public string ClientCountry { get; set; }
}

public class AppContext
{
[JsonProperty(
"odata.metadata")]
public string metadata { get; set; }
public List<AppDetails> value { get; set; }
}

public class AppDetails
{
public string objectId { get; set; }
public string appId { get; set; }
}

public class ExtensionPropertiesContext
{
[JsonProperty(
"odata.metadata")]
public string metadata { get; set; }
public List<ExtensionProperty> value { get; set; }
}

public class ExtensionProperty
{
public string objectId { get; set; }
public string objectType { get; set; }
public string name { get; set; }
public string dataType { get; set; }
[JsonProperty(
"odata.metadata")]
public string odataMetadata { get; set; }
[JsonProperty(
"odata.type")]
public string odataType { get; set; }
public List<string> targetObjects { get; set; }
}
}


As you probably noticed, JsonProperty is highlighted, that is because we need to a add Newtonsoft Json Package to recognise the attributes. You can use the windows ones, but you get use to Newtonsoft I suspect. Ok in order to add the Nuget package go to your package console, select this project and type:
Install-Package Newtonsoft.Json
It should be something like this:
image 


Let’s go to add under our folder “Services” a new class called GraphAPIQuery.cs and add the code below.
You will need to install the following Nuget packages under your project


Install-Package Microsoft.IdentityModel.Clients.ActiveDirectory


 



using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using AADGraphAPIManager.Authentication;
using AADGraphAPIManager.Authentication.ExtensionMethods;
using AADGraphAPIManager.Models;
using System.Net;
using System.IO;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace AADGraphAPIManager.Services
{
public class GraphAPIQuery : IGraphAPIQuery
{
private string _graphUrl; //## https://graph.windows.net
private string _apiVersion; //## 1.5
private string _tenantIdClaimType; //## http://schemas.microsoft.com/identity/claims/tenantid
private string _LoginUrl; //## https://login.windows.net/{0}
private string _appPrincipalId; //## ConfigurationManager.AppSettings["ida:ClientID"];
private string _appKey; //## ConfigurationManager.AppSettings["ida:Password"];
private string _tenantId;
private string _appObjectId;

private ClaimsPrincipal _claim;

private AzureADAuthentication _aadAuthentication;

#region [ Enums ]
public enum ExtensionTarget {
User,
Group,
TenantDetail,
Application,
ServicePrincipal
}
#endregion

#region [ Properties ]
public bool IsGraphApiHasBeenInitializedProperly { get; set; }
public string ApiVersion
{
get { return _apiVersion; }
set { _apiVersion = value; }
}
public ClaimsPrincipal Claim
{
get { return _claim; }
}
#endregion

#region [ Constructor ]
public GraphAPIQuery(string AppPrincipalId, string AppKey, ClaimsPrincipal claim)
{
this.IsGraphApiHasBeenInitializedProperly = false;

try
{
//## DEFAULT Microsoft Azure AD Values
_graphUrl = @"https://graph.windows.net";
_tenantIdClaimType
= @"http://schemas.microsoft.com/identity/claims/tenantid";
_apiVersion
= "api-version=1.5";

//## Other vars
_claim = claim;
_tenantId
= _claim.FindFirst(_tenantIdClaimType).Value;
_LoginUrl
= String.Format(CultureInfo.InvariantCulture, "https://login.windows.net/{0}", _tenantId);
_appPrincipalId
= AppPrincipalId;
_appKey
= AppKey;

//## INITIALIZING AADAUTH
_aadAuthentication = new AzureADAuthentication();

//## GET AUTHENTICATION RESULT
_aadAuthentication.GetAuthenticationResult(_graphUrl, _tenantId, _LoginUrl, _appPrincipalId, _appKey);

//## GETTING THE APPLICATION OBJECT ID
_appObjectId = this.GetAppObjectId();

this.IsGraphApiHasBeenInitializedProperly = true; ;
}
catch (WebException ex)
{
System.Diagnostics.Trace.TraceError(ex.ToString());
}
}
#endregion

#region [ Methods ]

#region [ Public Methods ]
public UserDetails GetUserByObjectId(string userId)
{
try
{

// check if token is expired or about to expire in 2 minutes
if (this._aadAuthentication.AadAuthenticationResult.IsExpired() || this._aadAuthentication.AadAuthenticationResult.WillExpireIn(2))
{
//this._aadAuthentication.AadAuthenticationResult = this._aadAuthentication.GetNewAuthenticationResult(ref strErrors);
}

string authnHeader = "Authorization: " + this._aadAuthentication.AadAuthenticationResult.AccessToken;

string uri = this._graphUrl + "/" + this._tenantId + "/users/" + userId + "?" + this._apiVersion;

try
{
HttpWebRequest request
= (HttpWebRequest)WebRequest.Create(uri);
System.Text.ASCIIEncoding encoding
= new System.Text.ASCIIEncoding();

request.Headers.Add(authnHeader);
request.Method
= "GET";
using (var response = request.GetResponse())
{
using (var stream = response.GetResponseStream())
{
StreamReader reader
= new StreamReader(stream);
string responseString = reader.ReadToEnd();
UserDetails getUser
= JsonConvert.DeserializeObject<UserDetails>(responseString);

return getUser;
}
}
}
catch (WebException ex)
{
System.Diagnostics.Trace.TraceError(ex.ToString());
return null;
}
}
catch (WebException ex)
{
System.Diagnostics.Trace.TraceError(ex.ToString());
return null;
}
}

public UserDetails GetUserByName(string name)
{
try
{
// check if token is expired or about to expire in 2 minutes
if (this._aadAuthentication.AadAuthenticationResult.IsExpired() || this._aadAuthentication.AadAuthenticationResult.WillExpireIn(2))
{
//this._aadAuthentication.AadAuthenticationResult = this._aadAuthentication.GetNewAuthenticationResult(ref strErrors);
}

string authnHeader = "Authorization: " + this._aadAuthentication.AadAuthenticationResult.AccessToken;

string uri = this._graphUrl + "/" + this._tenantId + "/users/" + name + "?" + this._apiVersion;

try
{
HttpWebRequest request
= (HttpWebRequest)WebRequest.Create(uri);
System.Text.ASCIIEncoding encoding
= new System.Text.ASCIIEncoding();

request.Headers.Add(authnHeader);
request.Method
= "GET";
using (var response = request.GetResponse())
{
using (var stream = response.GetResponseStream())
{
StreamReader reader
= new StreamReader(stream);
string responseString = reader.ReadToEnd();
UserDetails getUser
= JsonConvert.DeserializeObject<UserDetails>(responseString);

return getUser;
}
}
}
catch (WebException ex)
{
System.Diagnostics.Trace.TraceError(ex.ToString());
return null;
}
}
catch (WebException ex)
{
System.Diagnostics.Trace.TraceError(ex.ToString());
return null;
}
}

public AadGroups GetGroups()
{
// check if token is expired or about to expire in 2 minutes
if (this._aadAuthentication.AadAuthenticationResult.IsExpired() || this._aadAuthentication.AadAuthenticationResult.WillExpireIn(2))
{
//this._aadAuthentication.AadAuthenticationResult = this._aadAuthentication.GetNewAuthenticationResult(ref strErrors);
}

if (this._aadAuthentication.AadAuthenticationResult == null)
return null;

string authnHeader = "Authorization: " + this._aadAuthentication.AadAuthenticationResult.AccessToken;

string uri = this._graphUrl + "/" + this._tenantId +"/groups" + "?" + this.ApiVersion;

try
{
HttpWebRequest request
= (HttpWebRequest)WebRequest.Create(uri);
System.Text.ASCIIEncoding encoding
= new System.Text.ASCIIEncoding();

request.Headers.Add(authnHeader);
request.Method
= "GET";

using (HttpWebResponse response = request.GetResponse() as HttpWebResponse)
{
if (response.StatusCode != HttpStatusCode.OK)
throw new Exception(String.Format(
"Server error (HTTP {0}: {1}).",
response.StatusCode,
response.StatusDescription));
else
using (var stream = response.GetResponseStream())
{
StreamReader reader
= new StreamReader(stream);
string responseString = reader.ReadToEnd();
AadGroups groupList
= JsonConvert.DeserializeObject<AadGroups>(responseString);

return groupList;
}
}
}

catch (WebException ex)
{
System.Diagnostics.Trace.TraceError(ex.ToString());
return null;
}
}

public IList<object> GetUserGroups(string name)
{
//AadGroups groups = null;
IList<object> groups = new List<object>();
try
{
// check if token is expired or about to expire in 2 minutes
if (this._aadAuthentication.AadAuthenticationResult.IsExpired() || this._aadAuthentication.AadAuthenticationResult.WillExpireIn(2))
{
//this._aadAuthentication.AadAuthenticationResult = this._aadAuthentication.GetNewAuthenticationResult(ref strErrors);
}

string authnHeader = "Authorization: " + this._aadAuthentication.AadAuthenticationResult.AccessToken;

string uri = this._graphUrl + "/" + this._tenantId + "/users/" + name + "/memberOf" +"?" + this._apiVersion;

try
{
HttpWebRequest request
= (HttpWebRequest)WebRequest.Create(uri);
System.Text.ASCIIEncoding encoding
= new System.Text.ASCIIEncoding();

request.Headers.Add(authnHeader);
request.Method
= "GET";
using (var response = request.GetResponse())
{
using (var stream = response.GetResponseStream())
{
StreamReader reader
= new StreamReader(stream);
string responseString = reader.ReadToEnd();
dynamic _groups
= JsonConvert.DeserializeObject<dynamic>(responseString);
foreach (object item in _groups.value)
{
groups.Add(item);
}
Newtonsoft.Json.Linq.JObject groups1
= Newtonsoft.Json.Linq.JObject.Parse(responseString);

}
}
}
catch (WebException ex)
{
System.Diagnostics.Trace.TraceError(ex.ToString());
return null;
}
}
catch (WebException ex)
{
System.Diagnostics.Trace.TraceError(ex.ToString());
return null;
}

return groups;
}

private ExtensionRepresentation CreateExtension(ExtensionRepresentation extension)
{
if (this._aadAuthentication.AadAuthenticationResult.IsExpired() || this._aadAuthentication.AadAuthenticationResult.WillExpireIn(2))
{
//this.aadAuthentication.AadAuthenticationResult = this.aadAuthentication.GetNewAuthenticationResult(ref strErrors);
}

if (this._aadAuthentication.AadAuthenticationResult == null)
return null;

string authnHeader = "Authorization: " + this._aadAuthentication.AadAuthenticationResult.AccessToken;

string uri = this._graphUrl + "/" + this._tenantId + "/applications/" + this._appObjectId + "/extensionProperties" + "?" + this.ApiVersion;

JsonSerializerSettings jsonSettings
= new JsonSerializerSettings();
jsonSettings.NullValueHandling
= NullValueHandling.Ignore;
jsonSettings.DefaultValueHandling
= DefaultValueHandling.Ignore;
JsonSerializer serializer
= JsonSerializer.CreateDefault(jsonSettings);
string body = JsonConvert.SerializeObject(JObject.FromObject(extension, serializer), Formatting.None, jsonSettings);

try
{
HttpWebRequest request
= (HttpWebRequest)WebRequest.Create(uri);
System.Text.ASCIIEncoding encoding
= new System.Text.ASCIIEncoding();

request.Headers.Add(authnHeader);
request.Method
= "POST";
request.ContentType
= "application/json";
byte[] data = encoding.GetBytes(body);
request.ContentLength
= data.Length;
using (Stream stream = request.GetRequestStream())
{
stream.Write(data,
0, data.Length);
}

using (HttpWebResponse response = request.GetResponse() as HttpWebResponse)
{
if (response.StatusCode != HttpStatusCode.Created)
{
throw new Exception(String.Format(
"Server error (HTTP {0}: {1}).",
response.StatusCode,
response.StatusDescription));
}
else
using (var stream = response.GetResponseStream())
{
string payload;
using (System.IO.StreamReader reader = new System.IO.StreamReader(stream))
{
payload
= reader.ReadToEnd();
}

return JObject.Parse(payload).ToObject<ExtensionRepresentation>();
}
}
}
catch (WebException ex)
{
System.Diagnostics.Trace.TraceError(ex.ToString());
return null;
}
}

public AadGroup CreateNewGroup(string groupName)
{
AadGroup groupObject;

GroupCreationModel group
= new GroupCreationModel();
group.displayName
= groupName;
group.mailNickname
= groupName;
group.mailEnabled
="false";
group.securityEnabled
= "true";


if (this._aadAuthentication.AadAuthenticationResult.IsExpired() || this._aadAuthentication.AadAuthenticationResult.WillExpireIn(2))
{
//this.aadAuthentication.AadAuthenticationResult = this.aadAuthentication.GetNewAuthenticationResult(ref strErrors);
}

if (this._aadAuthentication.AadAuthenticationResult == null)
return null;

string authnHeader = "Authorization: " + this._aadAuthentication.AadAuthenticationResult.AccessToken;

string uri = this._graphUrl + "/" + this._tenantId + "/groups" + "?" + this.ApiVersion;

JsonSerializerSettings jsonSettings
= new JsonSerializerSettings();
jsonSettings.NullValueHandling
= NullValueHandling.Ignore;
jsonSettings.DefaultValueHandling
= DefaultValueHandling.Ignore;
JsonSerializer serializer
= JsonSerializer.CreateDefault(jsonSettings);
string body = JsonConvert.SerializeObject(JObject.FromObject(group, serializer), Formatting.None, jsonSettings);

try
{
HttpWebRequest request
= (HttpWebRequest)WebRequest.Create(uri);
System.Text.ASCIIEncoding encoding
= new System.Text.ASCIIEncoding();

request.Headers.Add(authnHeader);
request.Method
= "POST";
request.ContentType
= "application/json";
byte[] data = encoding.GetBytes(body);
request.ContentLength
= data.Length;
using (Stream stream = request.GetRequestStream())
{
stream.Write(data,
0, data.Length);
}

using (HttpWebResponse response = request.GetResponse() as HttpWebResponse)
{
if (response.StatusCode != HttpStatusCode.Created)
{
throw new Exception(String.Format(
"Server error (HTTP {0}: {1}).",
response.StatusCode,
response.StatusDescription));
}
else
using (var stream = response.GetResponseStream())
{
string payload;
using (System.IO.StreamReader reader = new System.IO.StreamReader(stream))
{
payload
= reader.ReadToEnd();
groupObject
= JsonConvert.DeserializeObject<AadGroup>(payload);
}
}
}
}
catch (WebException ex)
{
System.Diagnostics.Trace.TraceError(ex.ToString());
return null;
}

return groupObject;
}

public bool RegisterNewExtension(string strExtension, ExtensionTarget extensionTarget)
{
string target = "";

if (CheckExtensionRegistered(strExtension) == null)
{
// setup the extension definition
ExtensionRepresentation extension = new ExtensionRepresentation();
extension.name
= strExtension;
extension.dataType
= "String";
switch (extensionTarget)
{
case ExtensionTarget.User: target = "User"; break;
case ExtensionTarget.Group: target = "Group"; break;
case ExtensionTarget.TenantDetail: target = "TenantDetail"; break;
case ExtensionTarget.Application: target = "Application"; break;
case ExtensionTarget.ServicePrincipal: target = "ServicePrincipal"; break;
default: target = "User"; break;
}
extension.targetObjects.Add(target);

// Execute the POST to create new extension
ExtensionRepresentation returnedExtension = this.CreateExtension(extension);
return returnedExtension != null;
}
else
return true;
}

public bool UnRegisterExtension(string strExtension)
{
string extensionObjectId = this.CheckExtensionRegisteredAndReturningExtensionObjectId(strExtension);

if (extensionObjectId != null)
{
return this.RemoveExtensionRegistered(extensionObjectId);
}
else
return true;
}

public bool RegisterClientExtensionsInGroups()
{
bool result = false;

if (RegisterNewExtension("ClientId", AADGraphAPIManager.Services.GraphAPIQuery.ExtensionTarget.Group))
{
if (RegisterNewExtension("ClientName", AADGraphAPIManager.Services.GraphAPIQuery.ExtensionTarget.Group))
{
if (RegisterNewExtension("ClientAddress", AADGraphAPIManager.Services.GraphAPIQuery.ExtensionTarget.Group))
{
if (RegisterNewExtension("ClientPostCode", AADGraphAPIManager.Services.GraphAPIQuery.ExtensionTarget.Group))
{
if (RegisterNewExtension("ClientCountry", AADGraphAPIManager.Services.GraphAPIQuery.ExtensionTarget.Group))
{
result
= true;
}
}
}
}
}

return result;
}
#endregion

#region [ Private Methods ]
public string CheckExtensionRegistered(string extensionName)
{
string responseString = string.Empty;
// check if token is expired or about to expire in 2 minutes
if (this._aadAuthentication.AadAuthenticationResult.IsExpired() || this._aadAuthentication.AadAuthenticationResult.WillExpireIn(2))
{
//this._aadAuthentication.AadAuthenticationResult = this._aadAuthentication.GetNewAuthenticationResult(ref strErrors);
}

string authnHeader = "Authorization: " + this._aadAuthentication.AadAuthenticationResult.AccessToken;

string uri = this._graphUrl + "/" + this._tenantId + "/applications/" + this._appObjectId + "/extensionProperties" + "?" + this._apiVersion;

try
{
HttpWebRequest request
= (HttpWebRequest)WebRequest.Create(uri);
System.Text.ASCIIEncoding encoding
= new System.Text.ASCIIEncoding();

request.Headers.Add(authnHeader);
request.Method
= "GET";
using (var response = request.GetResponse())
{
using (var stream = response.GetResponseStream())
{
StreamReader reader
= new StreamReader(stream);
responseString
= reader.ReadToEnd();
}
}
}
catch (WebException ex)
{
System.Diagnostics.Trace.TraceError(ex.ToString());
return null;
}

//##
var extensionproperties = JsonConvert.DeserializeObject<ExtensionPropertiesContext>(responseString);

if (extensionproperties.value.Count == 0)
return null;
else
{
var extensions
= extensionproperties.value;
for (int i = 0; i < extensionproperties.value.Count; i++)
{
if (extensionproperties.value[i].name.Contains(extensionName))
return extensionproperties.value[i].name;
}
}

return null;
}

private string CheckExtensionRegisteredAndReturningExtensionObjectId(string extensionName)
{
string responseString = string.Empty;
// check if token is expired or about to expire in 2 minutes
if (this._aadAuthentication.AadAuthenticationResult.IsExpired() || this._aadAuthentication.AadAuthenticationResult.WillExpireIn(2))
{
//this._aadAuthentication.AadAuthenticationResult = this._aadAuthentication.GetNewAuthenticationResult(ref strErrors);
}

string authnHeader = "Authorization: " + this._aadAuthentication.AadAuthenticationResult.AccessToken;

string uri = this._graphUrl + "/" + this._tenantId + "/applications/" + this._appObjectId + "/extensionProperties" + "?" + this._apiVersion;

try
{
HttpWebRequest request
= (HttpWebRequest)WebRequest.Create(uri);
System.Text.ASCIIEncoding encoding
= new System.Text.ASCIIEncoding();

request.Headers.Add(authnHeader);
request.Method
= "GET";
using (var response = request.GetResponse())
{
using (var stream = response.GetResponseStream())
{
StreamReader reader
= new StreamReader(stream);
responseString
= reader.ReadToEnd();
}
}
}
catch (WebException ex)
{
System.Diagnostics.Trace.TraceError(ex.ToString());
return null;
}

//##
var extensionproperties = JsonConvert.DeserializeObject<ExtensionPropertiesContext>(responseString);

if (extensionproperties.value.Count == 0)
return null;
else
{
var extensions
= extensionproperties.value;
for (int i = 0; i < extensionproperties.value.Count; i++)
{
if (extensionproperties.value[i].name.Contains(extensionName))
return extensionproperties.value[i].objectId;
}
}

return null;
}

private bool RemoveExtensionRegistered(string extensionObjectId)
{
string responseString = string.Empty;
// check if token is expired or about to expire in 2 minutes
if (this._aadAuthentication.AadAuthenticationResult.IsExpired() || this._aadAuthentication.AadAuthenticationResult.WillExpireIn(2))
{
//this._aadAuthentication.AadAuthenticationResult = this._aadAuthentication.GetNewAuthenticationResult(ref strErrors);
}

string authnHeader = "Authorization: " + this._aadAuthentication.AadAuthenticationResult.AccessToken;

string uri = this._graphUrl + "/" + this._tenantId + "/applications/" + this._appObjectId + "/extensionProperties/" + extensionObjectId + "?" + this._apiVersion;

try
{
HttpWebRequest request
= (HttpWebRequest)WebRequest.Create(uri);
System.Text.ASCIIEncoding encoding
= new System.Text.ASCIIEncoding();
request.Headers.Add(authnHeader);
request.Method
= "DELETE";
using (HttpWebResponse response = request.GetResponse() as HttpWebResponse)
{
if (response.StatusCode != HttpStatusCode.NoContent)
{
throw new Exception(String.Format(
"Server error (HTTP {0}: {1}).",
response.StatusCode,
response.StatusDescription));

return false;
}
else
{
return true;
}
}
}
catch (WebException ex)
{
System.Diagnostics.Trace.TraceError(ex.ToString());
return false;
}

//##
}

public bool SetExtensionValue(string entityName, string extensionName, string extensionValue, ExtensionTarget extensionTarget)
{
string target = string.Empty;

switch (extensionTarget)
{
case ExtensionTarget.User: target = "users"; break;
case ExtensionTarget.Group: target = "groups"; break;
case ExtensionTarget.TenantDetail: target = "tenantdetails"; break;
case ExtensionTarget.Application: target = "applications"; break;
case ExtensionTarget.ServicePrincipal: target = "servicePrincipals"; break;
default: target = "users"; break;
}
////Get the objectId for this particular app
//string requestUrl = String.Format(
// CultureInfo.InvariantCulture,
// GraphExtensionValueUrl,
// HttpUtility.UrlEncode(tenantId),
// HttpUtility.UrlEncode(upn));

//HttpClient client = new HttpClient();
//client.DefaultRequestHeaders.ExpectContinue = false;
////PATCH isn't a default method
//HttpRequestMessage request = new HttpRequestMessage(new HttpMethod("PATCH"), requestUrl);
//request.Headers.TryAddWithoutValidation("Authorization", authHeader);

//string extensionProperty = "{\"" + extensionName + "\":\"" + extensionValue + "\"}";

//request.Content = new StringContent(extensionProperty, System.Text.Encoding.UTF8, "application/json");

//HttpResponseMessage response = await client.SendAsync(request);
//string responseString = await response.Content.ReadAsStringAsync();
//if (response.StatusCode == HttpStatusCode.NoContent)
//{
// return true;
//}
//else
// return false;

//##
if (this._aadAuthentication.AadAuthenticationResult.IsExpired() || this._aadAuthentication.AadAuthenticationResult.WillExpireIn(2))
{
//this._aadAuthentication.AadAuthenticationResult = this._aadAuthentication.GetNewAuthenticationResult(ref strErrors);
}

string authnHeader = "Authorization: " + this._aadAuthentication.AadAuthenticationResult.AccessToken;

string uri = this._graphUrl + "/" + this._tenantId + "/" + target + "/" + entityName + "" + "?" + this._apiVersion;

try
{
HttpWebRequest request
= (HttpWebRequest)WebRequest.Create(uri);
System.Text.ASCIIEncoding encoding
= new System.Text.ASCIIEncoding();

string body = "{\"" + extensionName + "\":\"" + extensionValue + "\"}";

request.Headers.Add(authnHeader);
request.Method
= "PATCH";
request.ContentType
= "application/json";
byte[] data = encoding.GetBytes(body);
request.ContentLength
= data.Length;
using (Stream stream = request.GetRequestStream())
{
stream.Write(data,
0, data.Length);
}

using (HttpWebResponse response = request.GetResponse() as HttpWebResponse)
{
if (response.StatusCode != HttpStatusCode.NoContent)
return true;
else
return false;
}
}

catch (WebException ex)
{
System.Diagnostics.Trace.TraceError(ex.ToString());
return false;
}
}

public bool CheckIfGroupExists(string groupName)
{
string responseString = string.Empty;
AadGroups groupList;
// check if token is expired or about to expire in 2 minutes
if (this._aadAuthentication.AadAuthenticationResult.IsExpired() || this._aadAuthentication.AadAuthenticationResult.WillExpireIn(2))
{
//this._aadAuthentication.AadAuthenticationResult = this._aadAuthentication.GetNewAuthenticationResult(ref strErrors);
}

string authnHeader = "Authorization: " + this._aadAuthentication.AadAuthenticationResult.AccessToken;

string uri = this._graphUrl + "/" + this._tenantId + "/groups?$filter=displayName eq '" + groupName+"'"+ "&" + this._apiVersion;

try
{
HttpWebRequest request
= (HttpWebRequest)WebRequest.Create(uri);
System.Text.ASCIIEncoding encoding
= new System.Text.ASCIIEncoding();

request.Headers.Add(authnHeader);
request.Method
= "GET";
using (var response = request.GetResponse())
{
using (var stream = response.GetResponseStream())
{
StreamReader reader
= new StreamReader(stream);
responseString
= reader.ReadToEnd();
groupList
= JsonConvert.DeserializeObject<AadGroups>(responseString);
}
}
}
catch (WebException ex)
{
System.Diagnostics.Trace.TraceError(ex.ToString());
return false;
}

return groupList.group.Count>0?true:false;
}

private string GetAppObjectId()
{
string appObjectId = string.Empty;

// check if token is expired or about to expire in 2 minutes
if (this._aadAuthentication.AadAuthenticationResult.IsExpired() || this._aadAuthentication.AadAuthenticationResult.WillExpireIn(2))
{
//this._aadAuthentication.AadAuthenticationResult = this._aadAuthentication.GetNewAuthenticationResult(ref strErrors);
}

if (this._aadAuthentication.AadAuthenticationResult == null)
return null;

string authnHeader = "Authorization: " + this._aadAuthentication.AadAuthenticationResult.AccessToken;

string uri = this._graphUrl + "/" + this._tenantId + "/applications" + "?" + this.ApiVersion;

try
{
HttpWebRequest request
= (HttpWebRequest)WebRequest.Create(uri);
System.Text.ASCIIEncoding encoding
= new System.Text.ASCIIEncoding();

request.Headers.Add(authnHeader);
request.Method
= "GET";

using (HttpWebResponse response = request.GetResponse() as HttpWebResponse)
{
if (response.StatusCode != HttpStatusCode.OK)
throw new Exception(String.Format(
"Server error (HTTP {0}: {1}).",
response.StatusCode,
response.StatusDescription));
else
using (var stream = response.GetResponseStream())
{
StreamReader reader
= new StreamReader(stream);
string responseString = reader.ReadToEnd();
AppContext appContext
= JsonConvert.DeserializeObject<AppContext>(responseString);

//Iterate through the list to find the correct application,
//and retrieve it's object id
for (int i = 0; i < appContext.value.Count; i++)
{
if (appContext.value[i].appId == this._appPrincipalId)
{
appObjectId
= appContext.value[i].objectId;

}
}
}
}
}

catch (WebException ex)
{
System.Diagnostics.Trace.TraceError(ex.ToString());
return null;
}


return appObjectId;
}

#endregion

#endregion

}
}


 


Let’s go to add under our folder “Services” a new interface called IGraphAPIQuery.cs and add the code below.


using AADGraphAPIManager.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace AADGraphAPIManager.Services
{
public interface IGraphAPIQuery
{
UserDetails GetUserByObjectId(
string userId);
UserDetails GetUserByName(
string name);
AadGroups GetGroups();
IList
<object> GetUserGroups(string name);
AadGroup CreateNewGroup(
string groupName);
bool RegisterNewExtension(string strExtension, AADGraphAPIManager.Services.GraphAPIQuery.ExtensionTarget extensionTarget);
bool UnRegisterExtension(string strExtension);
bool RegisterClientExtensionsInGroups();
string CheckExtensionRegistered(string extensionName);
bool SetExtensionValue(string entityName, string extensionName, string extensionValue, AADGraphAPIManager.Services.GraphAPIQuery.ExtensionTarget extensionTarget);
bool CheckIfGroupExists(string groupName);
}
}

All these operations are the basic ones, we will extend the code doing a wrapper around the GraphAPIQuery. I am not going to explain all the process, but you have to be focus in adding extensions. To add an extension to a group or user you need to create the group or the user first. That basically means you have to do a double call. Unfortunately there is no way around you.


Another issue comes when you go to Azure AD to edit the groups or users, don’t expect to find the extensions them on the Azure portal, so you need to create a User Interface for that with CRUD operations via Web.


I am going to post the code, so you can have a look, but remember this is a beta version Smile , there is plenty of stuff it needs to be cleaned. At least you will be able to see how you can create extensions for Groups and Users, so you can extend it for your own purposes. I have mixed different ways to do the calls as well as serialization, so you can grab your favourites.


To make this work, you need to instantiate UserOperations and pass the appPrincipalId (which is the ClientID of the Azure Application Instance (ie: string like this "8353c878-c925-4567-b900-0985b0805e6a")), the appKey (which is the unique key of the Azure Application Instance (ie: string like this "GJu+cHCkinvK9HvSY60LDH7347x4CgMvXJz2udiEzes=")) and the ClaimsPrincipal (you just need to pass ClaimPrincipal.Current).


UserOperations useroperations = new UserOperations("8353c878-c925-4567-b900-0985b0805e6a","GJu+cHCkinvK9HvSY60LDH7347x4CgMvXJz2udiEzes=",ClaimsPrincipal.Current);

Download Code Here