Skip to content

Commit a94cf24

Browse files
committed
refactor the middelware.
now the business logik for the different methods are in seperate services.
1 parent 644b73a commit a94cf24

7 files changed

Lines changed: 383 additions & 196 deletions

File tree

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
namespace KK.AspNetCore.EasyAuthAuthentication
2+
{
3+
/// <summary>
4+
/// This class contains all header names that are possible to make an authentication.
5+
/// The source of the list can find here: https://docs.microsoft.com/en-us/azure/app-service/app-service-authentication-how-to#retrieve-tokens-in-app-code
6+
/// </summary>
7+
public static class AuthTokenHeaderNames
8+
{
9+
#region AzureAd
10+
public const string AADIdToken = "X-MS-TOKEN-AAD-ID-TOKEN";
11+
public const string AADAccessToken = "X-MS-TOKEN-AAD-ACCESS-TOKEN";
12+
public const string AADExpiresOn = "X-MS-TOKEN-AAD-EXPIRES-ON";
13+
public const string AADRefreshToken = "X-MS-TOKEN-AAD-REFRESH-TOKEN";
14+
#endregion
15+
#region Facebook
16+
public const string FacebookAccessToken = "X-MS-TOKEN-FACEBOOK-ACCESS-TOKEN";
17+
public const string FacebookExpiresOn = "X-MS-TOKEN-FACEBOOK-EXPIRES-ON";
18+
#endregion
19+
#region Google
20+
public const string GoogleIdToken = "X-MS-TOKEN-GOOGLE-ID-TOKEN";
21+
public const string GoogleAccessToken = "X-MS-TOKEN-GOOGLE-ACCESS-TOKEN";
22+
public const string GoogleExpiresOn = "X-MS-TOKEN-GOOGLE-EXPIRES-ON";
23+
public const string GoogleRefreshToken = "X-MS-TOKEN-GOOGLE-REFRESH-TOKEN";
24+
25+
#endregion
26+
#region Microsoft Account
27+
public const string MicrosoftAccessToken = "X-MS-TOKEN-MICROSOFTACCOUNT-ACCESS-TOKEN";
28+
public const string MicrosoftExpiresOn = "X-MS-TOKEN-MICROSOFTACCOUNT-EXPIRES-ON";
29+
public const string MicrosoftAuthenticationToken = "X-MS-TOKEN-MICROSOFTACCOUNT-AUTHENTICATION-TOKEN";
30+
public const string MicrosoftRefreshToken = "X-MS-TOKEN-MICROSOFTACCOUNT-REFRESH-TOKEN";
31+
#endregion
32+
#region Twitter
33+
public const string TwitterAccessToken = "X-MS-TOKEN-TWITTER-ACCESS-TOKEN";
34+
public const string TwitterAccessTokenSecret = "X-MS-TOKEN-TWITTER-ACCESS-TOKEN-SECRET";
35+
#endregion
36+
}
37+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
using System.Security.Claims;
4+
using System.Security.Principal;
5+
using Microsoft.AspNetCore.Authentication;
6+
using Newtonsoft.Json.Linq;
7+
8+
namespace KK.AspNetCore.EasyAuthAuthentication
9+
{
10+
public static class AuthenticationTicketBuilder
11+
{
12+
/// <summary>
13+
/// Build a `AuthenticationTicket` from the given payload, the principal name and the provider name
14+
/// </summary>
15+
/// <param name="claimsPayload">A array of JObjects that have a `type` and a `val` property</param>
16+
/// <param name="principalName">The principal name of the user.</param>
17+
/// /// <param name="providerName">The provider name of the current auth provider.</param>
18+
/// <returns>A `AuthenticationTicket`</returns>
19+
public static AuthenticationTicket Build(IEnumerable<JObject> claimsPayload, string principalName, string providerName)
20+
{
21+
var identity = new GenericIdentity(principalName, AuthenticationTypesNames.Federation); // setting ClaimsIdentity.AuthenticationType to value that azuread non-easyauth setups use
22+
identity.AddClaims(createClaims(claimsPayload));
23+
addScpClaim(identity);
24+
identity.AddClaim(new Claim("provider_name", providerName));
25+
var genericPrincipal = new GenericPrincipal(identity, null);
26+
return new AuthenticationTicket(genericPrincipal, EasyAuthAuthenticationDefaults.AuthenticationScheme);
27+
}
28+
29+
private static IEnumerable<JObject> getTheClaimsNodeFromPayload(JObject payload)
30+
{
31+
return payload["user_claims"].Children<JObject>();
32+
}
33+
34+
private static IEnumerable<Claim> createClaims(IEnumerable<JObject> claimsAsJson)
35+
{
36+
foreach (var claim in claimsAsJson)
37+
{
38+
var claimType = claim["typ"].ToString();
39+
switch (claimType)
40+
{
41+
case Schemas.AuthMethod:
42+
foreach (var item in claim["val"].ToString().Split(','))
43+
{
44+
yield return new Claim(ClaimTypes.Authentication, item);
45+
}
46+
break;
47+
case "roles":
48+
foreach (var item in claim["val"].ToString().Split(','))
49+
{
50+
yield return new Claim(ClaimTypes.Role, item);
51+
}
52+
break;
53+
default:
54+
yield return new Claim(claimType, claim["val"].ToString());
55+
break;
56+
}
57+
}
58+
}
59+
60+
private static void addScpClaim(ClaimsIdentity identity)
61+
{
62+
if (!identity.Claims.Any(claim => claim.Type == "scp"))
63+
{
64+
identity.AddClaim(new Claim("scp", "user_impersonation")); // not sure why easyauth is dropping this
65+
}
66+
}
67+
}
68+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
namespace KK.AspNetCore.EasyAuthAuthentication
2+
{
3+
/// <summary>
4+
/// This class contains all Authentication type names.
5+
/// Source of this is: https://docs.microsoft.com/en-us/dotnet/api/system.security.claims.authenticationtypes?view=netframework-4.7.2
6+
/// </summary>
7+
public class AuthenticationTypesNames
8+
{
9+
public const string Basic = "AuthenticationTypes.Basic";
10+
public const string Federation = "AuthenticationTypes.Federation";
11+
public const string Kerberos = "AuthenticationTypes.Kerberos";
12+
public const string Negotiate = "AuthenticationTypes.Negotiate";
13+
public const string Password = "AuthenticationTypes.Password";
14+
public const string Signature = "AuthenticationTypes.Signature";
15+
public const string Windows = "AuthenticationTypes.Windows";
16+
public const string X509 = "AuthenticationTypes.X509";
17+
}
18+
}

src/KK.AspNetCore.EasyAuthAuthentication/EasyAuthAuthenticationHandler.cs

Lines changed: 19 additions & 196 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ namespace KK.AspNetCore.EasyAuthAuthentication
1111
using System.Text;
1212
using System.Text.Encodings.Web;
1313
using System.Threading.Tasks;
14+
using KK.AspNetCore.EasyAuthAuthentication.Services;
1415
using Microsoft.AspNetCore.Authentication;
16+
using Microsoft.AspNetCore.Http;
1517
using Microsoft.Extensions.Logging;
1618
using Microsoft.Extensions.Options;
1719
using Newtonsoft.Json;
@@ -37,219 +39,40 @@ public EasyAuthAuthenticationHandler(
3739
{
3840
}
3941

42+
private static Func<ClaimsPrincipal, bool> isContextUserNotAuthenticated = user => (user == null || user.Identity == null || user.Identity.IsAuthenticated == false);
43+
private static Func<IHeaderDictionary, bool> isAADIdTokenNotSet = headers => !string.IsNullOrEmpty(headers[AuthTokenHeaderNames.AADIdToken].ToString());
44+
private Func<IHeaderDictionary, ClaimsPrincipal, bool> canUseHeaderAuth => (headers, user) => isContextUserNotAuthenticated(user) && !isAADIdTokenNotSet(headers);
45+
private Func<IHeaderDictionary, ClaimsPrincipal, HttpRequest, bool> canUseEasyAuthJson => (headers, user, request) =>
46+
isContextUserNotAuthenticated(user)
47+
&& isAADIdTokenNotSet(headers)
48+
&& request.Path != "/" + $"{this.Options.AuthEndpoint}";
49+
4050
/// <inheritdoc/>
4151
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
4252
{
4353
this.Logger.LogInformation("starting authentication handler for app service authentication");
4454

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()))
55+
if (canUseHeaderAuth(this.Context.Request.Headers, this.Context.User))
4756
{
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.BuildIdentityFromEasyAuthRequestHeaders(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);
57+
return EasyAuthWithHeaderService.AuthUser(this.Logger, this.Context);
5658
}
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}"))
59+
else if (canUseEasyAuthJson(this.Context.Request.Headers, this.Context.User, this.Context.Request))
6060
{
61-
var cookieContainer = new CookieContainer();
62-
var handler = this.CreateHandler(ref cookieContainer);
63-
var httpRequest = this.CreateAuthRequest(ref cookieContainer);
64-
65-
JArray payload = null;
66-
try
67-
{
68-
payload = await this.GetAuthMe(handler, httpRequest);
69-
}
70-
catch (Exception ex)
71-
{
72-
return AuthenticateResult.Fail(ex.Message);
73-
}
74-
75-
// build up identity from json...
76-
var ticket = this.BuildIdentityFromEasyAuthMeJson((JObject)payload[0]);
77-
78-
this.Logger.LogInformation("Set identity to user context object.");
79-
this.Context.User = ticket.Principal;
80-
81-
this.Logger.LogInformation("identity build was a success, returning ticket");
82-
return AuthenticateResult.Success(ticket);
61+
return await EasyAuthWithAuthMeService.AuthUser(this.Logger, this.Context, this.Options.AuthEndpoint);
8362
}
8463
else
8564
{
86-
this.Logger.LogInformation("identity already set, skipping middleware");
87-
return AuthenticateResult.NoResult();
88-
}
89-
}
90-
91-
private AuthenticationTicket BuildIdentityFromEasyAuthRequestHeaders(Microsoft.AspNetCore.Http.IHeaderDictionary requestHeaders)
92-
{
93-
var name = requestHeaders["X-MS-CLIENT-PRINCIPAL-NAME"][0];
94-
this.Logger.LogDebug("payload was fetched from easyauth headers, name: {0}", name);
95-
96-
var identity = new GenericIdentity(name, "AuthenticationTypes.Federation"); // setting ClaimsIdentity.AuthenticationType to value that azuread non-easyauth setups use
97-
98-
this.Logger.LogInformation("building claims from payload...");
99-
100-
var xMsClientPrincipal = JObject.Parse(Encoding.UTF8.GetString(Convert.FromBase64String(requestHeaders["X-MS-CLIENT-PRINCIPAL"][0])));
101-
//var nameidentifier = xMsClientPrincipal["claims"].Children<JObject>().FirstOrDefault(c => c["typ"].ToString() == ClaimTypes.NameIdentifier)?["val"].ToString();
102-
var claims = new List<Claim>();
103-
foreach (var claim in xMsClientPrincipal["claims"].Children<JObject>())
104-
{
105-
if (claim["typ"].ToString() == "http://schemas.microsoft.com/claims/authnmethodsreferences")
106-
{
107-
foreach (var item in claim["val"].ToString().Split(','))
108-
{
109-
claims.Add(new Claim(ClaimTypes.Authentication, item));
110-
}
111-
}
112-
else if (claim["typ"].ToString() == "roles")
113-
{
114-
foreach (var item in claim["val"].ToString().Split(','))
115-
{
116-
//(User.Identity as ClaimsIdentity).RoleClaimType must match type that role claims are assigned to for Authorization and IsInRole to work
117-
claims.Add(new Claim(ClaimTypes.Role, item));
118-
}
119-
}
120-
else
121-
{
122-
//(User.Identity as ClaimsIdentity).NameClaimType must be what name claim is assigned to for User.Identity.Name to work
123-
claims.Add(new Claim(claim["typ"].ToString(), claim["val"].ToString()));
124-
}
125-
}
126-
127-
this.Logger.LogInformation("Add claims to new identity");
128-
129-
identity.AddClaims(claims);
130-
//identity.AddClaim(new Claim("id_token", idToken)); // don't think we should be including this
131-
//identity.AddClaim(new Claim("http://schemas.microsoft.com/claims/authnclassreference", 1)); // don't think we need to add this
132-
if (!(identity.Claims as List<Claim>).Exists(claim => claim.Type == "scp")) identity.AddClaim(new Claim("scp", "user_impersonation")); // not sure why easyauth is dropping this
133-
identity.AddClaim(new Claim("provider_name", requestHeaders["X-MS-CLIENT-PRINCIPAL-IDP"][0]));
134-
var genericPrincipal = new GenericPrincipal(identity, null);
135-
return new AuthenticationTicket(genericPrincipal, EasyAuthAuthenticationDefaults.AuthenticationScheme);
136-
}
137-
138-
private AuthenticationTicket BuildIdentityFromEasyAuthMeJson(JObject payload)
139-
{
140-
var name = payload["user_id"].Value<string>(); // X-MS-CLIENT-PRINCIPAL-NAME
141-
this.Logger.LogDebug("payload was fetched from easyauth me json, name: {0}", name);
142-
143-
var identity = new GenericIdentity(name, "AuthenticationTypes.Federation"); // setting ClaimsIdentity.AuthenticationType to value that azuread non-easyauth setups use
144-
145-
this.Logger.LogInformation("building claims from payload...");
146-
147-
var claims = new List<Claim>();
148-
foreach (var claim in payload["user_claims"])
149-
{
150-
if (claim["typ"].ToString() == "http://schemas.microsoft.com/claims/authnmethodsreferences")
151-
{
152-
foreach (var item in claim["val"].ToString().Split(','))
153-
{
154-
claims.Add(new Claim(ClaimTypes.Authentication, item));
155-
}
156-
}
157-
else if (claim["typ"].ToString() == "roles")
65+
if (isContextUserNotAuthenticated(this.Context.User))
15866
{
159-
foreach (var item in claim["val"].ToString().Split(','))
160-
{
161-
//(User.Identity as ClaimsIdentity).RoleClaimType must match type that role claims are assigned to for Authorization and IsInRole to work
162-
claims.Add(new Claim(ClaimTypes.Role, item));
163-
}
67+
// TODO: If this the only auth middleware we maybe must return a `AuthenticateResult.Fail()`
68+
this.Logger.LogInformation("The identity isn't set by easy auth.");
16469
}
16570
else
16671
{
167-
//(User.Identity as ClaimsIdentity).NameClaimType must be what name claim is assigned to for User.Identity.Name to work
168-
claims.Add(new Claim(claim["typ"].ToString(), claim["val"].ToString()));
169-
}
170-
}
171-
172-
this.Logger.LogInformation("Add claims to new identity");
173-
174-
identity.AddClaims(claims);
175-
//identity.AddClaim(new Claim("id_token", idToken)); // don't think we should be including this
176-
//identity.AddClaim(new Claim("http://schemas.microsoft.com/claims/authnclassreference", 1)); // don't think we need to add this
177-
if (!(identity.Claims as List<Claim>).Exists(claim => claim.Type == "scp")) identity.AddClaim(new Claim("scp", "user_impersonation")); // not sure why easyauth is dropping this
178-
identity.AddClaim(new Claim("provider_name", payload["provider_name"].Value<string>())); // X-MS-CLIENT-PRINCIPAL-IDP
179-
var genericPrincipal = new GenericPrincipal(identity, null);
180-
return new AuthenticationTicket(genericPrincipal, EasyAuthAuthenticationDefaults.AuthenticationScheme);
181-
}
182-
183-
private HttpRequestMessage CreateAuthRequest(ref CookieContainer cookieContainer)
184-
{
185-
this.Logger.LogInformation($"identity not found, attempting to fetch from auth endpoint '/{this.Options.AuthEndpoint}'");
186-
187-
var uriString = $"{this.Context.Request.Scheme}://{this.Context.Request.Host}";
188-
189-
this.Logger.LogDebug("host uri: {0}", uriString);
190-
191-
foreach (var c in this.Context.Request.Cookies)
192-
{
193-
cookieContainer.Add(new Uri(uriString), new Cookie(c.Key, c.Value));
194-
}
195-
196-
this.Logger.LogDebug("found {0} cookies in request", cookieContainer.Count);
197-
198-
foreach (var cookie in this.Context.Request.Cookies)
199-
{
200-
this.Logger.LogDebug(cookie.Key);
201-
}
202-
203-
// fetch value from endpoint
204-
var authMeEndpoint = string.Empty;
205-
if (this.Options.AuthEndpoint.StartsWith("http")) authMeEndpoint = this.Options.AuthEndpoint; // enable pulling from places like storage account public blob container
206-
else authMeEndpoint = $"{uriString}/{this.Options.AuthEndpoint}"; // localhost relative path, e.g. wwwroot/.auth/me.json
207-
208-
var request = new HttpRequestMessage(HttpMethod.Get, authMeEndpoint);
209-
foreach (var header in this.Context.Request.Headers)
210-
{
211-
if (header.Key.StartsWith("X-ZUMO-"))
212-
{
213-
request.Headers.Add(header.Key, header.Value[0]);
214-
}
215-
}
216-
217-
return request;
218-
}
219-
220-
private HttpClientHandler CreateHandler(ref CookieContainer container)
221-
{
222-
var handler = new HttpClientHandler()
223-
{
224-
CookieContainer = container
225-
};
226-
return handler;
227-
}
228-
229-
private async Task<JArray> GetAuthMe(HttpClientHandler handler, HttpRequestMessage httpRequest)
230-
{
231-
JArray payload = null;
232-
using (var client = new HttpClient(handler))
233-
{
234-
var response = await client.SendAsync(httpRequest);
235-
if (!response.IsSuccessStatusCode)
236-
{
237-
this.Logger.LogDebug("auth endpoint was not sucessful. Status code: {0}, reason {1}", response.StatusCode, response.ReasonPhrase);
238-
throw new WebException("Unable to fetch user information from auth endpoint.");
239-
}
240-
241-
var content = await response.Content.ReadAsStringAsync();
242-
try
243-
{
244-
payload = JArray.Parse(content);
245-
}
246-
catch (Exception)
247-
{
248-
throw new JsonSerializationException("Could not retreive json from /me endpoint.");
72+
this.Logger.LogInformation("identity already set, skipping middleware");
24973
}
74+
return AuthenticateResult.NoResult();
25075
}
251-
252-
return payload;
25376
}
25477
}
25578
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace KK.AspNetCore.EasyAuthAuthentication
2+
{
3+
public class Schemas
4+
{
5+
public const string AuthMethod = "http://schemas.microsoft.com/claims/authnmethodsreferences";
6+
}
7+
}

0 commit comments

Comments
 (0)