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

damienbod / angular-auth-oidc-client / 5678835292

pending completion
5678835292

Pull #1812

github

web-flow
Merge a6a5b3270 into 015bdacae
Pull Request #1812: Check if savedRouteForRedirect is null

664 of 700 branches covered (94.86%)

Branch coverage included in aggregate %.

1 of 1 new or added line in 1 file covered. (100.0%)

2472 of 2530 relevant lines covered (97.71%)

8.6 hits per line

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

96.73
/projects/angular-auth-oidc-client/src/lib/validation/state-validation.service.ts
1
import { Injectable } from '@angular/core';
2
import { Observable, of } from 'rxjs';
3
import { map, mergeMap } from 'rxjs/operators';
4
import { OpenIdConfiguration } from '../config/openid-configuration';
5
import { CallbackContext } from '../flows/callback-context';
6
import { LoggerService } from '../logging/logger.service';
7
import { StoragePersistenceService } from '../storage/storage-persistence.service';
8
import { EqualityService } from '../utils/equality/equality.service';
9
import { FlowHelper } from '../utils/flowHelper/flow-helper.service';
10
import { TokenHelperService } from '../utils/tokenHelper/token-helper.service';
11
import { StateValidationResult } from './state-validation-result';
12
import { TokenValidationService } from './token-validation.service';
13
import { ValidationResult } from './validation-result';
14

