diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 75428c0..e886905 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -4,12 +4,17 @@ on: branches: [ main, master ] pull_request: branches: [ main, master ] + workflow_dispatch: jobs: spa-test: timeout-minutes: 60 runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - name: Install certutil (for https support) + run: | + sudo apt update + sudo apt install libnss3-tools -y - uses: actions/setup-node@v4 with: node-version: lts/* diff --git a/MockOidcApp.Api/MockOidcApp.Api.csproj b/MockOidcApp.Api/MockOidcApp.Api.csproj index 87736d1..dc59334 100644 --- a/MockOidcApp.Api/MockOidcApp.Api.csproj +++ b/MockOidcApp.Api/MockOidcApp.Api.csproj @@ -12,7 +12,7 @@ - + diff --git a/MockOidcApp.AppHost/Program.cs b/MockOidcApp.AppHost/Program.cs index 5f2d205..477084b 100644 --- a/MockOidcApp.AppHost/Program.cs +++ b/MockOidcApp.AppHost/Program.cs @@ -1,3 +1,5 @@ +using System.Text.Json; + var builder = DistributedApplication.CreateBuilder(args); var api = builder.AddProject("api") @@ -14,6 +16,90 @@ var viteNpmInstall = builder.AddExecutable("vite-npm-install", "npm", "../MockOidcApp.Vite", "install"); vite.WaitForCompletion(viteNpmInstall); + + var clientId = Guid.NewGuid().ToString(); + var tenantId = Guid.NewGuid().ToString(); + var certPassword = Guid.NewGuid().ToString(); + var certExportExe = builder.AddExecutable("cert-export-exe", "dotnet", ".", "dev-certs", "https", "-ep", "./dev-certificates/aspnetapp.pfx", "-p", certPassword, "--trust", "--verbose"); + + var mockEntra = builder.AddContainer("mock-entra", "ghcr.io/soluto/oidc-server-mock") + .WaitForCompletion(certExportExe) + .WithEnvironment("ASPNETCORE_Kestrel__Certificates__Default__Password", certPassword) + .WithEnvironment("ASPNETCORE_Kestrel__Certificates__Default__Path", "/https/aspnetapp.pfx") + .WithBindMount("./dev-certificates", "/https") + .WithEnvironment("ASPNETCORE_URLS", "https://+:443") + .WithHttpsEndpoint(targetPort: 443); + + mockEntra + .WithEnvironment("ASPNET_SERVICES_OPTIONS_INLINE", System.Text.Json.JsonSerializer.Serialize(new { BasePath = $"/{tenantId}/v2.0" })) + .WithEnvironment( + "CLIENTS_CONFIGURATION_INLINE", + () => System.Text.Json.JsonSerializer.Serialize(new[] + { + new + { + ClientId = clientId, + ClientName = "Sample App", + AllowedGrantTypes = new [] { "authorization_code" }, + RedirectUris = new [] { vite.GetEndpoint("http").Url }, + PostLogoutRedirectUris = new [] { vite.GetEndpoint("http").Url }, + AllowedScopes = new [] { "openid", "profile", $"api://{clientId}/access_as_user" }, + AllowOfflineAccess = true, + RequireClientSecret = false, + AlwaysSendClientClaims = true, + Claims = new [] { + new { Type = "aud", Value = $"api://{clientId}" }, + new { Type = "ver", Value = $"1.0" }, + }, + ClientClaimsPrefix = string.Empty, + AlwaysIncludeUserClaimsInIdToken = true + } + })) + .WithEnvironment("USERS_CONFIGURATION_INLINE", () => System.Text.Json.JsonSerializer.Serialize(new[] + { + new + { + SubjectId = "1", + Username = "admin@test.com", + Password = "Password123", + Claims = new [] + { + new { Type = "name", Value = "Frank Gardner" }, + new { Type = "scp", Value = "access_as_user"}, + } + } + })) + .WithEnvironment( + "SERVER_OPTIONS_INLINE", + () => JsonSerializer.Serialize(new + { + Cors = new + { + CorsPaths = new[] + { + $"/{tenantId}/v2.0/.well-known/openid-configuration", + $"/{tenantId}/v2.0/connect/token" + } + }, + })) + .WithEnvironment( + "API_SCOPES_INLINE", + () => JsonSerializer.Serialize(new[] + { + new + { + Name = $"api://{clientId}/access_as_user", + UserClaims = new[] + { + "scp" + } + } + })); + + api + .WithEnvironment("AzureAd__Instance", mockEntra.GetEndpoint("https")) + .WithEnvironment("AzureAd__ClientId", clientId) + .WithEnvironment("AzureAd__TenantId", tenantId); } builder.Build().Run(); diff --git a/MockOidcApp.AppHost/dev-certificates/.gitignore b/MockOidcApp.AppHost/dev-certificates/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/MockOidcApp.AppHost/dev-certificates/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/MockOidcApp.SpaIntegrationTests/playwright.config.ts b/MockOidcApp.SpaIntegrationTests/playwright.config.ts index 518d871..923550a 100644 --- a/MockOidcApp.SpaIntegrationTests/playwright.config.ts +++ b/MockOidcApp.SpaIntegrationTests/playwright.config.ts @@ -30,6 +30,7 @@ export default defineConfig({ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: 'retain-on-failure', + ignoreHTTPSErrors: true, }, /* Configure projects for major browsers */ diff --git a/MockOidcApp.SpaIntegrationTests/tests/homepage.spec.ts b/MockOidcApp.SpaIntegrationTests/tests/homepage.spec.ts index ede41bf..45f67da 100644 --- a/MockOidcApp.SpaIntegrationTests/tests/homepage.spec.ts +++ b/MockOidcApp.SpaIntegrationTests/tests/homepage.spec.ts @@ -14,7 +14,11 @@ describe('Homepage', () => { const button = await page.getByTestId("auth-button"); await button.waitFor(); await button.click(); - throw new Error("Not implemented - How can we login with Entra credentials when the login page could change at any time?"); + const loginButton = page.getByRole('button', { name: 'Login' }); + await loginButton.waitFor(); + await page.getByRole('textbox', { name: 'Username' }).fill("admin@test.com"); + await page.getByRole('textbox', { name: 'Password' }).fill("Password123"); + await loginButton.click(); await expect(page.getByTestId("auth-button")).toHaveText("Logout"); await expect(page.getByTestId("welcome-message")).toHaveText("Hello Frank Gardner"); diff --git a/MockOidcApp.Vite/src/LoginButton.tsx b/MockOidcApp.Vite/src/LoginButton.tsx index ce2b435..cf8e5c3 100644 --- a/MockOidcApp.Vite/src/LoginButton.tsx +++ b/MockOidcApp.Vite/src/LoginButton.tsx @@ -12,11 +12,11 @@ function LoginButton() { }) .catch((error) => console.log(error)); }; + const account = instance.getActiveAccount(); const handleLogout = () => { - instance.logoutRedirect(); + instance.logoutRedirect({ idTokenHint: account?.idToken }); }; - const account = instance.getActiveAccount(); return ( <> diff --git a/MockOidcApp.Vite/src/authConfig.ts b/MockOidcApp.Vite/src/authConfig.ts index 580dd7a..8ba530a 100644 --- a/MockOidcApp.Vite/src/authConfig.ts +++ b/MockOidcApp.Vite/src/authConfig.ts @@ -13,6 +13,7 @@ import { Configuration } from '@azure/msal-browser'; auth: { clientId: settings.auth.clientId, authority: settings.auth.authority, + knownAuthorities: [settings.auth.authority], redirectUri: window.location.origin, postLogoutRedirectUri: '/', }, diff --git a/README.md b/README.md index 851534e..d1aa9a3 100644 --- a/README.md +++ b/README.md @@ -12,29 +12,15 @@ The demo is written in C# and Typescript. To run the system: 1. [Node 18](https://nodejs.org/en/download/current) 1. Install recommended VSCode extensions 1. Install the dotnet development certificate by running the following command `dotnet dev-certs https --trust` -1. Set up an Entra tenant and app registration for use by this Demo - (https://github.com/Azure-Samples/active-directory-dotnet-native-aspnetcore-v2/tree/master/1.%20Desktop%20app%20calls%20Web%20API#pre-requisites) - 1. Ensure you have access to an Microsoft Entra Tenant. For more information on how to get a Microsoft Entra tenant, see [How to get a Microsoft Entra tenant](https://azure.microsoft.com/documentation/articles/active-directory-howto-tenant/) - 1. [Register the sample application with your Entra tenant](https://github.com/Azure-Samples/active-directory-dotnet-native-aspnetcore-v2/tree/master/1.%20Desktop%20app%20calls%20Web%20API#step-2--register-the-sample-application-with-your-microsoft-entra-tenant) - - Make a note of the TenantId and ClientId from your app registration - 1. Create a new json file and add following content: - ```json - { - "AzureAd": { - "Instance": "https://login.microsoftonline.com", - "ClientId": "", - "TenantId": "" - } - } - ``` - Save this file at either : - - `~/.microsoft/usersecrets/5998d696-d9d8-48ef-b1ce-a783155ec3e4/secrets.json` on a linux/macOS system - - `%APPDATA%\Microsoft\UserSecrets\5998d696-d9d8-48ef-b1ce-a783155ec3e4\secrets.json` on a windows system 1. Run the solution: 1. Via VSCode by opening the MockOidcApp.AppHost/Program.cs file and using the Play button that appears in the top left corner - You may need to use the ".NET: Close Solution" and ".NET: Open Solution" commands in VSCode to enable this to work 1. Via Command line from the MockOidcApp.AppHost directory: `dotnet run` 1. Navigate to http://localhost:5100 -1. Use the Login and Logout button to observe how the demo application interacts with Entra and displays weather forecasts when logged in. +1. Use the Login and Logout button to observe how the demo application interacts with a Mock identity service and displays weather forecasts when logged in. + - The demo is setup to have the following credentials: + - **Username:** `admin@test.com` + - **Password:** `Password123` ### Running integration tests @@ -45,9 +31,8 @@ As described in the blog post there are integration tests provided, to run these ### Running GitHub actions -A sample Github action workflow is provided to run the integration tests in Github actions. You can test these locally using [act](https://github.com/nektos/act): +A sample Github action workflow is provided to run the integration tests in Github actions. Sadly you cannot test these locally using [act](https://github.com/nektos/act) due to an issue relating to the dev container bind mount. It may work when you try it though, you can attempt to run them using these instructions: 1. Open a Terminal in the root of the Repository 1. Run `act` -At present one of the tests fail and therefore this workflow run will report itself failing. \ No newline at end of file