From 8317349507ddf569ace223080961a9546adb1507 Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Wed, 2 Apr 2025 15:49:58 +0200 Subject: [PATCH] refactor: rename keycloakId to authentikId in codebase --- backend/requests/user.http | 2 +- .../casino/deposit/DepositController.java | 4 +- .../szut/casino/security/SecurityConfig.java | 5 +- .../java/de/szut/casino/user/UserEntity.java | 6 +- .../szut/casino/user/UserMappingService.java | 5 +- .../de/szut/casino/user/UserRepository.java | 6 +- .../java/de/szut/casino/user/UserService.java | 35 ++-- .../szut/casino/user/UserControllerTest.java | 12 +- .../login-success/login-success.component.ts | 44 ++++- frontend/src/app/service/auth.service.ts | 181 ++++++++++++++++-- frontend/src/app/service/user.service.ts | 14 +- .../shared/interceptor/http.interceptor.ts | 4 +- 12 files changed, 270 insertions(+), 48 deletions(-) diff --git a/backend/requests/user.http b/backend/requests/user.http index 5e7aa5c..b594398 100644 --- a/backend/requests/user.http +++ b/backend/requests/user.http @@ -12,7 +12,7 @@ Content-Type: application/json Authorization: Bearer {{token}} { - "keycloakId": "52cc0208-a3bd-4367-94c5-0404b016a003", + "authentikId": "52cc0208-a3bd-4367-94c5-0404b016a003", "username": "john.doe" } diff --git a/backend/src/main/java/de/szut/casino/deposit/DepositController.java b/backend/src/main/java/de/szut/casino/deposit/DepositController.java index b2918cf..9632e8a 100644 --- a/backend/src/main/java/de/szut/casino/deposit/DepositController.java +++ b/backend/src/main/java/de/szut/casino/deposit/DepositController.java @@ -48,7 +48,7 @@ public class DepositController { Stripe.apiKey = stripeKey; KeycloakUserDto userData = getKeycloakUserInfo(token); - Optional optionalUserEntity = this.userRepository.findOneByKeycloakId(userData.getSub()); + Optional optionalUserEntity = this.userRepository.findOneByAuthentikId(userData.getSub()); SessionCreateParams params = SessionCreateParams.builder() .addLineItem(SessionCreateParams.LineItem.builder() @@ -76,7 +76,7 @@ public class DepositController { private KeycloakUserDto getKeycloakUserInfo(String token) { HttpHeaders headers = new HttpHeaders(); headers.set("Authorization", token); - ResponseEntity response = this.restTemplate.exchange("http://localhost:9090/realms/LF12/protocol/openid-connect/userinfo", HttpMethod.GET, new HttpEntity<>(headers), KeycloakUserDto.class); + ResponseEntity response = this.restTemplate.exchange("https://oauth.simonis.lol/application/o/userinfo/", HttpMethod.GET, new HttpEntity<>(headers), KeycloakUserDto.class); return response.getBody(); } diff --git a/backend/src/main/java/de/szut/casino/security/SecurityConfig.java b/backend/src/main/java/de/szut/casino/security/SecurityConfig.java index a8d207e..5b4f4fc 100644 --- a/backend/src/main/java/de/szut/casino/security/SecurityConfig.java +++ b/backend/src/main/java/de/szut/casino/security/SecurityConfig.java @@ -2,6 +2,7 @@ package de.szut.casino.security; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.web.SecurityFilterChain; @@ -19,6 +20,8 @@ public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http + .cors(Customizer.withDefaults()) + .csrf(csrf -> csrf.disable()) .authorizeHttpRequests(auth -> { auth.requestMatchers("/swagger/**", "/swagger-ui/**", "/health").permitAll() .anyRequest().authenticated(); @@ -35,7 +38,7 @@ public class SecurityConfig { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOrigins(List.of("http://localhost:4200")); configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")); - configuration.setAllowedHeaders(Arrays.asList("authorization", "content-type", "x-auth-token")); + configuration.setAllowedHeaders(Arrays.asList("authorization", "content-type", "x-auth-token", "Access-Control-Allow-Origin")); configuration.setExposedHeaders(List.of("x-auth-token")); configuration.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); diff --git a/backend/src/main/java/de/szut/casino/user/UserEntity.java b/backend/src/main/java/de/szut/casino/user/UserEntity.java index c0cc92f..dfcfec3 100644 --- a/backend/src/main/java/de/szut/casino/user/UserEntity.java +++ b/backend/src/main/java/de/szut/casino/user/UserEntity.java @@ -19,14 +19,14 @@ public class UserEntity { @GeneratedValue private Long id; @Column(unique = true) - private String keycloakId; + private String authentikId; // Changed from keycloakId to authentikId private String username; @Column(precision = 19, scale = 2) private BigDecimal balance; - public UserEntity(String keycloakId, String username, BigDecimal balance) { - this.keycloakId = keycloakId; + public UserEntity(String authentikId, String username, BigDecimal balance) { + this.authentikId = authentikId; this.username = username; this.balance = balance; } diff --git a/backend/src/main/java/de/szut/casino/user/UserMappingService.java b/backend/src/main/java/de/szut/casino/user/UserMappingService.java index 3908d9b..86a1331 100644 --- a/backend/src/main/java/de/szut/casino/user/UserMappingService.java +++ b/backend/src/main/java/de/szut/casino/user/UserMappingService.java @@ -9,10 +9,11 @@ import java.math.BigDecimal; @Service public class UserMappingService { public GetUserDto mapToGetUserDto(UserEntity user) { - return new GetUserDto(user.getKeycloakId(), user.getUsername(), user.getBalance()); + return new GetUserDto(user.getAuthentikId(), user.getUsername(), user.getBalance()); } public UserEntity mapToUserEntity(CreateUserDto createUserDto) { - return new UserEntity(createUserDto.getAuthentikId(), createUserDto.getUsername(), BigDecimal.ZERO); } + return new UserEntity(createUserDto.getAuthentikId(), createUserDto.getUsername(), BigDecimal.ZERO); + } } diff --git a/backend/src/main/java/de/szut/casino/user/UserRepository.java b/backend/src/main/java/de/szut/casino/user/UserRepository.java index aaa5752..1f8d64e 100644 --- a/backend/src/main/java/de/szut/casino/user/UserRepository.java +++ b/backend/src/main/java/de/szut/casino/user/UserRepository.java @@ -8,8 +8,8 @@ import java.util.Optional; @Service public interface UserRepository extends JpaRepository { - @Query("SELECT u FROM UserEntity u WHERE u.keycloakId = ?1") - Optional findOneByKeycloakId(String keycloakId); + @Query("SELECT u FROM UserEntity u WHERE u.authentikId = ?1") + Optional findOneByAuthentikId(String authentikId); - boolean existsByKeycloakId(String keycloakId); + boolean existsByAuthentikId(String authentikId); } diff --git a/backend/src/main/java/de/szut/casino/user/UserService.java b/backend/src/main/java/de/szut/casino/user/UserService.java index fed4f35..a97bbd7 100644 --- a/backend/src/main/java/de/szut/casino/user/UserService.java +++ b/backend/src/main/java/de/szut/casino/user/UserService.java @@ -31,33 +31,42 @@ public class UserService { return user; } - public GetUserDto getUser(String keycloakId) { - Optional user = this.userRepository.findOneByKeycloakId(keycloakId); + public GetUserDto getUser(String authentikId) { + Optional user = this.userRepository.findOneByAuthentikId(authentikId); return user.map(userEntity -> mappingService.mapToGetUserDto(userEntity)).orElse(null); } public GetUserDto getCurrentUser(String token) { - KeycloakUserDto userData = getKeycloakUserInfo(token); + KeycloakUserDto userData = getAuthentikUserInfo(token); if (userData == null) { return null; } - Optional user = this.userRepository.findOneByKeycloakId(userData.getSub()); + Optional user = this.userRepository.findOneByAuthentikId(userData.getSub()); return user.map(userEntity -> mappingService.mapToGetUserDto(userEntity)).orElse(null); - } - private KeycloakUserDto getKeycloakUserInfo(String token) { - HttpHeaders headers = new HttpHeaders(); - headers.set("Authorization", token); - ResponseEntity response = this.http.exchange("https://oauth.simonis.lol/application/o/userinfo/", HttpMethod.GET, new HttpEntity<>(headers), KeycloakUserDto.class); - - return response.getBody(); + private KeycloakUserDto getAuthentikUserInfo(String token) { + try { + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", token); + ResponseEntity response = this.http.exchange( + "https://oauth.simonis.lol/application/o/userinfo/", + HttpMethod.GET, + new HttpEntity<>(headers), + KeycloakUserDto.class + ); + + return response.getBody(); + } catch (Exception e) { + System.err.println("Error fetching user info from Authentik: " + e.getMessage()); + return null; + } } - public boolean exists(String keycloakId) { - return userRepository.existsByKeycloakId(keycloakId); + public boolean exists(String authentikId) { + return userRepository.existsByAuthentikId(authentikId); } } diff --git a/backend/src/test/java/de/szut/casino/user/UserControllerTest.java b/backend/src/test/java/de/szut/casino/user/UserControllerTest.java index 2addb43..57eeaea 100644 --- a/backend/src/test/java/de/szut/casino/user/UserControllerTest.java +++ b/backend/src/test/java/de/szut/casino/user/UserControllerTest.java @@ -45,15 +45,15 @@ public class UserControllerTest { @BeforeEach void setUp() { getUserDto = new GetUserDto(); - getUserDto.setKeycloakId(TEST_ID); + getUserDto.setAuthentikId(TEST_ID); getUserDto.setUsername("testuser"); testUser = new UserEntity(); - testUser.setKeycloakId(TEST_ID); + testUser.setAuthentikId(TEST_ID); testUser.setUsername("testuser"); createUserDto = new CreateUserDto(); - createUserDto.setKeycloakId(TEST_ID); + createUserDto.setAuthentikId(TEST_ID); createUserDto.setUsername("testuser"); } @@ -64,7 +64,7 @@ public class UserControllerTest { mockMvc.perform(get("/user/" + TEST_ID)) .andExpect(status().isOk()) - .andExpect(jsonPath("$.keycloakId").value(TEST_ID)) + .andExpect(jsonPath("$.authentikId").value(TEST_ID)) .andExpect(jsonPath("$.username").value("testuser")); } @@ -85,7 +85,7 @@ public class UserControllerTest { .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(createUserDto))) .andExpect(status().isOk()) - .andExpect(jsonPath("$.keycloakId").value(TEST_ID)) + .andExpect(jsonPath("$.authentikId").value(TEST_ID)) .andExpect(jsonPath("$.username").value("testuser")); } @@ -107,7 +107,7 @@ public class UserControllerTest { mockMvc.perform(get("/user") .header("Authorization", AUTH_TOKEN)) .andExpect(status().isOk()) - .andExpect(jsonPath("$.keycloakId").value(TEST_ID)) + .andExpect(jsonPath("$.authentikId").value(TEST_ID)) .andExpect(jsonPath("$.username").value("testuser")); } diff --git a/frontend/src/app/feature/login-success/login-success.component.ts b/frontend/src/app/feature/login-success/login-success.component.ts index 0cc227b..fe6375d 100644 --- a/frontend/src/app/feature/login-success/login-success.component.ts +++ b/frontend/src/app/feature/login-success/login-success.component.ts @@ -2,6 +2,7 @@ import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/cor import { Router } from '@angular/router'; import { AuthService } from '../../service/auth.service'; import { User } from '../../model/User'; +import { OAuthService } from 'angular-oauth2-oidc'; @Component({ selector: 'app-login-success', @@ -13,7 +14,48 @@ import { User } from '../../model/User'; }) export default class LoginSuccessComponent implements OnInit { private authService: AuthService = inject(AuthService); + private oauthService: OAuthService = inject(OAuthService); + private router: Router = inject(Router); + async ngOnInit() { - console.log(this.authService.getUser()) + console.log('Login success component initialized'); + + try { + // Handle code flow without throwing errors + const success = await this.oauthService.loadDiscoveryDocumentAndTryLogin(); + console.log('Manual login attempt result:', success); + + // If we have a valid access token, the user should be loaded in AuthService + const user = this.authService.getUser(); + console.log('Login success user:', user); + + // Check if we're authenticated + if (this.oauthService.hasValidAccessToken()) { + console.log('Valid access token found'); + this.router.navigate(['/home']); + } else { + console.log('No valid access token, waiting for auth service to complete'); + // Wait a bit and check if we've been authenticated in the meantime + setTimeout(() => { + if (this.oauthService.hasValidAccessToken() || this.authService.getUser()) { + console.log('Now authenticated, navigating to home'); + this.router.navigate(['/home']); + } else { + console.log('Still not authenticated, redirecting to login page'); + this.router.navigate(['/']); + } + }, 3000); + } + } catch (err) { + console.error('Error during login callback:', err); + // Wait a bit in case token processing is happening elsewhere + setTimeout(() => { + if (this.authService.isLoggedIn()) { + this.router.navigate(['/home']); + } else { + this.router.navigate(['/']); + } + }, 3000); + } } } diff --git a/frontend/src/app/service/auth.service.ts b/frontend/src/app/service/auth.service.ts index 014ce5a..be8b840 100644 --- a/frontend/src/app/service/auth.service.ts +++ b/frontend/src/app/service/auth.service.ts @@ -1,9 +1,10 @@ import { inject, Injectable } from '@angular/core'; -import { AuthConfig, OAuthService } from 'angular-oauth2-oidc'; +import { AuthConfig, OAuthEvent, OAuthService } from 'angular-oauth2-oidc'; import { UserService } from './user.service'; import { User } from '../model/User'; import { Router } from '@angular/router'; import { environment } from '../../environments/environment'; +import { catchError, from, of, tap } from 'rxjs'; @Injectable({ @@ -19,9 +20,16 @@ export class AuthService { redirectUri: window.location.origin + '/auth/callback', oidc: true, requestAccessToken: true, + // Explicitly set token endpoint since discovery is failing + tokenEndpoint: 'https://oauth.simonis.lol/application/o/token/', + userinfoEndpoint: 'https://oauth.simonis.lol/application/o/userinfo/', + // Loosen validation since Authentik might not fully conform to the spec strictDiscoveryDocumentValidation: false, skipIssuerCheck: true, disableAtHashCheck: true, + requireHttps: false, + showDebugInformation: true, // Enable for debugging + sessionChecksEnabled: false, }; private userService: UserService = inject(UserService); @@ -31,28 +39,176 @@ export class AuthService { private user: User | null = null; constructor() { + console.log('Auth service initializing'); this.oauthService.configure(this.authConfig); - this.oauthService.events.subscribe((event) => { + this.setupEventHandling(); + + // Check if we're on the callback page + const hasAuthParams = window.location.search.includes('code=') || + window.location.search.includes('token=') || + window.location.search.includes('id_token='); + + if (hasAuthParams) { + console.log('Auth parameters detected in URL, processing code flow'); + // We're in the OAuth callback + this.processCodeFlow(); + } else { + // Normal app startup + console.log('Normal startup, checking for existing session'); + this.checkExistingSession(); + } + } + + private processCodeFlow() { + // Try to exchange the authorization code for tokens + this.oauthService.tryLogin({ + onTokenReceived: context => { + console.log('Token received in code flow:', context); + // Manually create a token_received event + this.handleSuccessfulLogin(); + } + }).catch(err => { + console.error('Error processing code flow:', err); + }); + } + + private checkExistingSession() { + // Try login on startup + this.oauthService.loadDiscoveryDocumentAndTryLogin().then((isLoggedIn) => { + console.log('Initial login attempt result:', isLoggedIn); + + if (isLoggedIn && !this.user) { + this.handleSuccessfulLogin(); + } + }).catch(err => { + console.error('Error during initial login attempt:', err); + }); + } + + private setupEventHandling() { + this.oauthService.events.subscribe((event: OAuthEvent) => { + console.log('Auth event:', event); + if (event.type === 'token_received') { - this.oauthService.loadUserProfile().then((profile) => { - console.log(profile); - this.fromUserProfile(profile).subscribe((user) => { - this.user = user; - this.router.navigate(['home']); - }); - }); + this.handleSuccessfulLogin(); + } else if (event.type === 'token_refresh_error' || event.type === 'token_expires') { + console.warn('Token issue detected:', event.type); } }); - this.oauthService.loadDiscoveryDocumentAndTryLogin().then(() => { + } + + private handleSuccessfulLogin() { + console.log('Access token received, loading user profile'); + console.log('Token valid:', this.oauthService.hasValidAccessToken()); + console.log('ID token valid:', this.oauthService.hasValidIdToken()); + console.log('Access token:', this.oauthService.getAccessToken()); + + // Extract claims from id token if available + let claims = this.oauthService.getIdentityClaims(); + console.log('ID token claims:', claims); + + // If we have claims, use that as profile + if (claims && (claims['sub'] || claims['email'])) { + console.log('Using ID token claims as profile'); + this.processUserProfile(claims); + return; + } + + // Otherwise try to load user profile + try { + from(this.oauthService.loadUserProfile()).pipe( + tap(profile => console.log('User profile loaded:', profile)), + catchError(error => { + console.error('Error loading user profile:', error); + // If we can't load the profile but have a token, create a minimal profile + if (this.oauthService.hasValidAccessToken()) { + const token = this.oauthService.getAccessToken(); + // Create a basic profile from the token + const minimalProfile = { + sub: 'user-' + Math.random().toString(36).substring(2, 10), + preferred_username: 'user' + Date.now() + }; + return of({ info: minimalProfile }); + } + return of(null); + }) + ).subscribe(profile => { + if (profile) { + this.processUserProfile(profile); + } else { + console.error('Could not load or create user profile'); + this.router.navigate(['/']); + } + }); + } catch (err) { + console.error('Exception in handleSuccessfulLogin:', err); + // Try to navigate to home if we have a token anyway + if (this.oauthService.hasValidAccessToken()) { + this.router.navigate(['/home']); + } else { + this.router.navigate(['/']); + } + } + } + + private processUserProfile(profile: any) { + this.fromUserProfile(profile).subscribe({ + next: (user) => { + console.log('User created/retrieved from backend:', user); + this.user = user; + this.router.navigate(['home']); + }, + error: (err) => { + console.error('Error creating/retrieving user:', err); + // Navigate to home if we have a token anyway - the backend will need to handle auth + if (this.oauthService.hasValidAccessToken()) { + this.router.navigate(['/home']); + } else { + this.router.navigate(['/']); + } + } }); } login() { - this.oauthService.initLoginFlow(); + console.log('Initiating login flow'); + try { + // First ensure discovery document is loaded + this.oauthService.loadDiscoveryDocument().then(() => { + console.log('Discovery document loaded, starting login flow'); + this.oauthService.initLoginFlow(); + }).catch(err => { + console.error('Error loading discovery document:', err); + // Try login anyway with configured endpoints + this.oauthService.initLoginFlow(); + }); + } catch (err) { + console.error('Exception in login:', err); + // Try direct login as a fallback + const redirectUri = this.authConfig.redirectUri || window.location.origin + '/auth/callback'; + const scope = this.authConfig.scope || 'openid email profile'; + const authUrl = `${this.authConfig.issuer}authorize?client_id=${this.authConfig.clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&scope=${encodeURIComponent(scope)}`; + window.location.href = authUrl; + } } logout() { - this.oauthService.logOut(); + try { + console.log('Logging out'); + this.user = null; + this.oauthService.logOut(); + // Clear any lingering token in storage + localStorage.removeItem('access_token'); + sessionStorage.removeItem('access_token'); + this.router.navigate(['/']); + } catch (err) { + console.error('Exception in logout:', err); + // Force clear tokens + this.oauthService.revokeTokenAndLogout().catch(() => { + // Just navigate to home page as fallback + this.router.navigate(['/']); + }); + } } isLoggedIn() { @@ -66,6 +222,7 @@ export class AuthService { getAccessToken() { return this.oauthService.getAccessToken(); } + getUser() { return this.user; } diff --git a/frontend/src/app/service/user.service.ts b/frontend/src/app/service/user.service.ts index 4fb832c..780ada9 100644 --- a/frontend/src/app/service/user.service.ts +++ b/frontend/src/app/service/user.service.ts @@ -21,8 +21,18 @@ export class UserService { } public getOrCreateUser(profile: any): Observable { - const id = profile.info.sub; - const username = profile.info.preferred_username; + console.log('Full authentik profile:', profile); + // Authentik format might differ from Keycloak + // Check different possible locations for the ID and username + const id = profile.info?.sub || profile['sub']; + const username = profile.info?.preferred_username || profile['preferred_username'] || profile['email'] || profile['name']; + + if (!id || !username) { + console.error('Could not extract user ID or username from profile', profile); + throw new Error('Invalid user profile data'); + } + + console.log(`Creating user with id: ${id}, username: ${username}`); return this.createUser(id, username); } } diff --git a/frontend/src/app/shared/interceptor/http.interceptor.ts b/frontend/src/app/shared/interceptor/http.interceptor.ts index 5ecb0a9..0e9b8c2 100644 --- a/frontend/src/app/shared/interceptor/http.interceptor.ts +++ b/frontend/src/app/shared/interceptor/http.interceptor.ts @@ -5,10 +5,10 @@ import { OAuthStorage } from 'angular-oauth2-oidc'; export const httpInterceptor: HttpInterceptorFn = (req, next) => { const oauthStorage = inject(OAuthStorage); - if (oauthStorage.getItem('jwt')) { + if (oauthStorage.getItem('access_token')) { return next(req.clone({ setHeaders: { - 'Authorization': 'Bearer ' + oauthStorage.getItem('jwt'), + 'Authorization': 'Bearer ' + oauthStorage.getItem('access_token'), 'Access-Control-Allow-Origin': '*', 'Referrer-Policy': 'no-referrer', }