Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion 1-Authentication/2-sign-in-angular/SPA/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"start": "ng serve --port 4002",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test --watch=false --no-progress --browsers=ChromeHeadlessCI",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const routes: Routes = [
path: 'guarded',
component: GuardedComponent,
canActivate: [
//here Msal Guard
MsalGuard
]
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,7 @@ footer {
text-align: center;
flex: 1 1 auto;
}

.container {
padding-bottom: 72px; /* prevent fixed footer from overlapping page content */
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export class AppComponent implements OnInit, OnDestroy {
private readonly _destroying$ = new Subject<void>();

constructor(
//here
@Inject(MSAL_GUARD_CONFIG) private msalGuardConfig: MsalGuardConfiguration,
private authService: MsalService,
private msalBroadcastService: MsalBroadcastService,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export function MsalGuardConfigurationFactory(): MsalGuardConfiguration {
MsalModule,
],
providers: [
// MSAL Interceptor to add scopes to outgoing requests
{
provide: HTTP_INTERCEPTORS,
useClass: MsalInterceptor,
Expand Down
13 changes: 10 additions & 3 deletions 1-Authentication/2-sign-in-angular/SPA/src/app/auth-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ import {
*/
export const msalConfig: Configuration = {
auth: {
clientId: 'Enter_the_Application_Id_Here', // This is the ONLY mandatory field that you need to supply.
authority: 'https://Enter_the_Tenant_Subdomain_Here.ciamlogin.com/', // Replace the placeholder with your tenant subdomain
clientId: '675bdfbe-4b0b-480d-802d-d3d6405dda47', // This is the ONLY mandatory field that you need to supply.
authority: 'https://login.microsoftonline.com/b6078821-12c7-4949-9827-52da66c836c7', // Replace the placeholder with your tenant subdomain
redirectUri: '/', // Points to window.location.origin by default. You must register this URI on Microsoft Entra admin center/App Registration.
postLogoutRedirectUri: '/', // Points to window.location.origin by default.
},
Expand All @@ -44,5 +44,12 @@ export const msalConfig: Configuration = {
* https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent#openid-connect-scopes
*/
export const loginRequest = {
scopes: [],
scopes: [
// 'openid',
// 'offline_access',
'api://675bdfbe-4b0b-480d-802d-d3d6405dda47/.default',
],
};

// Print configured scopes for debugging/verification
console.log('loginRequest scopes:', loginRequest.scopes);
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#table-container {
#table-container,
#access-table-container {
height: '100vh';
overflow: auto;
}
Expand All @@ -16,6 +17,29 @@ table {
padding: 8px 8px 8px 0;
}

.mat-column-value {
word-break: break-all;
overflow-wrap: break-word;
white-space: normal;
}

/* Sensitive claim redaction */
.redacted {
background-color: #111;
color: #111;
border-radius: 3px;
cursor: pointer;
user-select: none;
text-decoration: line-through;
transition: background-color 0.2s, color 0.2s;
}

.redacted:hover {
background-color: transparent;
color: inherit;
text-decoration: none;
}

p {
text-align: center;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,57 @@
<!-- Value Column -->
<ng-container matColumnDef="value">
<th mat-header-cell *matHeaderCellDef> Value </th>
<td mat-cell *matCellDef="let element"> {{element.value}} </td>
<td mat-cell *matCellDef="let element"
[class.redacted]="isSensitive(element.claim)"
[title]="isSensitive(element.claim) ? formatValue(element.value) : ''"> {{formatValue(element.value)}} </td>
</ng-container>
<!-- Description Column -->
<ng-container matColumnDef="description">
<th mat-header-cell *matHeaderCellDef> Description </th>
<td mat-cell *matCellDef="let element"> {{element.description}} </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns sticky: true"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</div>

<!-- Access token claims table -->
<br />
<p class="text-center" *ngIf="loginDisplay">
See below the claims in your <strong> Access token </strong> (if available).
</p>
<div id="access-table-container" *ngIf="loginDisplay">
<table mat-table [dataSource]="accessDataSource" class="mat-elevation-z8" *ngIf="accessDataSource && accessDataSource.length">
<!-- Claim Column -->
<ng-container matColumnDef="claim">
<th mat-header-cell *matHeaderCellDef> Claim </th>
<td mat-cell *matCellDef="let element"> {{element.claim}} </td>
</ng-container>

<!-- Value Column -->
<ng-container matColumnDef="value">
<th mat-header-cell *matHeaderCellDef> Value </th>
<td mat-cell *matCellDef="let element"
[class.redacted]="isSensitive(element.claim)"
[title]="isSensitive(element.claim) ? formatValue(element.value) : ''"> {{formatValue(element.value)}} </td>
</ng-container>

<!-- Description Column -->
<ng-container matColumnDef="description">
<th mat-header-cell *matHeaderCellDef> Description </th>
<td mat-cell *matCellDef="let element"> {{element.description}} </td>
</ng-container>

<tr mat-header-row *matHeaderRowDef="displayedColumns sticky: true"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>

<!-- If token exists but no parsed claims, show raw token -->
<mat-card *ngIf="accessToken && (!accessDataSource || accessDataSource.length === 0)">
<mat-card-title>Raw Access Token</mat-card-title>
<mat-card-content>
<pre style="word-break:break-all">{{ accessToken }}</pre>
</mat-card-content>
</mat-card>

</div>
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { MsalBroadcastService, MsalGuardConfiguration, MsalService, MSAL_GUARD_C
import { AuthenticationResult, InteractionStatus, InteractionType } from '@azure/msal-browser';

import { createClaimsTable } from '../claim-utils';
import { loginRequest } from '../auth-config';

@Component({
selector: 'app-home',
Expand All @@ -16,6 +17,30 @@ export class HomeComponent implements OnInit {
loginDisplay = false;
dataSource: any = [];
displayedColumns: string[] = ['claim', 'value', 'description'];
// Access token data
accessToken: string | null = null;
accessDataSource: any = [];

/** Claims whose values are considered sensitive (PII / timestamps) */
private readonly sensitiveClaims = new Set<string>([
'ipaddr',
'upn',
'sub',
'oid',
'sid',
'nonce'
]);

isSensitive(claim: string): boolean {
return this.sensitiveClaims.has(claim);
}

formatValue(value: any): string {
if (Array.isArray(value)) {
return '[ ' + value.join(', ') + ' ]';
}
return value;
}

private readonly _destroying$ = new Subject<void>();

Expand All @@ -37,6 +62,10 @@ export class HomeComponent implements OnInit {
this.getClaims(
this.authService.instance.getActiveAccount()?.idTokenClaims
);
// Try to acquire and display an access token (if available)
if (this.loginDisplay) {
this.getAccessToken();
}
});
}

Expand All @@ -51,18 +80,55 @@ export class HomeComponent implements OnInit {
}
}

/**
* Acquire an access token silently and populate access token claims table
*/
getAccessToken(): void {
const account = this.authService.instance.getActiveAccount();
if (!account) return;

this.authService.acquireTokenSilent({
scopes: loginRequest.scopes,
account,
}).subscribe((result: AuthenticationResult) => {
if (result && result.accessToken) {
this.accessToken = result.accessToken;
const claims = this.decodeJwt(result.accessToken);
if (claims) {
this.accessDataSource = [...createClaimsTable(claims)];
}
}
}, (error) => {
// silent acquire may fail; ignore here — token will be acquired on-demand by guarded flows
console.error('Failed to acquire access token silently', error);
});
}

private decodeJwt(token: string): any | null {
try {
const payload = token.split('.')[1];
const base64 = payload.replace(/-/g, '+').replace(/_/g, '/');
const json = decodeURIComponent(atob(base64).split('').map((c) => {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
return JSON.parse(json);
} catch (e) {
return null;
}
}

signUp() {
if (this.msalGuardConfig.interactionType === InteractionType.Popup) {
this.authService.loginPopup({
scopes: [],
scopes: loginRequest.scopes,
prompt: 'create',
})
.subscribe((response: AuthenticationResult) => {
this.authService.instance.setActiveAccount(response.account);
});
} else {
this.authService.loginRedirect({
scopes: [],
scopes: loginRequest.scopes,
prompt: 'create',
});
}
Expand Down