Forge
csharp3407750f
1using AgentForge;
2using AgentForge.Abstractions;
3using AgentForge.Data.Entities;
4using AgentForge.Plugin.OAuth.Web.Pages;
5using AgentForge.Plugin.View;
6using AgentForge.Services;
7using Microsoft.AspNetCore.Http;
8
9namespace AgentForge.Plugin.OAuth.Web;
10
11public static class ForgeAuthViewEndpoints
12{
13 public static WebApplication Map(WebApplication app)
14 {
15 app.MapGet("/view/login", (ForgeOAuthProviderRegistry providers, IForgeOAuthLoginContributorCatalog oauthUi, HttpContext http, string? error) =>
16 RenderLoginView(providers, oauthUi, http, error));
17 app.MapGet("/view/auth/oauth/start", StartViewOAuth);
18 app.MapGet("/view/auth/oauth/callback", CompleteViewOAuth);
19 app.MapPost("/view/auth/session", SetViewSession);
20 app.MapPost("/view/auth/logout", ClearViewSession);
21 app.MapGet("/view/auth/device", RenderDeviceLoginView);
22 return app;
23 }
24
25 private static IResult SetViewSession(HttpContext http, ForgeAuthService auth)
26 {
27 var token = http.Request.Form["token"].ToString().Trim();
28 if (token.Length == 0)
29 token = http.Request.Query["token"].ToString().Trim();
30
31 if (token.Length == 0)
32 return Results.Redirect("/view/login?error=missing_token");
33
34 if (auth.ValidateApiToken(token) is null)
35 return Results.Redirect("/view/login?error=invalid_token");
36
37 ForgeViewSession.SetSessionCookie(http.Response, token);
38 var returnUrl = http.Request.Query["return"].ToString();
39 if (string.IsNullOrWhiteSpace(returnUrl) || !returnUrl.StartsWith("/view", StringComparison.Ordinal))
40 returnUrl = "/view";
41 return Results.Redirect(returnUrl);
42 }
43
44 private static IResult ClearViewSession(HttpContext http)
45 {
46 ForgeViewSession.ClearSessionCookie(http.Response);
47 return Results.Redirect("/view/login");
48 }
49
50 private static IResult StartViewOAuth(
51 string? provider,
52 string? returnUrl,
53 ForgeGitHubOAuthService github,
54 ForgeUrls urls,
55 HttpContext http)
56 {
57 if (!string.Equals(provider ?? "github", "github", StringComparison.OrdinalIgnoreCase) || !github.IsConfigured)
58 return Results.Redirect("/view/login?error=oauth_not_configured");
59
60 var callback = urls.ViewOAuthCallback();
61 if (!ForgeOAuthRedirectAllowlist.IsAllowed(callback, urls.PublicBaseUrl))
62 return Results.Problem("OAuth callback URL is not allowed for this forge host.", statusCode: StatusCodes.Status503ServiceUnavailable);
63
64 var state = github.CreateState(callback, codeChallenge: null, codeChallengeMethod: null, "read,write,accept_merge");
65 http.Response.Cookies.Append(
66 ForgeOAuthViewConstants.ReturnCookieName,
67 ForgeOAuthViewConstants.SanitizeReturnUrl(returnUrl),
68 new CookieOptions
69 {
70 HttpOnly = true,
71 SameSite = SameSiteMode.Lax,
72 Path = "/",
73 MaxAge = TimeSpan.FromMinutes(15),
74 IsEssential = true,
75 });
76
77 return Results.Redirect(github.BuildAuthorizeUrl(state));
78 }
79
80 private static IResult CompleteViewOAuth(
81 string? code,
82 string? state,
83 ForgeOAuthLoginService oauthLogin,
84 HttpContext http)
85 {
86 if (string.IsNullOrWhiteSpace(code) || string.IsNullOrWhiteSpace(state))
87 return Results.Redirect("/view/login?error=oauth_failed");
88
89 try
90 {
91 var issued = oauthLogin.ExchangeAuthorizationCodeForView(code, state, http.RequestAborted);
92 ForgeViewSession.SetSessionCookie(http.Response, issued.AccessToken);
93 }
94 catch (ForgeApiException ex) when (ex.StatusCode == 403)
95 {
96 return Results.Redirect("/view/login?error=access_denied");
97 }
98 catch (ForgeApiException)
99 {
100 return Results.Redirect("/view/login?error=oauth_failed");
101 }
102
103 var returnUrl = "/view";
104 if (http.Request.Cookies.TryGetValue(ForgeOAuthViewConstants.ReturnCookieName, out var stored))
105 returnUrl = ForgeOAuthViewConstants.SanitizeReturnUrl(stored);
106
107 http.Response.Cookies.Delete(ForgeOAuthViewConstants.ReturnCookieName, new CookieOptions { Path = "/" });
108 return Results.Redirect(returnUrl);
109 }
110
111 private static IResult RenderLoginView(
112 ForgeOAuthProviderRegistry providers,
113 IForgeOAuthLoginContributorCatalog oauthUi,
114 HttpContext http,
115 string? error)
116 {
117 var viewer = ForgeViewPresenter.ResolveViewer(http);
118 return ForgeViewPresenter.Page("Sign in", ForgeAuthPages.RenderLogin(providers, oauthUi, viewer, error), http);
119 }
120
121 private static IResult RenderDeviceLoginView(string? code, ForgeDeviceAuthService deviceAuth, HttpContext http)
122 {
123 var userCode = NormalizeDeviceUserCode(code);
124 var pending = userCode.Length == 0 ? null : deviceAuth.GetPendingByUserCode(userCode);
125 return ForgeViewPresenter.Page("Device login", ForgeAuthPages.RenderDeviceLogin(userCode, pending), http);
126 }
127
128 private static string NormalizeDeviceUserCode(string? code) =>
129 (code ?? "").Trim().ToUpperInvariant().Replace(" ", "");
130}
131
View only · write via MCP/CIDE