1
- using Microsoft . IdentityModel . Protocols . OpenIdConnect ;
1
+ using Microsoft . Identity . Client ;
2
+ using Microsoft . IdentityModel . Protocols . OpenIdConnect ;
2
3
using Microsoft . IdentityModel . Tokens ;
3
4
using Microsoft . Owin . Security ;
4
5
using Microsoft . Owin . Security . Cookies ;
5
6
using Microsoft . Owin . Security . Notifications ;
6
7
using Microsoft . Owin . Security . OpenIdConnect ;
7
8
using Owin ;
8
9
using System ;
9
- using System . Configuration ;
10
- using System . Threading . Tasks ;
11
- using System . Web . Http ;
12
- using TaskWebApp . Models ;
13
10
using System . Net ;
14
11
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 ;
19
16
20
17
namespace TaskWebApp
21
18
{
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
54
23
*/
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
+ /*
108
67
* 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.
109
68
* If so, use that policy when making the call. Also, don't request a code (since it won't be needed).
110
69
*/
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" ) ;
114
73
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
+ }
121
80
122
- return Task . FromResult ( 0 ) ;
123
- }
81
+ return Task . FromResult ( 0 ) ;
82
+ }
124
83
125
- /*
84
+ /*
126
85
* Catch any failures received by the authentication middleware and handle appropriately
127
86
*/
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
154
112
*/
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