Skip to content

Commit 94014e2

Browse files
committed
styr
1 parent 1e66434 commit 94014e2

6 files changed

Lines changed: 270 additions & 50 deletions

File tree

README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,14 @@ To use this application, you need:
1818
- **Azure Service Bus Data Sender** - To send messages
1919
- **Azure Service Bus Data Owner** - For full access (delete, purge, dead-letter operations)
2020

21-
The app will prompt you to sign in with your Microsoft account and request consent for Azure Resource Manager access to discover your Service Bus resources.
21+
### Authentication Flow
22+
23+
The app requires consent for two separate Azure AD resources:
24+
25+
1. **Azure Management API** - Granted at initial login to browse your Service Bus resources
26+
2. **Azure Service Bus API** - Requested via popup after login for message operations
27+
28+
This two-step consent is required because Azure AD doesn't allow requesting multiple resources in a single OAuth flow. When you first sign in, you'll grant Management API access. After login, the home page will show a consent warning with a "Grant Permission" button that opens a popup for Service Bus access.
2229

2330
## Features
2431

src/Pages/Home.razor

Lines changed: 184 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
@page "/"
2+
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
3+
@using Microsoft.JSInterop
4+
@using System.Text.Json
25
@inject IAuthenticationService AuthService
36
@inject IAzureResourceService ResourceService
7+
@inject IAccessTokenProvider TokenProvider
48
@inject NavigationManager Navigation
59
@inject NavigationStateService NavState
10+
@inject IConfiguration Configuration
11+
@inject IJSRuntime JSRuntime
612

713
<PageTitle>bussin</PageTitle>
814

@@ -12,54 +18,105 @@
1218
<h1>bussin</h1>
1319
<p class="lead mb-3">A zero-backend PWA for managing Azure Service Bus—peek messages, purge queues, monitor metrics, and more. All processing happens in your browser, with no data sent to external servers.</p>
1420

