Skip to content
This repository was archived by the owner on Jan 23, 2020. It is now read-only.

Commit 2ce2750

Browse files
committed
Updating the sample to ASP.NET Core 2.0
The way I proceeded is starting from: - the TodoListService of the following sample https://github.com/Azure-Samples/active-directory-dotnet-native-aspnetcore (the code for the TodoListService is identifical) - the open id connect WebApp which is the object of the following sample: https://github.com/Azure-Samples/active-directory-dotnet-webapp-openidconnect-aspnetcore sample This was the object of commit bdd265d - In the TodoListService and the TodoListWebApp upgrading from AspNetCore 2.0.3 to 2.0.5 Then, from the the OpenIdConnect WebApp for ASP.Net Core, I've enabled calling the TodoListService - Updating the AzureAdOptions to compute the Authority from the instance and the tenantID, and adding two other configuration options for the resourceId of the TodoListService (its clientId) and the base address for this service. - Adding a TodoListItem in models - Adding a NaiveSessionCache class in a new Utils folder - Adding a TodoListController and a TodoList view, as well as a "Todo List" entry in the toolbar of the Web API - Update the SignOut() method of the AccountController to clear the cache for the user. - Updating AzureAdAuthenticationBuilderExtensions to request an authorization code, and redeem it so that the token cache contains a token for the user, so that it can be used by the TodoController
1 parent bdd265d commit 2ce2750

File tree

12 files changed

+437
-19
lines changed

12 files changed

+437
-19
lines changed

TodoListService/TodoListService.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
</ItemGroup>
1212

1313
<ItemGroup>
14-
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.3" />
14+
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.5" />
1515
</ItemGroup>
1616

1717
<ItemGroup>

TodoListWebApp/Controllers/AccountController.cs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
using System;
2-
using System.Collections.Generic;
3-
using System.Linq;
4-
using System.Threading.Tasks;
5-
using Microsoft.AspNetCore.Authentication;
1+
using Microsoft.AspNetCore.Authentication;
62
using Microsoft.AspNetCore.Authentication.Cookies;
73
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
84
using Microsoft.AspNetCore.Mvc;
5+
using Microsoft.IdentityModel.Clients.ActiveDirectory;
6+
using System.Security.Claims;
7+
using TodoListWebApp;
98

