Skip to content

Commit 56e4fff

Browse files
author
robertob
committed
updates to address issue 7, essentially switching to use of easyauth header data to create identity and principal vs unnecessary network request to /.auth/me
1 parent 4488f2d commit 56e4fff

2 files changed

Lines changed: 117 additions & 19 deletions

File tree

src/KK.AspNetCore.EasyAuthAuthentication/EasyAuthAuthenticationHandler.cs

Lines changed: 111 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@ namespace KK.AspNetCore.EasyAuthAuthentication
22
{
33
using System;
44
using System.Collections.Generic;
5+
using System.IdentityModel.Tokens.Jwt;
6+
using System.Linq; // required by Children<JObject>.FirstOrDefault requires using System.Linq;
57
using System.Net;
68
using System.Net.Http;
79
using System.Security.Claims;
810
using System.Security.Principal;
11+
using System.Text;
912
using System.Text.Encodings.Web;
1013
using System.Threading.Tasks;
1114
using Microsoft.AspNetCore.Authentication;
@@ -39,11 +42,21 @@ protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
3942
{
4043
this.Logger.LogInformation("starting authentication handler for app service authentication");
4144

42-
if (
43-
(this.Context.User == null ||
44-
this.Context.User.Identity == null ||
45-
this.Context.User.Identity.IsAuthenticated == false)
46-
&& this.Context.Request.Path != "/" + $"{this.Options.AuthEndpoint}")
45+
if ((this.Context.User == null || this.Context.User.Identity == null || this.Context.User.Identity.IsAuthenticated == false)
46+
&& !string.IsNullOrEmpty(this.Context.Request.Headers["X-MS-TOKEN-AAD-ID-TOKEN"].ToString()))
47+
{
48+
// build up identity from X-MS-TOKEN-AAD-ID-TOKEN header set by EasyAuth filters if user openid connect session cookie or oauth bearer token authenticated ...
49+
var ticket = this.BuildIdentityFromEasyAuthHeaders(this.Context.Request.Headers);
50+
51+
this.Logger.LogInformation("Set identity to user context object.");
52+
this.Context.User = ticket.Principal;
53+
54+
this.Logger.LogInformation("identity build was a success, returning ticket");
55+
return AuthenticateResult.Success(ticket);
56+
}
57+
else if ((this.Context.User == null || this.Context.User.Identity == null || this.Context.User.Identity.IsAuthenticated == false)
58+
&& string.IsNullOrEmpty(this.Context.Request.Headers["X-MS-TOKEN-AAD-ID-TOKEN"].ToString())
59+
&& (this.Context.Request.Host.Value.StartsWith("localhost") && this.Context.Request.Path != "/" + $"{this.Options.AuthEndpoint}"))
4760
{
4861
var cookieContainer = new CookieContainer();
4962
var handler = this.CreateHandler(ref cookieContainer);
@@ -60,7 +73,7 @@ protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
6073
}
6174

6275
// build up identity from json...
63-
var ticket = this.BuildIdentityFromJsonPayload((JObject)payload[0]);
76+
var ticket = this.BuildIdentityFromEasyAuthMeJson((JObject)payload[0]);
6477

6578
this.Logger.LogInformation("Set identity to user context object.");
6679
this.Context.User = ticket.Principal;
@@ -75,33 +88,108 @@ protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
7588
}
7689
}
7790

78-
private AuthenticationTicket BuildIdentityFromJsonPayload(JObject payload)
91+
private AuthenticationTicket BuildIdentityFromEasyAuthHeaders(Microsoft.AspNetCore.Http.IHeaderDictionary requestHeaders)
7992
{
80-
var id = payload["user_id"].Value<string>();
81-
var idToken = payload["id_token"].Value<string>();
82-
var providerName = payload["provider_name"].Value<string>();
93+
var id = requestHeaders["X-MS-CLIENT-PRINCIPAL-NAME"].ToString();
94+
var idToken = requestHeaders["X-MS-TOKEN-AAD-ID-TOKEN"].ToString();
95+
var providerName = requestHeaders["X-MS-CLIENT-PRINCIPAL-IDP"].ToString();
96+
97+
this.Logger.LogDebug("payload was fetched from easyauth headers, id: {0}", id);
8398

84-
this.Logger.LogDebug("payload was fetched from endpoint. id: {0}", id);
99+
var identity = new GenericIdentity(id, "AuthenticationTypes.Federation"); // setting ClaimsIdentity.AuthenticationType to value that azuread non-easyauth setups use
85100

86-
var identity = new GenericIdentity(id);
101+
this.Logger.LogInformation("building claims from payload...");
102+
103+
// jwt token decode c# -> https://stackoverflow.com/questions/38340078/how-to-decode-jwt-token/38911599#38911599
104+
// nuget.org search on "System.IdentityModel.Tokens.Jwt MicrosoftIdentityModel.Tokens" ->
105+
// using System.IdentityModel.Tokens.Jwt 27.8m vs MicrosoftIdentityModel.Tokens 17.5m downloads both v5.3.0 released 10/05/2018
106+
var idTokenJwt = new JwtSecurityToken(idToken);
107+
var claims = new List<Claim>();
108+
foreach (var claim in idTokenJwt.Claims as List<Claim>)
109+
{
110+
if (claim.Type == "amr")
111+
{
112+
foreach (var item in claim.Value.Split(','))
113+
{
114+
claims.Add(new Claim(ClaimTypes.Authentication, item));
115+
}
116+
}
117+
else if (claim.Type == "roles")
118+
{
119+
foreach (var item in claim.Value.Split(','))
120+
{
121+
//(User.Identity as ClaimsIdentity).RoleClaimType must match type that role claims are assigned to for Authorization and IsInRole to work
122+
claims.Add(new Claim(ClaimTypes.Role, item));
123+
}
124+
}
125+
else // if (claim.Type != "c_hash")
126+
{
127+
//(User.Identity as ClaimsIdentity).NameClaimType must be what name claim is assigned to for User.Identity.Name to work
128+
claims.Add(new Claim(claim.Type, claim.Value));
129+
}
130+
}
131+
132+
this.Logger.LogInformation("Add claims to new identity");
133+
134+
identity.AddClaims(claims);
135+
var xMsClientPrincipal = JObject.Parse(Encoding.UTF8.GetString(Convert.FromBase64String(requestHeaders["X-MS-CLIENT-PRINCIPAL"].ToString())));
136+
var nameidentifier = xMsClientPrincipal["claims"].Children<JObject>().FirstOrDefault(c => c["typ"].ToString() == ClaimTypes.NameIdentifier)?["val"].ToString();
137+
//foreach (var claim in xMsClientPrincipal["claims"]) { if (claim["typ"].ToString() == ClaimTypes.NameIdentifier) { nameidentifier = claim["val"].ToString(); } } // line above works not required
138+
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, nameidentifier));
139+
//identity.AddClaim(new Claim("id_token", idToken)); // don't think we should be including this
140+
//identity.AddClaim(new Claim("http://schemas.microsoft.com/claims/authnclassreference", 1)); // don't think we need to add this
141+
if (!(identity.Claims as List<Claim>).Exists(claim => claim.Type == "scp")) identity.AddClaim(new Claim("scp", "user_impersonation")); // not sure why easyauth not including this
142+
identity.AddClaim(new Claim("provider_name", providerName));
143+
var genericPrincipal = new GenericPrincipal(identity, null);
144+
return new AuthenticationTicket(genericPrincipal, EasyAuthAuthenticationDefaults.AuthenticationScheme);
145+
}
146+
147+
private AuthenticationTicket BuildIdentityFromEasyAuthMeJson(JObject payload)
148+
{
149+
var id = payload["user_id"].Value<string>(); // X-MS-CLIENT-PRINCIPAL-NAME
150+
var idToken = payload["id_token"].Value<string>(); // X-MS-TOKEN-AAD-ID-TOKEN
151+
var providerName = payload["provider_name"].Value<string>(); // X-MS-CLIENT-PRINCIPAL-IDP
152+
153+
this.Logger.LogDebug("payload was fetched from easyauth me json, id: {0}", id);
154+
155+
var identity = new GenericIdentity(id, "AuthenticationTypes.Federation"); // setting ClaimsIdentity.AuthenticationType to value that azuread non-easyauth setups use
87156

88157
this.Logger.LogInformation("building claims from payload...");
89158

90159
var claims = new List<Claim>();
91160
foreach (var claim in payload["user_claims"])
92161
{
93-
claims.Add(new Claim(claim["typ"].ToString(), claim["val"].ToString()));
162+
if (claim["typ"].ToString() == "amr")
163+
{
164+
foreach (var item in claim["val"].ToString().Split(','))
165+
{
166+
claims.Add(new Claim(ClaimTypes.Authentication, item));
167+
}
168+
}
169+
else if (claim["typ"].ToString() == "roles")
170+
{
171+
foreach (var item in claim["val"].ToString().Split(','))
172+
{
173+
//(User.Identity as ClaimsIdentity).RoleClaimType must match type that role claims are assigned to for Authorization and IsInRole to work
174+
claims.Add(new Claim(ClaimTypes.Role, item));
175+
}
176+
}
177+
else // if (claim["typ"].ToString() != "c_hash")
178+
{
179+
//(User.Identity as ClaimsIdentity).NameClaimType must be what name claim is assigned to for User.Identity.Name to work
180+
claims.Add(new Claim(claim["typ"].ToString(), claim["val"].ToString()));
181+
}
94182
}
95183

96184
this.Logger.LogInformation("Add claims to new identity");
97185

98186
identity.AddClaims(claims);
99-
identity.AddClaim(new Claim("id_token", idToken));
187+
//identity.AddClaim(new Claim("id_token", idToken)); // don't think we should be including this
188+
//identity.AddClaim(new Claim("http://schemas.microsoft.com/claims/authnclassreference", 1)); // don't think we need to add this
189+
if (!(identity.Claims as List<Claim>).Exists(claim => claim.Type == "scp")) identity.AddClaim(new Claim("scp", "user_impersonation")); // not sure why easyauth not including this
100190
identity.AddClaim(new Claim("provider_name", providerName));
101-
var p = new GenericPrincipal(identity, null);
102-
return new AuthenticationTicket(
103-
p,
104-
EasyAuthAuthenticationDefaults.AuthenticationScheme);
191+
var genericPrincipal = new GenericPrincipal(identity, null);
192+
return new AuthenticationTicket(genericPrincipal, EasyAuthAuthenticationDefaults.AuthenticationScheme);
105193
}
106194