15-
@if (isRefreshing)
21+
@if (needsServiceBusConsent && !isGrantingConsent)
1622
{
17-
<div class="d-flex align-items-center gap-2 mb-3">
18-
<div class="spinner-border spinner-border-sm" role="status">
19-
<span class="visually-hidden">Refreshing...</span>
23+
<div class="alert alert-warning">
24+
<h5><i class="bi bi-shield-lock"></i> One More Permission Needed</h5>
25+
<p class="mb-3">
26+
You can see your Service Bus namespaces below, but to actually <strong>send and receive messages</strong>,
27+
you need to grant one more permission.
28+
</p>
29+
<div class="card mb-3">
30+
<div class="card-body">
31+
<h6 class="card-title">What you're granting now:</h6>
32+
<ul class="mb-0">
33+
<li><strong>Azure Service Bus API</strong> - Send and receive messages from queues, topics, and subscriptions</li>
34+
</ul>
35+
</div>
36+
</div>
37+
<div class="card mb-3 bg-light">
38+
<div class="card-body">
39+
<h6 class="card-title">What you already granted:</h6>
40+
<ul class="mb-0">
41+
<li><strong>Azure Management API</strong> - Browse and list your Service Bus resources (this is why you can see namespaces)</li>
42+
</ul>
43+
</div>
2044
</div>
21-
<small class="text-muted">Refreshing...</small>
45+
<p class="text-muted small mb-3">
46+
<strong>Why two separate permissions?</strong> Azure AD requires separate consent for different services.
47+
Management API lets you browse resources, while Service Bus API lets you send/receive messages.
48+
</p>
49+
@if (!string.IsNullOrEmpty(consentErrorMessage))
50+
{
51+
<div class="alert alert-danger mb-3">
52+
@consentErrorMessage
53+
</div>
54+
}
55+
<button class="btn btn-warning btn-lg" @onclick="GrantConsent" disabled="@isGrantingConsent">
56+
<i class="bi bi-check-circle"></i> Grant Service Bus Permission
57+
</button>
2258
</div>
2359
}
24-
25-
@if (namespaces.Count == 0)
60+
else if (isGrantingConsent)
2661
{
27-
@if (!isRefreshing)
28-
{
29-
<div class="alert alert-info">
30-
<h5>No Service Bus namespaces found</h5>
31-
<p>You don't have access to any Service Bus namespaces, or none exist in your subscriptions.</p>
32-
<p>Make sure you have the appropriate permissions to view Service Bus resources.</p>
62+
<div class="alert alert-info">
63+
<div class="d-flex align-items-center gap-2">
64+
<div class="spinner-border spinner-border-sm" role="status"></div>
65+
<span>Opening consent popup...</span>
3366
</div>
34-
}
67+
</div>
3568
}
3669
else
3770
{
38-
<div class="mb-3">
39-
<input type="text" class="form-control" placeholder="Search namespaces..."
40-
@bind="searchTerm" @bind:event="oninput" />
41-
</div>
71+
@if (isRefreshing)
72+
{
73+
<div class="d-flex align-items-center gap-2 mb-3">
74+
<div class="spinner-border spinner-border-sm" role="status">
75+
<span class="visually-hidden">Refreshing...</span>
76+
</div>
77+
<small class="text-muted">Refreshing...</small>
78+
</div>
79+
}
4280

43-
<div class="row">
44-
@foreach (var ns in FilteredNamespaces)
81+
@if (namespaces.Count == 0)
82+
{
83+
@if (!isRefreshing)
4584
{
46-
<div class="col-md-4 mb-3">
47-
<div class="card h-100 namespace-card" @onclick="() => NavigateToExplorer(ns)" style="cursor: pointer;">
48-
<div class="card-body">
49-
<h5 class="card-title">@ns.Name</h5>
50-
<p class="card-text">
51-
<small class="text-muted">
52-
<strong>Subscription:</strong> @ns.SubscriptionName<br />
53-
<strong>Resource Group:</strong> @ns.ResourceGroup<br />
54-
<strong>Location:</strong> @ns.Location<br />
55-
<strong>Tenant:</strong> @ns.TenantId
56-
</small>
57-
</p>
58-
</div>
59-
</div>
85+
<div class="alert alert-info">
86+
<h5>No Service Bus namespaces found</h5>
87+
<p>You don't have access to any Service Bus namespaces, or none exist in your subscriptions.</p>
88+
<p>Make sure you have the appropriate permissions to view Service Bus resources.</p>
6089
</div>
6190
}
62-
</div>
91+
}
92+
else
93+
{
94+
<div class="mb-3">
95+
<input type="text" class="form-control" placeholder="Search namespaces..."
96+
@bind="searchTerm" @bind:event="oninput" />
97+
</div>
98+
99+
<div class="row">
100+
@foreach (var ns in FilteredNamespaces)
101+
{
102+
<div class="col-md-4 mb-3">
103+
<div class="card h-100 namespace-card" @onclick="() => NavigateToExplorer(ns)" style="cursor: pointer;">
104+
<div class="card-body">
105+
<h5 class="card-title">@ns.Name</h5>
106+
<p class="card-text">
107+
<small class="text-muted">
108+
<strong>Subscription:</strong> @ns.SubscriptionName<br />
109+
<strong>Resource Group:</strong> @ns.ResourceGroup<br />
110+
<strong>Location:</strong> @ns.Location<br />
111+
<strong>Tenant:</strong> @ns.TenantId
112+
</small>
113+
</p>
114+
</div>
115+
</div>
116+
</div>
117+
}
118+
</div>
119+
}
63120
}
64121

