Skip to content

Commit 53884b3

Browse files
committed
Better?
1 parent 254bbf1 commit 53884b3

3 files changed

Lines changed: 83 additions & 92 deletions

File tree

API/Controller/Account/Authenticated/OAuthConnectionAdd.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,6 @@ public async Task<IActionResult> AddOAuthConnection([FromRoute] string provider,
1313
if (!await schemeProvider.IsSupportedOAuthScheme(provider))
1414
return Problem(OAuthError.ProviderNotSupported);
1515

16-
return Challenge(new AuthenticationProperties { RedirectUri = $"/oauth/{provider}/complete", Parameters = {{ "flow", "link" }} }, authenticationSchemes: [provider]);
16+
return Challenge(new AuthenticationProperties { RedirectUri = $"/oauth/{provider}/complete", Items = {{ "flow", "link" }} }, authenticationSchemes: [provider]);
1717
}
1818
}

API/Controller/OAuth/Complete.cs

Lines changed: 81 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,7 @@
66
using OpenShock.API.OAuth.FlowStore;
77
using OpenShock.API.Services.Account;
88
using OpenShock.Common.Authentication;
9-
using OpenShock.Common.Authentication.Services;
109
using OpenShock.Common.Errors;
11-
using OpenShock.Common.OpenShockDb;
12-
using OpenShock.Common.Utils;
1310
using Scalar.AspNetCore;
1411
using System.Security.Claims;
1512

