Skip to content

Commit 9ce9653

Browse files
committed
Updated challenge from Web API and other minor updates
1 parent 16e2c06 commit 9ce9653

File tree

6 files changed

+179
-30
lines changed

6 files changed

+179
-30
lines changed

4-WebApp-your-API/4-3-AnyOrg/Readme.md

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ When you start the Web API from Visual Studio, depending on the browser you use,
256256

257257
This behavior is expected as you are not authenticated. The client application will be authenticated, so it will be able to access the Web API.
258258

259-
Explore the sample by signing in into the TodoList client, adding items to the To Do list and assigning them to users. If you stop the application without signing out, the next time you run the application, you won't be prompted to sign in again.
259+
Explore the sample by signing in into the TodoList client, adding items to the To Do list and assigning them to users. After sign-in, you will see a link for `Admin Consent` that can be used to provide consent for by admin of a tenant to other users of the same tenant. If you stop the application without signing out, the next time you run the application, you won't be prompted to sign in again.
260260

261261
> NOTE: Remember, the To-Do list is stored in memory in this `TodoListService` app. Each time you run the projects, your To-Do list will get emptied.
262262

@@ -266,7 +266,9 @@ Explore the sample by signing in into the TodoList client, adding items to the T
266266

267267
### Code for the Web app (TodoListClient)
268268

269-
In Startup.cs, below lines of code enables Microsoft identity platform endpoint. This endpoint is capable of signing-in users both with their Work and School and Microsoft Personal accounts.
269+
####
270+
271+
In `Startup.cs`, below lines of code enables Microsoft identity platform endpoint. This endpoint is capable of signing-in users both with their Work and School and Microsoft Personal accounts.
270272
```csharp
271273
services.AddSignIn(Configuration).
272274
AddWebAppCallsProtectedWebApi(Configuration, new string[]
@@ -275,9 +277,81 @@ services.AddSignIn(Configuration).
275277
```
276278

277279
The following code injects the ToDoList service implementation in the client
280+
278281
```CSharp
279282
services.AddTodoListService(Configuration);
280283
```
284+
285+
#### Admin Consent
286+
287+
In `TodoListController.cs`, the below method redirects to admin consent URI and an admin can consent on behalf of organization.
288+
289+
```csharp
290+
public IActionResult AdminConsent()
291+
{
292+
293+
var tenantId = User.GetTenantId();
294+
295+
string adminConsent = "https://login.microsoftonline.com/" +
296+
tenantId + "/v2.0/adminconsent?client_id=" + _ClientId
297+
+ "&redirect_uri=" + _RedirectUri + "&scope=" + _TodoListScope;
298+
return Redirect(adminConsent);
299+
}
300+
```
301+
#### Handle MsalUiRequiredException from Web API
302+
303+
In `ToDoListService.cs`, below method is called when `MsalUiRequiredException` is thrown from Web API in the on-behalf of flow. It creates consent URI and throws `WebApiMsalUiRequiredException`.
304+
305+
```csharp
306+
private void HandleChallengeFromWebApi(HttpResponseMessage response)
307+
{
308+
//proposedAction="consent"
309+
List<string> result = new List<string>();
310+
AuthenticationHeaderValue bearer = response.Headers.WwwAuthenticate.First(v => v.Scheme == "Bearer");
311+
IEnumerable<string> parameters = bearer.Parameter.Split(',').Select(v => v.Trim()).ToList();
312+
string proposedAction = GetParameter(parameters, "proposedAction");
313+
314+
if (proposedAction == "consent")
315+
{
316+
string consentUri = GetParameter(parameters, "consentUri");
317+
318+
var uri = new Uri(consentUri);
319+
320+
var queryString = System.Web.HttpUtility.ParseQueryString(uri.Query);
321+
queryString.Set("client_id", _ClientId);
322+
queryString.Set("redirect_uri", _RedirectUri);
323+
queryString.Set("scope", _TodoListScope);
324+
queryString.Add("prompt", "consent");
325+
326+
var uriBuilder = new UriBuilder(uri);
327+
uriBuilder.Query = queryString.ToString();
328+
var updateConsentUri = uriBuilder.Uri.ToString();
329+
result.Add("consentUri");
330+
result.Add(updateConsentUri);
331+
332+
throw new WebApiMsalUiRequiredException(updateConsentUri);
333+
}
334+
}
335+
```
336+
337+
The following code in `ToDoListController.cs` catches the `WebApiMsalUiRequiredException` exception and redirects to consent Uri.
338+
339+
```csharp
340+
public async Task<IActionResult> Create()
341+
{
342+
ToDoItem todo = new ToDoItem();
343+
try
344+
{
345+
...
346+
}
347+
catch (WebApiMsalUiRequiredException ex)
348+
{
349+
var a = ex.Message;
350+
return Redirect(ex.Message);
351+
}
352+
}
353+
```
354+
281355
### Code for the Web API (TodoListService)
282356

283357
#### Choosing which scopes to expose

