Skip to content

Commit 9bcea81

Browse files
author
Tiago Brenck
committed
- Removed file cache logic
- Implemented InMemory cache logic, following our other samples standard - Updated README
1 parent 37bf57d commit 9bcea81

12 files changed

+287
-352
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ This sample uses three types of policies: a unified sign-up/sign-in policy, a pr
7070

7171
If you already have existing policies in your Azure AD B2C tenant, feel free to re-use those policies in this sample.
7272

73+
Make sure that all the three policies return **User's Object ID** and **Display Name** on **Application Claims**. To do that, on Azure Portal, go to your B2C Directory then click **User flows (policies)** on the left menu and select your policy. Then click on **Application claims** and make sure that **User's Object ID** and **Display Name** is checked.
74+
7375
### Step 3: Register your ASP.NET Web API with Azure AD B2C
7476

7577
Follow the instructions at [register a Web API with Azure AD B2C](https://docs.microsoft.com/azure/active-directory-b2c/active-directory-b2c-app-registration#register-a-web-api) to register the ASP.NET Web API sample with your tenant. Registering your Web API allows you to define the scopes that your ASP.NET Web Application will request access tokens for.
@@ -109,6 +111,7 @@ In this section, you will change the code in both projects to use your tenant.
109111

110112
1. Open the `Web.config` file for the `TaskWebApp` project.
111113
1. Find the key `ida:Tenant` and replace the value with your `<your-tenant-name>.onmicrosoft.com`.
114+
1. Find the key `ida:TenantId` and replace the value with your Directory ID.
112115
1. Find the key `ida:ClientId` and replace the value with the Application ID from your web application `My Test ASP.NET Web Application` registration in the Azure portal.
113116
1. Find the key `ida:ClientSecret` and replace the value with the Client secret from your web application in in the Azure portal.
114117
1. Find the keys representing the policies, e.g. `ida:SignUpSignInPolicyId` and replace the values with the corresponding policy names you created, e.g. `b2c_1_SiUpIn`

TaskWebApp/App_Start/Startup.Auth.cs

Lines changed: 118 additions & 172 deletions
Original file line numberDiff line numberDiff line change
@@ -1,194 +1,140 @@
1-
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
1+
using Microsoft.Identity.Client;
2+
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
23
using Microsoft.IdentityModel.Tokens;
34
using Microsoft.Owin.Security;
45
using Microsoft.Owin.Security.Cookies;
56
using Microsoft.Owin.Security.Notifications;
67
using Microsoft.Owin.Security.OpenIdConnect;
78
using Owin;
89
using System;
9-
using System.Configuration;
10-
using System.Threading.Tasks;
11-
using System.Web.Http;
12-
using TaskWebApp.Models;
1310
using System.Net;
1411
using System.Net.Http;
15-
using System.Collections.Generic;
16-
using System.Web;
17-
using System.Diagnostics;
18-
using Microsoft.Identity.Client;
12+
using System.Security.Claims;
13+
using System.Threading.Tasks;
14+
using System.Web.Http;
15+
using TaskWebApp.Utils;
1916

2017
namespace TaskWebApp
2118
{
22-
public partial class Startup
23-
{
24-
// App config settings
25-
public static string ClientId = ConfigurationManager.AppSettings["ida:ClientId"];
26-
public static string ClientSecret = ConfigurationManager.AppSettings["ida:ClientSecret"];
27-
public static string AadInstance = ConfigurationManager.AppSettings["ida:AadInstance"];
28-
public static string Tenant = ConfigurationManager.AppSettings["ida:Tenant"];
29-
public static string RedirectUri = ConfigurationManager.AppSettings["ida:RedirectUri"];
30-
public static string ServiceUrl = ConfigurationManager.AppSettings["api:TaskServiceUrl"];
31-
32-
// B2C policy identifiers
33-
public static string SignUpSignInPolicyId = ConfigurationManager.AppSettings["ida:SignUpSignInPolicyId"];
34-
public static string EditProfilePolicyId = ConfigurationManager.AppSettings["ida:EditProfilePolicyId"];
35-
public static string ResetPasswordPolicyId = ConfigurationManager.AppSettings["ida:ResetPasswordPolicyId"];
36-
37-
public static string DefaultPolicy = SignUpSignInPolicyId;
38-
39-
// API Scopes
40-
public static string ApiIdentifier = ConfigurationManager.AppSettings["api:ApiIdentifier"];
41-
public static string ReadTasksScope = ApiIdentifier + ConfigurationManager.AppSettings["api:ReadScope"];
42-
public static string WriteTasksScope = ApiIdentifier + ConfigurationManager.AppSettings["api:WriteScope"];
43-
public static string[] Scopes = new string[] { ReadTasksScope, WriteTasksScope };
44-
45-
// OWIN auth middleware constants
46-
public const string ObjectIdElement = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier";
47-
48-
// Authorities
49-
public static string B2CAuthority = string.Format(AadInstance, Tenant, DefaultPolicy);
50-
static string WellKnownMetadata = $"{AadInstance}/v2.0/.well-known/openid-configuration";
51-
52-
/*
53-
* Configure the OWIN middleware
19+
public partial class Startup
20+
{
21+
/*
22+
* Configure the OWIN middleware
5423
*/
55-
public void ConfigureAuth(IAppBuilder app)
56-
{
57-
58-
// Required for Azure webapps, as by default they force TLS 1.2 and this project attempts 1.0
59-
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
60-
61-
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
62-
63-
app.UseCookieAuthentication(new CookieAuthenticationOptions());
64-
65-
app.UseOpenIdConnectAuthentication(
66-
new OpenIdConnectAuthenticationOptions
67-
{
68-
// Generate the metadata address using the tenant and policy information
69-
MetadataAddress = String.Format(WellKnownMetadata, Tenant, DefaultPolicy),
70-
71-
// These are standard OpenID Connect parameters, with values pulled from web.config
72-
ClientId = ClientId,
73-
RedirectUri = RedirectUri,
74-
PostLogoutRedirectUri = RedirectUri,
75-
76-
// Specify the callbacks for each type of notifications
77-
Notifications = new OpenIdConnectAuthenticationNotifications
78-
{
79-
RedirectToIdentityProvider = OnRedirectToIdentityProvider,
80-
AuthorizationCodeReceived = OnAuthorizationCodeReceived,
81-
AuthenticationFailed = OnAuthenticationFailed,
82-
83-
},
84-
85-
// Specify the claim type that specifies the Name property.
86-
TokenValidationParameters = new TokenValidationParameters
87-
{
88-
NameClaimType = "name",
89-
ValidateIssuer = false
90-
},
91-
92-
// Specify the scope by appending all of the scopes requested into one string (separated by a blank space)
93-
Scope = $"openid profile offline_access {ReadTasksScope} {WriteTasksScope}"
94-
}
95-
);
96-
}
97-
98-
internal static ConfidentialClientApplication GetConfidential()
99-
{
100-
return (ConfidentialClientApplication)ConfidentialClientApplicationBuilder.
101-
Create(ClientId).
102-
WithB2CAuthority(B2CAuthority).
103-
WithClientSecret(ClientSecret).
104-
WithRedirectUri(RedirectUri).Build();
105-
106-
}
107-
/*
24+
25+
public void ConfigureAuth(IAppBuilder app)
26+
{
27+
// Required for Azure webapps, as by default they force TLS 1.2 and this project attempts 1.0
28+
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
29+
30+
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
31+
32+
app.UseCookieAuthentication(new CookieAuthenticationOptions());
33+
34+
app.UseOpenIdConnectAuthentication(
35+
new OpenIdConnectAuthenticationOptions
36+
{
37+
// Generate the metadata address using the tenant and policy information
38+
MetadataAddress = String.Format(Globals.WellKnownMetadata, Globals.Tenant, Globals.DefaultPolicy),
39+
40+
// These are standard OpenID Connect parameters, with values pulled from web.config
41+
ClientId = Globals.ClientId,
42+
RedirectUri = Globals.RedirectUri,
43+
PostLogoutRedirectUri = Globals.RedirectUri,
44+
45+
// Specify the callbacks for each type of notifications
46+
Notifications = new OpenIdConnectAuthenticationNotifications
47+
{
48+
RedirectToIdentityProvider = OnRedirectToIdentityProvider,
49+
AuthorizationCodeReceived = OnAuthorizationCodeReceived,
50+
AuthenticationFailed = OnAuthenticationFailed,
51+
},
52+
53+
// Specify the claim type that specifies the Name property.
54+
TokenValidationParameters = new TokenValidationParameters
55+
{
56+
NameClaimType = "name",
57+
ValidateIssuer = false
58+
},
59+
60+
// Specify the scope by appending all of the scopes requested into one string (separated by a blank space)
61+
Scope = $"openid profile offline_access {Globals.ReadTasksScope} {Globals.WriteTasksScope}"
62+
}
63+
);
64+
}
65+
66+
/*
10867
* On each call to Azure AD B2C, check if a policy (e.g. the profile edit or password reset policy) has been specified in the OWIN context.
10968
* If so, use that policy when making the call. Also, don't request a code (since it won't be needed).
11069
*/
111-
private Task OnRedirectToIdentityProvider(RedirectToIdentityProviderNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
112-
{
113-
var policy = notification.OwinContext.Get<string>("Policy");
70+
private Task OnRedirectToIdentityProvider(RedirectToIdentityProviderNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
71+
{
72+
var policy = notification.OwinContext.Get<string>("Policy");
11473

115-
if (!string.IsNullOrEmpty(policy) && !policy.Equals(DefaultPolicy))
116-
{
117-
notification.ProtocolMessage.Scope = OpenIdConnectScope.OpenId;
118-
notification.ProtocolMessage.ResponseType = OpenIdConnectResponseType.IdToken;
119-
notification.ProtocolMessage.IssuerAddress = notification.ProtocolMessage.IssuerAddress.ToLower().Replace(DefaultPolicy.ToLower(), policy.ToLower());
120-
}
74+
if (!string.IsNullOrEmpty(policy) && !policy.Equals(Globals.DefaultPolicy))
75+
{
76+
notification.ProtocolMessage.Scope = OpenIdConnectScope.OpenId;
77+
notification.ProtocolMessage.ResponseType = OpenIdConnectResponseType.IdToken;
78+
notification.ProtocolMessage.IssuerAddress = notification.ProtocolMessage.IssuerAddress.ToLower().Replace(Globals.DefaultPolicy.ToLower(), policy.ToLower());
79+
}
12180

122-
return Task.FromResult(0);
123-
}
81+
return Task.FromResult(0);
82+
}
12483

125-
/*
84+
/*
12685
* Catch any failures received by the authentication middleware and handle appropriately
12786
*/
128-
private Task OnAuthenticationFailed(AuthenticationFailedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
129-
{
130-
notification.HandleResponse();
131-
132-
// Handle the error code that Azure AD B2C throws when trying to reset a password from the login page
133-
// because password reset is not supported by a "sign-up or sign-in policy"
134-
if (notification.ProtocolMessage.ErrorDescription != null && notification.ProtocolMessage.ErrorDescription.Contains("AADB2C90118"))
135-
{
136-
// If the user clicked the reset password link, redirect to the reset password route
137-
notification.Response.Redirect("/Account/ResetPassword");
138-
}
139-
else if (notification.Exception.Message == "access_denied")
140-
{
141-
notification.Response.Redirect("/");
142-
}
143-
else
144-
{
145-
notification.Response.Redirect("/Home/Error?message=" + notification.Exception.Message);
146-
}
147-
148-
return Task.FromResult(0);
149-
}
150-
151-
152-
/*
153-
* Callback function when an authorization code is received
87+
private Task OnAuthenticationFailed(AuthenticationFailedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
88+
{
89+
notification.HandleResponse();
90+
91+
// Handle the error code that Azure AD B2C throws when trying to reset a password from the login page
92+
// because password reset is not supported by a "sign-up or sign-in policy"
93+
if (notification.ProtocolMessage.ErrorDescription != null && notification.ProtocolMessage.ErrorDescription.Contains("AADB2C90118"))
94+
{
95+
// If the user clicked the reset password link, redirect to the reset password route
96+
notification.Response.Redirect("/Account/ResetPassword");
97+
}
98+
else if (notification.Exception.Message == "access_denied")
99+
{
100+
notification.Response.Redirect("/");
101+
}
102+
else
103+
{
104+
notification.Response.Redirect("/Home/Error?message=" + notification.Exception.Message);
105+
}
106+
107+
return Task.FromResult(0);
108+
}
109+
110+
/*
111+
* Callback function when an authorization code is received
154112
*/
155-
private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedNotification notification)
156-
{
157-
// Extract the code from the response notification
158-
var code = notification.Code;
159-
160-
string signedInUserID = notification.JwtSecurityToken.Subject; //notification.AuthenticationTicket.Identity.FindFirst(Startup.ClaimsSubject).Value;
161-
ConfidentialClientApplication cca = GetConfidential();
162-
var httpContextBase = notification.OwinContext.Environment["System.Web.HttpContextBase"] as HttpContextBase;
163-
HttpContext httpContext = httpContextBase.ApplicationInstance.Context;
164-
TokenCacheHelper.EnablePersistence(cca.UserTokenCache);
165-
166-
try
167-
{
168-
AuthenticationResult result = await cca.AcquireTokenByAuthorizationCode(new List<string>(Scopes), code)
169-
.ExecuteAsync();
170-
}
171-
catch (Exception ex)
172-
{
173-
throw new HttpResponseException(new HttpResponseMessage
174-
{
175-
StatusCode = HttpStatusCode.BadRequest,
176-
ReasonPhrase = $"Unable to get authorization code {ex.Message}."
177-
});
178-
179-
}
180-
}
181-
182-
internal static IAccount GetAccountByPolicy(IEnumerable<IAccount> accounts, string policy)
183-
{
184-
foreach (var account in accounts)
185-
{
186-
string userIdentifier = account.HomeAccountId.ObjectId.Split('.')[0];
187-
Debug.WriteLine($"{account.HomeAccountId} {userIdentifier} {account.Username}");
188-
189-
if (userIdentifier.EndsWith(policy.ToLower())) return account;
190-
}
191-
return null;
192-
}
193-
}
194-
}
113+
private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedNotification notification)
114+
{
115+
try
116+
{
117+
/*
118+
The `MSALPerUserMemoryTokenCache` is created and hooked in the `UserTokenCache` used by `IConfidentialClientApplication`.
119+
At this point, if you inspect `ClaimsPrinciple.Current` you will notice that the Identity is still unauthenticated and it has no claims,
120+
but `MSALPerUserMemoryTokenCache` needs the claims to work properly. Because of this sync problem, we are using the constructor that
121+
receives `ClaimsPrincipal` as argument and we are getting the claims from the object `AuthorizationCodeReceivedNotification context`.
122+
This object contains the property `AuthenticationTicket.Identity`, which is a `ClaimsIdentity`, created from the token received from
123+
Azure AD and has a full set of claims.
124+
*/
125+
IConfidentialClientApplication confidentialClient = MsalAppBuilder.BuildConfidentialClientApplication(new ClaimsPrincipal(notification.AuthenticationTicket.Identity));
126+
127+
// Upon successful sign in, get & cache a token using MSAL
128+
AuthenticationResult result = await confidentialClient.AcquireTokenByAuthorizationCode(Globals.Scopes, notification.Code).ExecuteAsync();
129+
}
130+
catch (Exception ex)
131+
{
132+
throw new HttpResponseException(new HttpResponseMessage
133+
{
134+
StatusCode = HttpStatusCode.BadRequest,
135+
ReasonPhrase = $"Unable to get authorization code {ex.Message}."
136+
});
137+
}
138+
}
139+
}
140+
}

0 commit comments

Comments
 (0)