65122
@if (!string.IsNullOrEmpty(errorMessage))
@@ -92,11 +149,24 @@
92149
{
93150
<div class="card-body">
94151
<h5>What permissions are requested?</h5>
95-
<p>When you sign in, this application requests consent for two Azure permissions:</p>
96-
<ul>
97-
<li><strong>Azure Management API</strong> - To list your Service Bus namespaces, queues, topics, and subscriptions</li>
98-
<li><strong>Azure Service Bus</strong> - To peek, send, and manage messages in your Service Bus entities</li>
99-
</ul>
152+
<p>This application requires two separate Azure permissions:</p>
153+
<div class="mb-3">
154+
<strong>1. Azure Management API</strong> (requested at initial sign-in)
155+
<ul>
156+
<li>Browse and list your Service Bus namespaces, queues, topics, and subscriptions</li>
157+
</ul>
158+
</div>
159+
<div class="mb-3">
160+
<strong>2. Azure Service Bus API</strong> (requested via popup after sign-in)
161+
<ul>
162+
<li>Send and receive messages from queues, topics, and subscriptions</li>
163+
<li>Perform message operations (peek, delete, purge, etc.)</li>
164+
</ul>
165+
</div>
166+
<p class="text-muted small">
167+
<strong>Why two permissions?</strong> Azure AD requires separate consent for different services.
168+
You'll be asked to consent to each one separately.
169+
</p>
100170

101171
<h5 class="mt-3">Is this safe?</h5>
102172
<p>Yes! This application is completely safe:</p>
@@ -131,6 +201,9 @@
131201
private bool showSecurityInfo = false;
132202
private string searchTerm = "";
133203
private CancellationTokenSource? _loadCts;
204+
private bool needsServiceBusConsent = false;
205+
private bool isGrantingConsent = false;
206+
private string? consentErrorMessage = null;
134207

135208
private IEnumerable<ServiceBusNamespaceInfo> FilteredNamespaces =>
136209
string.IsNullOrWhiteSpace(searchTerm)
@@ -150,10 +223,81 @@
150223
var isAuthenticated = await AuthService.IsAuthenticatedAsync();
151224
if (isAuthenticated)
152225
{
226+
await CheckServiceBusConsent();
153227
_ = LoadNamespacesAsync();
154228
}
155229
}
156230

