Skip to content

Commit d25c799

Browse files
authored
Merge pull request #674 from openziti/670-jwt-signer-test-update
Added additional error handling cases for External JWT Signer test authentication
2 parents c40fce6 + 4b94d6d commit d25c799

File tree

15 files changed

+249
-43
lines changed

15 files changed

+249
-43
lines changed

projects/app-ziti-console/src/app/app-routing.module.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,12 @@ import {
5656
SessionsPageComponent,
5757
APISessionsPageComponent,
5858
SessionFormComponent,
59-
APISessionFormComponent
59+
APISessionFormComponent,
60+
CallbackComponent
6061
} from "ziti-console-lib";
6162
import {environment} from "./environments/environment";
6263
import {URLS} from "./app-urls.constants";
6364
import {AuthenticationGuard} from "./guards/authentication.guard";
64-
import {CallbackComponent} from "./login/callback.component";
6565

6666
const routes: Routes = [
6767
{

projects/app-ziti-console/src/app/app.module.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ import {
5252
IDENTITY_EXTENSION_SERVICE,
5353
SERVICE_POLICY_EXTENSION_SERVICE,
5454
EDGE_ROUTER_POLICY_EXTENSION_SERVICE,
55-
SERVICE_EDGE_ROUTER_POLICY_EXTENSION_SERVICE,
55+
SERVICE_EDGE_ROUTER_POLICY_EXTENSION_SERVICE
5656
} from "ziti-console-lib";
5757

5858
import {AppRoutingModule} from "./app-routing.module";
@@ -70,7 +70,6 @@ import {NodeLoginService} from "./login/node-login.service";
7070
import {NodeSettingsService} from "./services/node-settings.service";
7171
import {NoopHttpInterceptor} from "./interceptors/noop-http.interceptor";
7272
import {NodeApiInterceptor} from "./interceptors/node-api.interceptor";
73-
import {CallbackComponent} from "./login/callback.component";
7473

7574
let loginService, zitiDataService, settingsService, wrapperService, apiInterceptor;
7675
if (environment.nodeIntegration) {
@@ -91,8 +90,7 @@ if (environment.nodeIntegration) {
9190
declarations: [
9291
AppComponent,
9392
PageNotFoundComponent,
94-
LoginComponent,
95-
CallbackComponent
93+
LoginComponent
9694
],
9795
imports: [
9896
BrowserModule,

projects/app-ziti-console/src/app/login/login.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ export class LoginComponent implements OnInit, OnDestroy {
106106
handleOAuthLogin(extJwtSigner: any) {
107107
this.oauthLoading = extJwtSigner.name;
108108
this.authService.configureOAuth(extJwtSigner).then((result) => {
109-
if (result) {
109+
if (result?.success) {
110110
delay(() => {
111111
this.oauthLoading = '';
112112
}, 4000);
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
<div id="jsoneditor" [ngClass]="{'read-only': readOnly}" #editor (keyup.enter)="enterKeyPressed($event)" (keyup)="jsonChanged($event)"></div>
2-
<div *ngIf="showCopy" class="icon-copy copy" (click)="copyToClipboad()"></div>
1+
<div id="jsoneditor" [ngClass]="{'read-only': readOnly, 'hide-line-numbers': !showLineNumbers}" #editor (keyup.enter)="enterKeyPressed($event)" (keyup)="jsonChanged($event)"></div>
2+
<div *ngIf="showCopy" class="icon-copy copy" (click)="copyToClipboad()"></div>

projects/ziti-console-lib/src/lib/features/json-view/json-view.component.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,11 @@ export class JsonViewComponent implements AfterViewInit, OnChanges {
4343
@Input() readOnly: boolean = false;
4444
@Input() jsonInvalid: boolean = false;
4545
@Input() showCopy: boolean = false;
46+
@Input() showLineNumbers = true;
4647
@Output() dataChange: EventEmitter<any> = new EventEmitter();
4748
@Output() jsonChange: EventEmitter<any> = new EventEmitter();
4849
@Output() jsonInvalidChange: EventEmitter<any> = new EventEmitter();
50+
4951
onChangeDebounced;
5052
schemaRefs: any;
5153
content: any;

projects/ziti-console-lib/src/lib/features/projectable-forms/jwt-signer/jwt-signer-form.component.html

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@
155155
<lib-form-field-container
156156
[title]="'Verify OIDC Authentication'"
157157
class="form-field-advanced"
158-
[ngClass]="{'formFieldError': !oidcVerified && oidcErrorMessage, 'formFieldSuccess': oidcVerified}"
158+
[ngClass]="{'formFieldError': !oidcVerified && oidcErrorMessageDetail, 'formFieldSuccess': oidcVerified}"
159159
>
160160
<div class="form-field-row" #oidcVerification>
161161
<div class="form-field-label-container">
@@ -181,7 +181,15 @@
181181
<input type="checkbox" [disabled]="true" [checked]="true" class="success-check" *ngIf="oidcVerified">
182182
</div>
183183
</div>
184-
<span class="oidc-error-message" *ngIf="oidcErrorMessage">{{oidcErrorMessage}}</span>
184+
<div class="oidc-error-message-container" *ngIf="oidcErrorMessageDetail">
185+
<code class="oidc-error-message-source">{{oidcErrorMessageSource}}: </code>
186+
<code class="oidc-error-message-detail">{{oidcErrorMessageDetail}}</code>
187+
<code class="oidc-error-message-detail2" *ngIf="oidcErrorMessageDetail2">{{oidcErrorMessageDetail2}}</code>
188+
</div>
189+
<div class="oidc-token-claims-container" *ngIf="oidcAuthTokenClaims">
190+
<code class="oidc-token-claims-title">OAuth Token Claims: </code>
191+
<lib-json-view [showLineNumbers]="false" [(data)]="oidcAuthTokenClaims"></lib-json-view>
192+
</div>
185193
</lib-form-field-container>
186194
<lib-form-field-container
187195
[title]="'Custom Tags'"
@@ -218,7 +226,7 @@
218226
<span class="form-field-title">JWKS Endpoint</span>
219227
</div>
220228
</div>
221-
<input class="form-field-input read-only" placeholder="Enter endpoint URL" [(ngModel)]="formData.jwksEndpoint" [ngClass]="{error: errors['certPem']}"/>
229+
<input class="form-field-input read-only" placeholder="Enter endpoint URL" [(ngModel)]="formData.jwksEndpoint" [ngClass]="{error: errors['jwksEndpoint']}"/>
222230
</div>
223231
</div>
224232
<div *ngIf="signatureMethod === 'CERT_PEM'" class="form-field-input-group">
@@ -274,7 +282,7 @@
274282
<input class="form-field-input" [value]="apiCallURL"/>
275283
<div class="icon-copy copy" (click)="copyToClipboard(apiCallURL)"></div>
276284
</div>
277-
<lib-json-view *ngIf="formData" [(data)]="apiData" [readOnly]="true" [showCopy]="true"></lib-json-view>
285+
<lib-json-view *ngIf="formData" [showLineNumbers]="false" [(data)]="apiData" [readOnly]="true" [showCopy]="true"></lib-json-view>
278286
</lib-form-field-container>
279287
</div>
280288
</div>

projects/ziti-console-lib/src/lib/features/projectable-forms/jwt-signer/jwt-signer-form.component.scss

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -234,16 +234,68 @@
234234
}
235235
}
236236

237-
.oidc-error-message {
237+
.oidc-token-claims-container {
238+
background-color: var(--backgroundMuted);
239+
height: fit-content;
240+
display: flex;
241+
flex-direction: column;
242+
align-items: flex-start;
243+
justify-content: flex-start;
244+
padding: var(--paddingMedium);
245+
border-radius: var(--inputBorderRadius);
246+
color: var(--tableText);
247+
font-weight: 600;
248+
overflow: auto;
249+
250+
.oidc-token-claims-title {
251+
font-weight: 600;
252+
font-size: 1rem;
253+
white-space: nowrap;
254+
margin-bottom: var(--marginMedium);
255+
}
256+
}
257+
258+
.oidc-error-message-container {
238259
background-color: var(--lightRed);
239-
height: var(--defaultInputHeight);
260+
height: fit-content;
240261
display: flex;
241-
flex-direction: row;
242-
align-items: center;
262+
flex-direction: column;
263+
align-items: flex-start;
264+
justify-content: flex-start;
243265
padding: var(--paddingMedium);
244266
border-radius: var(--inputBorderRadius);
245267
color: var(--red);
246268
font-weight: 600;
269+
overflow: auto;
270+
271+
.oidc-error-message-source {
272+
font-weight: 600;
273+
font-size: 1rem;
274+
white-space: nowrap;
275+
}
276+
277+
.oidc-error-message-detail2,
278+
.oidc-error-message-detail {
279+
margin-top: var(--marginSmall);
280+
font-weight: 400;
281+
white-space: nowrap;
282+
}
283+
284+
&::-webkit-scrollbar {
285+
height: 6px;
286+
width: 6px;
287+
}
288+
289+
&::-webkit-scrollbar-thumb {
290+
background-color: var(--menu);
291+
border-radius: 10px;
292+
}
293+
294+
&:hover {
295+
&::-webkit-scrollbar-thumb {
296+
background-clip: unset !important;
297+
}
298+
}
247299
}
248300

249301
.read-only-2 {

projects/ziti-console-lib/src/lib/features/projectable-forms/jwt-signer/jwt-signer-form.component.ts

Lines changed: 110 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,18 @@ import {GrowlerService} from "../../messaging/growler.service";
2929
import {ExtensionService, SHAREDZ_EXTENSION} from "../../extendable/extensions-noop.service";
3030

3131
import semver from 'semver';
32-
import {cloneDeep, defer, delay, forOwn, keys, invert, isEmpty, isNil, unset, trim} from 'lodash';
32+
import {cloneDeep, defer, delay, forOwn, get, keys, invert, invoke, isEmpty, isNil, unset, trim} from 'lodash';
3333
import {GrowlerModel} from "../../messaging/growler.model";
3434
import {SETTINGS_SERVICE, SettingsService} from "../../../services/settings.service";
3535
import {ZITI_DATA_SERVICE, ZitiDataService} from "../../../services/ziti-data.service";
3636
import {ActivatedRoute, Router} from "@angular/router";
3737
import {JwtSigner} from "../../../models/jwt-signer";
3838
import {Location} from "@angular/common";
3939
import {ValidationService} from "../../../services/validation.service";
40-
import {OAuthService} from "angular-oauth2-oidc";
40+
import {OAuthErrorEvent, OAuthService} from "angular-oauth2-oidc";
4141
import {LoginServiceClass, ZAC_LOGIN_SERVICE} from "../../../services/login-service.class";
4242
import {AuthService} from "../../../services/auth.service";
43+
import {HttpClient} from "@angular/common/http";
4344

4445
@Component({
4546
selector: 'lib-configuration',
@@ -65,8 +66,14 @@ export class JwtSignerFormComponent extends ProjectableForm implements OnInit, O
6566
configKey = 'oauth_test_callback_config';
6667
tokenTypeKey = 'oauth_test_callback_token_type';
6768
oidcVerified = false;
68-
oidcErrorMessage;
69+
oidcErrorMessageSource;
70+
oidcErrorMessageDetail;
71+
oidcErrorMessageDetail2;
72+
jwksValidationError;
73+
oidcAuthTokenClaims;
74+
oidcClaims;
6975
overrideFormData;
76+
override usePreviousLocation = false
7077
override entityType = 'external-jwt-signers';
7178
override entityClass = JwtSigner;
7279

@@ -86,6 +93,7 @@ export class JwtSignerFormComponent extends ProjectableForm implements OnInit, O
8693
private validationService: ValidationService,
8794
private authService: AuthService,
8895
private oauthService: OAuthService,
96+
private http: HttpClient,
8997
@Inject(ZAC_LOGIN_SERVICE) private loginService: LoginServiceClass,
9098

9199
) {
@@ -456,7 +464,7 @@ export class JwtSignerFormComponent extends ProjectableForm implements OnInit, O
456464
}
457465

458466
get callbackURL() {
459-
return window.location.origin + this.baseHref + `callback`
467+
return window.location.origin + (this.baseHref + this.loginService.callbackRoute).replace('//', '/');
460468
}
461469

462470
copyCallbackURL() {
@@ -466,35 +474,125 @@ export class JwtSignerFormComponent extends ProjectableForm implements OnInit, O
466474
);
467475
}
468476

469-
testOIDCAuthentication() {
477+
async testOIDCAuthentication() {
470478
this.oauthLoading = true;
471-
const callbackParams = `redirectRoute=jwt-signers/${this.formData.id}/test-auth`;
479+
this.oidcAuthTokenClaims = undefined;
480+
this.oidcErrorMessageSource = undefined;
481+
this.oidcErrorMessageDetail = undefined;
482+
this.oidcErrorMessageDetail2 = undefined;
483+
this.jwksValidationError = undefined;
484+
const jwksValid = await this.testJWKS();
485+
if (!jwksValid) {
486+
this.oidcVerified = false;
487+
this.oauthLoading = false
488+
return;
489+
}
490+
const callbackParams = `${this.loginService.callbackRoute}?redirectRoute=jwt-signers/${this.formData.id}/test-auth`;
472491
localStorage.setItem('oidc_callback_test_config', JSON.stringify(this.formData));
473492
this.authService.configureOAuth(this.formData, callbackParams).then((result) => {
474-
if (result) {
493+
if (result.success) {
475494
delay(() => {
476495
this.oauthLoading = false;
477496
}, 4000);
478497
} else {
479498
delay(() => {
499+
this.oidcErrorMessageDetail = result.message;
500+
this.oidcErrorMessageSource = 'OAuth Configuration Error';
501+
if (this.jwksValidationError) {
502+
this.oidcErrorMessageDetail2 = this.jwksValidationError;
503+
}
480504
this.oauthLoading = false;
505+
this.oidcVerified = false;
481506
}, 700);
507+
482508
}
483509
});
484510
}
485511

486512
handleOAuthCallback() {
487513
this.showMore = true;
488-
const errorMessage = this.route.snapshot.queryParamMap.get('oidcAuthErrorMessage');
514+
const errorMessageDetail = this.route.snapshot.queryParamMap.get('oidcAuthErrorMessageDetail');
515+
const errorMessageSource = this.route.snapshot.queryParamMap.get('oidcAuthErrorMessageSource');
516+
const oidcAuthTokenClaims = this.route.snapshot.queryParamMap.get('oidcAuthTokenClaims');
489517
const result = this.route.snapshot.queryParamMap.get('oidcAuthResult');
518+
const formData = localStorage.getItem('oidc_callback_test_config');
519+
if (!isEmpty(formData)) {
520+
this.overrideFormData = JSON.parse(formData);
521+
}
490522
if (result === 'success') {
491523
this.oidcVerified = true;
524+
if (oidcAuthTokenClaims) {
525+
this.oidcAuthTokenClaims = JSON.parse(oidcAuthTokenClaims);
526+
}
492527
} else {
493-
this.oidcErrorMessage = errorMessage;
528+
this.oidcErrorMessageDetail = errorMessageDetail;
529+
this.oidcErrorMessageSource = errorMessageSource;
530+
if (oidcAuthTokenClaims) {
531+
this.oidcAuthTokenClaims = JSON.parse(oidcAuthTokenClaims);
532+
this.validateTokenClaims(this.oidcAuthTokenClaims, this.overrideFormData);
533+
}
494534
}
495-
const formData = localStorage.getItem('oidc_callback_test_config');
496-
if (!isEmpty(formData)) {
497-
this.overrideFormData = JSON.parse(formData);
535+
}
536+
537+
validateTokenClaims(token, formData) {
538+
if (!token.aud) {
539+
this.errors.audience = true;
540+
this.oidcErrorMessageDetail2 = 'Audience is missing from token claims'
541+
} else {
542+
let audError = true;
543+
token.aud.forEach((audItem) => {
544+
if (audItem === formData.audience) {
545+
audError = false;
546+
}
547+
});
548+
if (audError) {
549+
this.errors.audience = true;
550+
this.oidcErrorMessageDetail2 = 'Audience in token does not match External JWT Signer configuration.'
551+
}
552+
}
553+
}
554+
555+
async testJWKS() {
556+
this.errors.jwksEndpoint = false;
557+
try {
558+
const response: any = await this.http.get(this.formData.jwksEndpoint).toPromise().catch((error) => {
559+
this.oidcErrorMessageSource = 'JWKS Endpoint Error';
560+
this.oidcErrorMessageDetail = `HTTP error returned. Status: ${error.status}. Message: ${error.message}`;
561+
this.errors.jwksEndpoint = true;
562+
return false;
563+
});
564+
if (response) {
565+
// Basic structural validation
566+
if (!response?.keys || !Array.isArray(response?.keys)) {
567+
this.oidcErrorMessageSource = 'JWKS Endpoint Error';
568+
this.oidcErrorMessageDetail = `Invalid JWKS: "keys" property missing or not an array`;
569+
this.errors.jwksEndpoint = true;
570+
} else {
571+
const logError: any = get(this.oauthService, 'logger.error');
572+
this.oauthService['logger'].error = (...args) => {
573+
this.jwksValidationError = '';
574+
args.forEach((arg) => {
575+
this.jwksValidationError += arg + ' ';
576+
});
577+
logError.apply(this, args);
578+
}
579+
const result = invoke(this.oauthService, 'validateDiscoveryDocument', response);
580+
}
581+
}
582+
} catch (error: any) {
583+
this.oidcErrorMessageSource = 'JWKS Endpoint Error';
584+
this.oidcErrorMessageDetail = `${error.message}`;
585+
this.errors.jwksEndpoint = true;
586+
}
587+
const growlerData = new GrowlerModel(
588+
'error',
589+
'Invalid',
590+
this.oidcErrorMessageSource,
591+
this.oidcErrorMessageDetail,
592+
);
593+
if (this.errors.jwksEndpoint) {
594+
this.growlerService.show(growlerData);
498595
}
596+
return !this.errors.jwksEndpoint;
499597
}
500598
}

projects/ziti-console-lib/src/lib/features/projectable-forms/projectable-form.class.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ export abstract class ProjectableForm extends ExtendableComponent implements DoC
7373
protected entityType = 'identities';
7474
protected entityClass: any = Entity;
7575
protected isLoading = false;
76+
protected usePreviousLocation = true;
7677
moreActions: any[] = [];
7778
tagElements: any = [];
7879
tagData: any = [];
@@ -282,7 +283,7 @@ export abstract class ProjectableForm extends ExtendableComponent implements DoC
282283
}
283284

284285
returnToListPage() {
285-
if (this.location && this.previousRoute) {
286+
if (this.location && this.previousRoute && this.usePreviousLocation) {
286287
this.location.back();
287288
} else {
288289
this.router?.navigateByUrl(`${this.basePath}`);

0 commit comments

Comments
 (0)