66using OpenShock . API . OAuth . FlowStore ;
77using OpenShock . API . Services . Account ;
88using OpenShock . Common . Authentication ;
9- using OpenShock . Common . Authentication . Services ;
109using OpenShock . Common . Errors ;
11- using OpenShock . Common . OpenShockDb ;
12- using OpenShock . Common . Utils ;
1310using Scalar . AspNetCore ;
1411using 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}
0 commit comments