Skip to content

Commit 56b50c5

Browse files
committed
feat(auth): implement complete GitHub OAuth authentication system
- Add GitHub OAuth-based user authentication with secure session management - Implement team-based authorization with GitHub API integration - Create authentication UI components (Login, Logout, AccessDenied, Onboarding) - Add AuthorizationService for team membership verification - Secure all administrative endpoints with authentication requirements - Add comprehensive authentication documentation - Update README with OAuth setup instructions and application URLs - Add AspNet.Security.OAuth.GitHub dependency for OAuth integration BREAKING CHANGE: Application now requires authentication for all dashboard access
1 parent 1ea702b commit 56b50c5

32 files changed

+1694
-39
lines changed
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
<!-- 71b416db-4a21-4a16-88c9-4334eb7f72c1 e85163ce-c2f0-49b5-b924-bc011a8e060a -->
2+
# User Authentication Implementation Plan
3+
4+
## Overview
5+
6+
Implement GitHub OAuth App authentication flow for the Blazor web dashboard, including team-based authorization, secure session management, logout functionality, and first-time user onboarding wizard.
7+
8+
## Phase 1: NuGet Packages & Configuration
9+
10+
### 1.1 Add Required NuGet Packages
11+
12+
Add to `10xGitHubPolicies.App.csproj`:
13+
14+
- `AspNet.Security.OAuth.GitHub` (latest stable version)
15+
- `Microsoft.AspNetCore.Authentication.Cookies` (included in .NET 8, verify)
16+
17+
### 1.2 Configure User Secrets
18+
19+
Set up OAuth App credentials using .NET Secret Manager:
20+
21+
```bash
22+
cd 10xGitHubPolicies.App
23+
dotnet user-secrets set "GitHub:ClientId" "YOUR_OAUTH_APP_CLIENT_ID"
24+
dotnet user-secrets set "GitHub:ClientSecret" "YOUR_OAUTH_APP_CLIENT_SECRET"
25+
```
26+
27+
### 1.3 Update appsettings.json
28+
29+
Add placeholder for OAuth configuration (actual values in secrets):
30+
31+
```json
32+
"GitHub": {
33+
"ClientId": "",
34+
"ClientSecret": ""
35+
}
36+
```
37+
38+
## Phase 2: Authentication Infrastructure
39+
40+
### 2.1 Configure Authentication in Program.cs
41+
42+
Add authentication middleware before `var app = builder.Build();`:
43+
44+
```csharp
45+
// Add authentication services
46+
builder.Services.AddAuthentication(options =>
47+
{
48+
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
49+
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
50+
options.DefaultChallengeScheme = "GitHub";
51+
})
52+
.AddCookie(options =>
53+
{
54+
options.LoginPath = "/login";
55+
options.LogoutPath = "/logout";
56+
options.AccessDeniedPath = "/access-denied";
57+
options.ExpireTimeSpan = TimeSpan.FromHours(24);
58+
options.SlidingExpiration = false;
59+
})
60+
.AddGitHub(options =>
61+
{
62+
options.ClientId = builder.Configuration["GitHub:ClientId"];
63+
options.ClientSecret = builder.Configuration["GitHub:ClientSecret"];
64+
options.CallbackPath = "/signin-github";
65+
options.Scope.Add("read:org");
66+
options.SaveTokens = true; // Save access token for team verification
67+
});
68+
69+
builder.Services.AddAuthorization();
70+
builder.Services.AddHttpContextAccessor();
71+
```
72+
73+
### 2.2 Add Authentication Middleware
74+
75+
Add after `app.UseRouting();` in Program.cs:
76+
77+
```csharp
78+
app.UseAuthentication();
79+
app.UseAuthorization();
80+
```
81+
82+
## Phase 3: Create Authentication Pages
83+
84+
### 3.1 Create Login Page (`Pages/Login.razor`)
85+
86+
- Display "Login with GitHub" button
87+
- Trigger OAuth challenge on click
88+
- Show application branding/description
89+
90+
### 3.2 Create Access Denied Page (`Pages/AccessDenied.razor`)
91+
92+
- Display clear message about insufficient permissions
93+
- Show which team is required (from config)
94+
- Provide instructions on requesting access
95+
- Include logout button
96+
97+
### 3.3 Create Logout Page (`Pages/Logout.razor`)
98+
99+
- Handle sign-out logic
100+
- Clear authentication cookies
101+
- Redirect to login page
102+
103+
### 3.4 Create Onboarding Page (`Pages/Onboarding.razor`)
104+
105+
- Step-by-step wizard for first-time setup
106+
- Display config.yaml template with copy button
107+
- Instructions for creating `.github` repository
108+
- Instructions for placing config.yaml file
109+
- "Check Configuration" button to verify setup
110+
111+
## Phase 4: Authorization Service
112+
113+
### 4.1 Create Authorization Service Interface
114+
115+
Create `Services/Authorization/IAuthorizationService.cs`:
116+
117+
```csharp
118+
public interface IAuthorizationService
119+
{
120+
Task<bool> IsUserAuthorizedAsync(ClaimsPrincipal user);
121+
Task<string?> GetAuthorizedTeamAsync();
122+
}
123+
```
124+
125+
### 4.2 Implement Authorization Service
126+
127+
Create `Services/Authorization/AuthorizationService.cs`:
128+
129+
- Inject `IGitHubService` and `IConfigurationService`
130+
- Extract user's access token from claims
131+
- Retrieve authorized team from config.yaml
132+
- Call `IGitHubService.IsUserMemberOfTeamAsync()` to verify membership
133+
- Handle configuration errors gracefully
134+
135+
### 4.3 Register Service in Program.cs
136+
137+
```csharp
138+
builder.Services.AddScoped<IAuthorizationService, AuthorizationService>();
139+
```
140+
141+
## Phase 5: Protect Dashboard & Add Authorization Logic
142+
143+
### 5.1 Update Index.razor
144+
145+
- Add `@attribute [Authorize]` at the top
146+
- Inject `IAuthorizationService` and `IConfigurationService`
147+
- In `OnInitializedAsync()`:
148+
- Check if config.yaml exists (catch `ConfigurationNotFoundException`)
149+
- If missing, redirect to `/onboarding`
150+
- If present, verify team membership via `IAuthorizationService`
151+
- If not authorized, redirect to `/access-denied`
152+
- If authorized, load dashboard data as normal
153+
154+
### 5.2 Update App.razor
155+
156+
Replace with `CascadingAuthenticationState` wrapper:
157+
158+
```razor
159+
<CascadingAuthenticationState>
160+
<Router AppAssembly="@typeof(App).Assembly">
161+
<Found Context="routeData">
162+
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
163+
<NotAuthorized>
164+
<RedirectToLogin />
165+
</NotAuthorized>
166+
</AuthorizeRouteView>
167+
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
168+
</Found>
169+
<NotFound>
170+
<PageTitle>Not found</PageTitle>
171+
<LayoutView Layout="@typeof(MainLayout)">
172+
<p role="alert">Sorry, there's nothing at this address.</p>
173+
</LayoutView>
174+
</NotFound>
175+
</Router>
176+
</CascadingAuthenticationState>
177+
```
178+
179+
### 5.3 Create RedirectToLogin Component
180+
181+
Create `Shared/RedirectToLogin.razor`:
182+
183+
- Use `NavigationManager` to redirect to `/login`
184+
185+
## Phase 6: Update UI Components
186+
187+
### 6.1 Update MainLayout.razor
188+
189+
- Add `<AuthorizeView>` to show user info when authenticated
190+
- Display username from claims
191+
- Add logout button in header/navigation
192+
- Show login prompt for unauthenticated users
193+
194+
### 6.2 Update _Imports.razor
195+
196+
Add required namespaces:
197+
198+
```razor
199+
@using Microsoft.AspNetCore.Components.Authorization
200+
@using Microsoft.AspNetCore.Authorization
201+
@using System.Security.Claims
202+
```
203+
204+
## Phase 7: Testing & Validation
205+
206+
### 7.1 Manual Testing Checklist
207+
208+
- Unauthenticated user redirected to login
209+
- OAuth flow completes successfully
210+
- Authorized team member can access dashboard
211+
- Non-team member sees access denied page
212+
- Missing config.yaml shows onboarding wizard
213+
- Logout clears session and redirects to login
214+
- Session expires after 24 hours
215+
- Callback URL `/signin-github` works correctly
216+
217+
### 7.2 Error Scenarios
218+
219+
- Handle GitHub OAuth errors gracefully
220+
- Handle network failures during team verification
221+
- Handle invalid/expired access tokens
222+
- Handle malformed config.yaml
223+
224+
## Phase 8: Documentation Updates
225+
226+
### 8.1 Update README.md
227+
228+
- Add OAuth App setup instructions
229+
- Document required GitHub OAuth App configuration
230+
- Add callback URL setup instructions
231+
- Update local development setup steps
232+
233+
### 8.2 Create Authentication Documentation
234+
235+
Create `docs/authentication.md`:
236+
237+
- Detailed OAuth flow explanation
238+
- Team-based authorization process
239+
- Session management details
240+
- Troubleshooting guide
241+
242+
### 8.3 Update CHANGELOG.md
243+
244+
Add entry for authentication feature implementation
245+
246+
## Key Files to Create/Modify
247+
248+
**New Files:**
249+
250+
- `Pages/Login.razor`
251+
- `Pages/Logout.razor`
252+
- `Pages/AccessDenied.razor`
253+
- `Pages/Onboarding.razor`
254+
- `Services/Authorization/IAuthorizationService.cs`
255+
- `Services/Authorization/AuthorizationService.cs`
256+
- `Shared/RedirectToLogin.razor`
257+
- `docs/authentication.md`
258+
259+
**Modified Files:**
260+
261+
- `Program.cs` (authentication configuration)
262+
- `10xGitHubPolicies.App.csproj` (NuGet packages)
263+
- `Pages/Index.razor` (authorization checks)
264+
- `App.razor` (authentication state)
265+
- `Shared/MainLayout.razor` (user info, logout)
266+
- `_Imports.razor` (namespaces)
267+
- `appsettings.json` (OAuth config placeholders)
268+
- `README.md` (setup instructions)
269+
- `CHANGELOG.md` (feature entry)
270+
271+
## Security Considerations
272+
273+
1. **Token Storage**: OAuth access tokens stored securely in authentication cookie (encrypted by ASP.NET Core)
274+
2. **HTTPS Only**: Ensure cookies are marked as secure in production
275+
3. **CSRF Protection**: Blazor Server provides built-in anti-forgery protection
276+
4. **Session Timeout**: 24-hour fixed expiration prevents indefinite sessions
277+
5. **Minimal Scopes**: Only request `read:org` scope (least privilege)
278+
6. **Secret Management**: Use .NET Secret Manager for local dev, Azure Key Vault for production
279+
280+
## Dependencies
281+
282+
- Existing `IGitHubService.IsUserMemberOfTeamAsync()` method
283+
- Existing `IConfigurationService.GetConfigAsync()` method
284+
- GitHub OAuth App must be created and configured with correct callback URL
285+
286+
### To-dos
287+
288+
- [ ] Add authentication NuGet packages and configure user secrets
289+
- [ ] Configure authentication middleware in Program.cs with Cookie and GitHub OAuth
290+
- [ ] Create IAuthorizationService and implementation for team-based authorization
291+
- [ ] Create Login.razor page with GitHub OAuth button
292+
- [ ] Create Logout.razor page to handle sign-out
293+
- [ ] Create AccessDenied.razor page with clear messaging and instructions
294+
- [ ] Create Onboarding.razor wizard for first-time setup with config template
295+
- [ ] Update App.razor with CascadingAuthenticationState and AuthorizeRouteView
296+
- [ ] Add authorization logic to Index.razor with config check and team verification
297+
- [ ] Update MainLayout.razor with user info display and logout button
298+
- [ ] Add authentication namespaces to _Imports.razor
299+
- [ ] Test complete authentication flow including OAuth, authorization, and error scenarios
300+
- [ ] Update README.md, create docs/authentication.md, and update CHANGELOG.md
File renamed without changes.

.github/config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Access control: Specify the GitHub team authorized to access the dashboard.
22
# Format: 'organization-slug/team-slug'
33
access_control:
4-
authorized_team: 'my-org/security-team'
4+
authorized_team: 'mackowski-corp/appsec'
55

66
# Policies: Define the rules to enforce across your organization's repositories.
77
policies:

10xGitHubPolicies.App/10xGitHubPolicies.App.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@
2424
<PackageReference Include="Microsoft.FluentUI.AspNetCore.Components.Icons" Version="4.13.0" />
2525
<PackageReference Include="Octokit" Version="12.0.0" />
2626
<PackageReference Include="Octokit.Extensions" Version="1.0.7" />
27-
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
2827
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.6.0" />
2928
<PackageReference Include="YamlDotNet" Version="16.3.0" />
29+
<PackageReference Include="AspNet.Security.OAuth.GitHub" Version="8.0.0" />
3030
</ItemGroup>
3131

3232
</Project>

10xGitHubPolicies.App/App.razor

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
1-
<Router AppAssembly="@typeof(App).Assembly">
2-
<Found Context="routeData">
3-
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
4-
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
5-
</Found>
6-
<NotFound>
7-
<PageTitle>Not found</PageTitle>
8-
<LayoutView Layout="@typeof(MainLayout)">
9-
<p role="alert">Sorry, there's nothing at this address.</p>
10-
</LayoutView>
11-
</NotFound>
12-
</Router>
1+
<CascadingAuthenticationState>
2+
<Router AppAssembly="@typeof(App).Assembly">
3+
<Found Context="routeData">
4+
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
5+
<NotAuthorized>
6+
<RedirectToLogin />
7+
</NotAuthorized>
8+
</AuthorizeRouteView>
9+
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
10+
</Found>
11+
<NotFound>
12+
<PageTitle>Not found</PageTitle>
13+
<LayoutView Layout="@typeof(MainLayout)">
14+
<p role="alert">Sorry, there's nothing at this address.</p>
15+
</LayoutView>
16+
</NotFound>
17+
</Router>
18+
</CascadingAuthenticationState>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using Hangfire.Dashboard;
2+
3+
namespace _10xGitHubPolicies.App.Authorization;
4+
5+
/// <summary>
6+
/// Custom authorization filter for Hangfire dashboard that requires user authentication.
7+
/// </summary>
8+
public class HangfireAuthorizationFilter : IDashboardAuthorizationFilter
9+
{
10+
/// <summary>
11+
/// Determines whether the current user is authorized to access the Hangfire dashboard.
12+
/// </summary>
13+
/// <param name="context">The dashboard context containing HTTP context information.</param>
14+
/// <returns>True if the user is authenticated, false otherwise.</returns>
15+
public bool Authorize(DashboardContext context)
16+
{
17+
var httpContext = context.GetHttpContext();
18+
return httpContext.User.Identity?.IsAuthenticated == true;
19+
}
20+
}

0 commit comments

Comments
 (0)