feat: add authentik for authentication #58

Merged
jank merged 23 commits from feature/authentik into main 2025-04-04 13:26:03 +00:00
5 changed files with 4 additions and 41 deletions
Showing only changes of commit 02453449cd - Show all commits

View file

@ -19,7 +19,7 @@ public class UserEntity {
@GeneratedValue @GeneratedValue
private Long id; private Long id;
@Column(unique = true) @Column(unique = true)
private String authentikId; // Changed from keycloakId to authentikId private String authentikId;
private String username; private String username;
@Column(precision = 19, scale = 2) @Column(precision = 19, scale = 2)

View file

@ -10,8 +10,6 @@ import lombok.Setter;
@AllArgsConstructor @AllArgsConstructor
@NoArgsConstructor @NoArgsConstructor
public class KeycloakUserDto { public class KeycloakUserDto {
// Renamed class but kept for backward compatibility
// This now contains Authentik user info
private String sub; private String sub;
private String preferred_username; private String preferred_username;
} }

View file

@ -18,11 +18,9 @@ export default class LoginSuccessComponent implements OnInit {
async ngOnInit() { async ngOnInit() {
try { try {
jank marked this conversation as resolved

Remove comments

Remove comments
// Check if we're authenticated
if (this.oauthService.hasValidAccessToken()) { if (this.oauthService.hasValidAccessToken()) {
this.router.navigate(['/home']); this.router.navigate(['/home']);
} else { } else {
// Wait a bit and check if we've been authenticated in the meantime
setTimeout(() => { setTimeout(() => {
if (this.oauthService.hasValidAccessToken() || this.authService.getUser()) { if (this.oauthService.hasValidAccessToken() || this.authService.getUser()) {
this.router.navigate(['/home']); this.router.navigate(['/home']);
@ -33,7 +31,6 @@ export default class LoginSuccessComponent implements OnInit {
} }
} catch (err) { } catch (err) {
console.error('Error during login callback:', err); console.error('Error during login callback:', err);
// Wait a bit in case token processing is happening elsewhere
setTimeout(() => { setTimeout(() => {
if (this.authService.isLoggedIn()) { if (this.authService.isLoggedIn()) {
this.router.navigate(['/home']); this.router.navigate(['/home']);

View file

@ -17,16 +17,12 @@ export class AuthService {
scope: `openid email profile ${environment.OAUTH_CLIENT_ID}`, scope: `openid email profile ${environment.OAUTH_CLIENT_ID}`,
responseType: 'code', responseType: 'code',
redirectUri: window.location.origin + '/auth/callback', redirectUri: window.location.origin + '/auth/callback',
// Important - use empty post logout redirect URI to prevent auto-redirect
postLogoutRedirectUri: '', postLogoutRedirectUri: '',
// Don't use redirect URI as fallback for post logout
redirectUriAsPostLogoutRedirectUriFallback: false, redirectUriAsPostLogoutRedirectUriFallback: false,
oidc: true, oidc: true,
requestAccessToken: true, requestAccessToken: true,
// Explicitly set token endpoint since discovery is failing
tokenEndpoint: 'https://oauth.simonis.lol/application/o/token/', tokenEndpoint: 'https://oauth.simonis.lol/application/o/token/',
userinfoEndpoint: 'https://oauth.simonis.lol/application/o/userinfo/', userinfoEndpoint: 'https://oauth.simonis.lol/application/o/userinfo/',
// Loosen validation since Authentik might not fully conform to the spec
strictDiscoveryDocumentValidation: false, strictDiscoveryDocumentValidation: false,
skipIssuerCheck: true, skipIssuerCheck: true,
disableAtHashCheck: true, disableAtHashCheck: true,
@ -45,27 +41,22 @@ export class AuthService {
this.oauthService.configure(this.authConfig); this.oauthService.configure(this.authConfig);
this.setupEventHandling(); this.setupEventHandling();
// Check if we're on the callback page
const hasAuthParams = const hasAuthParams =
window.location.search.includes('code=') || window.location.search.includes('code=') ||
window.location.search.includes('token=') || window.location.search.includes('token=') ||
window.location.search.includes('id_token='); window.location.search.includes('id_token=');
if (hasAuthParams) { if (hasAuthParams) {
// We're in the OAuth callback
this.processCodeFlow(); this.processCodeFlow();
} else { } else {
// Normal app startup
this.checkExistingSession(); this.checkExistingSession();
} }
} }
private processCodeFlow() { private processCodeFlow() {
// Try to exchange the authorization code for tokens
this.oauthService this.oauthService
.tryLogin({ .tryLogin({
onTokenReceived: () => { onTokenReceived: () => {
// Manually create a token_received event
this.handleSuccessfulLogin(); this.handleSuccessfulLogin();
}, },
}) })
@ -75,7 +66,6 @@ export class AuthService {
} }
private checkExistingSession() { private checkExistingSession() {
// Try login on startup
this.oauthService this.oauthService
.loadDiscoveryDocumentAndTryLogin() .loadDiscoveryDocumentAndTryLogin()
.then((isLoggedIn) => { .then((isLoggedIn) => {
@ -97,25 +87,20 @@ export class AuthService {
} }
private handleSuccessfulLogin() { private handleSuccessfulLogin() {
// Extract claims from id token if available
const claims = this.oauthService.getIdentityClaims(); const claims = this.oauthService.getIdentityClaims();
// If we have claims, use that as profile
if (claims && (claims['sub'] || claims['email'])) { if (claims && (claims['sub'] || claims['email'])) {
this.processUserProfile(claims); this.processUserProfile(claims);
return; return;
} }
// Otherwise try to load user profile
try { try {
from(this.oauthService.loadUserProfile()) from(this.oauthService.loadUserProfile())
.pipe( .pipe(
catchError((error) => { catchError((error) => {
console.error('Error loading user profile:', 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()) { if (this.oauthService.hasValidAccessToken()) {
this.oauthService.getAccessToken(); // Get token but don't use it directly this.oauthService.getAccessToken();
// Create a basic profile from the token
const minimalProfile = { const minimalProfile = {
sub: 'user-' + Math.random().toString(36).substring(2, 10), sub: 'user-' + Math.random().toString(36).substring(2, 10),
preferred_username: 'user' + Date.now(), preferred_username: 'user' + Date.now(),
@ -134,7 +119,6 @@ export class AuthService {
}); });
} catch (err) { } catch (err) {
console.error('Exception in handleSuccessfulLogin:', err); console.error('Exception in handleSuccessfulLogin:', err);
// Try to navigate to home if we have a token anyway
if (this.oauthService.hasValidAccessToken()) { if (this.oauthService.hasValidAccessToken()) {
this.router.navigate(['/home']); this.router.navigate(['/home']);
} else { } else {
@ -151,7 +135,6 @@ export class AuthService {
}, },
error: (err) => { error: (err) => {
console.error('Error creating/retrieving user:', 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()) { if (this.oauthService.hasValidAccessToken()) {
this.router.navigate(['/home']); this.router.navigate(['/home']);
} else { } else {
@ -163,7 +146,6 @@ export class AuthService {
login() { login() {
try { try {
// First ensure discovery document is loaded
this.oauthService this.oauthService
.loadDiscoveryDocument() .loadDiscoveryDocument()
.then(() => { .then(() => {
@ -171,12 +153,10 @@ export class AuthService {
}) })
.catch((err) => { .catch((err) => {
console.error('Error loading discovery document:', err); console.error('Error loading discovery document:', err);
// Try login anyway with configured endpoints
this.oauthService.initLoginFlow(); this.oauthService.initLoginFlow();
}); });
} catch (err) { } catch (err) {
console.error('Exception in login:', err); console.error('Exception in login:', err);
// Try direct login as a fallback
const redirectUri = this.authConfig.redirectUri || window.location.origin + '/auth/callback'; const redirectUri = this.authConfig.redirectUri || window.location.origin + '/auth/callback';
const scope = this.authConfig.scope || 'openid email profile'; 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)}`; const authUrl = `${this.authConfig.issuer}authorize?client_id=${this.authConfig.clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&scope=${encodeURIComponent(scope)}`;
@ -188,20 +168,12 @@ export class AuthService {
try { try {
this.user = null; this.user = null;
// Prevent redirect to Authentik by doing a local logout only this.oauthService.logOut(true);
// 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 (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; window.location.href = window.location.origin;
} }
// Clear any lingering tokens manually
localStorage.removeItem('access_token'); localStorage.removeItem('access_token');
localStorage.removeItem('id_token'); localStorage.removeItem('id_token');
localStorage.removeItem('refresh_token'); localStorage.removeItem('refresh_token');
@ -209,12 +181,10 @@ export class AuthService {
sessionStorage.removeItem('id_token'); sessionStorage.removeItem('id_token');
sessionStorage.removeItem('refresh_token'); sessionStorage.removeItem('refresh_token');
// Navigate to landing page
this.router.navigate(['/']); this.router.navigate(['/']);
} catch (err) { } catch (err) {
console.error('Exception in logout:', err); console.error('Exception in logout:', err);
// Force clear tokens locally localStorage.clear();
localStorage.clear(); // Clear all local storage as a last resort
sessionStorage.clear(); sessionStorage.clear();
this.router.navigate(['/']); this.router.navigate(['/']);
} }

View file

@ -43,8 +43,6 @@ export class UserService {
} }
public getOrCreateUser(profile: Record<string, unknown>): Observable<User> { public getOrCreateUser(profile: Record<string, unknown>): Observable<User> {
// Authentik format might differ from Keycloak
// Check different possible locations for the ID and username
const info = profile['info'] as Record<string, unknown> | undefined; const info = profile['info'] as Record<string, unknown> | undefined;
const id = (info?.['sub'] as string) || (profile['sub'] as string); const id = (info?.['sub'] as string) || (profile['sub'] as string);
const username = const username =