Forge
csharp3407750f
1using AgentForge.Plugin.Sdk;
2using AgentForge.Contracts;
3using AgentForge.Services;
4
5namespace AgentForge.Endpoints;
6
7internal static class ForgeAuthEndpoints
8{
9 internal static RouteGroupBuilder MapAuthEndpoints(this RouteGroupBuilder api)
10 {
11 api.MapGet("/auth/providers", ListProviders);
12 api.MapGet("/auth/login", BeginOAuthLogin);
13 api.MapGet("/auth/callback/github", GitHubOAuthCallback);
14 api.MapPost("/auth/token", ExchangeOAuthToken);
15 api.MapPost("/auth/device", BeginDeviceLogin);
16 api.MapPost("/auth/device/token", PollDeviceToken);
17 api.MapPost("/auth/device/approve", ApproveDeviceLogin);
18 return api;
19 }
20
21 private static IResult ListProviders(ForgeOAuthProviderRegistry providers) =>
22 Results.Ok(new { providers = providers.ListProviders() });
23
24 private static IResult BeginOAuthLogin(
25 string provider,
26 string redirect_uri,
27 string? code_challenge,
28 string? code_challenge_method,
29 string? scopes,
30 ForgeGitHubOAuthService github)
31 {
32 if (string.IsNullOrWhiteSpace(redirect_uri))
33 return Results.BadRequest(new { error = "redirect_uri required" });
34
35 if (!ForgeOAuthRedirectAllowlist.IsAllowed(redirect_uri))
36 return Results.BadRequest(new { error = "redirect_uri not allowed" });
37
38 var scopeStorage = string.IsNullOrWhiteSpace(scopes) ? "read,write,accept_merge" : scopes.Trim();
39
40 if (string.Equals(provider, "github", StringComparison.OrdinalIgnoreCase))
41 {
42 if (!github.IsConfigured)
43 return Results.Problem("GitHub OAuth is not configured", statusCode: StatusCodes.Status503ServiceUnavailable);
44
45 var state = github.CreateState(redirect_uri, code_challenge, code_challenge_method, scopeStorage);
46 return Results.Redirect(github.BuildAuthorizeUrl(state));
47 }
48
49 return Results.BadRequest(new { error = "unsupported_provider" });
50 }
51
52 private static IResult GitHubOAuthCallback(string? code, string? state, ForgeGitHubOAuthService github)
53 {
54 if (string.IsNullOrWhiteSpace(code) || string.IsNullOrWhiteSpace(state))
55 return Results.BadRequest(new { error = "code and state required" });
56
57 var oauthState = github.GetValidState(state);
58 if (oauthState is null)
59 return Results.BadRequest(new { error = "invalid_state" });
60
61 return Results.Redirect(AppendAuthorizationCode(oauthState.RedirectUri, code, state));
62 }
63
64 private static IResult ExchangeOAuthToken(
65 OAuthTokenExchangeRequest request,
66 ForgeOAuthLoginService oauthLogin,
67 CancellationToken cancellationToken)
68 {
69 var response = oauthLogin.ExchangeAuthorizationCode(request, cancellationToken);
70 if (response is null)
71 return Results.BadRequest(new { error = "unsupported_grant_type" });
72
73 return Results.Ok(new
74 {
75 access_token = response.AccessToken,
76 token_type = "Bearer",
77 token_name = response.TokenName,
78 scopes = response.Scopes,
79 });
80 }
81
82 private static string AppendAuthorizationCode(string redirectUri, string code, string state)
83 {
84 var sep = redirectUri.Contains('?', StringComparison.Ordinal) ? '&' : '?';
85 return $"{redirectUri}{sep}code={Uri.EscapeDataString(code)}&state={Uri.EscapeDataString(state)}";
86 }
87
88 private static IResult BeginDeviceLogin(
89 BeginDeviceLoginRequest request,
90 ForgeDeviceAuthService deviceAuth,
91 ForgeUrls urls)
92 {
93 var response = deviceAuth.Begin(request.ClientName, request.Scopes);
94 return Results.Ok(new
95 {
96 device_code = response.DeviceCode,
97 user_code = response.UserCode,
98 verification_uri = urls.DeviceLogin(response.UserCode),
99 expires_in = response.ExpiresIn,
100 interval = response.Interval,
101 });
102 }
103
104 private static IResult PollDeviceToken(
105 PollDeviceTokenRequest request,
106 ForgeDeviceAuthService deviceAuth)
107 {
108 if (string.IsNullOrWhiteSpace(request.DeviceCode))
109 return Results.BadRequest(new { error = "invalid_request", error_description = "device_code is required." });
110
111 var response = deviceAuth.Poll(request.DeviceCode);
112 if (response.AccessToken is not null)
113 {
114 return Results.Ok(new
115 {
116 access_token = response.AccessToken,
117 token_type = response.TokenType,
118 token_name = response.TokenName,
119 });
120 }
121
122 return Results.BadRequest(new { error = response.Error });
123 }
124
125 private static IResult ApproveDeviceLogin(
126 ApproveDeviceLoginRequest request,
127 HttpContext context,
128 ForgeDeviceAuthService deviceAuth,
129 ForgeAuthService auth,
130 ForgeAuthorizationService authorization)
131 {
132 if (string.IsNullOrWhiteSpace(request.UserCode))
133 throw new ForgeApiException(400, "user_code is required.");
134
135 authorization.EnsureDeviceApproval(ForgePluginHttpAuth.GetBearerToken(context), auth);
136 var response = deviceAuth.Approve(request.UserCode);
137 return Results.Ok(response);
138 }
139}
140
View only · write via MCP/CIDE