Skip to content

Commit

Permalink
Add EasyAuth.Handlers (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
justinyoo authored Jan 5, 2025
1 parent cdd1fdf commit 90d7448
Show file tree
Hide file tree
Showing 22 changed files with 229 additions and 44 deletions.
1 change: 1 addition & 0 deletions Dockerfile.containerapp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ FROM mcr.microsoft.com/dotnet/sdk:9.0-alpine AS build

COPY ./src/EasyAuth.ContainerApp /source/EasyAuth.ContainerApp
COPY ./src/EasyAuth.Components /source/EasyAuth.Components
COPY ./src/EasyAuth.Handlers /source/EasyAuth.Handlers

WORKDIR /source/EasyAuth.ContainerApp

Expand Down
33 changes: 20 additions & 13 deletions EasyAuth.sln
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{09E22D62-2D4D-40BE-94ED-90EB8528124A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAuth.ContainerApp", "src\EasyAuth.ContainerApp\EasyAuth.ContainerApp.csproj", "{F2AACAFC-9022-4D1C-9B3F-F05D9D4DCCB6}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAuth.Components", "src\EasyAuth.Components\EasyAuth.Components.csproj", "{0CDA4CFC-514A-4F52-BBFA-7A0B4100D132}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAuth.SwaApp", "src\EasyAuth.SwaApp\EasyAuth.SwaApp.csproj", "{7EE7A101-712C-45B6-8501-05E7FECEDA8A}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAuth.Handlers", "src\EasyAuth.Handlers\EasyAuth.Handlers.csproj", "{C342238C-516D-4A56-B3A0-9D5112C31C9F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAuth.WebApp", "src\EasyAuth.WebApp\EasyAuth.WebApp.csproj", "{EDBFCBE0-3F6B-4DA4-84B0-0218CC4261C2}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAuth.Components", "src\EasyAuth.Components\EasyAuth.Components.csproj", "{0CDA4CFC-514A-4F52-BBFA-7A0B4100D132}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAuth.ContainerApp", "src\EasyAuth.ContainerApp\EasyAuth.ContainerApp.csproj", "{F2AACAFC-9022-4D1C-9B3F-F05D9D4DCCB6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAuth.SwaApp", "src\EasyAuth.SwaApp\EasyAuth.SwaApp.csproj", "{7EE7A101-712C-45B6-8501-05E7FECEDA8A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAuth.FunctionApp", "src\EasyAuth.FunctionApp\EasyAuth.FunctionApp.csproj", "{560AC983-7BF0-499D-B2B8-15C4B74633A3}"
EndProject
Expand All @@ -21,6 +23,18 @@ Global
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{0CDA4CFC-514A-4F52-BBFA-7A0B4100D132}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0CDA4CFC-514A-4F52-BBFA-7A0B4100D132}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0CDA4CFC-514A-4F52-BBFA-7A0B4100D132}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0CDA4CFC-514A-4F52-BBFA-7A0B4100D132}.Release|Any CPU.Build.0 = Release|Any CPU
{C342238C-516D-4A56-B3A0-9D5112C31C9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C342238C-516D-4A56-B3A0-9D5112C31C9F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C342238C-516D-4A56-B3A0-9D5112C31C9F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C342238C-516D-4A56-B3A0-9D5112C31C9F}.Release|Any CPU.Build.0 = Release|Any CPU
{EDBFCBE0-3F6B-4DA4-84B0-0218CC4261C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EDBFCBE0-3F6B-4DA4-84B0-0218CC4261C2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EDBFCBE0-3F6B-4DA4-84B0-0218CC4261C2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EDBFCBE0-3F6B-4DA4-84B0-0218CC4261C2}.Release|Any CPU.Build.0 = Release|Any CPU
{F2AACAFC-9022-4D1C-9B3F-F05D9D4DCCB6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F2AACAFC-9022-4D1C-9B3F-F05D9D4DCCB6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F2AACAFC-9022-4D1C-9B3F-F05D9D4DCCB6}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand All @@ -29,14 +43,6 @@ Global
{7EE7A101-712C-45B6-8501-05E7FECEDA8A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7EE7A101-712C-45B6-8501-05E7FECEDA8A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7EE7A101-712C-45B6-8501-05E7FECEDA8A}.Release|Any CPU.Build.0 = Release|Any CPU
{EDBFCBE0-3F6B-4DA4-84B0-0218CC4261C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EDBFCBE0-3F6B-4DA4-84B0-0218CC4261C2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EDBFCBE0-3F6B-4DA4-84B0-0218CC4261C2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EDBFCBE0-3F6B-4DA4-84B0-0218CC4261C2}.Release|Any CPU.Build.0 = Release|Any CPU
{0CDA4CFC-514A-4F52-BBFA-7A0B4100D132}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0CDA4CFC-514A-4F52-BBFA-7A0B4100D132}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0CDA4CFC-514A-4F52-BBFA-7A0B4100D132}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0CDA4CFC-514A-4F52-BBFA-7A0B4100D132}.Release|Any CPU.Build.0 = Release|Any CPU
{560AC983-7BF0-499D-B2B8-15C4B74633A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{560AC983-7BF0-499D-B2B8-15C4B74633A3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{560AC983-7BF0-499D-B2B8-15C4B74633A3}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand All @@ -46,10 +52,11 @@ Global
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{0CDA4CFC-514A-4F52-BBFA-7A0B4100D132} = {09E22D62-2D4D-40BE-94ED-90EB8528124A}
{C342238C-516D-4A56-B3A0-9D5112C31C9F} = {09E22D62-2D4D-40BE-94ED-90EB8528124A}
{EDBFCBE0-3F6B-4DA4-84B0-0218CC4261C2} = {09E22D62-2D4D-40BE-94ED-90EB8528124A}
{F2AACAFC-9022-4D1C-9B3F-F05D9D4DCCB6} = {09E22D62-2D4D-40BE-94ED-90EB8528124A}
{7EE7A101-712C-45B6-8501-05E7FECEDA8A} = {09E22D62-2D4D-40BE-94ED-90EB8528124A}
{EDBFCBE0-3F6B-4DA4-84B0-0218CC4261C2} = {09E22D62-2D4D-40BE-94ED-90EB8528124A}
{0CDA4CFC-514A-4F52-BBFA-7A0B4100D132} = {09E22D62-2D4D-40BE-94ED-90EB8528124A}
{560AC983-7BF0-499D-B2B8-15C4B74633A3} = {09E22D62-2D4D-40BE-94ED-90EB8528124A}
EndGlobalSection
EndGlobal
8 changes: 1 addition & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,6 @@ This provides sample [Blazor](https://learn.microsoft.com/aspnet/core/blazor/) a
dotnet restore && dotnet build
```

1. Create artifacts for each app

```bash
dotnet publish -c Release
```

1. Login to Azure.

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

## Known Limitations of Azure EasyAuth

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.
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.

## Clean Up

Expand Down
4 changes: 4 additions & 0 deletions infra/resources.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,10 @@ module easyauthWebapp 'br/public:avm/res/web/site:0.12.1' = {
name: 'WEBSITES_ENABLE_APP_SERVICE_STORAGE'
value: 'false'
}
{
name: 'USE_AUTH_DETAILS'
value: 'false'
}
]
ftpsState: 'FtpsOnly'
linuxFxVersion: 'DOTNETCORE|9.0'
Expand Down
1 change: 1 addition & 0 deletions src/EasyAuth.ContainerApp/EasyAuth.ContainerApp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

<ItemGroup>
<ProjectReference Include="..\EasyAuth.Components\EasyAuth.Components.csproj" />
<ProjectReference Include="..\EasyAuth.Handlers\EasyAuth.Handlers.csproj" />
</ItemGroup>

</Project>
8 changes: 4 additions & 4 deletions src/EasyAuth.ContainerApp/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"https": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:3030;http://localhost:3000",
"applicationUrl": "http://localhost:8040",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"http": {
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:3000",
"applicationUrl": "https://localhost:8041;http://localhost:8040",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
Expand Down
10 changes: 5 additions & 5 deletions src/EasyAuth.ContainerApp/Services/RequestService.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Text.Json;
using System.Text.Json;

using EasyAuth.Components.Services;
using EasyAuth.Handlers;

namespace EasyAuth.ContainerApp.Services;

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

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

return clientPrincipal;
return serialised;
}
}
2 changes: 1 addition & 1 deletion src/EasyAuth.FunctionApp/AuthDetailsHttpTrigger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public async Task<IActionResult> GetClientPrincipal([HttpTrigger(AuthorizationLe
return new OkObjectResult("No client principal found");
}

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

Expand Down
13 changes: 13 additions & 0 deletions src/EasyAuth.Handlers/EasyAuth.Handlers.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>

</Project>
19 changes: 19 additions & 0 deletions src/EasyAuth.Handlers/EasyAuthAuthenticationBuilderExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Microsoft.AspNetCore.Authentication;

namespace EasyAuth.Handlers;

public static class EasyAuthAuthenticationBuilderExtensions
{
public static AuthenticationBuilder AddAzureEasyAuthHandler(this AuthenticationBuilder builder, Action<EasyAuthAuthenticationOptions>? configure = null)
{
if (configure == null)
{
configure = o => { };
}

return builder.AddScheme<EasyAuthAuthenticationOptions, EasyAuthAuthenticationHandler>(
EasyAuthAuthenticationHandler.EASY_AUTH_SCHEME_NAME,
EasyAuthAuthenticationHandler.EASY_AUTH_SCHEME_NAME,
configure);
}
}
43 changes: 43 additions & 0 deletions src/EasyAuth.Handlers/EasyAuthAuthenticationHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System.Text.Encodings.Web;

using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace EasyAuth.Handlers;

public class EasyAuthAuthenticationHandler(IOptionsMonitor<EasyAuthAuthenticationOptions> options, ILoggerFactory logger, UrlEncoder encoder)
: AuthenticationHandler<EasyAuthAuthenticationOptions>(options, logger, encoder)
{
public const string EASY_AUTH_SCHEME_NAME = "EasyAuth";

protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
try
{
var easyAuthProvider = Context.Request.Headers["X-MS-CLIENT-PRINCIPAL-IDP"].FirstOrDefault() ?? "aad";
var encoded = Context.Request.Headers["X-MS-CLIENT-PRINCIPAL"].FirstOrDefault();
if (string.IsNullOrWhiteSpace(encoded) == true)
{
return AuthenticateResult.NoResult();
}

var principal = await MsClientPrincipal.ParseClaimsPrincipal(encoded!).ConfigureAwait(false);
if (principal == null)
{
return AuthenticateResult.NoResult();
}

var ticket = new AuthenticationTicket(principal, easyAuthProvider);
var success = AuthenticateResult.Success(ticket);

this.Context.User = principal;

return success;
}
catch (Exception ex)
{
return AuthenticateResult.Fail(ex);
}
}
}
11 changes: 11 additions & 0 deletions src/EasyAuth.Handlers/EasyAuthAuthenticationOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using Microsoft.AspNetCore.Authentication;

namespace EasyAuth.Handlers;

public class EasyAuthAuthenticationOptions : AuthenticationSchemeOptions
{
public EasyAuthAuthenticationOptions()
{
Events = new object();
}
}
51 changes: 51 additions & 0 deletions src/EasyAuth.Handlers/MsClientPrincipal.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using System.Security.Claims;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace EasyAuth.Handlers;

public class MsClientPrincipal
{
private static readonly JsonSerializerOptions options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };

[JsonPropertyName("auth_typ")]
public string? IdentityProvider { get; set; }

[JsonPropertyName("name_typ")]
public string? NameClaimType { get; set; }

[JsonPropertyName("role_typ")]
public string? RoleClaimType { get; set; }

[JsonPropertyName("claims")]
public IEnumerable<MsClientPrincipalClaim>? Claims { get; set; }

public static async Task<MsClientPrincipal?> ParseMsClientPrincipal(string value)
{
var decoded = Convert.FromBase64String(value);
using var stream = new MemoryStream(decoded);
var principal = await JsonSerializer.DeserializeAsync<MsClientPrincipal>(stream, options).ConfigureAwait(false);

return principal;
}

public static async Task<ClaimsPrincipal?> ParseClaimsPrincipal(string value)
{
var clientPrincipal = await ParseMsClientPrincipal(value).ConfigureAwait(false);
if (clientPrincipal == null || clientPrincipal.Claims?.Any() == false)
{
return null;
}

var claims = clientPrincipal.Claims!.Select(claim => new Claim(claim.Type!, claim.Value!));

// remap "roles" claims from easy auth to the more standard ClaimTypes.Role: "http://schemas.microsoft.com/ws/2008/06/identity/claims/role"
var easyAuthRoleClaims = claims.Where(claim => claim.Type == "roles");
var claimsAndRoles = claims.Concat(easyAuthRoleClaims.Select(role => new Claim(clientPrincipal.RoleClaimType!, role.Value)));

var identity = new ClaimsIdentity(claimsAndRoles, clientPrincipal.IdentityProvider, clientPrincipal.NameClaimType, clientPrincipal.RoleClaimType);
var claimsPrincipal = new ClaimsPrincipal(identity);

return claimsPrincipal;
}
}
12 changes: 12 additions & 0 deletions src/EasyAuth.Handlers/MsClientPrincipalClaim.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System.Text.Json.Serialization;

namespace EasyAuth.Handlers;

public class MsClientPrincipalClaim
{
[JsonPropertyName("typ")]
public string? Type { get; set; }

[JsonPropertyName("val")]
public string? Value { get; set; }
}
6 changes: 3 additions & 3 deletions src/EasyAuth.SwaApp/Services/RequestService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Text;
using System.Text;

using EasyAuth.Components.Services;

Expand Down Expand Up @@ -48,9 +48,9 @@ public async Task<string> GetAuthMe()
{
authMe = await http.GetStringAsync("/.auth/me");
}
catch
catch (Exception ex)
{
authMe = "Not authenticated";
authMe = ex.Message;
}

return authMe;
Expand Down
18 changes: 17 additions & 1 deletion src/EasyAuth.WebApp/Components/Pages/Home.razor
Original file line number Diff line number Diff line change
@@ -1,9 +1,25 @@
@page "/"
@inject IConfiguration Config

<PageTitle>Home</PageTitle>

<h1>Hello, world!</h1>

Welcome to your new app.

<AuthDetails @rendermode="RenderMode.InteractiveServer" />
@if (useAuthDetails == true)
{
<AuthDetails @rendermode="RenderMode.InteractiveServer" />
}

@code
{
private bool useAuthDetails;

protected override async Task OnInitializedAsync()
{
useAuthDetails = bool.TryParse(Config["USE_AUTH_DETAILS"], out var result) && result;

await Task.CompletedTask;
}
}
2 changes: 2 additions & 0 deletions src/EasyAuth.WebApp/Components/Pages/Weather.razor
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
@page "/weather"
@using Microsoft.AspNetCore.Authorization
@attribute [StreamRendering]
@attribute [Authorize(AuthenticationSchemes = "EasyAuth")]

<PageTitle>Weather</PageTitle>

Expand Down
1 change: 1 addition & 0 deletions src/EasyAuth.WebApp/EasyAuth.WebApp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

<ItemGroup>
<ProjectReference Include="..\EasyAuth.Components\EasyAuth.Components.csproj" />
<ProjectReference Include="..\EasyAuth.Handlers\EasyAuth.Handlers.csproj" />
</ItemGroup>

</Project>
Loading

0 comments on commit 90d7448

Please sign in to comment.