107195
private HttpRequestMessage CreateAuthRequest(ref CookieContainer cookieContainer)
@@ -125,7 +213,11 @@ private HttpRequestMessage CreateAuthRequest(ref CookieContainer cookieContainer
125213
}
126214

127215
// fetch value from endpoint
128-
var request = new HttpRequestMessage(HttpMethod.Get, $"{uriString}/{this.Options.AuthEndpoint}");
216+
var authMeEndpoint = string.Empty;
217+
if (this.Options.AuthEndpoint.StartsWith("http")) authMeEndpoint = this.Options.AuthEndpoint; // enable pulling from places like storage account public blob container
218+
else authMeEndpoint = $"{uriString}/{this.Options.AuthEndpoint}"; // localhost relative path, e.g. wwwroot/.auth/me.json
219+
220+
var request = new HttpRequestMessage(HttpMethod.Get, authMeEndpoint);
129221
foreach (var header in this.Context.Request.Headers)
130222
{
131223
if (header.Key.StartsWith("X-ZUMO-"))

src/KK.AspNetCore.EasyAuthAuthentication/KK.AspNetCore.EasyAuthAuthentication.csproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,11 @@
2626
</PropertyGroup>
2727
<ItemGroup>
2828
<PackageReference Include="StyleCop.Analyzers" Version="1.1.0-beta009" privateassets="all" />
29+
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="5.3.0" />
30+
</ItemGroup>
31+
<ItemGroup>
32+
<Reference Include="System.IdentityModel.Tokens.Jwt">
33+
<HintPath>C:\Program Files\dotnet\sdk\NuGetFallbackFolder\system.identitymodel.tokens.jwt\5.2.0\lib\netstandard1.4\System.IdentityModel.Tokens.Jwt.dll</HintPath>
34+
</Reference>
2935
</ItemGroup>
3036
</Project>

0 commit comments

Comments
 (0)