diff --git a/backend/src/main/java/de/szut/casino/security/GoogleController.java b/backend/src/main/java/de/szut/casino/security/GoogleController.java new file mode 100644 index 0000000..2219d5d --- /dev/null +++ b/backend/src/main/java/de/szut/casino/security/GoogleController.java @@ -0,0 +1,49 @@ +package de.szut.casino.security; + +import de.szut.casino.security.dto.AuthResponseDto; +import de.szut.casino.security.dto.GithubCallbackDto; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.view.RedirectView; + +@RestController +@RequestMapping("/oauth2/google") +public class GoogleController { + private static final Logger logger = LoggerFactory.getLogger(GoogleController.class); + + @Value("${spring.security.oauth2.client.registration.google.client-id}") + private String clientId; + + @Value("${spring.security.oauth2.client.provider.google.authorization-uri}") + private String authorizationUri; + + @Value("${spring.security.oauth2.client.registration.google.redirect-uri}") + private String redirectUri; + + @Autowired + private GoogleService googleService; + + @GetMapping("/authorize") + public RedirectView authorizeGoogle() { + logger.info("Redirecting to Google for authorization"); + + String authUrl = authorizationUri + + "?client_id=" + clientId + + "&redirect_uri=" + redirectUri + + "&response_type=code" + + "&scope=email profile"; + + return new RedirectView(authUrl); + } + + @PostMapping("/callback") + public ResponseEntity googleCallback(@RequestBody GithubCallbackDto callbackDto) { + String code = callbackDto.getCode(); + AuthResponseDto response = googleService.processGoogleCode(code); + return ResponseEntity.ok(response); + } +} diff --git a/backend/src/main/java/de/szut/casino/security/GoogleService.java b/backend/src/main/java/de/szut/casino/security/GoogleService.java new file mode 100644 index 0000000..703b85a --- /dev/null +++ b/backend/src/main/java/de/szut/casino/security/GoogleService.java @@ -0,0 +1,164 @@ +package de.szut.casino.security; + +import de.szut.casino.security.dto.AuthResponseDto; +import de.szut.casino.security.jwt.JwtUtils; +import de.szut.casino.user.AuthProvider; +import de.szut.casino.user.UserEntity; +import de.szut.casino.user.UserRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +import java.math.BigDecimal; +import java.util.*; + +@Service +public class GoogleService { + private static final Logger logger = LoggerFactory.getLogger(GoogleService.class); + + @Value("${spring.security.oauth2.client.registration.google.client-id}") + private String clientId; + + @Value("${spring.security.oauth2.client.registration.google.client-secret}") + private String clientSecret; + + @Value("${spring.security.oauth2.client.registration.google.redirect-uri}") + private String redirectUri; + + @Value("${spring.security.oauth2.client.provider.google.token-uri}") + private String tokenUri; + + @Value("${spring.security.oauth2.client.provider.google.user-info-uri}") + private String userInfoUri; + + @Autowired + private AuthenticationManager authenticationManager; + + @Autowired + private UserRepository userRepository; + + @Autowired + private JwtUtils jwtUtils; + + @Autowired + private PasswordEncoder oauth2PasswordEncoder; + + public AuthResponseDto processGoogleCode(String code) { + try { + RestTemplate restTemplate = new RestTemplate(); + + HttpHeaders tokenHeaders = new HttpHeaders(); + tokenHeaders.set("Content-Type", "application/x-www-form-urlencoded"); + + MultiValueMap tokenRequestBody = new LinkedMultiValueMap<>(); + tokenRequestBody.add("client_id", clientId); + tokenRequestBody.add("client_secret", clientSecret); + tokenRequestBody.add("code", code); + tokenRequestBody.add("redirect_uri", redirectUri); + tokenRequestBody.add("grant_type", "authorization_code"); + + HttpEntity> tokenRequestEntity = new HttpEntity<>(tokenRequestBody, tokenHeaders); + + ResponseEntity tokenResponse = restTemplate.exchange( + tokenUri, + HttpMethod.POST, + tokenRequestEntity, + Map.class + ); + + Map tokenResponseBody = tokenResponse.getBody(); + + if (tokenResponseBody == null || tokenResponseBody.containsKey("error")) { + String error = tokenResponseBody != null ? (String) tokenResponseBody.get("error") : "Unknown error"; + throw new RuntimeException("Google OAuth error: " + error); + } + + String accessToken = (String) tokenResponseBody.get("access_token"); + if (accessToken == null || accessToken.isEmpty()) { + throw new RuntimeException("Failed to receive access token from Google"); + } + + HttpHeaders userInfoHeaders = new HttpHeaders(); + userInfoHeaders.set("Authorization", "Bearer " + accessToken); + + HttpEntity userInfoRequestEntity = new HttpEntity<>(null, userInfoHeaders); + + ResponseEntity userResponse = restTemplate.exchange( + userInfoUri, + HttpMethod.GET, + userInfoRequestEntity, + Map.class + ); + + Map userAttributes = userResponse.getBody(); + if (userAttributes == null) { + throw new RuntimeException("Failed to fetch user data from Google"); + } + + String googleId = (String) userAttributes.get("sub"); + String email = (String) userAttributes.get("email"); + String name = (String) userAttributes.get("name"); + Boolean emailVerified = (Boolean) userAttributes.getOrDefault("email_verified", false); + + if (email == null) { + throw new RuntimeException("Google account does not have an email"); + } + + String username = name != null ? name.replaceAll("\\s+", "") : email.split("@")[0]; + + Optional userOptional = userRepository.findByProviderId(googleId); + UserEntity user; + + if (userOptional.isPresent()) { + user = userOptional.get(); + } else { + userOptional = userRepository.findByEmail(email); + + if (userOptional.isPresent()) { + user = userOptional.get(); + user.setProvider(AuthProvider.GOOGLE); + user.setProviderId(googleId); + } else { + user = new UserEntity(); + user.setEmail(email); + user.setUsername(username); + user.setProvider(AuthProvider.GOOGLE); + user.setProviderId(googleId); + user.setEmailVerified(emailVerified); + + user.setBalance(new BigDecimal("100.00")); + } + } + + String randomPassword = UUID.randomUUID().toString(); + user.setPassword(oauth2PasswordEncoder.encode(randomPassword)); + + userRepository.save(user); + + Authentication authentication = authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken(user.getEmail(), randomPassword) + ); + + String token = jwtUtils.generateToken(authentication); + + return new AuthResponseDto(token); + + } catch (Exception e) { + logger.error("Failed to process Google authentication", e); + throw new RuntimeException("Failed to process Google authentication", e); + } + } +} diff --git a/backend/src/main/java/de/szut/casino/security/oauth2/GoogleOAuth2UserInfo.java b/backend/src/main/java/de/szut/casino/security/oauth2/GoogleOAuth2UserInfo.java new file mode 100644 index 0000000..66fc99c --- /dev/null +++ b/backend/src/main/java/de/szut/casino/security/oauth2/GoogleOAuth2UserInfo.java @@ -0,0 +1,25 @@ +package de.szut.casino.security.oauth2; + +import java.util.Map; + +public class GoogleOAuth2UserInfo extends OAuth2UserInfo { + + public GoogleOAuth2UserInfo(Map attributes) { + super(attributes); + } + + @Override + public String getId() { + return (String) attributes.get("sub"); + } + + @Override + public String getName() { + return (String) attributes.get("name"); + } + + @Override + public String getEmail() { + return (String) attributes.get("email"); + } +} diff --git a/backend/src/main/java/de/szut/casino/security/oauth2/OAuth2UserInfoFactory.java b/backend/src/main/java/de/szut/casino/security/oauth2/OAuth2UserInfoFactory.java index b7d4365..8e0e936 100644 --- a/backend/src/main/java/de/szut/casino/security/oauth2/OAuth2UserInfoFactory.java +++ b/backend/src/main/java/de/szut/casino/security/oauth2/OAuth2UserInfoFactory.java @@ -10,6 +10,8 @@ public class OAuth2UserInfoFactory { public static OAuth2UserInfo getOAuth2UserInfo(String registrationId, Map attributes) { if (registrationId.equalsIgnoreCase(AuthProvider.GITHUB.toString())) { return new GitHubOAuth2UserInfo(attributes); + } else if (registrationId.equalsIgnoreCase(AuthProvider.GOOGLE.toString())) { + return new GoogleOAuth2UserInfo(attributes); } else { throw new OAuth2AuthenticationProcessingException("Sorry! Login with " + registrationId + " is not supported yet."); } diff --git a/backend/src/main/java/de/szut/casino/user/AuthProvider.java b/backend/src/main/java/de/szut/casino/user/AuthProvider.java index 2216da7..c26b45c 100644 --- a/backend/src/main/java/de/szut/casino/user/AuthProvider.java +++ b/backend/src/main/java/de/szut/casino/user/AuthProvider.java @@ -2,5 +2,6 @@ package de.szut.casino.user; public enum AuthProvider { LOCAL, - GITHUB + GITHUB, + GOOGLE } diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index e583c50..0fa2407 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -41,3 +41,13 @@ spring.security.oauth2.client.provider.github.user-name-attribute=login # OAuth Success and Failure URLs app.oauth2.authorizedRedirectUris=${app.frontend-host}/auth/oauth2/callback +# Google OAuth2 Configuration +spring.security.oauth2.client.registration.google.client-id=${GOOGLE_CLIENT_ID:350791038883-c1r7v4o793itq8a0rh7dut7itm7uneam.apps.googleusercontent.com} +spring.security.oauth2.client.registration.google.client-secret=${GOOGLE_CLIENT_SECRET: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} +spring.security.oauth2.client.registration.google.redirect-uri=${app.frontend-host}/oauth2/callback/google +spring.security.oauth2.client.registration.google.scope=email,profile +spring.security.oauth2.client.provider.google.authorization-uri=https://accounts.google.com/o/oauth2/v2/auth +spring.security.oauth2.client.provider.google.token-uri=https://oauth2.googleapis.com/token +spring.security.oauth2.client.provider.google.user-info-uri=https://www.googleapis.com/oauth2/v3/userinfo +spring.security.oauth2.client.provider.google.user-name-attribute=sub + diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index 6e9f9b7..44c342a 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -34,11 +34,25 @@ export const routes: Routes = [ ), }, { - path: 'oauth2/callback/github', - loadComponent: () => - import('./feature/auth/oauth2/oauth2-callback.component').then( - (m) => m.OAuth2CallbackComponent - ), + path: 'oauth2/callback', + children: [ + { + path: 'github', + loadComponent: () => + import('./feature/auth/oauth2/oauth2-callback.component').then( + (m) => m.OAuth2CallbackComponent + ), + data: { provider: 'github' }, + }, + { + path: 'google', + loadComponent: () => + import('./feature/auth/oauth2/oauth2-callback.component').then( + (m) => m.OAuth2CallbackComponent + ), + data: { provider: 'google' }, + }, + ], }, { path: 'game/blackjack', diff --git a/frontend/src/app/feature/auth/login/login.component.html b/frontend/src/app/feature/auth/login/login.component.html index 14a7d1e..044742c 100644 --- a/frontend/src/app/feature/auth/login/login.component.html +++ b/frontend/src/app/feature/auth/login/login.component.html @@ -89,7 +89,7 @@
-
+
+ +
diff --git a/frontend/src/app/feature/auth/login/login.component.ts b/frontend/src/app/feature/auth/login/login.component.ts index 2b78f29..09c1cdf 100644 --- a/frontend/src/app/feature/auth/login/login.component.ts +++ b/frontend/src/app/feature/auth/login/login.component.ts @@ -71,6 +71,11 @@ export class LoginComponent { window.location.href = `${environment.apiUrl}/oauth2/github/authorize`; } + loginWithGoogle(): void { + this.isLoading.set(true); + window.location.href = `${environment.apiUrl}/oauth2/google/authorize`; + } + switchToForgotPassword() { this.forgotPassword.emit(); } diff --git a/frontend/src/app/feature/auth/oauth2/oauth2-callback.component.ts b/frontend/src/app/feature/auth/oauth2/oauth2-callback.component.ts index f905f75..ebd2688 100644 --- a/frontend/src/app/feature/auth/oauth2/oauth2-callback.component.ts +++ b/frontend/src/app/feature/auth/oauth2/oauth2-callback.component.ts @@ -1,7 +1,7 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, computed, inject, OnInit, Signal } from '@angular/core'; import { CommonModule } from '@angular/common'; import { ActivatedRoute, Router } from '@angular/router'; -import { AuthService } from '@service/auth.service'; +import { Oauth2Service } from './oauth2.service'; @Component({ selector: 'app-oauth2-callback', @@ -10,51 +10,34 @@ import { AuthService } from '@service/auth.service'; template: `
-

Finishing authentication...

+

Authentifizierung...

-

{{ error }}

+

{{ error() }}

`, }) export class OAuth2CallbackComponent implements OnInit { - error: string | null = null; + error: Signal = computed(() => this.oauthService.error()); - constructor( - private route: ActivatedRoute, - private router: Router, - private authService: AuthService - ) {} + private route: ActivatedRoute = inject(ActivatedRoute); + private router: Router = inject(Router); + private oauthService: Oauth2Service = inject(Oauth2Service); ngOnInit(): void { - // Check for code in URL params this.route.queryParams.subscribe((params) => { const code = params['code']; + const provider = this.route.snapshot.data['provider'] || 'github'; if (code) { - // Exchange GitHub code for a JWT token - this.authService.githubAuth(code).subscribe({ - next: () => { - // Redirect to home after successful authentication - this.router.navigate(['/home']); - }, - error: (err) => { - console.error('GitHub authentication error:', err); - this.error = err.error?.message || 'Authentication failed. Please try again.'; - console.log('Error details:', err); - - // Redirect back to landing page after showing error - setTimeout(() => { - this.router.navigate(['/']); - }, 3000); - }, - }); + this.oauthService.oauth(provider, code); } else { - this.error = 'Authentication failed. No authorization code received.'; + this.oauthService.error.set( + 'Authentifizierung fehlgeschlagen. Bitte versuchen Sie es erneut.' + ); - // Redirect back to landing page after showing error setTimeout(() => { this.router.navigate(['/']); }, 3000); diff --git a/frontend/src/app/feature/auth/oauth2/oauth2.service.ts b/frontend/src/app/feature/auth/oauth2/oauth2.service.ts new file mode 100644 index 0000000..79ad6d9 --- /dev/null +++ b/frontend/src/app/feature/auth/oauth2/oauth2.service.ts @@ -0,0 +1,36 @@ +import { inject, Injectable, signal } from '@angular/core'; +import { Router } from '@angular/router'; +import { AuthService } from '@service/auth.service'; + +@Injectable({ + providedIn: 'root', +}) +export class Oauth2Service { + private router: Router = inject(Router); + private authService: AuthService = inject(AuthService); + private _error = signal(''); + + oauth(provider: string, code: string) { + const oauth$ = + provider === 'github' ? this.authService.githubAuth(code) : this.authService.googleAuth(code); + + oauth$.subscribe({ + next: () => { + this.router.navigate(['/home']); + }, + error: (err) => { + this._error.set( + err.error?.message || 'Authentifizierung fehlgeschlagen. Bitte versuchen Sie es erneut.' + ); + + setTimeout(() => { + this.router.navigate(['/']); + }, 3000); + }, + }); + } + + public get error() { + return this._error; + } +} diff --git a/frontend/src/app/service/auth.service.ts b/frontend/src/app/service/auth.service.ts index 081ad72..61b3e7d 100644 --- a/frontend/src/app/service/auth.service.ts +++ b/frontend/src/app/service/auth.service.ts @@ -79,6 +79,15 @@ export class AuthService { ); } + googleAuth(code: string): Observable { + return this.http.post(`${this.oauthUrl}/google/callback`, { code }).pipe( + tap((response) => { + this.setToken(response.token); + this.loadCurrentUser(); + }) + ); + } + logout(): void { localStorage.removeItem(TOKEN_KEY); localStorage.removeItem(USER_KEY);