• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

damienbod / angular-auth-oidc-client / 9308899843

30 May 2024 08:37PM CUT coverage: 92.928%. Remained the same
9308899843

Pull #1948

github

web-flow
Merge 9ccb2c964 into 33372edf1
Pull Request #1948: feat(core): adds angular 18 support

706 of 826 branches covered (85.47%)

Branch coverage included in aggregate %.

2566 of 2695 relevant lines covered (95.21%)

8.31 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

70.68
/projects/angular-auth-oidc-client/src/lib/iframe/check-session.service.ts
1
import { DOCUMENT } from '@angular/common';
2
import { Injectable, NgZone, OnDestroy, inject } from '@angular/core';
3
import { BehaviorSubject, Observable, of } from 'rxjs';
4
import { take } from 'rxjs/operators';
5
import { OpenIdConfiguration } from '../config/openid-configuration';
6
import { LoggerService } from '../logging/logger.service';
7
import { EventTypes } from '../public-events/event-types';
8
import { PublicEventsService } from '../public-events/public-events.service';
9
import { StoragePersistenceService } from '../storage/storage-persistence.service';
10
import { IFrameService } from './existing-iframe.service';
11

12
const IFRAME_FOR_CHECK_SESSION_IDENTIFIER = 'myiFrameForCheckSession';
1✔
13

14
// http://openid.net/specs/openid-connect-session-1_0-ID4.html
15