@@ -22,121 +19,115 @@ public sealed partial class OAuthController
2219
public async Task<IActionResult> OAuthComplete(
2320
[FromRoute] string provider,
2421
[FromServices] IAuthenticationSchemeProvider schemeProvider,
25-
[FromServices] IUserReferenceService userReferenceService,
2622
[FromServices] IAccountService accountService,
27-
[FromServices] IOAuthFlowStore store
28-
)
23+
[FromServices] IOAuthFlowStore store)
2924
{
3025
if (!await schemeProvider.IsSupportedOAuthScheme(provider))
3126
return Problem(OAuthError.ProviderNotSupported);
3227

33-
// External principal placed by the OAuth handler (SaveTokens=true, SignInScheme=OAuthFlowScheme)
28+
// Temp external principal (set by OAuth handler with SignInScheme=OAuthFlowScheme, SaveTokens=true)
3429
var auth = await HttpContext.AuthenticateAsync(OpenShockAuthSchemes.OAuthFlowScheme);
3530
if (!auth.Succeeded || auth.Principal is null)
3631
return BadRequest("OAuth sign-in not found or expired.");
3732

38-
var ext = auth.Principal;
3933
var props = auth.Properties;
34+
if (props is null || !props.Items.TryGetValue("flow", out var flow) || string.IsNullOrWhiteSpace(flow))
35+
{
36+
await HttpContext.SignOutAsync(OpenShockAuthSchemes.OAuthFlowScheme);
37+
Response.Cookies.Delete(OpenShockAuthSchemes.OAuthFlowCookie, new CookieOptions { Path = "/" });
38+
return BadRequest(new { error = "missing_flow" });
39+
}
40+
flow = flow.ToLowerInvariant();
4041

41-
// Essentials from external identity
42-
var externalId = ext.FindFirst(ClaimTypes.NameIdentifier)?.Value
43-
?? ext.FindFirst("sub")?.Value
44-
?? ext.FindFirst("id")?.Value;
45-
42+
var ext = auth.Principal;
43+
var externalId =
44+
ext.FindFirst(ClaimTypes.NameIdentifier)?.Value ??
45+
ext.FindFirst("sub")?.Value ??
46+
ext.FindFirst("id")?.Value;
4647
if (string.IsNullOrEmpty(externalId))
4748
return Problem("Missing external subject.", statusCode: 400);
4849

4950
var email = ext.FindFirst(ClaimTypes.Email)?.Value;
5051
var userName = ext.Identity?.Name;
51-
52-
var tokens = (props?.GetTokens() ?? Enumerable.Empty<AuthenticationToken>())
52+
var tokens = (props.GetTokens() ?? Enumerable.Empty<AuthenticationToken>())
5353
.ToDictionary(t => t.Name!, t => t.Value!);
5454

55-
// Who (if anyone) is currently signed into OUR site?
56-
User? currentUser = null;
57-
if (userReferenceService.AuthReference is not null && userReferenceService.AuthReference.Value.IsT0)
58-
{
59-
currentUser = HttpContext.RequestServices.GetRequiredService<IClientAuthService<User>>().CurrentClient;
60-
}
61-
62-
// Is this external already linked to someone?
6355
var connection = await accountService.GetOAuthConnectionAsync(provider, externalId);
6456

65-
// CASE A: External already linked
66-
if (connection is not null)
57+
switch (flow)
6758
{
68-
if (currentUser is not null)
69-
{
70-
// Already logged in locally.
71-
if (connection.UserId == currentUser.Id)
59+
case "login":
7260
{
73-
// Happy path: ensure session is fresh and go home.
61+
if (connection is not null)
62+
{
63+
// Already linked -> sign in and go home.
64+
// TODO: issue your UserSessionCookie/session here for connection.UserId
65+
await HttpContext.SignOutAsync(OpenShockAuthSchemes.OAuthFlowScheme);
66+
return Redirect("/");
67+
}
68+
69+
var flowId = await SaveSnapshotAsync(store, provider, externalId, email, userName, tokens);
70+
SetFlowCookie(flowId);
7471
await HttpContext.SignOutAsync(OpenShockAuthSchemes.OAuthFlowScheme);
75-
return Redirect("/");
72+
73+
var frontend = Environment.GetEnvironmentVariable("FRONTEND_ORIGIN") ?? "https://app.example.com";
74+
return Redirect($"{frontend}/{provider}/create");
7675
}
7776

78-
// Linked to a different local account → fail explicitly.
79-
await HttpContext.SignOutAsync(OpenShockAuthSchemes.OAuthFlowScheme);
80-
return Problem(
81-
detail: "This external account is already linked to another user.",
82-
statusCode: 409,
83-
title: "Account already linked");
84-
}
85-
86-
// Anonymous user: sign in as the linked account and go home.
87-
var loginAction = await _accountService.CreateUserLoginSessionAsync(/* ....... */, new LoginContext
88-
{
89-
Ip = HttpContext.GetRemoteIP().ToString(),
90-
UserAgent = HttpContext.GetUserAgent(),
91-
}, cancellationToken);
92-
93-
return loginAction.Match<IActionResult>(
94-
ok =>
77+
case "link":
9578
{
96-
HttpContext.SetSessionKeyCookie(ok.Token, "." + cookieDomainToUse);
79+
if (connection is not null)
80+
{
81+
await HttpContext.SignOutAsync(OpenShockAuthSchemes.OAuthFlowScheme);
82+
return Problem(
83+
detail: "This external account is already linked to another user.",
84+
statusCode: 409,
85+
title: "Account already linked");
86+
}
87+
88+
var flowId = await SaveSnapshotAsync(store, provider, externalId, email, userName, tokens);
89+
SetFlowCookie(flowId);
9790
await HttpContext.SignOutAsync(OpenShockAuthSchemes.OAuthFlowScheme);
98-
return Redirect("/");
99-
},
100-
deactivated => Problem(AccountError.AccountDeactivated),
101-
oauthOnly => Problem(AccountError.AccountOAuthOnly),
102-
notActivated => Problem(AccountError.AccountNotActivated),
103-
notFound => Problem(LoginError.InvalidCredentials)
104-
);
91+
92+
var frontend = Environment.GetEnvironmentVariable("FRONTEND_ORIGIN") ?? "https://app.example.com";
93+
return Redirect($"{frontend}/{provider}/link");
94+
}
95+
96+
default:
97+
await HttpContext.SignOutAsync(OpenShockAuthSchemes.OAuthFlowScheme);
98+
Response.Cookies.Delete(OpenShockAuthSchemes.OAuthFlowCookie, new CookieOptions { Path = "/" });
99+
return BadRequest(new { error = "unknown_flow", flow });
105100
}
106101