4-WebApp-your-API/4-3-AnyOrg/ToDoListClient/Controllers/ToDoListController.cs

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,27 @@
1212
using Microsoft.Identity.Web;
1313
using ToDoListClient.Models;
1414
using ToDoListClient.Services;
15+
using ToDoListClient.Utils;
1516

1617
namespace ToDoListClient.Controllers
1718
{
1819
[Authorize]
1920
public class ToDoListController : Controller
2021
{
2122
private IToDoListService _todoListService;
23+
private readonly string _TodoListScope = string.Empty;
24+
private readonly string _ClientId = string.Empty;
25+
private readonly string _RedirectUri = string.Empty;
2226

23-
public ToDoListController(IToDoListService todoListService)
27+
public ToDoListController(IToDoListService todoListService, IConfiguration configuration)
2428
{
2529
_todoListService = todoListService;
30+
_TodoListScope = configuration["TodoList:TodoListScope"];
31+
_ClientId = configuration["AzureAd:ClientId"];
32+
_RedirectUri = configuration["RedirectUri"];
2633
}
34+
35+
[AuthorizeForScopes(ScopeKeySection = "TodoList:TodoListScope")]
2736
// GET: TodoList
2837
public async Task<ActionResult> Index()
2938
{
@@ -35,14 +44,23 @@ public async Task<ActionResult> Index()
3544
public async Task<IActionResult> Create()
3645
{
3746
ToDoItem todo = new ToDoItem();
38-
TempData["UsersDropDown"] = (await _todoListService.GetAllUsersAsync())
47+
try
48+
{
49+
List<string> result = (await _todoListService.GetAllUsersAsync()).ToList();
50+
51+
TempData["UsersDropDown"] = result
3952
.Select(u => new SelectListItem
4053
{
4154
Text = u
4255
}).ToList();
43-
return View(todo);
56+
return View(todo);
57+
}
58+
catch (WebApiMsalUiRequiredException ex)
59+
{
60+
var a = ex.Message;
61+
return Redirect(ex.Message);
62+
}
4463
}
45-
4664
// POST: TodoList/Create
4765
[HttpPost]
4866
[ValidateAntiForgeryToken]
@@ -98,11 +116,11 @@ public async Task<ActionResult> Delete(int id, [Bind("Id,Title,Owner")] ToDoItem
98116

99117
public IActionResult AdminConsent()
100118
{
101-
string redirectUrl = Request.Scheme + Uri.SchemeDelimiter + Request.Host.Value;
102-
var tenantId = User.Claims.First(x => x.Type == "http://schemas.microsoft.com/identity/claims/tenantid" || x.Type=="tid").Value;
103-
104-
string adminConsent = _todoListService.GetAdminConsentEndpoint(tenantId, redirectUrl);
105-
119+
var tenantId = User.GetTenantId();
120+
121+
string adminConsent = "https://login.microsoftonline.com/" +
122+
tenantId + "/v2.0/adminconsent?client_id=" + _ClientId
123+
+ "&redirect_uri=" + _RedirectUri + "&scope=" + _TodoListScope;
106124
return Redirect(adminConsent);
107125
}
108126
}

4-WebApp-your-API/4-3-AnyOrg/ToDoListClient/Services/IToDoListService.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,5 @@ public interface IToDoListService
1818
Task<ToDoItem> AddAsync(ToDoItem todo);
1919

2020
Task<ToDoItem> EditAsync(ToDoItem todo);
21-
string GetAdminConsentEndpoint(string tenantId, string redirectUrl);
2221
}
2322
}

4-WebApp-your-API/4-3-AnyOrg/ToDoListClient/Services/ToDoListService.cs

Lines changed: 54 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using Microsoft.Graph;
66
using Microsoft.Identity.Web;
77
using Newtonsoft.Json;
8+
using System;
89
using System.Collections.Generic;
910
using System.Diagnostics;
1011
using System.Linq;
@@ -15,6 +16,7 @@
1516
using System.Text;
1617
using System.Threading.Tasks;
1718
using ToDoListClient.Models;
19+
using ToDoListClient.Utils;
1820

1921
namespace ToDoListClient.Services
2022
{
@@ -33,8 +35,8 @@ public class ToDoListService : IToDoListService
3335
private readonly string _TodoListScope = string.Empty;
3436
private readonly string _TodoListBaseAddress = string.Empty;
3537
private readonly string _ClientId = string.Empty;
38+
private readonly string _RedirectUri = string.Empty;
3639
private readonly ITokenAcquisition _tokenAcquisition;
37-
//private readonly IConfiguration _configuration;
3840

3941
public ToDoListService(ITokenAcquisition tokenAcquisition, HttpClient httpClient, IConfiguration configuration, IHttpContextAccessor contextAccessor)
4042
{
@@ -44,6 +46,7 @@ public ToDoListService(ITokenAcquisition tokenAcquisition, HttpClient httpClient
4446
_TodoListScope = configuration["TodoList:TodoListScope"];
4547
_TodoListBaseAddress = configuration["TodoList:TodoListBaseAddress"];
4648
_ClientId = configuration["AzureAd:ClientId"];
49+
_RedirectUri = configuration["RedirectUri"];
4750
}
4851
public async Task<ToDoItem> AddAsync(ToDoItem todo)
4952
{
@@ -122,15 +125,13 @@ public async Task<IEnumerable<string>> GetAllUsersAsync()
122125

123126
return Users;
124127
}
128+
else if (response.StatusCode == HttpStatusCode.Unauthorized)
129+
{
130+
HandleChallengeFromWebApi(response);
131+
}
132+
125133
throw new HttpRequestException($"Invalid status code in the HttpResponseMessage: {response.StatusCode}.");
126134
}
127-
private async Task PrepareAuthenticatedClient()
128-
{
129-
var accessToken = await _tokenAcquisition.GetAccessTokenForUserAsync(new[] { _TodoListScope });
130-
Debug.WriteLine($"access token-{accessToken}");
131-
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
132-
_httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
133-
}
134135

135136
public async Task<ToDoItem> GetAsync(int id)
136137
{
@@ -146,12 +147,52 @@ public async Task<ToDoItem> GetAsync(int id)
146147

147148
throw new HttpRequestException($"Invalid status code in the HttpResponseMessage: {response.StatusCode}.");
148149
}
149-
public string GetAdminConsentEndpoint(string tenantId, string redirectUrl)
150+
151+
private async Task PrepareAuthenticatedClient()
152+
{
153+
var accessToken = await _tokenAcquisition.GetAccessTokenForUserAsync(new[] { _TodoListScope });
154+
Debug.WriteLine($"access token-{accessToken}");
155+
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
156+
_httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
157+
}
158+
159+
private void HandleChallengeFromWebApi(HttpResponseMessage response)
160+
{
161+
//proposedAction="consent"
162+
List<string> result = new List<string>();
163+
AuthenticationHeaderValue bearer = response.Headers.WwwAuthenticate.First(v => v.Scheme == "Bearer");
164+
IEnumerable<string> parameters = bearer.Parameter.Split(',').Select(v => v.Trim()).ToList();
165+
string proposedAction = GetParameter(parameters, "proposedAction");
166+
167+
if (proposedAction == "consent")
168+
{
169+
string consentUri = GetParameter(parameters, "consentUri");
170+
171+
var uri = new Uri(consentUri);
172+
173+
//Set values of query string parameters
174+
var queryString = System.Web.HttpUtility.ParseQueryString(uri.Query);
175+
queryString.Set("client_id", _ClientId);
176+
queryString.Set("redirect_uri", _RedirectUri);
177+
queryString.Set("scope", _TodoListScope);
178+
queryString.Add("prompt", "consent");
179+
180+
//Update values in consent Uri
181+
var uriBuilder = new UriBuilder(uri);
182+
uriBuilder.Query = queryString.ToString();
183+
var updateConsentUri = uriBuilder.Uri.ToString();
184+
result.Add("consentUri");
185+
result.Add(updateConsentUri);
186+
187+
//throw custom exception
188+
throw new WebApiMsalUiRequiredException(updateConsentUri);
189+
}
190+
}
191+
192+
private static string GetParameter(IEnumerable<string> parameters, string parameterName)
150193
{
151-
string adminConsent = "https://login.microsoftonline.com/" +
152-
tenantId + "/v2.0/adminconsent?client_id="+ _ClientId
153-
+ "&redirect_uri="+ redirectUrl + "&scope="+_TodoListScope;
154-
return adminConsent;
194+
int offset = parameterName.Length + 1;
195+
return parameters.FirstOrDefault(p => p.StartsWith($"{parameterName}="))?.Substring(offset)?.Trim('"');
155196
}
156197
}
157198
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Threading.Tasks;
5+
6+
namespace ToDoListClient.Utils
7+
{
8+
public class WebApiMsalUiRequiredException:Exception
9+
{
10+
public WebApiMsalUiRequiredException(string message) : base(message) { }
11+
}
12+
}

4-WebApp-your-API/4-3-AnyOrg/TodoListService/Controllers/TodoListController.cs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,14 +70,19 @@ public async Task<ActionResult<TodoItem>> GetTodoItem(int id)
7070
public async Task<ActionResult<IEnumerable<string>>> GetAllUsers()
7171
{
7272
HttpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi);
73-
74-
List<string> Users = await CallGraphApiOnBehalfOfUser();
75-
if (Users == null)
73+
try
7674
{
77-
return NotFound();
75+
List<string> Users = await CallGraphApiOnBehalfOfUser();
76+
return Users;
77+
}
78+
catch (MsalUiRequiredException ex)
79+
{
80+
HttpContext.Response.ContentType = "text/plain";
81+
HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
82+
await HttpContext.Response.WriteAsync("An authentication error occurred while acquiring a token for downstream API\n" + ex.ErrorCode + "\n" + ex.Message);
7883
}
7984

80-
return Users;
85+
return null;
8186
}
8287
// PUT: api/TodoItems/5
8388
// To protect from overposting attacks, please enable the specific properties you want to bind to, for

0 commit comments

Comments
 (0)