I'm attempting to authenticate an Angular application that is already using the Angular MSAL library to Azure DevOps's API.
However, I am getting a HTTP 203
response when attempting to call the API:
this.http.get<any>(`https://dev.azure.com/${org}/${proj}/_apis/wit/workitems/${id}?api-version=7.1`).subscribe({
next: (x) => (this.debug = x),
error: (e) => (this.debug = e),
});
Returns:
I have registered the necessary permissions for my application on Azure Portal:
I have followed the configurations setup:
protectedResourceMap = new Map([
['https://graph.microsoft.com/v1.0/me', ['user.read']], // MS Graph
['499b84ac-1321-427f-aa17-267ca6975798', ['vso.work_full']], // DevOps
[`${config.auth}/authenticate/register.json`, [`api://${CLIENT_ID}/access_as_user`]], // LAPI
]);
and
authRequest = {
scopes: [
`api://${CLIENT_ID}/access_as_user`, // MS Graph
'https://app.vssps.visualstudio.com/user_impersonation', // DevOps
'499b84ac-1321-427f-aa17-267ca6975798/user_impersonation', // DevOps
'api://499b84ac-1321-427f-aa17-267ca6975798/.default', // DevOps
],
};
I've tried following this documentation:
I feel like I am probably missing something obvious, but can't get past the HTTP 203
error
The error occurred as you are using multiple values in "scopes" parameter that is creating confusion to pick the right scope. The correct scope to authenticate Azure DevOps API is 499b84ac-1321-427f-aa17-267ca6975798/.default
I registered one application and granted below Azure DevOps API permissions with consent:
In Authentication tab, I added http://localhost:4200/ as redirect URI in "Single-page application" platform and enabled public client flow option:
You can find below working code files of my Angular project to authenticate Azure DevOps API:
app.module.ts:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { MsalModule, MsalInterceptor } from '@azure/msal-angular';
import { PublicClientApplication, InteractionType } from '@azure/msal-browser';
import { AppComponent } from './app.component';
const msalConfig = {
auth: {
clientId: 'appId',
authority: 'https://login.microsoftonline.com/tenantId',
redirectUri: 'http://localhost:4200/',
},
cache: {
cacheLocation: 'localStorage',
storeAuthStateInCookie: false,
}
};
const protectedResourceMap = new Map([
['https://graph.microsoft.com/v1.0/me', ['user.read']],
['https://dev.azure.com/', ['499b84ac-1321-427f-aa17-267ca6975798/.default']], // Azure DevOps scope
]);
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
HttpClientModule,
MsalModule.forRoot(
new PublicClientApplication(msalConfig),
{
interactionType: InteractionType.Redirect,
authRequest: {
scopes: ['499b84ac-1321-427f-aa17-267ca6975798/.default'], // Default DevOps scope
},
},
{
interactionType: InteractionType.Redirect,
protectedResourceMap,
}
)
],
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: MsalInterceptor,
multi: true,
}
],
bootstrap: [AppComponent],
})
export class AppModule { }
app.component.ts:
import { Component, OnInit } from '@angular/core';
import { MsalService } from '@azure/msal-angular';
import { HttpClient } from '@angular/common/http';
import { AuthenticationResult } from '@azure/msal-browser';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent implements OnInit {
title = 'Angular-MSAL-DevOps-project';
debug: any;
loggedInUser: string | null = null;
constructor(private msalService: MsalService, private http: HttpClient) {}
async ngOnInit() {
console.log('AppComponent initialized');
try {
if (this.msalService.instance) {
await this.msalService.instance.initialize();
const response = await this.msalService.instance.handleRedirectPromise();
console.log('MSAL Response:', response);
if (response && response.account) {
this.msalService.instance.setActiveAccount(response.account);
this.loggedInUser = response.account.username;
console.log(`Login successful for user: ${this.loggedInUser}`);
}
} else {
console.error('MSAL Instance not initialized');
}
} catch (error) {
console.error('MSAL Initialization Error:', error);
}
}
login() {
console.log('Login button clicked');
this.msalService.loginRedirect({
scopes: ['499b84ac-1321-427f-aa17-267ca6975798/.default'], // Azure DevOps resource
});
}
getWorkItem() {
console.log('Get Work Item button clicked');
const org = 'devopsorgname';
const proj = 'projname';
const id = 'workitemId';
this.msalService.acquireTokenSilent({
scopes: ['499b84ac-1321-427f-aa17-267ca6975798/.default'], // Azure DevOps resource scope
}).subscribe({
next: (result: AuthenticationResult) => {
console.log('Token acquired:', result.accessToken);
const headers = { Authorization: `Bearer ${result.accessToken}` };
this.http
.get<any>(
`https://dev.azure.com/${org}/${proj}/_apis/wit/workitems/${id}?api-version=7.1`,
{ headers }
)
.subscribe({
next: (data) => {
console.log('Work Item Data:', data);
this.debug = data;
},
error: (err) => {
console.error('Work Item Fetch Error:', err);
this.debug = err;
},
});
},
error: (error: any) => {
console.error('Token acquisition failed:', error);
}
});
}
logout() {
console.log('Logout button clicked');
this.msalService.logoutRedirect();
}
}
app.component.html:
<div class="center-text">
<h1>Welcome to {{ title }}!</h1>
<div *ngIf="loggedInUser; else loginBlock">
<h3>Logged in as: {{ loggedInUser }}</h3>
<button (click)="logout()">Logout</button>
</div>
<ng-template #loginBlock>
<button (click)="login()">Login</button>
</ng-template>
<button (click)="getWorkItem()">Get Work Item</button>
<pre>{{ debug | json }}</pre>
</div>
app.component.css:
.center-text {
text-align: center;
margin-top: 50px;
}
h1 {
color: #2c3e50;
font-size: 36px;
}
button {
background-color: #3498db;
border: none;
color: white;
padding: 10px 20px;
text-align: center;
font-size: 16px;
margin: 10px;
cursor: pointer;
border-radius: 4px;
}
button:hover {
background-color: #2980b9;
}
pre {
background-color: #ecf0f1;
padding: 15px;
border-radius: 5px;
text-align: left;
font-size: 14px;
white-space: pre-wrap;
}
After running the project with ng serve
, visit http://localhost:4200/ URL in browser and get work item details after successful authentication: