-
Notifications
You must be signed in to change notification settings - Fork 10
Expand file tree
/
Copy pathSignupFinalize.cs
More file actions
124 lines (109 loc) · 4.95 KB
/
SignupFinalize.cs
File metadata and controls
124 lines (109 loc) · 4.95 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
using OpenShock.API.Models.Requests;
using OpenShock.API.Models.Response;
using OpenShock.API.OAuth;
using OpenShock.API.Services.OAuthConnection;
using OpenShock.Common.Errors;
using OpenShock.Common.Extensions;
using System.Security.Claims;
namespace OpenShock.API.Controller.OAuth;
public sealed partial class OAuthController
{
/// <summary>
/// Finalize an OAuth flow by creating a new account with the external identity.
/// </summary>
/// <remarks>
/// Authenticates via the temporary OAuth flow cookie (set during the provider callback).
/// Sets the regular session cookie on success. No access/refresh tokens are returned.
/// </remarks>
/// <param name="provider">Provider key (e.g. <c>discord</c>).</param>
/// <param name="body">Request body containing optional <c>Email</c> and <c>Username</c> overrides.</param>
/// <param name="connectionService"></param>
/// <param name="cancellationToken"></param>
[EnableRateLimiting("auth")]
[HttpPost("{provider}/signup-finalize")]
public async Task<IActionResult> OAuthSignupFinalize(
[FromRoute] string provider,
[FromBody] OAuthFinalizeRequest body,
[FromServices] IOAuthConnectionService connectionService,
CancellationToken cancellationToken)
{
// If domain is not supported for cookies, cancel the flow
var domain = GetCurrentCookieDomain();
if (string.IsNullOrEmpty(domain))
{
await HttpContext.SignOutAsync(OAuthConstants.FlowScheme);
return Problem(OAuthError.InternalError);
}
var result = await ValidateOAuthFlowAsync();
if (!result.TryPickT0(out var auth, out var error))
{
return error switch
{
OAuthValidationError.FlowStateMissing => Problem(OAuthError.FlowNotFound),
_ => Problem(OAuthError.InternalError)
};
}
if (User.HasOpenShockUserIdentity())
{
return Problem(OAuthError.FlowRequiresAnonymous);
}
// 1) Defense-in-depth: ensure the flow’s provider matches the route
if (!string.Equals(auth.Provider, provider, StringComparison.OrdinalIgnoreCase) || auth.Flow != OAuthFlow.LoginOrCreate)
{
await HttpContext.SignOutAsync(OAuthConstants.FlowScheme);
return Problem(OAuthError.FlowMismatch);
}
// External identity basics from claims (added by your handler)
var externalAccountEmail = auth.Principal.FindFirst(ClaimTypes.Email)?.Value;
var username = body.Username ?? auth.ExternalAccountDisplayName ?? auth.ExternalAccountName;
var email = body.Email ?? externalAccountEmail;
if (string.IsNullOrWhiteSpace(auth.ExternalAccountId) || string.IsNullOrWhiteSpace(email) || string.IsNullOrWhiteSpace(username))
{
return Problem(OAuthError.FlowMissingData);
}
var isVerifiedString = auth.Principal.FindFirst(OAuthConstants.ClaimEmailVerified)?.Value;
var isEmailTrusted = IsTruthy(isVerifiedString) && string.Equals(externalAccountEmail, email, StringComparison.InvariantCultureIgnoreCase);
// Do not allow creation if this external is already linked anywhere.
var existing = await connectionService.GetByProviderExternalIdAsync(provider, auth.ExternalAccountId, cancellationToken);
if (existing is not null)
{
await HttpContext.SignOutAsync(OAuthConstants.FlowScheme);
return Problem(OAuthError.ExternalAlreadyLinked);
}
var created = await _accountService.CreateOAuthOnlyAccountAsync(
email,
username,
provider,
auth.ExternalAccountId,
auth.ExternalAccountDisplayName ?? auth.ExternalAccountName,
isEmailTrusted
);
if (!created.TryPickT0(out var newUser, out _))
{
// Username or email already exists — conflict.
// Do NOT clear the flow cookie so the frontend can retry with a different username.
return Problem(SignupError.UsernameOrEmailExists);
}
// Authenticate the client if its activated (create session and set session cookie)
if (newUser.Value.ActivatedAt is not null)
{
await CreateSession(newUser.Value.Id, domain);
}
// Clear the temporary OAuth flow cookie.
await HttpContext.SignOutAsync(OAuthConstants.FlowScheme);
return Ok(LoginV2OkResponse.FromUser(newUser.Value));
static bool IsTruthy(string? value)
{
if (string.IsNullOrWhiteSpace(value)) return false;
return value.Trim().ToLowerInvariant() switch
{
"0" or "no" or "false" => false,
"1" or "yes" or "true" => true,
_ => false
};
}
}
}