15
@Injectable({ providedIn: 'root' })
16
export class StateValidationService {
1✔
17
  constructor(
18
    private readonly storagePersistenceService: StoragePersistenceService,
31✔
19
    private readonly tokenValidationService: TokenValidationService,
31✔
20
    private readonly tokenHelperService: TokenHelperService,
31✔
21
    private readonly loggerService: LoggerService,
31✔
22
    private readonly equalityService: EqualityService,
31✔
23
    private readonly flowHelper: FlowHelper
31✔
24
  ) {}
25

26
  getValidatedStateResult(
27
    callbackContext: CallbackContext,
28
    configuration: OpenIdConfiguration
29
  ): Observable<StateValidationResult> {
30
    if (!callbackContext || callbackContext.authResult.error) {
22✔
31
      return of(new StateValidationResult('', '', false, {}));
2✔
32
    }
33

34
    return this.validateState(callbackContext, configuration);
20✔
35
  }
36

37
  private validateState(
38
    callbackContext: CallbackContext,
39
    configuration: OpenIdConfiguration
40
  ): Observable<StateValidationResult> {
41
    const toReturn = new StateValidationResult();
20✔
42
    const authStateControl = this.storagePersistenceService.read(
20✔
43
      'authStateControl',
44
      configuration
45
    );
46

47
    if (
20✔
48
      !this.tokenValidationService.validateStateFromHashCallback(
49
        callbackContext.authResult.state,
50
        authStateControl,
51
        configuration
52
      )
53
    ) {
54
      this.loggerService.logWarning(
1✔
55
        configuration,
56
        'authCallback incorrect state'
57
      );
58
      toReturn.state = ValidationResult.StatesDoNotMatch;
1✔
59
      this.handleUnsuccessfulValidation(configuration);
1✔
60

61
      return of(toReturn);
1✔
62
    }
63

64
    const isCurrentFlowImplicitFlowWithAccessToken =
65
      this.flowHelper.isCurrentFlowImplicitFlowWithAccessToken(configuration);
19✔
66
    const isCurrentFlowCodeFlow =
67
      this.flowHelper.isCurrentFlowCodeFlow(configuration);
19✔
68

69
    if (isCurrentFlowImplicitFlowWithAccessToken || isCurrentFlowCodeFlow) {
19✔
70
      toReturn.accessToken = callbackContext.authResult.access_token;
15✔
71
    }
72

73
    const disableIdTokenValidation = configuration.disableIdTokenValidation;
19✔
74

75
    if (disableIdTokenValidation) {
19✔
76
      toReturn.state = ValidationResult.Ok;
2✔
77
      // TODO TESTING
78
      toReturn.authResponseIsValid = true;
2✔
79

80
      return of(toReturn);
2✔
81
    }
82

83
    const isInRefreshTokenFlow =
84
      callbackContext.isRenewProcess && !!callbackContext.refreshToken;
17✔
85
    const hasIdToken = !!callbackContext.authResult.id_token;
17✔
86

87
    if (isInRefreshTokenFlow && !hasIdToken) {
17✔
88
      toReturn.state = ValidationResult.Ok;
1✔
89
      // TODO TESTING
90
      toReturn.authResponseIsValid = true;
1✔
91

92
      return of(toReturn);
1✔
93
    }
94

95
    if (callbackContext.authResult.id_token) {
16✔
96
      const {
97
        clientId,
98
        issValidationOff,
99
        maxIdTokenIatOffsetAllowedInSeconds,
100
        disableIatOffsetValidation,
101
        ignoreNonceAfterRefresh,
102
        renewTimeBeforeTokenExpiresInSeconds,
103
      } = configuration;
15✔
104

105
      toReturn.idToken = callbackContext.authResult.id_token;
15✔
106
      toReturn.decodedIdToken = this.tokenHelperService.getPayloadFromToken(
15✔
107
        toReturn.idToken,
108
        false,
109
        configuration
110
      );
111

112
      return this.tokenValidationService
15✔
113
        .validateSignatureIdToken(
114
          toReturn.idToken,
115
          callbackContext.jwtKeys,
116
          configuration
117
        )
118
        .pipe(
119
          mergeMap((isSignatureIdTokenValid: boolean) => {
120
            if (!isSignatureIdTokenValid) {
15✔
121
              this.loggerService.logDebug(
1✔
122
                configuration,
123
                'authCallback Signature validation failed id_token'
124
              );
125
              toReturn.state = ValidationResult.SignatureFailed;
1✔
126
              this.handleUnsuccessfulValidation(configuration);
1✔
127

128
              return of(toReturn);
1✔
129
            }
130

131
            const authNonce = this.storagePersistenceService.read(
14✔
132
              'authNonce',
133
              configuration
134
            );
135

136
            if (
14✔
137
              !this.tokenValidationService.validateIdTokenNonce(
138
                toReturn.decodedIdToken,
139
                authNonce,
140
                ignoreNonceAfterRefresh,
141
                configuration
142
              )
143
            ) {
144
              this.loggerService.logWarning(
1✔
145
                configuration,
146
                'authCallback incorrect nonce, did you call the checkAuth() method multiple times?'
147
              );
148
              toReturn.state = ValidationResult.IncorrectNonce;
1✔
149
              this.handleUnsuccessfulValidation(configuration);
1✔
150

151
              return of(toReturn);
1✔
152
            }
153

154
            if (
13✔
155
              !this.tokenValidationService.validateRequiredIdToken(
156
                toReturn.decodedIdToken,
157
                configuration
158
              )
159
            ) {
160
              this.loggerService.logDebug(
1✔
161
                configuration,
162
                'authCallback Validation, one of the REQUIRED properties missing from id_token'
163
              );
164
              toReturn.state = ValidationResult.RequiredPropertyMissing;
1✔
165
              this.handleUnsuccessfulValidation(configuration);
1✔
166

167
              return of(toReturn);
1✔
168
            }
169

170
            if (
12✔
171
              !isInRefreshTokenFlow &&
24✔
172
              !this.tokenValidationService.validateIdTokenIatMaxOffset(
173
                toReturn.decodedIdToken,
174
                maxIdTokenIatOffsetAllowedInSeconds,
175
                disableIatOffsetValidation,
176
                configuration
177
              )
178
            ) {
179
              this.loggerService.logWarning(
1✔
180
                configuration,
181
                'authCallback Validation, iat rejected id_token was issued too far away from the current time'
182
              );
183
              toReturn.state = ValidationResult.MaxOffsetExpired;
1✔
184
              this.handleUnsuccessfulValidation(configuration);
1✔
185

186
              return of(toReturn);
1✔
187
            }
188

189
            const authWellKnownEndPoints = this.storagePersistenceService.read(
11✔
190
              'authWellKnownEndPoints',
191
              configuration
192
            );
193

194
            if (authWellKnownEndPoints) {
11✔
195
              if (issValidationOff) {
10✔
196
                this.loggerService.logDebug(
1✔
197
                  configuration,
198
                  'iss validation is turned off, this is not recommended!'
199
                );
200
              } else if (
9✔
201
                !issValidationOff &&
18✔
202
                !this.tokenValidationService.validateIdTokenIss(
203
                  toReturn.decodedIdToken,
204
                  authWellKnownEndPoints.issuer,
205
                  configuration
206
                )
207
              ) {
208
                this.loggerService.logWarning(
1✔
209
                  configuration,
210
                  'authCallback incorrect iss does not match authWellKnownEndpoints issuer'
211
                );
212
                toReturn.state = ValidationResult.IssDoesNotMatchIssuer;
1✔
213
                this.handleUnsuccessfulValidation(configuration);
1✔
214

215
                return of(toReturn);
1✔
216
              }
217
            } else {
218
              this.loggerService.logWarning(
1✔
219
                configuration,
220
                'authWellKnownEndpoints is undefined'
221
              );
222
              toReturn.state = ValidationResult.NoAuthWellKnownEndPoints;
1✔
223
              this.handleUnsuccessfulValidation(configuration);
1✔
224

225
              return of(toReturn);
1✔
226
            }
227

228
            if (
9✔
229
              !this.tokenValidationService.validateIdTokenAud(
230
                toReturn.decodedIdToken,
231
                clientId,
232
                configuration
233
              )
234
            ) {
235
              this.loggerService.logWarning(
1✔
236
                configuration,
237
                'authCallback incorrect aud'
238
              );
239
              toReturn.state = ValidationResult.IncorrectAud;
1✔
240
              this.handleUnsuccessfulValidation(configuration);
1✔
241

242
              return of(toReturn);
1✔
243
            }
244

245
            if (
8✔
246
              !this.tokenValidationService.validateIdTokenAzpExistsIfMoreThanOneAud(
247
                toReturn.decodedIdToken
248
              )
249
            ) {
250
              this.loggerService.logWarning(
1✔
251
                configuration,
252
                'authCallback missing azp'
253
              );
254
              toReturn.state = ValidationResult.IncorrectAzp;
1✔
255
              this.handleUnsuccessfulValidation(configuration);
1✔
256

257
              return of(toReturn);
1✔
258
            }
259

260
            if (
7✔
261
              !this.tokenValidationService.validateIdTokenAzpValid(
262
                toReturn.decodedIdToken,
263
                clientId
264
              )
265
            ) {
266
              this.loggerService.logWarning(
1✔
267
                configuration,
268
                'authCallback incorrect azp'
269
              );
270
              toReturn.state = ValidationResult.IncorrectAzp;
1✔
271
              this.handleUnsuccessfulValidation(configuration);
1✔
272

273
              return of(toReturn);
1✔
274
            }
275

276
            if (
6✔
277
              !this.isIdTokenAfterRefreshTokenRequestValid(
278
                callbackContext,
279
                toReturn.decodedIdToken,
280
                configuration
281
              )
282
            ) {
283
              this.loggerService.logWarning(
1✔
284
                configuration,
285
                'authCallback pre, post id_token claims do not match in refresh'
286
              );
287
              toReturn.state =
1✔
288
                ValidationResult.IncorrectIdTokenClaimsAfterRefresh;
289
              this.handleUnsuccessfulValidation(configuration);
1✔
290

291
              return of(toReturn);
1✔
292
            }
293

294
            if (
5✔
295
              !isInRefreshTokenFlow &&
10✔
296
              !this.tokenValidationService.validateIdTokenExpNotExpired(
297
                toReturn.decodedIdToken,
298
                configuration,
299
                renewTimeBeforeTokenExpiresInSeconds
300
              )
301
            ) {
302
              this.loggerService.logWarning(
1✔
303
                configuration,
304
                'authCallback id token expired'
305
              );
306
              toReturn.state = ValidationResult.TokenExpired;
1✔
307
              this.handleUnsuccessfulValidation(configuration);
1✔
308

309
              return of(toReturn);
1✔
310
            }
311

312
            return this.validateDefault(
4✔
313
              isCurrentFlowImplicitFlowWithAccessToken,
314
              isCurrentFlowCodeFlow,
315
              toReturn,
316
              configuration,
317
              callbackContext
318
            );
319
          })
320
        );
321
    } else {
322
      this.loggerService.logDebug(
1✔
323
        configuration,
324
        'No id_token found, skipping id_token validation'
325
      );
326
    }
327

328
    return this.validateDefault(
1✔
329
      isCurrentFlowImplicitFlowWithAccessToken,
330
      isCurrentFlowCodeFlow,
331
      toReturn,
332
      configuration,
333
      callbackContext
334
    );
335
  }
336

337
  private validateDefault(
338
    isCurrentFlowImplicitFlowWithAccessToken: boolean,
339
    isCurrentFlowCodeFlow: boolean,
340
    toReturn: StateValidationResult,
341
    configuration: OpenIdConfiguration,
342
    callbackContext: CallbackContext
343
  ): Observable<StateValidationResult> {
344
    // flow id_token
345
    if (!isCurrentFlowImplicitFlowWithAccessToken && !isCurrentFlowCodeFlow) {
5✔
346
      toReturn.authResponseIsValid = true;
1✔
347
      toReturn.state = ValidationResult.Ok;
1✔
348
      this.handleSuccessfulValidation(configuration);
1✔
349
      this.handleUnsuccessfulValidation(configuration);
1✔
350

351
      return of(toReturn);
1✔
352
    }
353

354
    // only do check if id_token returned, no always the case when using refresh tokens
355
    if (callbackContext.authResult.id_token) {
4✔
356
      const idTokenHeader = this.tokenHelperService.getHeaderFromToken(
3✔
357
        toReturn.idToken,
358
        false,
359
        configuration
360
      );
361

362
      if (
3!
363
        isCurrentFlowCodeFlow &&
3!
364
        !(toReturn.decodedIdToken.at_hash as string)
365
      ) {
366
        this.loggerService.logDebug(
×
367
          configuration,
368
          'Code Flow active, and no at_hash in the id_token, skipping check!'
369
        );
370
      } else {
371
        return this.tokenValidationService
3✔
372
          .validateIdTokenAtHash(
373
            toReturn.accessToken,
374
            toReturn.decodedIdToken.at_hash,
375
            idTokenHeader.alg, // 'RS256'
376
            configuration
377
          )
378
          .pipe(
379
            map((valid: boolean) => {
380
              if (!valid || !toReturn.accessToken) {
3✔
381
                this.loggerService.logWarning(
1✔
382
                  configuration,
383
                  'authCallback incorrect at_hash'
384
                );
385
                toReturn.state = ValidationResult.IncorrectAtHash;
1✔
386
                this.handleUnsuccessfulValidation(configuration);
1✔
387

388
                return toReturn;
1✔
389
              } else {
390
                toReturn.authResponseIsValid = true;
2✔
391
                toReturn.state = ValidationResult.Ok;
2✔
392
                this.handleSuccessfulValidation(configuration);
2✔
393

394
                return toReturn;
2✔
395
              }
396
            })
397
          );
398
      }
399
    }
400

401
    toReturn.authResponseIsValid = true;
1✔
402
    toReturn.state = ValidationResult.Ok;
1✔
403
    this.handleSuccessfulValidation(configuration);
1✔
404

405
    return of(toReturn);
1✔
406
  }
407

408
  private isIdTokenAfterRefreshTokenRequestValid(
409
    callbackContext: CallbackContext,
410
    newIdToken: any,
411
    configuration: OpenIdConfiguration
412
  ): boolean {
413
    const { useRefreshToken, disableRefreshIdTokenAuthTimeValidation } =
414
      configuration;
13✔
415

416
    if (!useRefreshToken) {
13✔
417
      return true;
5✔
418
    }
419

420
    if (!callbackContext.existingIdToken) {
8✔
421
      return true;
1✔
422
    }
423

424
    const decodedIdToken = this.tokenHelperService.getPayloadFromToken(
7✔
425
      callbackContext.existingIdToken,
426
      false,
427
      configuration
428
    );
429

430
    // Upon successful validation of the Refresh Token, the response body is the Token Response of Section 3.1.3.3
431
    // except that it might not contain an id_token.
432

433
    // If an ID Token is returned as a result of a token refresh request, the following requirements apply:
434

435
    // its iss Claim Value MUST be the same as in the ID Token issued when the original authentication occurred,
436
    if (decodedIdToken.iss !== newIdToken.iss) {
7✔
437
      this.loggerService.logDebug(
1✔
438
        configuration,
439
        `iss do not match: ${decodedIdToken.iss} ${newIdToken.iss}`
440
      );
441

442
      return false;
1✔
443
    }
444
    // its azp Claim Value MUST be the same as in the ID Token issued when the original authentication occurred;
445
    //   if no azp Claim was present in the original ID Token, one MUST NOT be present in the new ID Token, and
446
    // otherwise, the same rules apply as apply when issuing an ID Token at the time of the original authentication.
447
    if (decodedIdToken.azp !== newIdToken.azp) {
6✔
448
      this.loggerService.logDebug(
1✔
449
        configuration,
450
        `azp do not match: ${decodedIdToken.azp} ${newIdToken.azp}`
451
      );
452

453
      return false;
1✔
454
    }
455
    // its sub Claim Value MUST be the same as in the ID Token issued when the original authentication occurred,
456
    if (decodedIdToken.sub !== newIdToken.sub) {
5✔
457
      this.loggerService.logDebug(
1✔
458
        configuration,
459
        `sub do not match: ${decodedIdToken.sub} ${newIdToken.sub}`
460
      );
461

462
      return false;
1✔
463
    }
464

465
    // its aud Claim Value MUST be the same as in the ID Token issued when the original authentication occurred,
466
    if (
4✔
467
      !this.equalityService.isStringEqualOrNonOrderedArrayEqual(
468
        decodedIdToken?.aud,
469
        newIdToken?.aud
470
      )
471
    ) {
472
      this.loggerService.logDebug(
1✔
473
        configuration,
474
        `aud in new id_token is not valid: '${decodedIdToken?.aud}' '${newIdToken.aud}'`
475
      );
476

477
      return false;
1✔
478
    }
479

480
    if (disableRefreshIdTokenAuthTimeValidation) {
3✔
481
      return true;
1✔
482
    }
483

484
    // its iat Claim MUST represent the time that the new ID Token is issued,
485
    // if the ID Token contains an auth_time Claim, its value MUST represent the time of the original authentication
486
    // - not the time that the new ID token is issued,
487
    if (decodedIdToken.auth_time !== newIdToken.auth_time) {
2✔
488
      this.loggerService.logDebug(
1✔
489
        configuration,
490
        `auth_time do not match: ${decodedIdToken.auth_time} ${newIdToken.auth_time}`
491
      );
492

493
      return false;
1✔
494
    }
495

496
    return true;
1✔
497
  }
498

499
  private handleSuccessfulValidation(configuration: OpenIdConfiguration): void {
500
    const { autoCleanStateAfterAuthentication } = configuration;
4✔
501

502
    this.storagePersistenceService.write('authNonce', null, configuration);
4✔
503

504
    if (autoCleanStateAfterAuthentication) {
4!
505
      this.storagePersistenceService.write(
×
506
        'authStateControl',
507
        '',
508
        configuration
509
      );
510
    }
511
    this.loggerService.logDebug(
4✔
512
      configuration,
513
      'authCallback token(s) validated, continue'
514
    );
515
  }
516

517
  private handleUnsuccessfulValidation(
518
    configuration: OpenIdConfiguration
519
  ): void {
520
    const { autoCleanStateAfterAuthentication } = configuration;
14✔
521

522
    this.storagePersistenceService.write('authNonce', null, configuration);
14✔
523

524
    if (autoCleanStateAfterAuthentication) {
14!
525
      this.storagePersistenceService.write(
×
526
        'authStateControl',
527
        '',
528
        configuration
529
      );
530
    }
531
    this.loggerService.logDebug(configuration, 'authCallback token(s) invalid');
14✔
532
  }
533
}
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