231+
private async Task CheckServiceBusConsent()
232+
{
233+
try
234+
{
235+
var result = await TokenProvider.RequestAccessToken(new AccessTokenRequestOptions
236+
{
237+
Scopes = ["https://servicebus.azure.net/user_impersonation"]
238+
});
239+
240+
needsServiceBusConsent = result.Status == AccessTokenResultStatus.RequiresRedirect;
241+
}
242+
catch (Exception ex)
243+
{
244+
Console.WriteLine($"Error checking Service Bus consent: {ex.Message}");
245+
}
246+
}
247+
248+
private async Task GrantConsent()
249+
{
250+
isGrantingConsent = true;
251+
consentErrorMessage = null;
252+
StateHasChanged();
253+
254+
try
255+
{
256+
var clientId = Configuration["AzureAd:ClientId"];
257+
var authority = Configuration["AzureAd:Authority"];
258+
259+
await JSRuntime.InvokeVoidAsync("eval", $@"
260+
window.msalConfig = {{
261+
clientId: '{clientId}',
262+
authority: '{authority}'
263+
}};
264+
");
265+
266+
var result = await JSRuntime.InvokeAsync<JsonElement>("msalHelper.acquireTokenPopup",
267+
"https://servicebus.azure.net/user_impersonation");
268+
269+
if (result.TryGetProperty("success", out var success) && success.GetBoolean())
270+
{
271+
Console.WriteLine("✓ Service Bus consent granted successfully - reloading page to refresh tokens");
272+
Navigation.NavigateTo(Navigation.Uri, forceLoad: true);
273+
return;
274+
}
275+
else if (result.TryGetProperty("error", out var error))
276+
{
277+
var errorMsg = error.GetString() ?? "Unknown error";
278+
if (!errorMsg.Contains("user_cancelled"))
279+
{
280+
consentErrorMessage = errorMsg;
281+
Console.WriteLine($"✗ Consent failed: {consentErrorMessage}");
282+
}
283+
else
284+
{
285+
Console.WriteLine("Consent cancelled by user");
286+
}
287+
}
288+
}
289+
catch (Exception ex)
290+
{
291+
consentErrorMessage = ex.Message;
292+
Console.WriteLine($"✗ Error during consent: {ex}");
293+
}
294+
finally
295+
{
296+
isGrantingConsent = false;
297+
StateHasChanged();
298+
}
299+
}
300+
157301
private async Task LoadNamespacesAsync()
158302
{
159303
_loadCts?.Cancel();

src/Program.cs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,17 @@
99

1010
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
1111

12-
// Configure MSAL authentication
1312
builder.Services.AddMsalAuthentication(options =>
1413
{
1514
builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
1615

17-
// Request both Management API and Service Bus scopes
16+
// Only Management API at login - Service Bus scope requested via popup (see Home.razor)
1817
options.ProviderOptions.DefaultAccessTokenScopes.Add("https://management.azure.com/user_impersonation");
19-
options.ProviderOptions.AdditionalScopesToConsent.Add("https://servicebus.azure.net/user_impersonation");
2018

21-
// Use redirect mode for better PWA compatibility
2219
options.ProviderOptions.LoginMode = "redirect";
23-
24-
// Cache location - use localStorage for PWA persistence
2520
options.ProviderOptions.Cache.CacheLocation = "localStorage";
2621
});
2722

28-
// Register application services
2923
builder.Services.AddScoped<IAuthenticationService, AuthenticationService>();
3024
builder.Services.AddSingleton<ServiceBusEntityCache>();
3125
builder.Services.AddSingleton<IAzureResourceService, AzureResourceService>();

src/Services/AuthenticationService.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44

55
namespace ServiceBusExplorer.Blazor.Services;
66

7-
public sealed class AuthenticationService(IAccessTokenProvider tokenProvider, AuthenticationStateProvider authStateProvider) : IAuthenticationService
7+
public sealed class AuthenticationService(
8+
IAccessTokenProvider tokenProvider,
9+
AuthenticationStateProvider authStateProvider) : IAuthenticationService
810
{
911
public async Task<bool> IsAuthenticatedAsync()
1012
{
@@ -47,6 +49,7 @@ public async Task<bool> IsAuthenticatedAsync()
4749
public async Task<string?> GetServiceBusTokenAsync()
4850
{
4951
// Get Service Bus token for message operations
52+
// User should have consented to this scope during initial login
5053
var result = await tokenProvider.RequestAccessToken(new AccessTokenRequestOptions
5154
{
5255
Scopes = ["https://servicebus.azure.net/user_impersonation"]
@@ -59,9 +62,12 @@ public async Task<bool> IsAuthenticatedAsync()
5962
}
6063

6164
Console.WriteLine($"✗ Failed to get Service Bus token. Status: {result.Status}");
65+
6266
if (result.Status == AccessTokenResultStatus.RequiresRedirect)
6367
{
64-
Console.WriteLine("⚠ Requires redirect - user needs to consent to Service Bus scope");
68+
Console.WriteLine("⚠ Service Bus scope requires consent.");
69+
Console.WriteLine(" This usually means the user needs to sign out and sign in again to consent to all required scopes.");
70+
Console.WriteLine(" Or admin consent wasn't granted for the Service Bus API permission.");
6571
}
6672

6773
return null;

src/wwwroot/index.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@
5656

5757
<script src="js/storage.js"></script>
5858
<script src="js/analytics.js"></script>
59+
<script src="https://alcdn.msauth.net/browser/2.33.0/js/msal-browser.min.js"></script>
5960
<script src="_content/Microsoft.Authentication.WebAssembly.Msal/AuthenticationService.js"></script>
61+
<script src="js/msal-helper.js"></script>
6062
<script src="js/servicebus-api.js"></script>
6163
<script src="_framework/blazor.webassembly.js"></script>
6264

0 commit comments

Comments
 (0)