16
@Injectable({ providedIn: 'root' })
17
export class CheckSessionService implements OnDestroy {
1✔
18
  private readonly loggerService = inject(LoggerService);
23✔
19

20
  private readonly storagePersistenceService = inject(
23✔
21
    StoragePersistenceService
22
  );
23

24
  private readonly iFrameService = inject(IFrameService);
23✔
25

26
  private readonly eventService = inject(PublicEventsService);
23✔
27

28
  private readonly zone = inject(NgZone);
23✔
29

30
  private readonly document = inject(DOCUMENT);
23✔
31

32
  private checkSessionReceived = false;
23✔
33

34
  private scheduledHeartBeatRunning: number|null= null
23✔
35

36
  private lastIFrameRefresh = 0;
23✔
37

38
  private outstandingMessages = 0;
23✔
39

40
  private readonly heartBeatInterval = 3000;
23✔
41

42
  private readonly iframeRefreshInterval = 60000;
23✔
43

44
  private readonly checkSessionChangedInternal$ = new BehaviorSubject<boolean>(
23✔
45
    false
46
  );
47

48
  private iframeMessageEventListener?: (this:Window, ev: MessageEvent<any>) => any;
49

50
  get checkSessionChanged$(): Observable<boolean> {
51
    return this.checkSessionChangedInternal$.asObservable();
109✔
52
  }
53

54
  ngOnDestroy(): void {
55
    this.stop();
×
56
    const windowAsDefaultView = this.document.defaultView;
×
57

58
    if (windowAsDefaultView && this.iframeMessageEventListener) {
×
59
      windowAsDefaultView.removeEventListener(
×
60
        'message',
61
        this.iframeMessageEventListener,
62
        false
63
      );
64
    }
65
  }
66

67
  isCheckSessionConfigured(configuration: OpenIdConfiguration): boolean {
68
    const { startCheckSession } = configuration;
2✔
69

70
    return Boolean(startCheckSession);
2✔
71
  }
72

73
  start(configuration: OpenIdConfiguration): void {
74
    if (!!this.scheduledHeartBeatRunning) {
2✔
75
      return;
1✔
76
    }
77

78
    const { clientId } = configuration;
1✔
79

80
    this.pollServerSession(clientId, configuration);
1✔
81
  }
82

83
  stop(): void {
84
    if (!this.scheduledHeartBeatRunning) {
2✔
85
      return;
1✔
86
    }
87

88
    this.clearScheduledHeartBeat();
1✔
89
    this.checkSessionReceived = false;
1✔
90
  }
91

92
  serverStateChanged(configuration: OpenIdConfiguration): boolean {
93
    const { startCheckSession } = configuration;
3✔
94

95
    return Boolean(startCheckSession) && this.checkSessionReceived;
3✔
96
  }
97

98
  getExistingIframe(): HTMLIFrameElement | null {
99
    return this.iFrameService.getExistingIFrame(
4✔
100
      IFRAME_FOR_CHECK_SESSION_IDENTIFIER
101
    );
102
  }
103

104
  private init(configuration: OpenIdConfiguration): Observable<any> {
105
    if (this.lastIFrameRefresh + this.iframeRefreshInterval > Date.now()) {
3✔
106
      return of(undefined);
1✔
107
    }
108

109
    const authWellKnownEndPoints = this.storagePersistenceService.read(
2✔
110
      'authWellKnownEndPoints',
111
      configuration
112
    );
113

114
    if (!authWellKnownEndPoints) {
2✔
115
      this.loggerService.logWarning(
1✔
116
        configuration,
117
        'CheckSession - init check session: authWellKnownEndpoints is undefined. Returning.'
118
      );
119

120
      return of();
1✔
121
    }
122

123
    const existingIframe = this.getOrCreateIframe(configuration);
1✔
124

125
    // https://www.w3.org/TR/2000/REC-DOM-Level-2-Events-20001113/events.html#Events-EventTarget-addEventListener
126
    // If multiple identical EventListeners are registered on the same EventTarget with the same parameters the duplicate instances are discarded. They do not cause the EventListener to be called twice and since they are discarded they do not need to be removed with the removeEventListener method.
127
    // this is done even if iframe exists for HMR to work, since iframe exists on service init
128
    this.bindMessageEventToIframe(configuration);
1✔
129
    const checkSessionIframe = authWellKnownEndPoints.checkSessionIframe;
1✔
130
    const contentWindow = existingIframe.contentWindow;
1✔
131

132
    if (!checkSessionIframe) {
1✔
133
      this.loggerService.logWarning(
1✔
134
        configuration,
135
        'CheckSession - init check session: checkSessionIframe is not configured to run'
136
      );
137
    }
138

139
    if (!contentWindow) {
1!
140
      this.loggerService.logWarning(
×
141
        configuration,
142
        'CheckSession - init check session: IFrame contentWindow does not exist'
143
      );
144
    } else {
145
      contentWindow.location.replace(checkSessionIframe);
1✔
146
    }
147

148
    return new Observable((observer) => {
1✔
149
      existingIframe.onload = (): void => {
×
150
        this.lastIFrameRefresh = Date.now();
×
151
        observer.next();
×
152
        observer.complete();
×
153
      };
154
    });
155
  }
156

157
  private pollServerSession(
158
    clientId: string | undefined,
159
    configuration: OpenIdConfiguration
160
  ): void {
161
    this.outstandingMessages = 0;
5✔
162

163
    const pollServerSessionRecur = (): void => {
5✔
164
      this.init(configuration)
5✔
165
        .pipe(take(1))
166
        .subscribe(() => {
167
          const existingIframe = this.getExistingIframe();
5✔
168

169
          if (existingIframe && clientId) {
5✔
170
            this.loggerService.logDebug(
3✔
171
              configuration,
172
              `CheckSession - clientId : '${clientId}' - existingIframe: '${existingIframe}'`
173
            );
174
            const sessionState = this.storagePersistenceService.read(
3✔
175
              'session_state',
176
              configuration
177
            );
178
            const authWellKnownEndPoints = this.storagePersistenceService.read(
3✔
179
              'authWellKnownEndPoints',
180
              configuration
181
            );
182
            const contentWindow = existingIframe.contentWindow;
3✔
183

184
            if (
3✔
185
              sessionState &&
6✔
186
              authWellKnownEndPoints?.checkSessionIframe &&
187
              contentWindow
188
            ) {
189
              const iframeOrigin = new URL(
1✔
190
                authWellKnownEndPoints.checkSessionIframe
191
              )?.origin;
192

193
              this.outstandingMessages++;
1✔
194
              contentWindow.postMessage(
1✔
195
                clientId + ' ' + sessionState,
196
                iframeOrigin
197
              );
198
            } else {
199
              this.loggerService.logDebug(
2✔
200
                configuration,
201
                `CheckSession - session_state is '${sessionState}' - AuthWellKnownEndPoints is '${JSON.stringify(
202
                  authWellKnownEndPoints,
203
                  null,
204
                  2
205
                )}'`
206
              );
207
              this.checkSessionChangedInternal$.next(true);
2✔
208
            }
209
          } else {
210
            this.loggerService.logWarning(
2✔
211
              configuration,
212
              `CheckSession - OidcSecurityCheckSession pollServerSession checkSession IFrame does not exist:
213
               clientId : '${clientId}' - existingIframe: '${existingIframe}'`
214
            );
215
          }
216

217
          // after sending three messages with no response, fail.
218
          if (this.outstandingMessages > 3) {
5!
219
            this.loggerService.logError(
×
220
              configuration,
221
              `CheckSession - OidcSecurityCheckSession not receiving check session response messages.
222
                            Outstanding messages: '${this.outstandingMessages}'. Server unreachable?`
223
            );
224
          }
225

226
          this.zone.runOutsideAngular(() => {
5✔
227
            this.scheduledHeartBeatRunning = this.document?.defaultView?.setTimeout(
5!
228
              () => this.zone.run(pollServerSessionRecur),
×
229
              this.heartBeatInterval
230
            ) ?? null;
231
          });
232
        });
233
    };
234

235
    pollServerSessionRecur();
5✔
236
  }
237

238
  private clearScheduledHeartBeat(): void {
239
    if(this.scheduledHeartBeatRunning !== null) {
1✔
240
      clearTimeout(this.scheduledHeartBeatRunning);
1✔
241
      this.scheduledHeartBeatRunning = null;
1✔
242
    }
243
  }
244

245
  private messageHandler(configuration: OpenIdConfiguration, e: any): void {
246
    const existingIFrame = this.getExistingIframe();
×
247
    const authWellKnownEndPoints = this.storagePersistenceService.read(
×
248
      'authWellKnownEndPoints',
249
      configuration
250
    );
251
    const startsWith = !!authWellKnownEndPoints?.checkSessionIframe?.startsWith(
×
252
      e.origin
253
    );
254

255
    this.outstandingMessages = 0;
×
256

257
    if (
×
258
      existingIFrame &&
×
259
      startsWith &&
260
      e.source === existingIFrame.contentWindow
261
    ) {
262
      if (e.data === 'error') {
×
263
        this.loggerService.logWarning(
×
264
          configuration,
265
          'CheckSession - error from check session messageHandler'
266
        );
267
      } else if (e.data === 'changed') {
×
268
        this.loggerService.logDebug(
×
269
          configuration,
270
          `CheckSession - ${e} from check session messageHandler`
271
        );
272
        this.checkSessionReceived = true;
×
273
        this.eventService.fireEvent(EventTypes.CheckSessionReceived, e.data);
×
274
        this.checkSessionChangedInternal$.next(true);
×
275
      } else {
276
        this.eventService.fireEvent(EventTypes.CheckSessionReceived, e.data);
×
277
        this.loggerService.logDebug(
×
278
          configuration,
279
          `CheckSession - ${e.data} from check session messageHandler`
280
        );
281
      }
282
    }
283
  }
284

285
  private bindMessageEventToIframe(configuration: OpenIdConfiguration): void {
286
    this.iframeMessageEventListener = this.messageHandler.bind(
1✔
287
      this,
288
      configuration
289
    );
290

291
    const defaultView = this.document.defaultView;
1✔
292

293
    if (defaultView) {
1✔
294
      defaultView.addEventListener(
1✔
295
        'message',
296
        this.iframeMessageEventListener,
297
        false
298
      );
299
    }
300
  }
301

302
  private getOrCreateIframe(
303
    configuration: OpenIdConfiguration
304
  ): HTMLIFrameElement {
305
    return (
4✔
306
      this.getExistingIframe() ||
7✔
307
      this.iFrameService.addIFrameToWindowBody(
308
        IFRAME_FOR_CHECK_SESSION_IDENTIFIER,
309
        configuration
310
      )
311
    );
312
  }
313
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc