11using System . ComponentModel ;
22using System . Diagnostics ;
33using System . IO ;
4+ using System . Security . Cryptography ;
5+ using System . Text ;
46using System . Windows . Media ;
57using Wpf . Ui . Controls ;
68using Memory ;
@@ -358,6 +360,8 @@ private async Task InitializeViewModel()
358360 CheckForEventUpdates ( ) ;
359361 CheckForXboxGamesDatabaseUpdate ( ) ;
360362 LoadSettings ( ) ;
363+ if ( Settings . OAuthLogin )
364+ OAuthLogin ( ) ;
361365 _isInitialized = true ;
362366 if ( Settings . AutoLaunchXboxAppEnabled && Process . GetProcessesByName ( ProcessNames . XboxPcApp ) . Length == 0 )
363367 {
@@ -530,44 +534,83 @@ private async void OAuthLogin()
530534 if ( LoginText == "Logout" )
531535 {
532536 oauth . Signout ( ) ;
533- File . Delete ( AuthFilePath ) ;
537+ try { File . Delete ( AuthFilePath ) ; } catch { }
538+ ClearProfileState ( ) ;
534539 LoginText = "Login" ;
535540 return ;
536541 }
537542 Settings . OAuthLogin = true ;
538543
539- //check for previous session auth otherwise do interactive login (should almost certainly make this more secure
540- if ( File . Exists ( AuthFilePath ) )
544+ // Use saved session if valid; otherwise interactive login
545+ MicrosoftOAuthResponse ? response = await TryRestoreSessionAsync ( ) ;
546+ if ( response == null )
547+ response = await TryInteractiveLoginAsync ( ) ;
548+ }
549+
550+ private void DeleteAuthFile ( )
551+ {
552+ try { File . Delete ( AuthFilePath ) ; } catch { }
553+ }
554+
555+ private void CompleteLogin ( MicrosoftOAuthResponse response , string ? successMessage = null )
556+ {
557+ writeSession ( response ) ;
558+ if ( ! string . IsNullOrEmpty ( successMessage ) )
559+ _snackbarService . Show ( "Success" , successMessage , ControlAppearance . Success , new SymbolIcon ( SymbolRegular . Checkmark24 ) , _snackbarDuration ) ;
560+ GenerateTokens ( response ) ;
561+ }
562+
563+ private async Task < MicrosoftOAuthResponse ? > TryRestoreSessionAsync ( )
564+ {
565+ if ( ! File . Exists ( AuthFilePath ) )
566+ return null ;
567+
568+ MicrosoftOAuthResponse ? response ;
569+ try
541570 {
542- MicrosoftOAuthResponse response = readSession ( ) ;
543- try
544- {
545- response = await oauth . AuthenticateSilently ( response ? . RefreshToken ) ;
546- writeSession ( response ) ;
547- _snackbarService . Show ( "Success" , "Logged in with previous session" , ControlAppearance . Success , new SymbolIcon ( SymbolRegular . Checkmark24 ) , _snackbarDuration ) ;
548- GenerateTokens ( response ) ;
549- }
550- catch
551- {
552- _snackbarService . Show ( "Session invalid" , "You are required to log in again as the session has expired" , ControlAppearance . Danger , new SymbolIcon ( SymbolRegular . ErrorCircle24 ) , _snackbarDuration ) ;
553- response = await oauth . AuthenticateInteractively ( ) ;
554- }
571+ response = readSession ( ) ;
572+ }
573+ catch
574+ {
575+ DeleteAuthFile ( ) ;
576+ _snackbarService . Show ( "Session invalid" , "Saved session could not be read. Please log in again." , ControlAppearance . Danger , new SymbolIcon ( SymbolRegular . ErrorCircle24 ) , _snackbarDuration ) ;
577+ return null ;
578+ }
555579
580+ if ( response == null || ! response . Validate ( ) || string . IsNullOrEmpty ( response . RefreshToken ) )
581+ {
582+ DeleteAuthFile ( ) ;
583+ if ( response != null && ! response . Validate ( ) )
584+ _snackbarService . Show ( "Session expired" , "Your saved session has expired. Please log in again." , ControlAppearance . Danger , new SymbolIcon ( SymbolRegular . ErrorCircle24 ) , _snackbarDuration ) ;
585+ return null ;
556586 }
557- else
587+
588+ try
558589 {
559- try
560- {
590+ response = await oauth . AuthenticateSilently ( response . RefreshToken ! ) ;
591+ CompleteLogin ( response , "Logged in with previous session" ) ;
592+ return response ;
593+ }
594+ catch
595+ {
596+ _snackbarService . Show ( "Session invalid" , "You are required to log in again as the session has expired" , ControlAppearance . Danger , new SymbolIcon ( SymbolRegular . ErrorCircle24 ) , _snackbarDuration ) ;
597+ ClearProfileState ( ) ;
598+ return await TryInteractiveLoginAsync ( ) ;
599+ }
600+ }
561601
562- MicrosoftOAuthResponse response = await oauth . AuthenticateInteractively ( ) ;
563- writeSession ( response ) ;
564- _snackbarService . Show ( "Success" , "Logged in" , ControlAppearance . Success , new SymbolIcon ( SymbolRegular . Checkmark24 ) , _snackbarDuration ) ;
565- GenerateTokens ( response ) ;
566- }
567- catch
568- {
569- _snackbarService . Show ( "Error" , "Failed to authenticate" , ControlAppearance . Danger , new SymbolIcon ( SymbolRegular . ErrorCircle24 ) , _snackbarDuration ) ;
570- }
602+ private async Task < MicrosoftOAuthResponse ? > TryInteractiveLoginAsync ( )
603+ {
604+ try
605+ {
606+ var response = await oauth . AuthenticateInteractively ( ) ;
607+ CompleteLogin ( response , "Logged in" ) ;
608+ return response ;
609+ }
610+ catch
611+ {
612+ _snackbarService . Show ( "Error" , "Failed to authenticate" , ControlAppearance . Danger , new SymbolIcon ( SymbolRegular . ErrorCircle24 ) , _snackbarDuration ) ;
613+ return null ;
571614 }
572615 }
573616
@@ -584,6 +627,24 @@ private async void GenerateTokens(MicrosoftOAuthResponse response)
584627 try
585628 {
586629 XAUTH = $ "XBL3.0 x={ sisuResult . AuthorizationToken . XuiClaims . UserHash } ;{ sisuResult . AuthorizationToken . Token } ";
630+ var xui = sisuResult . AuthorizationToken . XuiClaims ;
631+ XUIDOnly = xui ? . XboxUserId ?? "" ;
632+ if ( ! string . IsNullOrEmpty ( XUIDOnly ) )
633+ {
634+ IsLoggedIn = true ;
635+ XAUTHTested = true ;
636+ InitComplete = true ;
637+ if ( Settings . PrivacyMode )
638+ {
639+ GamerTag = "Gamertag: Hidden" ;
640+ Xuid = "XUID: Hidden" ;
641+ }
642+ else
643+ {
644+ GamerTag = $ "Gamertag: { xui ? . Gamertag ?? "Unknown" } ";
645+ Xuid = $ "XUID: { XUIDOnly } ";
646+ }
647+ }
587648 }
588649 catch
589650 {
@@ -607,18 +668,66 @@ private async void GenerateTokens(MicrosoftOAuthResponse response)
607668 }
608669 LoginText = "Logout" ;
609670 XauthWorker_ProgressChanged ( null , null ) ;
671+ if ( IsLoggedIn && ! GrabbedProfile )
672+ GrabProfile ( ) ;
610673 }
611- private MicrosoftOAuthResponse readSession ( )
674+ private void ClearProfileState ( )
612675 {
613- var file = File . ReadAllText ( AuthFilePath ) ;
614- var response = JsonConvert . DeserializeObject < MicrosoftOAuthResponse > ( file ) ;
676+ IsLoggedIn = false ;
677+ XAUTHTested = false ;
678+ GrabbedProfile = false ;
679+ XAUTH = "" ;
680+ XUIDOnly = "" ;
681+ GamerTag = "Gamertag: Unknown " ;
682+ Xuid = "XUID: Unknown" ;
683+ GamerPic = "pack://application:,,,/Assets/cirno.png" ;
684+ GamerScore = "Gamerscore: Unknown" ;
685+ ProfileRep = "Reputation: Unknown" ;
686+ AccountTier = "Tier: Unknown" ;
687+ CurrentlyPlaying = "Currently Playing: Unknown" ;
688+ ActiveDevice = "Active Device: Unknown" ;
689+ IsVerified = "Verified: Unknown" ;
690+ Location = "Location: Unknown" ;
691+ Tenure = "Tenure: Unknown" ;
692+ Following = "Following: Unknown" ;
693+ Followers = "Followers: Unknown" ;
694+ Gamepass = "Gamepass: Unknown" ;
695+ Bio = "Bio: Unknown" ;
696+ Watermarks . Clear ( ) ;
697+ XauthWorker_ProgressChanged ( null , null ) ;
698+ }
615699
700+ private static readonly byte [ ] AuthFileMagic = Encoding . ASCII . GetBytes ( "XAU1" ) ;
701+ private static readonly byte [ ] AuthDpapiEntropy = Encoding . UTF8 . GetBytes ( "XAU-Auth-v1" ) ;
702+
703+ private MicrosoftOAuthResponse readSession ( )
704+ {
705+ var raw = File . ReadAllBytes ( AuthFilePath ) ;
706+ string json ;
707+ if ( raw . Length >= AuthFileMagic . Length && raw . AsSpan ( 0 , AuthFileMagic . Length ) . SequenceEqual ( AuthFileMagic ) )
708+ {
709+ var encrypted = raw . AsSpan ( AuthFileMagic . Length ) . ToArray ( ) ;
710+ var plain = ProtectedData . Unprotect ( encrypted , AuthDpapiEntropy , DataProtectionScope . CurrentUser ) ;
711+ json = Encoding . UTF8 . GetString ( plain ) ;
712+ }
713+ else
714+ {
715+ json = Encoding . UTF8 . GetString ( raw ) ;
716+ }
717+ var response = JsonConvert . DeserializeObject < MicrosoftOAuthResponse > ( json ) ;
616718 return response ;
617719 }
720+
618721 private void writeSession ( MicrosoftOAuthResponse response )
619722 {
620723 var json = JsonConvert . SerializeObject ( response ) ;
621- File . WriteAllText ( AuthFilePath , json ) ;
724+ var plain = Encoding . UTF8 . GetBytes ( json ) ;
725+ var encrypted = ProtectedData . Protect ( plain , AuthDpapiEntropy , DataProtectionScope . CurrentUser ) ;
726+ using ( var fs = new FileStream ( AuthFilePath , FileMode . Create , FileAccess . Write , FileShare . None ) )
727+ {
728+ fs . Write ( AuthFileMagic , 0 , AuthFileMagic . Length ) ;
729+ fs . Write ( encrypted , 0 , encrypted . Length ) ;
730+ }
622731 }
623732
624733 #endregion
0 commit comments