Skip to content

Commit 669a9c4

Browse files
authored
Merge pull request #9 from myusrn/master
collaboration on updates to address issue #7
2 parents 4488f2d + d0e7f88 commit 669a9c4

10 files changed

Lines changed: 421 additions & 129 deletions

KK.AspNetCore.EasyAuthAuthentication.sln

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
1+
22
Microsoft Visual Studio Solution File, Format Version 12.00
33
# Visual Studio 15
44
VisualStudioVersion = 15.0.26124.0

README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,9 +106,10 @@ For example:
106106
107107
## Authors
108108

109-
* **Kirsten Kluge** - *Initial work* - [kirkone](https://github.com/kirkone)
110-
* **paule96** - *Refactoring* - [paule96](https://github.com/paule96)
111-
* **Christoph Sonntag** - *Made things even more uber* - [Compufreak345](https://github.com/Compufreak345)
109+
- **Kirsten Kluge** - _Initial work_ - [kirkone](https://github.com/kirkone)
110+
- **paule96** - _Refactoring_ - [paule96](https://github.com/paule96)
111+
- **Christoph Sonntag** - _Made things even more uber_ - [Compufreak345](https://github.com/Compufreak345)
112+
- **myusrn** - _Dropped some knowledge about making IsInRoles work_ - [myusrn](https://github.com/myusrn)
112113

113114
See also the list of [contributors](https://github.com/kirkone/KK.AspNetCore.EasyAuthAuthentication/graphs/contributors) who participated in this project.
114115

@@ -118,4 +119,4 @@ This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md
118119

119120
## Acknowledgments
120121

121-
* Inspired by this [StackOverflow post](https://stackoverflow.com/a/42402163/6526640) and this [GitHub](https://github.com/lpunderscore/azureappservice-authentication-middleware) repo
122+
- Inspired by this [StackOverflow post](https://stackoverflow.com/a/42402163/6526640) and this [GitHub](https://github.com/lpunderscore/azureappservice-authentication-middleware) repo
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: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
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="providerName">The provider name of the current auth provider.</param>
17+
/// <returns>A `AuthenticationTicket`</returns>
18+
public static AuthenticationTicket Build(IEnumerable<JObject> claimsPayload, string providerName)
19+
{
20+
var identity = new ClaimsIdentity(
21+
createClaims(claimsPayload),
22+
// setting ClaimsIdentity.AuthenticationType to value that Azure AD non-EasyAuth setups use
23+
AuthenticationTypesNames.Federation
24+
);
25+
26+
addScopeClaim(identity);
27+
addProviderNameClaim(identity, providerName);
28+
var genericPrincipal = new ClaimsPrincipal(identity);
29+
30+
return new AuthenticationTicket(genericPrincipal, EasyAuthAuthenticationDefaults.AuthenticationScheme);
31+
}
32+
33+
private static IEnumerable<Claim> createClaims(IEnumerable<JObject> claimsAsJson)
34+
{
35+
foreach (var claim in claimsAsJson)
36+
{
37+
var claimType = claim["typ"].ToString();
38+
switch (claimType)
39+
{
40+
case Schemas.AuthMethod:
41+
foreach (var item in claim["val"].ToString().Split(','))
42+
{
43+
yield return new Claim(ClaimTypes.Authentication, item);
44+
}
45+
break;
46+
case "roles":
47+
foreach (var item in claim["val"].ToString().Split(','))
48+
{
49+
yield return new Claim(ClaimTypes.Role, item);
50+
}
51+
break;
52+
default:
53+
yield return new Claim(claimType, claim["val"].ToString());
54+
break;
55+
}
56+
}
57+
}
58+
59+
private static void addScopeClaim(ClaimsIdentity identity)
60+
{
61+
if (!identity.Claims.Any(claim => claim.Type == "scp"))
62+
{
63+
// We are not sure if we should add this in to match what non-EasyAuth authenticated result would look like
64+
// with EasyAuth + Express based application configurations the scope claim will always be `user_impersonation`
65+
identity.AddClaim(new Claim("scp", "user_impersonation"));
66+
}
67+
}
68+
69+
private static void addProviderNameClaim(ClaimsIdentity identity, string providerName)
70+
{
71+
if (!identity.Claims.Any(claim => claim.Type == "provider_name"))
72+
{
73+
identity.AddClaim(new Claim("provider_name", providerName));
74+
}
75+
}
76+
}
77+
}
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: 29 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,18 @@ 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;
14+
using KK.AspNetCore.EasyAuthAuthentication.Services;
1115
using Microsoft.AspNetCore.Authentication;
16+
using Microsoft.AspNetCore.Http;
1217
using Microsoft.Extensions.Logging;
1318
using Microsoft.Extensions.Options;
1419
using Newtonsoft.Json;
@@ -34,142 +39,44 @@ public EasyAuthAuthenticationHandler(
3439
{
3540
}
3641

42+
private static Func<ClaimsPrincipal, bool> isContextUserNotAuthenticated =
43+
user => (user == null || user.Identity == null || user.Identity.IsAuthenticated == false);
44+
private static Func<IHeaderDictionary, string, bool> isHeaderSet =
45+
(headers, headerName) => !string.IsNullOrEmpty(headers[headerName].ToString());
46+
private Func<IHeaderDictionary, ClaimsPrincipal, bool> canUseHeaderAuth =
47+
(headers, user) => isContextUserNotAuthenticated(user) &&
48+
isHeaderSet(headers, AuthTokenHeaderNames.AADIdToken);
49+
private static Func<IHeaderDictionary, ClaimsPrincipal, HttpRequest, string, bool> canUseEasyAuthJson =
50+
(headers, user, request, authEndpoint) =>
51+
isContextUserNotAuthenticated(user)
52+
&& !isHeaderSet(headers, AuthTokenHeaderNames.AADIdToken)
53+
&& request.Path != "/" + $"{authEndpoint}";
54+
3755
/// <inheritdoc/>
3856
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
3957
{
4058
this.Logger.LogInformation("starting authentication handler for app service authentication");
4159

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}")
60+
if (canUseHeaderAuth(this.Context.Request.Headers, this.Context.User))
4761
{
48-
var cookieContainer = new CookieContainer();
49-
var handler = this.CreateHandler(ref cookieContainer);
50-
var httpRequest = this.CreateAuthRequest(ref cookieContainer);
51-
52-
JArray payload = null;
53-
try
54-
{
55-
payload = await this.GetAuthMe(handler, httpRequest);
56-
}
57-
catch (Exception ex)
58-
{
59-
return AuthenticateResult.Fail(ex.Message);
60-
}
61-
62-
// build up identity from json...
63-
var ticket = this.BuildIdentityFromJsonPayload((JObject)payload[0]);
64-
65-
this.Logger.LogInformation("Set identity to user context object.");
66-
this.Context.User = ticket.Principal;
67-
68-
this.Logger.LogInformation("identity build was a success, returning ticket");
69-
return AuthenticateResult.Success(ticket);
62+
return EasyAuthWithHeaderService.AuthUser(this.Logger, this.Context);
7063
}
71-
else
64+
else if (canUseEasyAuthJson(this.Context.Request.Headers, this.Context.User, this.Context.Request, this.Options.AuthEndpoint))
7265
{
73-
this.Logger.LogInformation("identity already set, skipping middleware");
74-
return AuthenticateResult.NoResult();
66+
return await EasyAuthWithAuthMeService.AuthUser(this.Logger, this.Context, this.Options.AuthEndpoint);
7567
}
76-
}
77-
78-
private AuthenticationTicket BuildIdentityFromJsonPayload(JObject payload)
79-
{
80-
var id = payload["user_id"].Value<string>();
81-
var idToken = payload["id_token"].Value<string>();
82-
var providerName = payload["provider_name"].Value<string>();
83-
84-
this.Logger.LogDebug("payload was fetched from endpoint. id: {0}", id);
85-
86-
var identity = new GenericIdentity(id);
87-
88-
this.Logger.LogInformation("building claims from payload...");
89-
90-
var claims = new List<Claim>();
91-
foreach (var claim in payload["user_claims"])
92-
{
93-
claims.Add(new Claim(claim["typ"].ToString(), claim["val"].ToString()));
94-
}
95-
96-
this.Logger.LogInformation("Add claims to new identity");
97-
98-
identity.AddClaims(claims);
99-
identity.AddClaim(new Claim("id_token", idToken));
100-
identity.AddClaim(new Claim("provider_name", providerName));
101-
var p = new GenericPrincipal(identity, null);
102-
return new AuthenticationTicket(
103-
p,
104-
EasyAuthAuthenticationDefaults.AuthenticationScheme);
105-
}
106-
107-
private HttpRequestMessage CreateAuthRequest(ref CookieContainer cookieContainer)
108-
{
109-
this.Logger.LogInformation($"identity not found, attempting to fetch from auth endpoint '/{this.Options.AuthEndpoint}'");
110-
111-
var uriString = $"{this.Context.Request.Scheme}://{this.Context.Request.Host}";
112-
113-
this.Logger.LogDebug("host uri: {0}", uriString);
114-
115-
foreach (var c in this.Context.Request.Cookies)
116-
{
117-
cookieContainer.Add(new Uri(uriString), new Cookie(c.Key, c.Value));
118-
}
119-
120-
this.Logger.LogDebug("found {0} cookies in request", cookieContainer.Count);
121-
122-
foreach (var cookie in this.Context.Request.Cookies)
123-
{
124-
this.Logger.LogDebug(cookie.Key);
125-
}
126-
127-
// fetch value from endpoint
128-
var request = new HttpRequestMessage(HttpMethod.Get, $"{uriString}/{this.Options.AuthEndpoint}");
129-
foreach (var header in this.Context.Request.Headers)
130-
{
131-
if (header.Key.StartsWith("X-ZUMO-"))
132-
{
133-
request.Headers.Add(header.Key, header.Value[0]);
134-
}
135-
}
136-
137-
return request;
138-
}
139-
140-
private HttpClientHandler CreateHandler(ref CookieContainer container)
141-
{
142-
var handler = new HttpClientHandler()
143-
{
144-
CookieContainer = container
145-
};
146-
return handler;
147-
}
148-
149-
private async Task<JArray> GetAuthMe(HttpClientHandler handler, HttpRequestMessage httpRequest)
150-
{
151-
JArray payload = null;
152-
using (var client = new HttpClient(handler))
68+
else
15369
{
154-
var response = await client.SendAsync(httpRequest);
155-
if (!response.IsSuccessStatusCode)
156-
{
157-
this.Logger.LogDebug("auth endpoint was not sucessful. Status code: {0}, reason {1}", response.StatusCode, response.ReasonPhrase);
158-
throw new WebException("Unable to fetch user information from auth endpoint.");
159-
}
160-
161-
var content = await response.Content.ReadAsStringAsync();
162-
try
70+
if (isContextUserNotAuthenticated(this.Context.User))
16371
{
164-
payload = JArray.Parse(content);
72+
this.Logger.LogInformation("The identity isn't set by easy auth.");
16573
}
166-
catch (Exception)
74+
else
16775
{
168-
throw new JsonSerializationException("Could not retreive json from /me endpoint.");
76+
this.Logger.LogInformation("identity already set, skipping middleware");
16977
}
78+
return AuthenticateResult.NoResult();
17079
}
171-
172-
return payload;
17380
}
17481
}
175-
}
82+
}

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
<ItemGroup>
1717
<PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.1.2" />
1818
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.1.1" />
19-
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
19+
<PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
20+
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="5.3.0" />
2021
</ItemGroup>
2122

2223
<!-- StyleCop Settings -->
@@ -25,6 +26,6 @@
2526
<CodeAnalysisRuleSet>..\..\StyleCop.ruleset</CodeAnalysisRuleSet>
2627
</PropertyGroup>
2728
<ItemGroup>
28-
<PackageReference Include="StyleCop.Analyzers" Version="1.1.0-beta009" privateassets="all" />
29+
<PackageReference Include="StyleCop.Analyzers" Version="1.1.1-beta.61" privateassets="all" />
2930
</ItemGroup>
3031
</Project>
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)