Last active
August 29, 2015 13:58
-
-
Save ahelland/10165845 to your computer and use it in GitHub Desktop.
Code for the blog post "Extending Your Azure Active Directory - Part 1" - on http://mobilitydojo.net/2014/04/08/extending-your-azure-active-directory-part-1/
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Microsoft.IdentityModel.Clients.ActiveDirectory; | |
using Newtonsoft.Json; | |
using System; | |
using System.Configuration; | |
using System.Globalization; | |
using System.Net.Http; | |
using System.Security.Claims; | |
using System.Threading.Tasks; | |
using System.Web; | |
using System.Web.Mvc; | |
using DirectoryExtensions.Models; | |
using System.Net; | |
namespace DirectoryExtensions.Controllers | |
{ | |
[Authorize] | |
public class AzureADController : Controller | |
{ | |
private const string TenantIdClaimType = "http://schemas.microsoft.com/identity/claims/tenantid"; | |
private const string LoginUrl = "https://login.windows.net/{0}"; | |
private const string ApiVersion = "1.21-preview"; | |
private const string GraphUrl = "https://graph.windows.net"; | |
private const string GraphUserUrl = "https://graph.windows.net/{0}/users/{1}?api-version=" + ApiVersion; | |
private const string GraphExtensionValueUrl = "https://graph.windows.net/{0}/users/{1}?api-version=" + ApiVersion; | |
private const string GraphApps = "https://graph.windows.net/{0}/applications?api-version=" + ApiVersion; | |
private const string GraphAppUrl = "https://graph.windows.net/{0}/applications/{1}/extensionProperties?api-version=" + ApiVersion; | |
private const string GraphExtensionUrl = "https://graph.windows.net/{0}/applications/{1}/extensionProperties?api-version=" + ApiVersion; | |
private static readonly string AppPrincipalId = ConfigurationManager.AppSettings["ida:ClientID"]; | |
private static readonly string AppKey = ConfigurationManager.AppSettings["ida:Password"]; | |
public async Task<ActionResult> Index() | |
{ | |
string tenantId = ClaimsPrincipal.Current.FindFirst(TenantIdClaimType).Value; | |
// Get a token for calling the Windows Azure Active Directory Graph | |
AuthenticationContext authContext = new AuthenticationContext(String.Format(CultureInfo.InvariantCulture, LoginUrl, tenantId)); | |
ClientCredential credential = new ClientCredential(AppPrincipalId, AppKey); | |
AuthenticationResult assertionCredential = authContext.AcquireToken(GraphUrl, credential); | |
string authHeader = assertionCredential.CreateAuthorizationHeader(); | |
string requestUrl = String.Format( | |
CultureInfo.InvariantCulture, | |
GraphUserUrl, | |
HttpUtility.UrlEncode(tenantId), | |
HttpUtility.UrlEncode(User.Identity.Name)); | |
HttpClient client = new HttpClient(); | |
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUrl); | |
request.Headers.TryAddWithoutValidation("Authorization", authHeader); | |
HttpResponseMessage response = await client.SendAsync(request); | |
string responseString = await response.Content.ReadAsStringAsync(); | |
UserDetails user = JsonConvert.DeserializeObject<UserDetails>(responseString); | |
//Since the extension name isn't known in advance it's not included in the default serialization, | |
//so we extract it manually after looking up the name. | |
string appObjectId = await getAppObjectId(tenantId, authHeader); | |
string extensionName = string.Empty; | |
extensionName = await checkExtensionRegistered(tenantId, authHeader, appObjectId); | |
Newtonsoft.Json.Linq.JObject jUser = Newtonsoft.Json.Linq.JObject.Parse(responseString); | |
string YubiKeyValue = (string)jUser[extensionName]; | |
user.YubiKeyId = YubiKeyValue; | |
return View(user); | |
} | |
[HttpPost] | |
[ValidateAntiForgeryToken] | |
public async Task<ActionResult> Index(UserDetails user, string YubiKeyAction) | |
{ | |
string tenantId = ClaimsPrincipal.Current.FindFirst(TenantIdClaimType).Value; | |
// Get a token for calling the Windows Azure Active Directory Graph | |
AuthenticationContext authContext = new AuthenticationContext(String.Format(CultureInfo.InvariantCulture, LoginUrl, tenantId)); | |
ClientCredential credential = new ClientCredential(AppPrincipalId, AppKey); | |
AuthenticationResult assertionCredential = authContext.AcquireToken(GraphUrl, credential); | |
string authHeader = assertionCredential.CreateAuthorizationHeader(); | |
string appObjectId = await getAppObjectId(tenantId, authHeader); | |
string extensionName = string.Empty; | |
//Check if YubiKeyId extension is registered by trying to get the id | |
extensionName = await checkExtensionRegistered(tenantId, authHeader, appObjectId); | |
if (extensionName == "false") | |
extensionName = await registerExtension(tenantId,authHeader,appObjectId); | |
if (YubiKeyAction == "Register") | |
await setExtensionValue(tenantId, authHeader, user.userPrincipalName , extensionName, user.YubiKeyId); | |
if (YubiKeyAction == "Unregister") | |
{ | |
bool unregOK = await setExtensionValue(tenantId, authHeader, user.userPrincipalName, extensionName, ""); | |
if (unregOK) | |
user.YubiKeyId = string.Empty; | |
} | |
//We want to reload the page so we return RedirectToAction(...) instead of View(...) | |
return RedirectToAction("Index"); | |
} | |
private async Task<bool> setExtensionValue(string tenantId, string authHeader, string upn, string extensionName, string extensionValue) | |
{ | |
//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; | |
} | |
private async Task<string> getAppObjectId(string tenantId, string authHeader) | |
{ | |
string appObjectId = string.Empty; | |
//Get the objectId for this particular app | |
string requestUrl = String.Format( | |
CultureInfo.InvariantCulture, | |
GraphApps, | |
HttpUtility.UrlEncode(tenantId)); | |
HttpClient client = new HttpClient(); | |
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUrl); | |
request.Headers.TryAddWithoutValidation("Authorization", authHeader); | |
HttpResponseMessage response = await client.SendAsync(request); | |
string responseString = await response.Content.ReadAsStringAsync(); | |
var apps = JsonConvert.DeserializeObject<AppContext>(responseString); | |
//Iterate through the list to find the correct application, | |
//and retrieve it's object id | |
for (int i = 0; i < apps.value.Count; i++) | |
{ | |
if (apps.value[i].appId == AppPrincipalId) | |
{ | |
appObjectId = apps.value[i].objectId; | |
} | |
} | |
return appObjectId; | |
} | |
private async Task<string> checkExtensionRegistered(string tenantId, string authHeader, string appObjectId) | |
{ | |
//Get extensions for this app | |
string requestUrl = String.Format( | |
CultureInfo.InvariantCulture, | |
GraphAppUrl, | |
HttpUtility.UrlEncode(tenantId), | |
HttpUtility.UrlEncode(appObjectId)); | |
HttpClient client = new HttpClient(); | |
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUrl); | |
request.Headers.TryAddWithoutValidation("Authorization", authHeader); | |
HttpResponseMessage response = await client.SendAsync(request); | |
string responseString = await response.Content.ReadAsStringAsync(); | |
var extensionproperties = JsonConvert.DeserializeObject<ExtensionPropertiesContext>(responseString); | |
if (extensionproperties.value.Count == 0) | |
return "false"; | |
else | |
{ | |
//Hardcoded "YubiKeyId" as extension value | |
var extensions = extensionproperties.value; | |
for (int i=0; i<extensionproperties.value.Count;i++) | |
{ | |
if (extensionproperties.value[i].name.Contains("YubiKeyId")) | |
return extensionproperties.value[i].name; | |
} | |
} | |
return "false"; | |
} | |
private async Task<string> registerExtension(string tenantId, string authHeader, string appObjectId) | |
{ | |
//Get the objectId for this particular app | |
string requestUrl = String.Format( | |
CultureInfo.InvariantCulture, | |
GraphExtensionUrl, | |
HttpUtility.UrlEncode(tenantId), | |
HttpUtility.UrlEncode(appObjectId)); | |
HttpClient client = new HttpClient(); | |
client.DefaultRequestHeaders.ExpectContinue = false; | |
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, requestUrl); | |
request.Headers.TryAddWithoutValidation("Authorization", authHeader); | |
request.Content = new StringContent("{\"name\": \"YubiKeyId\",\"dataType\": \"String\",\"targetObjects\": [\"User\"]}", System.Text.Encoding.UTF8, "application/json"); | |
HttpResponseMessage response = await client.SendAsync(request); | |
string responseString = await response.Content.ReadAsStringAsync(); | |
if (response.StatusCode == HttpStatusCode.Created) | |
{ | |
var extension = JsonConvert.DeserializeObject<ExtensionProperty>(responseString); | |
return extension.name; | |
} | |
else | |
return string.Empty; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Newtonsoft.Json; | |
using System.Collections.Generic; | |
using System.ComponentModel; | |
namespace DirectoryExtensions.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; } | |
[DisplayName("YubiKey ID")] | |
public string YubiKeyId { 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; } | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@model DirectoryExtensions.Models.UserDetails | |
@{ | |
ViewBag.Title = "UserProfile - " + User.Identity.Name; | |
} | |
<h4>Current User: @User.Identity.Name</h4> | |
@{ | |
using (Html.BeginForm()) | |
{ | |
@Html.AntiForgeryToken() | |
@Html.ValidationSummary(true) | |
//We want all values except the YubiKey to be read-only, | |
//yet we want to POST them so we include them as hidden fields as well | |
@Html.HiddenFor(model => model.userPrincipalName) | |
@Html.HiddenFor(model => model.displayName) | |
@Html.HiddenFor(model => model.givenName) | |
@Html.HiddenFor(model => model.surname) | |
@Html.HiddenFor(model => model.jobTitle) | |
@Html.HiddenFor(model => model.department) | |
@Html.HiddenFor(model => model.mobile) | |
@Html.HiddenFor(model => model.city) | |
@Html.HiddenFor(model => model.streetAddress) | |
@Html.HiddenFor(model => model.country) | |
@Html.HiddenFor(model => model.postalCode) | |
@Html.HiddenFor(model => model.telephoneNumber) | |
@Html.HiddenFor(model => model.lastDirSyncTime) | |
<table class="table"> | |
<tr> | |
<td>@Html.DisplayNameFor(model => model.displayName)</td> | |
<td>@Html.DisplayFor(model => model.displayName)</td> | |
</tr> | |
<tr> | |
<td>@Html.DisplayNameFor(model => model.givenName)</td> | |
<td>@Html.DisplayFor(model => model.givenName)</td> | |
</tr> | |
<tr> | |
<td>@Html.DisplayNameFor(model => model.surname)</td> | |
<td>@Html.DisplayFor(model => model.surname)</td> | |
</tr> | |
<tr> | |
<td>@Html.DisplayNameFor(model => model.jobTitle)</td> | |
<td>@Html.DisplayFor(model => model.jobTitle)</td> | |
</tr> | |
<tr> | |
<td>@Html.DisplayNameFor(model => model.department)</td> | |
<td>@Html.DisplayFor(model => model.department)</td> | |
</tr> | |
<tr> | |
<td>@Html.DisplayNameFor(model => model.mobile)</td> | |
<td>@Html.DisplayFor(model => model.mobile)</td> | |
</tr> | |
<tr> | |
<td>@Html.DisplayNameFor(model => model.city)</td> | |
<td>@Html.DisplayFor(model => model.city)</td> | |
</tr> | |
<tr> | |
<td>@Html.DisplayNameFor(model => model.streetAddress)</td> | |
<td>@Html.DisplayFor(model => model.streetAddress)</td> | |
</tr> | |
<tr> | |
<td>@Html.DisplayNameFor(model => model.country)</td> | |
<td>@Html.DisplayFor(model => model.country)</td> | |
</tr> | |
<tr> | |
<td>@Html.DisplayNameFor(model => model.postalCode)</td> | |
<td>@Html.DisplayFor(model => model.postalCode)</td> | |
</tr> | |
<tr> | |
<td>@Html.DisplayNameFor(model => model.telephoneNumber)</td> | |
<td>@Html.DisplayFor(model => model.telephoneNumber)</td> | |
</tr> | |
<tr> | |
<td>@Html.DisplayNameFor(model => model.lastDirSyncTime) </td> | |
<td>@Html.DisplayFor(model => model.lastDirSyncTime)</td> | |
</tr> | |
<tr> | |
<td>@Html.DisplayNameFor(model => model.YubiKeyId)</td> | |
<td>@Html.EditorFor(model => model.YubiKeyId)</td> | |
</tr> | |
</table> | |
if (Model.YubiKeyId.IsEmpty()) | |
{ | |
@Html.Hidden("YubiKeyAction","Register") | |
<div class="form-group"> | |
<div class="col-md-offset-2 col-md-10"> | |
<input type="submit" value="Register YubiKey" class="btn btn-default" /> | |
</div> | |
</div> | |
} | |
if (!Model.YubiKeyId.IsEmpty()) | |
{ | |
@Html.Hidden("YubiKeyAction","Unregister") | |
<div class="form-group"> | |
<div class="col-md-offset-2 col-md-10"> | |
<input type="submit" value="Unregister YubiKey" class="btn btn-default" /> | |
</div> | |
</div> | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment