Last active
August 29, 2015 13:56
-
-
Save pksorensen/9145353 to your computer and use it in GitHub Desktop.
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 System; | |
using System.Threading.Tasks; | |
using Microsoft.Owin; | |
using Owin; | |
using Microsoft.Owin.Security.WsFederation; | |
using System.Linq; | |
using System.Globalization; | |
using System.Web; | |
using Microsoft.Owin.Security.Cookies; | |
using System.Collections.Concurrent; | |
using Microsoft.Owin.Logging; | |
using System.Configuration; | |
using Microsoft.IdentityModel.Clients.ActiveDirectory; | |
using System.Net.Http; | |
using System.Security.Claims; | |
using Newtonsoft.Json; | |
using Microsoft.WindowsAzure.Subscriptions; | |
using Microsoft.WindowsAzure; | |
using System.Threading; | |
[assembly: OwinStartup(typeof(WebApplication35.Startup))] | |
namespace WebApplication35 | |
{ | |
public class DatabaseIssuerNameRegistry | |
{ | |
private static ConcurrentDictionary<string, DateTimeOffset> PerAppInMemoryLookup = | |
new ConcurrentDictionary<string, DateTimeOffset>(); | |
private static ConcurrentBag<string> Tenants = new ConcurrentBag<string>(); | |
internal static void CleanUpExpiredSignupTokens() | |
{ | |
DateTimeOffset now = DateTimeOffset.UtcNow; | |
foreach (var key in PerAppInMemoryLookup.Where(token => token.Value <= now).Select(k => k.Key)) | |
{ | |
DateTimeOffset _; | |
PerAppInMemoryLookup.TryRemove(key, out _); | |
} | |
} | |
internal static void AddSignupToken(string signupToken, DateTimeOffset expirationTime) | |
{ | |
PerAppInMemoryLookup.TryAdd(signupToken, expirationTime); | |
} | |
internal static bool ContainsTenant(string tenantId) | |
{ | |
return Tenants.Any(t => t == tenantId); | |
} | |
internal static bool TryAddTenant(string tenantId, string signupToken) | |
{ | |
if (!ContainsTenant(tenantId)) | |
{ | |
string existingToken = PerAppInMemoryLookup.Where(token => token.Key == signupToken) | |
.Select(token => token.Key).FirstOrDefault(); | |
if (existingToken != null) | |
{ | |
DateTimeOffset _; | |
PerAppInMemoryLookup.TryRemove(existingToken, out _); | |
Tenants.Add(tenantId); | |
return true; | |
} | |
} | |
return false; | |
} | |
} | |
public class Startup | |
{ | |
private const string TenantIdClaimType = "http://schemas.microsoft.com/identity/claims/tenantid"; | |
private const string LoginUrl = "https://login.windows.net/{0}"; | |
private const string GraphUrl = "https://graph.windows.net"; | |
private const string GraphUserUrl = "https://graph.windows.net/{0}/users/{1}?api-version=2013-04-05"; | |
private const string ConsentUrlFormat = "https://account.activedirectory.windowsazure.com/Consent.aspx?ClientId={0}"; | |
private static readonly string AppPrincipalId = ConfigurationManager.AppSettings["ida:ClientID"]; | |
private static readonly string AppKey = ConfigurationManager.AppSettings["ida:Password"]; | |
public void Configuration(IAppBuilder app) | |
{ | |
// For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=316888 | |
app.UseCookieAuthentication(new CookieAuthenticationOptions | |
{ | |
AuthenticationType = WsFederationAuthenticationDefaults.AuthenticationType | |
}); | |
app.UseWsFederationAuthentication(new WsFederationAuthenticationOptions | |
{ | |
TokenValidationParameters = new System.IdentityModel.Tokens.TokenValidationParameters | |
{ | |
//UPDATE Bug has been confirmed that this is not working as intented. | |
// I will update this demo as more information is avalibe. | |
//ValidateIssuer = false -> this causes IssuereValidator to not be run. | |
IssuerValidator = (issuer, token) => | |
{ | |
return false; // it still sign in the user even though its false. | |
return DatabaseIssuerNameRegistry.ContainsTenant(issuer); | |
} | |
}, | |
Wtrealm = "https://owindemo.s-innovations.net", | |
MetadataAddress = "https://login.windows.net/802626c6-0f5c-4293-a8f5-198ecd481fe3/FederationMetadata/2007-06/FederationMetadata.xml" | |
}); | |
app.Map("/login", map => | |
{ | |
map.Run(async ctx => | |
{ | |
if (ctx.Authentication.User == null || | |
!ctx.Authentication.User.Identity.IsAuthenticated) | |
{ | |
ctx.Response.StatusCode = 401; | |
} | |
else | |
{ | |
ctx.Response.Redirect("/"); | |
} | |
}); | |
}); | |
app.Map("/logout", map => | |
{ | |
map.Run(async ctx => | |
{ | |
ctx.Authentication.SignOut(); | |
ctx.Response.Redirect("/"); | |
}); | |
}); | |
app.Map("/signup", map => | |
{ | |
map.Run(async ctx => | |
{ | |
string signupToken = Guid.NewGuid().ToString(); | |
UriBuilder replyUrl = new UriBuilder(ctx.Request.Uri); | |
replyUrl.Path = "/signup-callback"; | |
replyUrl.Query = "signupToken=" + signupToken; | |
DatabaseIssuerNameRegistry.CleanUpExpiredSignupTokens(); | |
DatabaseIssuerNameRegistry.AddSignupToken(signupToken: signupToken, expirationTime: DateTimeOffset.UtcNow.AddMinutes(5)); | |
ctx.Response.Redirect(CreateConsentUrl( | |
clientId: AppPrincipalId, | |
requestedPermissions: "DirectoryReaders",//, CompanyAdministrator", | |
consentReturnUrl: replyUrl.Uri.AbsoluteUri)); | |
}); | |
}); | |
app.Map("/signup-callback", map => | |
{ | |
map.Run(async ctx => | |
{ | |
string tenantId = ctx.Request.Query["TenantId"]; | |
string signupToken = ctx.Request.Query["signupToken"]; | |
if (DatabaseIssuerNameRegistry.ContainsTenant(tenantId)) | |
{ | |
ctx.Response.Redirect("/login"); | |
} | |
string consent = ctx.Request.Query["Consent"]; | |
if (!String.IsNullOrEmpty(tenantId) && | |
String.Equals(consent, "Granted", StringComparison.OrdinalIgnoreCase)) | |
{ | |
if (DatabaseIssuerNameRegistry.TryAddTenant(tenantId, signupToken)) | |
{ | |
ctx.Response.Redirect("/login"); | |
} | |
} | |
await ctx.Response.WriteAsync(@"Sorry, you have to press accept :)"); | |
}); | |
}); | |
app.Map("/azure-management-token", map => | |
{ | |
map.Run(async ctx => | |
{ | |
if (!ctx.Authentication.User.Identity.IsAuthenticated) | |
{ | |
ctx.Response.Redirect("/login"); | |
return; | |
} | |
UriBuilder replyUrl = new UriBuilder(ctx.Request.Uri); | |
replyUrl.Path = "/show-azure-powers"; | |
string authorizationUrl = string.Format( | |
"https://login.windows.net/{0}/oauth2/authorize?api-version=1.0&response_type=code&client_id={1}&resource={2}&redirect_uri={3}", | |
ClaimsPrincipal.Current.FindFirst(TenantIdClaimType).Value, | |
AppPrincipalId, | |
"https://management.core.windows.net/", | |
replyUrl.Uri.AbsoluteUri); | |
ctx.Response.Redirect(authorizationUrl); | |
}); | |
}); | |
app.Map("/show-azure-powers", map => { | |
map.Run(async ctx => | |
{ | |
if(!ctx.Authentication.User.Identity.IsAuthenticated) | |
{ | |
ctx.Response.Redirect("/login"); | |
return; | |
} | |
string code = ctx.Request.Query["code"]; | |
if (code == null) | |
{ | |
ctx.Response.Redirect("/azure-management-token"); | |
return; | |
} | |
AuthenticationContext ac = | |
new AuthenticationContext(string.Format("https://login.windows.net/{0}", | |
ctx.Authentication.User.FindFirst(TenantIdClaimType).Value)); | |
UriBuilder replyUrl = new UriBuilder(ctx.Request.Uri); | |
replyUrl.Path = "/show-azure-powers"; | |
replyUrl.Query = ""; | |
ClientCredential clcred = | |
new ClientCredential(AppPrincipalId, AppKey); | |
AuthenticationResult ar = null; | |
try | |
{ | |
ar = ac.AcquireTokenByAuthorizationCode(code, | |
replyUrl.Uri, clcred); | |
}catch(Exception) | |
{ | |
ctx.Response.Redirect("/login"); | |
return; | |
} | |
ctx.Response.ContentType = "text/html"; | |
using(var azure = new SubscriptionClient(new TokenCloudCredentials(ar.AccessToken))) | |
{ | |
var subscriptions = await azure.Subscriptions.ListAsync(); | |
await ctx.Response.WriteAsync(string.Join("<br/>", subscriptions.Select(s => s.SubscriptionId))); | |
foreach(var cred in subscriptions.Select(id => new TokenCloudCredentials(id.SubscriptionId,ar.AccessToken))) | |
{ | |
await ctx.Response.WriteAsync("<b>Webspaces</b><br/>"); | |
using(var management = CloudContext.Clients.CreateWebSiteManagementClient(cred)) | |
{ | |
var websites = await management.WebSpaces.ListAsync(new System.Threading.CancellationToken()); | |
await ctx.Response.WriteAsync("<br/>" + string.Join("<br/>", websites.Select(w => | |
string.Format("{0}\t{1}\t{2}\t{3}\t{4}\t{5}", w.Name, w.Plan, w.Status, w.WorkerSize, w.GeoRegion, w.GeoLocation)))); | |
} | |
await ctx.Response.WriteAsync("<br/><b>Storage Accounts</b><br/>"); | |
using(var storage = CloudContext.Clients.CreateStorageManagementClient(cred)) | |
{ | |
var storages = await storage.StorageAccounts.ListAsync(new CancellationToken()); | |
await ctx.Response.WriteAsync(string.Join("<br/>", storages.Select(s => string.Format("{0}",s.ServiceName)))+"<br/>"); | |
} | |
} | |
} | |
await ctx.Response.WriteAsync( | |
@" | |
<div> | |
<a href=""/home"">home></a> | |
<a href=""/signup"">signup</a> | |
<a href=""/login"">login</a> | |
<a href=""/show-azure-powers"">show azure powers</a> | |
</div>"); | |
}); | |
}); | |
app.Run(async ctx => | |
{ | |
var user = ctx.Authentication.User; | |
var response = ctx.Response; | |
response.ContentType = "text/html"; | |
if (user.Identity.IsAuthenticated) | |
{ | |
var userProfile = GetUserProfile(user); | |
await response.WriteAsync(string.Format("<h2>{0}</h2>", | |
user.Claims.First().Issuer)); | |
await response.WriteAsync("<dl>"); | |
foreach (var claim in user.Claims) | |
{ | |
await response.WriteAsync(string.Format( | |
"<dt>{0}</dt> <dd>{1}</dd>", | |
claim.Type, | |
claim.Value)); | |
} | |
await response.WriteAsync("</dl>"); | |
await response.WriteAsync(JsonConvert.SerializeObject(await userProfile)); | |
await response.WriteAsync(@"<br/><a href=""/logout"" > logout</a>"); | |
} | |
else | |
{ | |
await ctx.Response.WriteAsync("<h2>anonymous</h2>"); | |
} | |
await ctx.Response.WriteAsync( | |
@" | |
<div> | |
<a href=""/home"">home></a> | |
<a href=""/signup"">signup</a> | |
<a href=""/login"">login</a> | |
<a href=""/show-azure-powers"">show azure powers</a> | |
</div>"); | |
}); | |
} | |
private async Task<UserProfile> GetUserProfile(ClaimsPrincipal user) | |
{ | |
string tenantId = user.FindFirst(TenantIdClaimType).Value; | |
var authContext = new Microsoft.IdentityModel.Clients.ActiveDirectory.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(); | |
return JsonConvert.DeserializeObject<UserProfile>(responseString); | |
} | |
private string CreateConsentUrl(string clientId, string requestedPermissions, | |
string consentReturnUrl) | |
{ | |
string consentUrl = String.Format( | |
CultureInfo.InvariantCulture, | |
ConsentUrlFormat, | |
HttpUtility.UrlEncode(clientId)); | |
if (!String.IsNullOrEmpty(requestedPermissions)) | |
{ | |
consentUrl += "&RequestedPermissions=" + HttpUtility.UrlEncode(requestedPermissions); | |
} | |
if (!String.IsNullOrEmpty(consentReturnUrl)) | |
{ | |
consentUrl += "&ConsentReturnURL=" + HttpUtility.UrlEncode(consentReturnUrl); | |
} | |
return consentUrl; | |
} | |
} | |
public class UserProfile | |
{ | |
public string DisplayName { get; set; } | |
public string GivenName { get; set; } | |
public string Surname { get; set; } | |
public string[] OtherMails { get; set; } | |
public string[] ProxyAddresses { get; set; } | |
public string MailNickname { get; set; } | |
public bool? AccountEnabled { get; set; } | |
public bool? DirSyncEnabled { get; set; } | |
public string JobTitle { get; set; } | |
public DateTime? LastDirSyncTime { get; set; } | |
public string Mobile { get; set; } | |
public string UsageLocation { get; set; } | |
public string TelephoneNumber { get; set; } | |
public string StreetAddress { get; set; } | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment