Skip to content

Commit

Permalink
Authn (#125)
Browse files Browse the repository at this point in the history
  • Loading branch information
Zhiyuan-Amos authored Apr 13, 2022
1 parent 13bd4f2 commit d7fa9e3
Show file tree
Hide file tree
Showing 20 changed files with 221 additions and 241 deletions.
47 changes: 47 additions & 0 deletions .github/workflows/azure-api-function.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# https://docs.microsoft.com/en-us/azure/azure-functions/functions-how-to-github-actions?tabs=dotnet#deploy-the-function-app

name: Deploy Api to Function App

on:
[push]

env:
AZURE_FUNCTIONAPP_NAME: couple-api
AZURE_FUNCTIONAPP_PROJ_PATH: 'Api'
DOTNET_VERSION: '6.0.x'

jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: 'Checkout'
uses: actions/checkout@v2

- name: Setup DotNet ${{ env.DOTNET_VERSION }} Environment
uses: actions/setup-dotnet@v1
with:
dotnet-version: ${{ env.DOTNET_VERSION }}

- name: 'Build'
shell: bash
run: |
pushd './${{ env.AZURE_FUNCTIONAPP_PROJ_PATH }}'
dotnet build --configuration Release --output ./output
popd
- name: 'Install Azure Functions Core Tools'
run: npm i -g azure-functions-core-tools@4 --unsafe-perm true

- name: 'Login to Azure'
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}

- name: 'Publish'
run: func azure functionapp publish ${{ env.AZURE_FUNCTIONAPP_NAME }}
working-directory: ${{ env.AZURE_FUNCTIONAPP_PROJ_PATH }}

- name: 'Logout of Azure'
run: |
az logout
if: always()
48 changes: 48 additions & 0 deletions .github/workflows/azure-client-storage.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# https://docs.microsoft.com/en-us/azure/storage/blobs/storage-blobs-static-site-github-actions

name: Deploy Client to Azure Storage

on:
[push]

env:
AZURE_CLIENT_PATH: 'Client'
DOTNET_VERSION: '6.0.x'

jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: 'Checkout'
uses: actions/checkout@v2

- name: Setup DotNet ${{ env.DOTNET_VERSION }} Environment
uses: actions/setup-dotnet@v1
with:
dotnet-version: ${{ env.DOTNET_VERSION }}

- name: 'Build'
shell: bash
run: |
pushd './${{ env.AZURE_CLIENT_PATH }}'
dotnet workload install wasm-tools
dotnet publish --configuration Release --output ./output
mv ./output/wwwroot/* ./output/
popd
- name: 'Login to Azure'
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}

- name: 'Deploy'
uses: azure/CLI@v1
with:
inlineScript: |
az storage blob delete-batch --account-name couple --auth-mode key --source '$web'
az storage blob upload-batch --account-name couple --auth-mode key --destination '$web' --source ./Client/output
- name: 'Logout of Azure'
run: |
az logout
if: always()
9 changes: 5 additions & 4 deletions .github/workflows/azure-messaging-function.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# https://docs.microsoft.com/en-us/azure/azure-functions/functions-how-to-github-actions?tabs=dotnet#deploy-the-function-app

name: Deploy DotNet project to function app with a Linux environment
name: Deploy Messaging to Function App

on:
[push]
Expand All @@ -14,21 +14,22 @@ jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: 'Checkout GitHub Action'
- name: 'Checkout'
uses: actions/checkout@v2

- name: Setup DotNet ${{ env.DOTNET_VERSION }} Environment
uses: actions/setup-dotnet@v1
with:
dotnet-version: ${{ env.DOTNET_VERSION }}

- name: 'Resolve Project Dependencies Using Dotnet'
- name: 'Build'
shell: bash
run: |
pushd './${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }}'
dotnet build --configuration Release --output ./output
popd
- name: 'Run Azure Functions Action'
- name: 'Deploy'
uses: Azure/functions-action@v1
id: fa
with:
Expand Down

This file was deleted.

1 change: 1 addition & 0 deletions Api/Couple.Api.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Cosmos" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="6.0.0" />
<PackageReference Include="Polly" Version="7.2.2" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.17.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Shared\Couple.Shared.csproj" />
Expand Down
14 changes: 0 additions & 14 deletions Api/Features/Utility/Warmer.cs

This file was deleted.

72 changes: 5 additions & 67 deletions Api/Infrastructure/CurrentUserService.cs
Original file line number Diff line number Diff line change
@@ -1,77 +1,15 @@
using System.IdentityModel.Tokens.Jwt;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Text;
using System.Text.Json;

namespace Couple.Api.Infrastructure;

public class CurrentUserService : ICurrentUserService
{
private const string ClaimTypePartnerId = "PartnerId";

public Claims GetClaims(HttpHeaders headers)
{
var clientPrincipal = StaticWebAppsAuth.Parse(headers);

var id = clientPrincipal.FindFirstValue(ClaimTypes.NameIdentifier)!;
var partnerId = clientPrincipal.FindFirstValue(ClaimTypePartnerId)!;

return new(id, partnerId);
}

// from https://docs.microsoft.com/en-us/azure/static-web-apps/user-information?tabs=csharp#api-functions
private static class StaticWebAppsAuth
{
public static ClaimsPrincipal Parse(HttpHeaders headers)
{
var data = headers.GetValues("x-ms-client-principal").First();
var decoded = Convert.FromBase64String(data);
var json = Encoding.ASCII.GetString(decoded);
var principal = JsonSerializer.Deserialize<ClientPrincipal>(json,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true })!;

#pragma warning disable CS8604
var roles = principal.UserRoles
#pragma warning restore CS8604
.Where(role => role != "anonymous"
&& role != "authenticated"
&& !role.StartsWith("id_")
&& !role.StartsWith("partnerid_"))
.Select(r => new Claim(ClaimTypes.Role, r))
.ToList();

if (!roles.Any())
{
return new();
}

var adminAssignedId = principal.UserRoles
.Single(role => role.StartsWith("id_"));
var partnerId = principal.UserRoles
.Single(role => role.StartsWith("partnerid_"));

var identity = new ClaimsIdentity(principal.IdentityProvider);

// Azure Static Web App does not allow us to add custom properties. Therefore,
// PartnerId needs to be stored as a role instead. However, the default Id generated by Azure SWA
// is longer than 25 characters, and Azure SWA disallows roles from having more than 25 characters.
// Therefore, this is the temporary workaround in order to store partnerId.
// By right we should be using principal.UserId, which is pending AAD B2C implementation:
// See https://github.com/Azure/static-web-apps/issues/3
// An alternative implementation is to return a custom Cookie / JWT, but that requires more effort
// with little gains, given the current state of the project.
identity.AddClaim(new(ClaimTypes.NameIdentifier, adminAssignedId[3..]));
identity.AddClaim(new(ClaimTypePartnerId, partnerId[10..]));
identity.AddClaims(roles);
return new(identity);
}

private class ClientPrincipal
{
public string? IdentityProvider { get; set; }
public string? UserId { get; set; }
public string? UserDetails { get; set; }
public IEnumerable<string>? UserRoles { get; set; }
}
var authValues = headers.GetValues("authorization");
var authHeader = AuthenticationHeaderValue.Parse(authValues.ToArray().First());
var jwt = new JwtSecurityTokenHandler().ReadJwtToken(authHeader.Parameter);
return new(jwt.Subject, jwt.Claims.First(c => c.Type == "name").Value);
}
}
27 changes: 15 additions & 12 deletions Client/App.razor
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
@inject NavigationManager _navigationManager

<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<RouteView DefaultLayout="@typeof(MainLayout)" RouteData="@routeData"/>
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
@{
_navigationManager.NavigateTo("");
}
</LayoutView>
</NotFound>
</Router>
<CascadingAuthenticationState>
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
</AuthorizeRouteView>
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
@{
_navigationManager.NavigateTo("");
}
</LayoutView>
</NotFound>
</Router>
</CascadingAuthenticationState>
16 changes: 12 additions & 4 deletions Client/Couple.Client.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,14 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="6.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="6.0.2" PrivateAssets="all" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="6.0.4" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="6.0.4" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="6.0.4" PrivateAssets="all" />
<PackageReference Include="Microsoft.Authentication.WebAssembly.Msal" Version="6.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.4" />
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
<PackageReference Include="System.Net.Http.Json" Version="6.0.0" />
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.0-pre20220207221914" />
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.0-pre20220318192836" />
</ItemGroup>

<ItemGroup>
Expand All @@ -30,4 +33,9 @@
<ServiceWorker Include="wwwroot\service-worker.js" PublishedContent="wwwroot\service-worker.published.js" />
</ItemGroup>

<!-- Remove after https://github.com/dotnet/aspnetcore/issues/38082 is fixed -->
<ItemGroup>
<TrimmerRootAssembly Include="Microsoft.Authentication.WebAssembly.Msal" />
</ItemGroup>

</Project>
18 changes: 18 additions & 0 deletions Client/Infrastructure/ApiAuthorizationMessageHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using Couple.Client.Utility;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

namespace Couple.Client.Infrastructure;

public class ApiAuthorizationMessageHandler : AuthorizationMessageHandler
{
public const string Scope = @"https://couplesg.onmicrosoft.com/api/all";

public ApiAuthorizationMessageHandler(IAccessTokenProvider provider,
NavigationManager navigationManager,
IConfiguration configuration)
: base(provider, navigationManager) =>
ConfigureHandler(
new[] { configuration[Constants.ApiPrefix]! },
new[] { Scope });
}
9 changes: 9 additions & 0 deletions Client/Pages/Authentication.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@page "/authentication/{action}"
<RemoteAuthenticatorView Action="@Action"/>

@code{

[Parameter]
public string? Action { get; set; }

}
Loading

0 comments on commit d7fa9e3

Please sign in to comment.