Skip to content

Commit 90d7448

Browse files
authored
Add EasyAuth.Handlers (#7)
1 parent cdd1fdf commit 90d7448

22 files changed

+229
-44
lines changed

Dockerfile.containerapp

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ FROM mcr.microsoft.com/dotnet/sdk:9.0-alpine AS build
44

55
COPY ./src/EasyAuth.ContainerApp /source/EasyAuth.ContainerApp
66
COPY ./src/EasyAuth.Components /source/EasyAuth.Components
7+
COPY ./src/EasyAuth.Handlers /source/EasyAuth.Handlers
78

89
WORKDIR /source/EasyAuth.ContainerApp
910

EasyAuth.sln

+20-13
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ VisualStudioVersion = 17.0.31903.59
55
MinimumVisualStudioVersion = 10.0.40219.1
66
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{09E22D62-2D4D-40BE-94ED-90EB8528124A}"
77
EndProject
8-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAuth.ContainerApp", "src\EasyAuth.ContainerApp\EasyAuth.ContainerApp.csproj", "{F2AACAFC-9022-4D1C-9B3F-F05D9D4DCCB6}"
8+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAuth.Components", "src\EasyAuth.Components\EasyAuth.Components.csproj", "{0CDA4CFC-514A-4F52-BBFA-7A0B4100D132}"
99
EndProject
10-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAuth.SwaApp", "src\EasyAuth.SwaApp\EasyAuth.SwaApp.csproj", "{7EE7A101-712C-45B6-8501-05E7FECEDA8A}"
10+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAuth.Handlers", "src\EasyAuth.Handlers\EasyAuth.Handlers.csproj", "{C342238C-516D-4A56-B3A0-9D5112C31C9F}"
1111
EndProject
1212
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAuth.WebApp", "src\EasyAuth.WebApp\EasyAuth.WebApp.csproj", "{EDBFCBE0-3F6B-4DA4-84B0-0218CC4261C2}"
1313
EndProject
14-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAuth.Components", "src\EasyAuth.Components\EasyAuth.Components.csproj", "{0CDA4CFC-514A-4F52-BBFA-7A0B4100D132}"
14+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAuth.ContainerApp", "src\EasyAuth.ContainerApp\EasyAuth.ContainerApp.csproj", "{F2AACAFC-9022-4D1C-9B3F-F05D9D4DCCB6}"
15+
EndProject
16+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAuth.SwaApp", "src\EasyAuth.SwaApp\EasyAuth.SwaApp.csproj", "{7EE7A101-712C-45B6-8501-05E7FECEDA8A}"
1517
EndProject
1618
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAuth.FunctionApp", "src\EasyAuth.FunctionApp\EasyAuth.FunctionApp.csproj", "{560AC983-7BF0-499D-B2B8-15C4B74633A3}"
1719
EndProject
@@ -21,6 +23,18 @@ Global
2123
Release|Any CPU = Release|Any CPU
2224
EndGlobalSection
2325
GlobalSection(ProjectConfigurationPlatforms) = postSolution
26+
{0CDA4CFC-514A-4F52-BBFA-7A0B4100D132}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
27+
{0CDA4CFC-514A-4F52-BBFA-7A0B4100D132}.Debug|Any CPU.Build.0 = Debug|Any CPU
28+
{0CDA4CFC-514A-4F52-BBFA-7A0B4100D132}.Release|Any CPU.ActiveCfg = Release|Any CPU
29+
{0CDA4CFC-514A-4F52-BBFA-7A0B4100D132}.Release|Any CPU.Build.0 = Release|Any CPU
30+
{C342238C-516D-4A56-B3A0-9D5112C31C9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
31+
{C342238C-516D-4A56-B3A0-9D5112C31C9F}.Debug|Any CPU.Build.0 = Debug|Any CPU
32+
{C342238C-516D-4A56-B3A0-9D5112C31C9F}.Release|Any CPU.ActiveCfg = Release|Any CPU
33+
{C342238C-516D-4A56-B3A0-9D5112C31C9F}.Release|Any CPU.Build.0 = Release|Any CPU
34+
{EDBFCBE0-3F6B-4DA4-84B0-0218CC4261C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
35+
{EDBFCBE0-3F6B-4DA4-84B0-0218CC4261C2}.Debug|Any CPU.Build.0 = Debug|Any CPU
36+
{EDBFCBE0-3F6B-4DA4-84B0-0218CC4261C2}.Release|Any CPU.ActiveCfg = Release|Any CPU
37+
{EDBFCBE0-3F6B-4DA4-84B0-0218CC4261C2}.Release|Any CPU.Build.0 = Release|Any CPU
2438
{F2AACAFC-9022-4D1C-9B3F-F05D9D4DCCB6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
2539
{F2AACAFC-9022-4D1C-9B3F-F05D9D4DCCB6}.Debug|Any CPU.Build.0 = Debug|Any CPU
2640
{F2AACAFC-9022-4D1C-9B3F-F05D9D4DCCB6}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -29,14 +43,6 @@ Global
2943
{7EE7A101-712C-45B6-8501-05E7FECEDA8A}.Debug|Any CPU.Build.0 = Debug|Any CPU
3044
{7EE7A101-712C-45B6-8501-05E7FECEDA8A}.Release|Any CPU.ActiveCfg = Release|Any CPU
3145
{7EE7A101-712C-45B6-8501-05E7FECEDA8A}.Release|Any CPU.Build.0 = Release|Any CPU
32-
{EDBFCBE0-3F6B-4DA4-84B0-0218CC4261C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
33-
{EDBFCBE0-3F6B-4DA4-84B0-0218CC4261C2}.Debug|Any CPU.Build.0 = Debug|Any CPU
34-
{EDBFCBE0-3F6B-4DA4-84B0-0218CC4261C2}.Release|Any CPU.ActiveCfg = Release|Any CPU
35-
{EDBFCBE0-3F6B-4DA4-84B0-0218CC4261C2}.Release|Any CPU.Build.0 = Release|Any CPU
36-
{0CDA4CFC-514A-4F52-BBFA-7A0B4100D132}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
37-
{0CDA4CFC-514A-4F52-BBFA-7A0B4100D132}.Debug|Any CPU.Build.0 = Debug|Any CPU
38-
{0CDA4CFC-514A-4F52-BBFA-7A0B4100D132}.Release|Any CPU.ActiveCfg = Release|Any CPU
39-
{0CDA4CFC-514A-4F52-BBFA-7A0B4100D132}.Release|Any CPU.Build.0 = Release|Any CPU
4046
{560AC983-7BF0-499D-B2B8-15C4B74633A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
4147
{560AC983-7BF0-499D-B2B8-15C4B74633A3}.Debug|Any CPU.Build.0 = Debug|Any CPU
4248
{560AC983-7BF0-499D-B2B8-15C4B74633A3}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -46,10 +52,11 @@ Global
4652
HideSolutionNode = FALSE
4753
EndGlobalSection
4854
GlobalSection(NestedProjects) = preSolution
55+
{0CDA4CFC-514A-4F52-BBFA-7A0B4100D132} = {09E22D62-2D4D-40BE-94ED-90EB8528124A}
56+
{C342238C-516D-4A56-B3A0-9D5112C31C9F} = {09E22D62-2D4D-40BE-94ED-90EB8528124A}
57+
{EDBFCBE0-3F6B-4DA4-84B0-0218CC4261C2} = {09E22D62-2D4D-40BE-94ED-90EB8528124A}
4958
{F2AACAFC-9022-4D1C-9B3F-F05D9D4DCCB6} = {09E22D62-2D4D-40BE-94ED-90EB8528124A}
5059
{7EE7A101-712C-45B6-8501-05E7FECEDA8A} = {09E22D62-2D4D-40BE-94ED-90EB8528124A}
51-
{EDBFCBE0-3F6B-4DA4-84B0-0218CC4261C2} = {09E22D62-2D4D-40BE-94ED-90EB8528124A}
52-
{0CDA4CFC-514A-4F52-BBFA-7A0B4100D132} = {09E22D62-2D4D-40BE-94ED-90EB8528124A}
5360
{560AC983-7BF0-499D-B2B8-15C4B74633A3} = {09E22D62-2D4D-40BE-94ED-90EB8528124A}
5461
EndGlobalSection
5562
EndGlobal

README.md

+1-7
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,6 @@ This provides sample [Blazor](https://learn.microsoft.com/aspnet/core/blazor/) a
3131
dotnet restore && dotnet build
3232
```
3333

34-
1. Create artifacts for each app
35-
36-
```bash
37-
dotnet publish -c Release
38-
```
39-
4034
1. Login to Azure.
4135

4236
```bash
@@ -79,7 +73,7 @@ This provides sample [Blazor](https://learn.microsoft.com/aspnet/core/blazor/) a
7973

8074
## Known Limitations of Azure EasyAuth
8175

82-
Azure EasyAuth is supposed to protect your entire app, not for specific pages. Therefore, if you want to protect certain pages of your app, you have to implement the authentication/authorisation logic by yourself.
76+
Azure EasyAuth is supposed to protect your entire app, not for specific pages. Therefore, if you want to protect certain pages of your app, you have to implement a custom authentication/authorisation logic by yourself. In this sample app, the [`EasyAuth.Handlers`](./src/EasyAuth.Handlers/) project is the one.
8377

8478
## Clean Up
8579

infra/resources.bicep

+4
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,10 @@ module easyauthWebapp 'br/public:avm/res/web/site:0.12.1' = {
202202
name: 'WEBSITES_ENABLE_APP_SERVICE_STORAGE'
203203
value: 'false'
204204
}
205+
{
206+
name: 'USE_AUTH_DETAILS'
207+
value: 'false'
208+
}
205209
]
206210
ftpsState: 'FtpsOnly'
207211
linuxFxVersion: 'DOTNETCORE|9.0'

src/EasyAuth.ContainerApp/EasyAuth.ContainerApp.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
<ItemGroup>
1111
<ProjectReference Include="..\EasyAuth.Components\EasyAuth.Components.csproj" />
12+
<ProjectReference Include="..\EasyAuth.Handlers\EasyAuth.Handlers.csproj" />
1213
</ItemGroup>
1314

1415
</Project>

src/EasyAuth.ContainerApp/Properties/launchSettings.json

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
{
22
"$schema": "https://json.schemastore.org/launchsettings.json",
33
"profiles": {
4-
"https": {
4+
"http": {
55
"commandName": "Project",
66
"dotnetRunMessages": true,
77
"launchBrowser": true,
8-
"applicationUrl": "https://localhost:3030;http://localhost:3000",
8+
"applicationUrl": "http://localhost:8040",
99
"environmentVariables": {
1010
"ASPNETCORE_ENVIRONMENT": "Development"
1111
}
1212
},
13-
"http": {
13+
"https": {
1414
"commandName": "Project",
1515
"dotnetRunMessages": true,
1616
"launchBrowser": true,
17-
"applicationUrl": "http://localhost:3000",
17+
"applicationUrl": "https://localhost:8041;http://localhost:8040",
1818
"environmentVariables": {
1919
"ASPNETCORE_ENVIRONMENT": "Development"
2020
}

src/EasyAuth.ContainerApp/Services/RequestService.cs

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
using System.Text.Json;
1+
using System.Text.Json;
22

33
using EasyAuth.Components.Services;
4+
using EasyAuth.Handlers;
45

56
namespace EasyAuth.ContainerApp.Services;
67

@@ -82,10 +83,9 @@ public async Task<string> GetClientPrincipal()
8283
return "No client principal found";
8384
}
8485

85-
var decoded = Convert.FromBase64String(encoded);
86-
using var stream = new MemoryStream(decoded);
87-
var clientPrincipal = JsonSerializer.Serialize(await JsonSerializer.DeserializeAsync<object>(stream), options);
86+
var principal = await MsClientPrincipal.ParseMsClientPrincipal(encoded!).ConfigureAwait(false);
87+
var serialised = JsonSerializer.Serialize(principal, options);
8888

89-
return clientPrincipal;
89+
return serialised;
9090
}
9191
}

src/EasyAuth.FunctionApp/AuthDetailsHttpTrigger.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ public async Task<IActionResult> GetClientPrincipal([HttpTrigger(AuthorizationLe
6666
return new OkObjectResult("No client principal found");
6767
}
6868

69-
var decoded = Convert.FromBase64String(encoded);
69+
var decoded = Convert.FromBase64String(encoded!);
7070
using var stream = new MemoryStream(decoded);
7171
var clientPrincipal = JsonSerializer.Serialize(await JsonSerializer.DeserializeAsync<object>(stream), options);
7272

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net9.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
</PropertyGroup>
8+
9+
<ItemGroup>
10+
<FrameworkReference Include="Microsoft.AspNetCore.App" />
11+
</ItemGroup>
12+
13+
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using Microsoft.AspNetCore.Authentication;
2+
3+
namespace EasyAuth.Handlers;
4+
5+
public static class EasyAuthAuthenticationBuilderExtensions
6+
{
7+
public static AuthenticationBuilder AddAzureEasyAuthHandler(this AuthenticationBuilder builder, Action<EasyAuthAuthenticationOptions>? configure = null)
8+
{
9+
if (configure == null)
10+
{
11+
configure = o => { };
12+
}
13+
14+
return builder.AddScheme<EasyAuthAuthenticationOptions, EasyAuthAuthenticationHandler>(
15+
EasyAuthAuthenticationHandler.EASY_AUTH_SCHEME_NAME,
16+
EasyAuthAuthenticationHandler.EASY_AUTH_SCHEME_NAME,
17+
configure);
18+
}
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using System.Text.Encodings.Web;
2+
3+
using Microsoft.AspNetCore.Authentication;
4+
using Microsoft.Extensions.Logging;
5+
using Microsoft.Extensions.Options;
6+
7+
namespace EasyAuth.Handlers;
8+
9+
public class EasyAuthAuthenticationHandler(IOptionsMonitor<EasyAuthAuthenticationOptions> options, ILoggerFactory logger, UrlEncoder encoder)
10+
: AuthenticationHandler<EasyAuthAuthenticationOptions>(options, logger, encoder)
11+
{
12+
public const string EASY_AUTH_SCHEME_NAME = "EasyAuth";
13+
14+
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
15+
{
16+
try
17+
{
18+
var easyAuthProvider = Context.Request.Headers["X-MS-CLIENT-PRINCIPAL-IDP"].FirstOrDefault() ?? "aad";
19+
var encoded = Context.Request.Headers["X-MS-CLIENT-PRINCIPAL"].FirstOrDefault();
20+
if (string.IsNullOrWhiteSpace(encoded) == true)
21+
{
22+
return AuthenticateResult.NoResult();
23+
}
24+
25+
var principal = await MsClientPrincipal.ParseClaimsPrincipal(encoded!).ConfigureAwait(false);
26+
if (principal == null)
27+
{
28+
return AuthenticateResult.NoResult();
29+
}
30+
31+
var ticket = new AuthenticationTicket(principal, easyAuthProvider);
32+
var success = AuthenticateResult.Success(ticket);
33+
34+
this.Context.User = principal;
35+
36+
return success;
37+
}
38+
catch (Exception ex)
39+
{
40+
return AuthenticateResult.Fail(ex);
41+
}
42+
}
43+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using Microsoft.AspNetCore.Authentication;
2+
3+
namespace EasyAuth.Handlers;
4+
5+
public class EasyAuthAuthenticationOptions : AuthenticationSchemeOptions
6+
{
7+
public EasyAuthAuthenticationOptions()
8+
{
9+
Events = new object();
10+
}
11+
}
+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
using System.Security.Claims;
2+
using System.Text.Json;
3+
using System.Text.Json.Serialization;
4+
5+
namespace EasyAuth.Handlers;
6+
7+
public class MsClientPrincipal
8+
{
9+
private static readonly JsonSerializerOptions options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
10+
11+
[JsonPropertyName("auth_typ")]
12+
public string? IdentityProvider { get; set; }
13+
14+
[JsonPropertyName("name_typ")]
15+
public string? NameClaimType { get; set; }
16+
17+
[JsonPropertyName("role_typ")]
18+
public string? RoleClaimType { get; set; }
19+
20+
[JsonPropertyName("claims")]
21+
public IEnumerable<MsClientPrincipalClaim>? Claims { get; set; }
22+
23+
public static async Task<MsClientPrincipal?> ParseMsClientPrincipal(string value)
24+
{
25+
var decoded = Convert.FromBase64String(value);
26+
using var stream = new MemoryStream(decoded);
27+
var principal = await JsonSerializer.DeserializeAsync<MsClientPrincipal>(stream, options).ConfigureAwait(false);
28+
29+
return principal;
30+
}
31+
32+
public static async Task<ClaimsPrincipal?> ParseClaimsPrincipal(string value)
33+
{
34+
var clientPrincipal = await ParseMsClientPrincipal(value).ConfigureAwait(false);
35+
if (clientPrincipal == null || clientPrincipal.Claims?.Any() == false)
36+
{
37+
return null;
38+
}
39+
40+
var claims = clientPrincipal.Claims!.Select(claim => new Claim(claim.Type!, claim.Value!));
41+
42+
// remap "roles" claims from easy auth to the more standard ClaimTypes.Role: "http://schemas.microsoft.com/ws/2008/06/identity/claims/role"
43+
var easyAuthRoleClaims = claims.Where(claim => claim.Type == "roles");
44+
var claimsAndRoles = claims.Concat(easyAuthRoleClaims.Select(role => new Claim(clientPrincipal.RoleClaimType!, role.Value)));
45+
46+
var identity = new ClaimsIdentity(claimsAndRoles, clientPrincipal.IdentityProvider, clientPrincipal.NameClaimType, clientPrincipal.RoleClaimType);
47+
var claimsPrincipal = new ClaimsPrincipal(identity);
48+
49+
return claimsPrincipal;
50+
}
51+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace EasyAuth.Handlers;
4+
5+
public class MsClientPrincipalClaim
6+
{
7+
[JsonPropertyName("typ")]
8+
public string? Type { get; set; }
9+
10+
[JsonPropertyName("val")]
11+
public string? Value { get; set; }
12+
}

src/EasyAuth.SwaApp/Services/RequestService.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System.Text;
1+
using System.Text;
22

33
using EasyAuth.Components.Services;
44

@@ -48,9 +48,9 @@ public async Task<string> GetAuthMe()
4848
{
4949
authMe = await http.GetStringAsync("/.auth/me");
5050
}
51-
catch
51+
catch (Exception ex)
5252
{
53-
authMe = "Not authenticated";
53+
authMe = ex.Message;
5454
}
5555

5656
return authMe;
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,25 @@
11
@page "/"
2+
@inject IConfiguration Config
23

34
<PageTitle>Home</PageTitle>
45

56
<h1>Hello, world!</h1>
67

78
Welcome to your new app.
89

9-
<AuthDetails @rendermode="RenderMode.InteractiveServer" />
10+
@if (useAuthDetails == true)
11+
{
12+
<AuthDetails @rendermode="RenderMode.InteractiveServer" />
13+
}
14+
15+
@code
16+
{
17+
private bool useAuthDetails;
18+
19+
protected override async Task OnInitializedAsync()
20+
{
21+
useAuthDetails = bool.TryParse(Config["USE_AUTH_DETAILS"], out var result) && result;
22+
23+
await Task.CompletedTask;
24+
}
25+
}

src/EasyAuth.WebApp/Components/Pages/Weather.razor

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
@page "/weather"
2+
@using Microsoft.AspNetCore.Authorization
23
@attribute [StreamRendering]
4+
@attribute [Authorize(AuthenticationSchemes = "EasyAuth")]
35

46
<PageTitle>Weather</PageTitle>
57

src/EasyAuth.WebApp/EasyAuth.WebApp.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
<ItemGroup>
1111
<ProjectReference Include="..\EasyAuth.Components\EasyAuth.Components.csproj" />
12+
<ProjectReference Include="..\EasyAuth.Handlers\EasyAuth.Handlers.csproj" />
1213
</ItemGroup>
1314

1415
</Project>

0 commit comments

Comments
 (0)