107-
// CASE B: Not linked yet → create flow snapshot and send to frontend for link/create
108-
var snapshot = new OAuthSnapshot(
109-
Provider: provider,
110-
ExternalId: externalId,
111-
Email: email,
112-
UserName: userName,
113-
Tokens: tokens,
114-
IssuedUtc: DateTimeOffset.UtcNow);
115-
116-
var flowId = await store.SaveAsync(snapshot, OAuthFlow.Ttl);
117-
118-
// Short-lived, non-HttpOnly cookie so the frontend can call /oauth/{provider}/data
119-
Response.Cookies.Append(
120-
OpenShockAuthSchemes.OAuthFlowCookie,
121-
flowId,
122-
new CookieOptions
123-
{
124-
Secure = HttpContext.Request.IsHttps,
125-
HttpOnly = false, // readable by frontend JS for one fetch
126-
SameSite = SameSiteMode.Lax,
127-
Expires = DateTimeOffset.UtcNow.Add(OAuthFlow.Ttl),
128-
Path = "/"
129-
});
130-
131-
// Clean up the temp external principal
132-
await HttpContext.SignOutAsync(OpenShockAuthSchemes.OAuthFlowScheme);
133-
134-
// Decide which UI route to send them to
135-
var frontend = Environment.GetEnvironmentVariable("FRONTEND_ORIGIN") ?? "https://app.example.com";
136-
var nextPath = (!string.IsNullOrEmpty(currentUserId))
137-
? $"/{provider}/link"
138-
: $"/{provider}/create";
139-
140-
return Redirect(frontend + nextPath);
102+
// --- local helpers ---
103+
async Task<string> SaveSnapshotAsync(
104+
IOAuthFlowStore s, string prov, string extId, string? mail, string? name,
105+
IDictionary<string, string> tks)
106+
{
107+
var snapshot = new OAuthSnapshot(
108+
Provider: prov,
109+
ExternalId: extId,
110+
Email: mail,
111+
UserName: name,
112+
Tokens: tks,
113+
IssuedUtc: DateTimeOffset.UtcNow);
114+
return await s.SaveAsync(snapshot, OAuthFlow.Ttl);
115+
}
116+
117+
void SetFlowCookie(string id)
118+
{
119+
Response.Cookies.Append(
120+
OpenShockAuthSchemes.OAuthFlowCookie,
121+
id,
122+
new CookieOptions
123+
{
124+
Secure = HttpContext.Request.IsHttps,
125+
HttpOnly = false, // frontend reads once for /oauth/{provider}/data
126+
SameSite = SameSiteMode.Lax,
127+
Expires = DateTimeOffset.UtcNow.Add(OAuthFlow.Ttl),
128+
Path = "/"
129+
});
130+
}
141131
}
132+
142133
}

API/Controller/OAuth/Login.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,6 @@ public async Task<IActionResult> OAuthLogin([FromRoute] string provider, [FromQu
1515
if (!await schemeProvider.IsSupportedOAuthScheme(provider))
1616
return Problem(OAuthError.ProviderNotSupported);
1717

18-
return Challenge(new AuthenticationProperties { RedirectUri = $"/oauth/{provider}/complete", Parameters = { { "flow", "login" } } }, authenticationSchemes: [provider]);
18+
return Challenge(new AuthenticationProperties { RedirectUri = $"/oauth/{provider}/complete", Items = { { "flow", "login" } } }, authenticationSchemes: [provider]);
1919
}
2020
}

0 commit comments

Comments
 (0)