This repository has been archived on 2025-06-18. You can view files and clone it, but you cannot make any changes to its state, such as pushing and creating new issues, pull requests or comments.
casino/frontend/src/app/service/auth.service.ts
Jan Klattenhoff 0e1946d190
Some checks failed
CI / Get Changed Files (pull_request) Successful in 7s
CI / eslint (pull_request) Successful in 25s
CI / test-build (pull_request) Successful in 32s
CI / prettier (pull_request) Failing after 59s
CI / Checkstyle Main (pull_request) Successful in 1m29s
refactor(auth): clean up login and logout logic
2025-04-02 16:33:28 +02:00

238 lines
7.8 KiB
TypeScript

import { inject, Injectable } from '@angular/core';
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 } from 'rxjs';
@Injectable({
providedIn: 'root',
})
export class AuthService {
private readonly authConfig: AuthConfig = {
issuer: 'https://oauth.simonis.lol/application/o/casino-dev/',
clientId: environment.OAUTH_CLIENT_ID,
dummyClientSecret: environment.OAUTH_CLIENT_SECRET,
scope: `openid email profile ${environment.OAUTH_CLIENT_ID}`,
responseType: 'code',
redirectUri: window.location.origin + '/auth/callback',
// Important - use empty post logout redirect URI to prevent auto-redirect
postLogoutRedirectUri: '',
// Don't use redirect URI as fallback for post logout
redirectUriAsPostLogoutRedirectUriFallback: false,
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: false,
sessionChecksEnabled: false,
};
private userService: UserService = inject(UserService);
private oauthService: OAuthService = inject(OAuthService);
private router: Router = inject(Router);
private user: User | null = null;
constructor() {
this.oauthService.configure(this.authConfig);
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) {
// We're in the OAuth callback
this.processCodeFlow();
} else {
// Normal app startup
this.checkExistingSession();
}
}
private processCodeFlow() {
// Try to exchange the authorization code for tokens
this.oauthService
.tryLogin({
onTokenReceived: () => {
// 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) => {
if (isLoggedIn && !this.user) {
this.handleSuccessfulLogin();
}
})
.catch((err) => {
console.error('Error during initial login attempt:', err);
});
}
private setupEventHandling() {
this.oauthService.events.subscribe((event: OAuthEvent) => {
if (event.type === 'token_received') {
this.handleSuccessfulLogin();
}
});
}
private handleSuccessfulLogin() {
// Extract claims from id token if available
const claims = this.oauthService.getIdentityClaims();
// If we have claims, use that as profile
if (claims && (claims['sub'] || claims['email'])) {
this.processUserProfile(claims);
return;
}
// Otherwise try to load user profile
try {
from(this.oauthService.loadUserProfile())
.pipe(
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()) {
this.oauthService.getAccessToken(); // Get token but don't use it directly
// 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 {
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: unknown) {
this.fromUserProfile(profile as Record<string, unknown>).subscribe({
next: (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() {
try {
// First ensure discovery document is loaded
this.oauthService
.loadDiscoveryDocument()
.then(() => {
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() {
try {
this.user = null;
// Prevent redirect to Authentik by doing a local logout only
// Instead of using oauthService.logOut() which redirects to the provider
// Clear tokens from storage without redirecting
// The parameter noRedirectToLogoutUrl=true prevents redirect to the identity provider
this.oauthService.logOut(true); // true means: don't redirect to Authentik logout page
// Override any post-logout redirect URI that might be configured
if (window.location.href.includes('id_token') || window.location.href.includes('logout')) {
// If we somehow ended up at a logout URL, redirect back to the app
window.location.href = window.location.origin;
}
// Clear any lingering tokens manually
localStorage.removeItem('access_token');
localStorage.removeItem('id_token');
localStorage.removeItem('refresh_token');
sessionStorage.removeItem('access_token');
sessionStorage.removeItem('id_token');
sessionStorage.removeItem('refresh_token');
// Navigate to landing page
this.router.navigate(['/']);
} catch (err) {
console.error('Exception in logout:', err);
// Force clear tokens locally
localStorage.clear(); // Clear all local storage as a last resort
sessionStorage.clear();
this.router.navigate(['/']);
}
}
isLoggedIn() {
return this.oauthService.hasValidAccessToken();
}
private fromUserProfile(profile: Record<string, unknown>) {
return this.userService.getOrCreateUser(profile);
}
getAccessToken() {
return this.oauthService.getAccessToken();
}
getUser() {
return this.user;
}
}