109
namespace WebApp_OpenIDConnect_DotNet.Controllers
1110
{
@@ -24,6 +23,13 @@ public IActionResult SignIn()
2423
[HttpGet]
2524
public IActionResult SignOut()
2625
{
26+
// Remove all cache entries for this user and send an OpenID Connect sign-out request.
27+
string userObjectID = User.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
28+
var authContext = new AuthenticationContext(AzureAdOptions.Settings.Authority,
29+
new NaiveSessionCache(userObjectID, HttpContext.Session));
30+
authContext.TokenCache.Clear();
31+
32+
// Let Azure AD sign-out
2733
var callbackUrl = Url.Action(nameof(SignedOut), "Account", values: null, protocol: Request.Scheme);
2834
return SignOut(
2935
new AuthenticationProperties { RedirectUri = callbackUrl },
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Net.Http;
5+
using System.Net.Http.Headers;
6+
using System.Threading.Tasks;
7+
using Microsoft.AspNetCore.Authentication;
8+
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
9+
using Microsoft.AspNetCore.Authorization;
10+
using Microsoft.AspNetCore.Mvc;
11+
using Microsoft.IdentityModel.Clients.ActiveDirectory;
12+
using Newtonsoft.Json;
13+
using TodoListWebApp.Models;
14+
15+
// For more information on enabling MVC for empty projects, visit http://go.microsoft.com/fwlink/?LinkID=397860
16+
17+
namespace TodoListWebApp.Controllers
18+
{
19+
[Authorize]
20+
public class TodoController : Controller
21+
{
22+
// GET: /<controller>/
23+
public async Task<IActionResult> Index()
24+
{
25+
AuthenticationResult result = null;
26+
List<TodoItem> itemList = new List<TodoItem>();
27+
28+
try
29+
{
30+
// Because we signed-in already in the WebApp, the userObjectId is know
31+
string userObjectID = (User.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier"))?.Value;
32+
33+
// Using ADAL.Net, get a bearer token to access the TodoListService
34+
AuthenticationContext authContext = new AuthenticationContext(AzureAdOptions.Settings.Authority, new NaiveSessionCache(userObjectID, HttpContext.Session));
35+
ClientCredential credential = new ClientCredential(AzureAdOptions.Settings.ClientId, AzureAdOptions.Settings.ClientSecret);
36+
result = await authContext.AcquireTokenSilentAsync(AzureAdOptions.Settings.TodoListResourceId, credential, new UserIdentifier(userObjectID, UserIdentifierType.UniqueId));
37+
38+
// Retrieve the user's To Do List.
39+
HttpClient client = new HttpClient();
40+
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, AzureAdOptions.Settings.TodoListBaseAddress + "/api/todolist");
41+
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
42+
HttpResponseMessage response = await client.SendAsync(request);
43+
44+
// Return the To Do List in the view.
45+
if (response.IsSuccessStatusCode)
46+
{
47+
List<Dictionary<String, String>> responseElements = new List<Dictionary<String, String>>();
48+
JsonSerializerSettings settings = new JsonSerializerSettings();
49+
String responseString = await response.Content.ReadAsStringAsync();
50+
responseElements = JsonConvert.DeserializeObject<List<Dictionary<String, String>>>(responseString, settings);
51+
foreach (Dictionary<String, String> responseElement in responseElements)
52+
{
53+
TodoItem newItem = new TodoItem();
54+
newItem.Title = responseElement["title"];
55+
newItem.Owner = responseElement["owner"];
56+
itemList.Add(newItem);
57+
}
58+
59+
return View(itemList);
60+
}
61+
else
62+
{
63+
//
64+
// If the call failed with access denied, then drop the current access token from the cache,
65+
// and show the user an error indicating they might need to sign-in again.
66+
//
67+
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
68+
{
69+
var todoTokens = authContext.TokenCache.ReadItems().Where(a => a.Resource == AzureAdOptions.Settings.TodoListResourceId);
70+
foreach (TokenCacheItem tci in todoTokens)
71+
authContext.TokenCache.DeleteItem(tci);
72+
73+
ViewBag.ErrorMessage = "UnexpectedError";
74+
TodoItem newItem = new TodoItem();
75+
newItem.Title = "(No items in list)";
76+
itemList.Add(newItem);
77+
return View(itemList);
78+
}
79+
}
80+
}
81+
catch (Exception ee)
82+
{
83+
if (HttpContext.Request.Query["reauth"] == "True")
84+
{
85+
//
86+
// Send an OpenID Connect sign-in request to get a new set of tokens.
87+
// If the user still has a valid session with Azure AD, they will not be prompted for their credentials.
88+
// The OpenID Connect middleware will return to this controller after the sign-in response has been handled.
89+
//
90+
return new ChallengeResult(OpenIdConnectDefaults.AuthenticationScheme);
91+
}
92+
//
93+
// The user needs to re-authorize. Show them a message to that effect.
94+
//
95+
TodoItem newItem = new TodoItem();
96+
newItem.Title = "(Sign-in required to view to do list.)";
97+
itemList.Add(newItem);
98+
ViewBag.ErrorMessage = "AuthorizationRequired";
99+
return View(itemList);
100+
}
101+
//
102+
// If the call failed for any other reason, show the user an error.
103+
//
104+
return View("Error");
105+
}
106+
107+
108+
109+
[HttpPost]
110+
public async Task<ActionResult> Index(string item)
111+
{
112+
if (ModelState.IsValid)
113+
{
114+
//
115+
// Retrieve the user's tenantID and access token since they are parameters used to call the To Do service.
116+
//
117+
AuthenticationResult result = null;
118+
List<TodoItem> itemList = new List<TodoItem>();
119+
120+
try
121+
{
122+
string userObjectID = (User.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier"))?.Value;
123+
AuthenticationContext authContext = new AuthenticationContext(AzureAdOptions.Settings.Authority, new NaiveSessionCache(userObjectID, HttpContext.Session));
124+
ClientCredential credential = new ClientCredential(AzureAdOptions.Settings.ClientId, AzureAdOptions.Settings.ClientSecret);
125+
result = await authContext.AcquireTokenSilentAsync(AzureAdOptions.Settings.TodoListResourceId, credential, new UserIdentifier(userObjectID, UserIdentifierType.UniqueId));
126+
127+
// Forms encode todo item, to POST to the todo list web api.
128+
HttpContent content = new StringContent(JsonConvert.SerializeObject(new { Title = item }), System.Text.Encoding.UTF8, "application/json");
129+
130+
//
131+
// Add the item to user's To Do List.
132+
//
133+
HttpClient client = new HttpClient();
134+
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, AzureAdOptions.Settings.TodoListBaseAddress + "/api/todolist");
135+
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
136+
request.Content = content;
137+
HttpResponseMessage response = await client.SendAsync(request);
138+
139+
//
140+
// Return the To Do List in the view.
141+
//
142+
if (response.IsSuccessStatusCode)
143+
{
144+
return RedirectToAction("Index");
145+
}
146+
else
147+
{
148+
//
149+
// If the call failed with access denied, then drop the current access token from the cache,
150+
// and show the user an error indicating they might need to sign-in again.
151+
//
152+
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
153+
{
154+
var todoTokens = authContext.TokenCache.ReadItems().Where(a => a.Resource == AzureAdOptions.Settings.TodoListResourceId);
155+
foreach (TokenCacheItem tci in todoTokens)
156+
authContext.TokenCache.DeleteItem(tci);
157+
158+
ViewBag.ErrorMessage = "UnexpectedError";
159+
TodoItem newItem = new TodoItem();
160+
newItem.Title = "(No items in list)";
161+
itemList.Add(newItem);
162+
return View(newItem);
163+
}
164+
}
165+
}
166+
catch (Exception ee)
167+
{
168+
//
169+
// The user needs to re-authorize. Show them a message to that effect.
170+
//
171+
TodoItem newItem = new TodoItem();
172+
newItem.Title = "(No items in list)";
173+
itemList.Add(newItem);
174+
ViewBag.ErrorMessage = "AuthorizationRequired";
175+
return View(itemList);
176+
}
177+
//
178+
// If the call failed for any other reason, show the user an error.
179+
//
180+
return View("Error");
181+
}
182+
return View("Error");
183+
}
184+
}
185+
}

TodoListWebApp/Extensions/AzureAdAuthenticationBuilderExtensions.cs

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1-
using System;
2-
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
3-
using Microsoft.Extensions.Configuration;
1+
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
42
using Microsoft.Extensions.DependencyInjection;
53
using Microsoft.Extensions.Options;
4+
using Microsoft.IdentityModel.Clients.ActiveDirectory;
5+
using System;
6+
using System.Threading.Tasks;
7+
using TodoListWebApp;
68

79
namespace Microsoft.AspNetCore.Authentication
810
{
911
public static class AzureAdAuthenticationBuilderExtensions
10-
{
12+
{
1113
public static AuthenticationBuilder AddAzureAd(this AuthenticationBuilder builder)
1214
=> builder.AddAzureAd(_ => { });
1315

@@ -19,7 +21,7 @@ public static AuthenticationBuilder AddAzureAd(this AuthenticationBuilder builde
1921
return builder;
2022
}
2123

22-
private class ConfigureAzureOptions: IConfigureNamedOptions<OpenIdConnectOptions>
24+
private class ConfigureAzureOptions : IConfigureNamedOptions<OpenIdConnectOptions>
2325
{
2426
private readonly AzureAdOptions _azureOptions;
2527

@@ -31,16 +33,56 @@ public ConfigureAzureOptions(IOptions<AzureAdOptions> azureOptions)
3133
public void Configure(string name, OpenIdConnectOptions options)
3234
{
3335
options.ClientId = _azureOptions.ClientId;
34-
options.Authority = $"{_azureOptions.Instance}{_azureOptions.TenantId}";
36+
options.Authority = _azureOptions.Authority;
3537
options.UseTokenLifetime = true;
3638
options.CallbackPath = _azureOptions.CallbackPath;
3739
options.RequireHttpsMetadata = false;
40+
options.ClientSecret = _azureOptions.ClientSecret;
41+
options.Resource = "https://graph.windows.net"; // AAD graph
42+
43+
// Without overriding the response type (which by default is id_token), the OnAuthorizationCodeReceived event is not called.
44+
// but instead OnTokenValidated event is called. Here we request both so that OnTokenValidated is called first which
45+
// ensures that context.Principal has a non-null value when OnAuthorizeationCodeReceived is called
46+
options.ResponseType = "id_token code";
47+
48+
// Subscribing to the OIDC events
49+
options.Events.OnAuthorizationCodeReceived = OnAuthorizationCodeReceived;
50+
options.Events.OnAuthenticationFailed = OnAuthenticationFailed;
3851
}
3952

4053
public void Configure(OpenIdConnectOptions options)
4154
{
4255
Configure(Options.DefaultName, options);
4356
}
57+
58+
/// <summary>
59+
/// Redeems the authorization code by calling AcquireTokenByAuthorizationCodeAsync in order to ensure
60+
/// that the cache has a token for the signed-in user, which will then enable the controllers (like the
61+
/// TodoController, to call AcquireTokenSilentAsync successfully.
62+
/// </summary>
63+
private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedContext context)
64+
{
65+
// Acquire a Token for the Graph API and cache it using ADAL. In the TodoListController, we'll use the cache to acquire a token for the Todo List API
66+
string userObjectId = (context.Principal.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier"))?.Value;
67+
var authContext = new AuthenticationContext(context.Options.Authority, new NaiveSessionCache(userObjectId, context.HttpContext.Session));
68+
var credential = new ClientCredential(context.Options.ClientId, context.Options.ClientSecret);
69+
70+
var authResult = await authContext.AcquireTokenByAuthorizationCodeAsync(context.TokenEndpointRequest.Code,
71+
new Uri(context.TokenEndpointRequest.RedirectUri, UriKind.RelativeOrAbsolute), credential, context.Options.Resource);
72+
73+
// Notify the OIDC middleware that we already took care of code redemption.
74+
context.HandleCodeRedemption(authResult.AccessToken, context.ProtocolMessage.IdToken);
75+
}
76+
77+
/// <summary>
78+
/// this method is invoked if exceptions are thrown during request processing
79+
/// </summary>
80+
private Task OnAuthenticationFailed(AuthenticationFailedContext context)
81+
{
82+
context.HandleResponse();
83+
context.Response.Redirect("/Home/Error?message=" + context.Exception.Message);
84+
return Task.FromResult(0);
85+
}
4486
}
4587
}
4688
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,66 @@
11
namespace Microsoft.AspNetCore.Authentication
22
{
3+
/// <summary>
4+
/// Settings relative to the AzureAD applications involved in this Web Application
5+
/// These are deserialized from the AzureAD section of the appsettings.json file
6+
/// </summary>
37
public class AzureAdOptions
48
{
9+
/// <summary>
10+
/// ClientId (Application Id) of this Web Application
11+
/// </summary>
512
public string ClientId { get; set; }
613

14+
/// <summary>
15+
/// Client Secret (Application password) added in the Azure portal in the Keys section for the application
16+
/// </summary>
717
public string ClientSecret { get; set; }
818

19+
/// <summary>
20+
/// Azure AD Cloud instance
21+
/// </summary>
922
public string Instance { get; set; }
1023

24+
/// <summary>
25+
/// domain of your tenant, e.g. contoso.onmicrosoft.com
26+
/// </summary>
1127
public string Domain { get; set; }
1228

29+
/// <summary>
30+
/// Tenant Id, as obtained from the Azure portal:
31+
/// (Select 'Endpoints' from the 'App registrations' blade and use the GUID in any of the URLs)
32+
/// </summary>
1333
public string TenantId { get; set; }
1434

35+
/// <summary>
36+
/// URL on which this Web App will be called back by Azure AD (normally "/signin-oidc")
37+
/// </summary>
1538
public string CallbackPath { get; set; }
39+
40+
/// <summary>
41+
/// Authority delivering the token for your tenant
42+
/// </summary>
43+
public string Authority
44+
{
45+
get
46+
{
47+
return $"{Instance}{TenantId}";
48+
}
49+
}
50+
51+
/// <summary>
52+
/// Client Id (Application ID) of the TodoListService, obtained from the Azure portal for that application
53+
/// </summary>
54+
public string TodoListResourceId { get; set; }
55+
56+
/// <summary>
57+
/// Base URL of the TodoListService
58+
/// </summary>
59+
public string TodoListBaseAddress { get; set; }
60+
61+
/// <summary>
62+
/// Instance of the settings for this Web application (to be used in controllers)
63+
/// </summary>
64+
public static AzureAdOptions Settings { set; get; }
1665
}
1766
}

TodoListWebApp/Models/TodoItem.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace TodoListWebApp.Models
2+
{
3+
public class TodoItem
4+
{
5+
public string Owner { get; set; }
6+
public string Title { get; set; }
7+
}
8+
}

0 commit comments

Comments
 (0)