angularazureazure-devopsazure-ad-msalmsal-angular

Unable to Authenticate to DevOps API with Angual MSAL


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:

enter image description here

I have registered the necessary permissions for my application on Azure Portal:

enter image description here

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


Solution

  • 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:

    enter image description here

    In Authentication tab, I added http://localhost:4200/ as redirect URI in "Single-page application" platform and enabled public client flow option:

    enter image description here

    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:

    enter image description here