diff --git a/npm/ng-packs/apps/dev-app/src/server.ts b/npm/ng-packs/apps/dev-app/src/server.ts index 076acc7e6c..95ef2d000d 100644 --- a/npm/ng-packs/apps/dev-app/src/server.ts +++ b/npm/ng-packs/apps/dev-app/src/server.ts @@ -28,18 +28,21 @@ const REDIRECT_URI = 'http://localhost:4200'; const SCOPE = 'offline_access MyProjectName'; const config = await oidc.discovery(ISSUER, CLIENT_ID, /* client_secret */ undefined); -const secureCookie = { httpOnly: false, sameSite: 'lax' as const, secure: environment.production, path: '/' }; +const secureCookie = { httpOnly: true, sameSite: 'lax' as const, secure: environment.production, path: '/' }; const tokenCookie = { ...secureCookie, httpOnly: false, maxAge: 60 * 60 * 24 * 1 }; // 30 days app.use(cookieParser()); -const sessions = new Map(); +const sessions = new Map(); app.get('/authorize', async (_req, res) => { const code_verifier = oidc.randomPKCECodeVerifier(); const code_challenge = await oidc.calculatePKCECodeChallenge(code_verifier); const state = oidc.randomState(); + const returnUrl = String(_req.query.returnUrl || null); + res.cookie('returnUrl', returnUrl, { ...secureCookie, maxAge: 5 * 60 * 1000 }); + const sid = crypto.randomUUID(); sessions.set(sid, { pkce: code_verifier, state }); res.cookie('sid', sid, secureCookie); @@ -54,6 +57,36 @@ app.get('/authorize', async (_req, res) => { res.redirect(url.toString()); }); +app.get('/logout', async (req, res) => { + try { + const sid = req.cookies.sid; + + if (sid && sessions.has(sid)) { + sessions.delete(sid); + } + + res.clearCookie('sid', secureCookie); + res.clearCookie('access_token', tokenCookie); + res.clearCookie('refresh_token', secureCookie); + res.clearCookie('expires_at', tokenCookie); + res.clearCookie('returnUrl', secureCookie); + + const endSessionEndpoint = config.serverMetadata().end_session_endpoint; + if (endSessionEndpoint) { + const logoutUrl = new URL(endSessionEndpoint); + logoutUrl.searchParams.set('post_logout_redirect_uri', REDIRECT_URI); + logoutUrl.searchParams.set('client_id', CLIENT_ID); + + return res.redirect(logoutUrl.toString()); + } + res.redirect('/'); + + } catch (error) { + console.error('Logout error:', error); + res.status(500).send('Logout error'); + } +}); + app.get('/', async (req, res, next) => { try { const { code, state } = req.query as any; @@ -98,6 +131,10 @@ app.get('/', async (req, res, next) => { res.cookie('access_token', tokens.access_token, tokenCookie); res.cookie('refresh_token', tokens.refresh_token, secureCookie); res.cookie('expires_at', String(accessExpiresAt.getTime()), tokenCookie); + + const returnUrl = req.cookies.returnUrl || '/'; + res.clearCookie('returnUrl', secureCookie); + return res.redirect('/'); } catch (e) { console.error('OIDC error:', e); diff --git a/npm/ng-packs/packages/oauth/src/lib/strategies/auth-code-flow-strategy.ts b/npm/ng-packs/packages/oauth/src/lib/strategies/auth-code-flow-strategy.ts index f488bad665..73c018781c 100644 --- a/npm/ng-packs/packages/oauth/src/lib/strategies/auth-code-flow-strategy.ts +++ b/npm/ng-packs/packages/oauth/src/lib/strategies/auth-code-flow-strategy.ts @@ -1,5 +1,5 @@ -import { Injector, PLATFORM_ID } from '@angular/core'; -import { noop } from '@abp/ng.core'; +import { DOCUMENT, Injector, PLATFORM_ID } from '@angular/core'; +import { APP_STARTED_WITH_SSR, noop } from '@abp/ng.core'; import { Params } from '@angular/router'; import { filter, from, of, take, tap } from 'rxjs'; import { AuthFlowStrategy } from './auth-flow-strategy'; @@ -9,25 +9,27 @@ import { isPlatformBrowser } from '@angular/common'; export class AuthCodeFlowStrategy extends AuthFlowStrategy { readonly isInternalAuth = false; private platformId: object; + protected appStartedWithSSR: boolean; + protected document: Document; constructor(protected injector: Injector) { super(injector); this.platformId = injector.get(PLATFORM_ID); + this.appStartedWithSSR = injector.get(APP_STARTED_WITH_SSR); + this.document = injector.get(DOCUMENT); } async init() { this.checkRememberMeOption(); this.listenToTokenReceived(); - - return super - .init() - .then(() => this.oAuthService.tryLogin().catch(noop)) - .then(() => { - //TODO:Investigate silent refresh issue in SSR - if (isPlatformBrowser(this.platformId)) { + if (!this.appStartedWithSSR && isPlatformBrowser(this.platformId)) { + return super + .init() + .then(() => this.oAuthService.tryLogin().catch(noop)) + .then(() => { this.oAuthService.setupAutomaticSilentRefresh(); - } - }); + }); + } } private checkRememberMeOption() { @@ -78,11 +80,12 @@ export class AuthCodeFlowStrategy extends AuthFlowStrategy { } protected listenToTokenReceived() { - if (isPlatformBrowser(this.platformId)) { + if (isPlatformBrowser(this.platformId) && !this.appStartedWithSSR) { this.oAuthService.events .pipe( filter(event => event.type === 'token_received'), tap(() => { + //TODO: Investigate how to handle with ssr this.setUICulture(); this.replaceURLParams(); }), @@ -94,13 +97,19 @@ export class AuthCodeFlowStrategy extends AuthFlowStrategy { navigateToLogin(queryParams?: Params) { if (isPlatformBrowser(this.platformId)) { - let additionalState = ''; - if (queryParams?.returnUrl) { - additionalState = queryParams.returnUrl; - } + if (APP_STARTED_WITH_SSR) { + if (this.document.defaultView) { + this.document.defaultView.location.replace('/authorize'); + } + } else { + let additionalState = ''; + if (queryParams?.returnUrl) { + additionalState = queryParams.returnUrl; + } - const cultureParams = this.getCultureParams(queryParams); - this.oAuthService.initCodeFlow(additionalState, cultureParams); + const cultureParams = this.getCultureParams(queryParams); + this.oAuthService.initCodeFlow(additionalState, cultureParams); + } } } @@ -113,11 +122,18 @@ export class AuthCodeFlowStrategy extends AuthFlowStrategy { logout(queryParams?: Params) { this.rememberMeService.remove(); - if (queryParams?.noRedirectToLogoutUrl) { - this.router.navigate(['/']); - return from(this.oAuthService.revokeTokenAndLogout(true)); + + if (APP_STARTED_WITH_SSR) { + if (this.document.defaultView) { + this.document.defaultView.location.replace('/logout'); + } + } else { + if (queryParams?.noRedirectToLogoutUrl) { + this.router.navigate(['/']); + return from(this.oAuthService.revokeTokenAndLogout(true)); + } + return from(this.oAuthService.revokeTokenAndLogout(this.getCultureParams(queryParams))); } - return from(this.oAuthService.revokeTokenAndLogout(this.getCultureParams(queryParams))); } login(queryParams?: Params) {