1
votes

I'm trying to build an ASP.NET Core webapi + Angular website where users can login using Microsoft personal or work or school email. I followed the instructions described here: https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-angular/README.md

But encouter this issue,

  1. The angular website load, and the protected component on the home page trigger the login procedure

  2. The microsoft web site appear

  3. I login

  4. The browser load the following url: https://localhost:44321/#id_token=...&state=...

  5. The website reload again (for a second time after the signin)

  6. And I see the following error

    Access to XMLHttpRequest at 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=...' (redirected from 'https://localhost:44321/Environment/GetUserInfo') from origin 'https://localhost:44321' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.

when debugging locally with Visual Studio 2019 16.4.2 and chrome 79.0.3945.88

Any idea?
Thanks

I've created my project using

dotnet new angular -o myapp

And created an app registration in Azure.Portal
Authentication with following redirect URI

  • https://localhost:44321/signin-microsoft
  • https://login.microsoftonline.com/
  • http://localhost:30662/
  • https://localhost:44321/signin-oidc
  • https://localhost:44321/

Checked all the "Suggested Redirect URIs for public clients"
Logout URL: https://localhost:44321/signout-callback-oidc
Implicit grant: Access tokens and ID tokens
Live SDK support: Yes
Default client type: No

Certificate & Secret
I've created a client secret, becaused I tried to use Microsoft provider (see commented code below) then tried with AzureAd

API Permission
Microsoft.Graph User.Read

Expose an API
Scope = [Application ID URI]/access_as_user, Admins Only
Client application = [CLIENT_ID_FROM_AZURE_PORTAL], scope above

Server side
appsettings.json

 "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "Domain": "[Application ID URI]",
    "ClientId": "[CLIENT_ID_FROM_AZURE_PORTAL]",
    "TenantId": "common",
    "CallbackPath": "/signin-oidc"
  },

Startup.cs

public void ConfigureServices(IServiceCollection services)
{
  services.AddCors(options =>
  {
    options.AddPolicy("AllowAllOrigins",
        builder =>
        {
          builder
            .AllowAnyMethod()
            .AllowAnyHeader()
            .AllowAnyOrigin();
        });
  });

  services.Configure<CookiePolicyOptions>(options =>
  {
    // This lambda determines whether user consent for non-essential cookies is needed for a given request.
    options.CheckConsentNeeded = context => true;
    options.MinimumSameSitePolicy = SameSiteMode.None;
  });

  // In production, the Angular files will be served from this directory
  services.AddSpaStaticFiles(configuration =>
  {
    configuration.RootPath = "ClientApp/dist";
  });

  //services
  //  .AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
  //  .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
  //  .AddMicrosoftAccount(microsoftOptions =>
  //{
  //  microsoftOptions.ClientId = "[CLIENT_ID_FROM_AZURE_PORTAL]";
  //  microsoftOptions.ClientSecret = "[CLIENT_SECRET_FROM_AZURE_PORTAL]";
  //});

  services.AddAuthentication(AzureADDefaults.AuthenticationScheme)
    .AddAzureAD(options => Configuration.Bind("AzureAd", options));

  services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options =>
  {
    options.Authority = options.Authority + "/v2.0/";
    options.TokenValidationParameters.ValidateIssuer = false;
  });

  services.AddControllers(options =>
  {
    var policy = new AuthorizationPolicyBuilder()
                    .RequireAuthenticatedUser()
                    .Build();
    options.Filters.Add(new AuthorizeFilter(policy));
  })
  .SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
  app.UseCors("AllowAllOrigins");

  if (env.IsDevelopment())
  {
    app.UseDeveloperExceptionPage();
  }
  else
  {
    app.UseExceptionHandler("/Error");
    app.UseHsts();
  }

  app.UseHttpsRedirection();
  app.UseCookiePolicy();
  app.UseStaticFiles();
  if (!env.IsDevelopment())
  {
    app.UseSpaStaticFiles();
  }

  app.UseRouting();

  app.UseAuthentication();
  app.UseAuthorization();

  app.UseEndpoints(endpoints =>
  {
    endpoints.MapControllers();
  });

  app.UseSpa(spa =>
  {
    spa.Options.SourcePath = "ClientApp";

    if (env.IsDevelopment())
    {
      spa.UseAngularCliServer(npmScript: "start");
    }
  });
}

Client side
app.module.ts

...
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { MsalModule, MsalGuard, MsalInterceptor } from '@azure/msal-angular';
...
export const protectedResourceMap: [string, string[]][] = [
  ['https://localhost:44321/Environment/GetUserInfo', ['[Application ID URI]/access_as_user']],
  ['https://localhost:44321/api/Environment/GetUserInfo', ['[Application ID URI]/access_as_user']],
  ['https://graph.microsoft.com/v1.0/me', ['user.read']],
  ['https://login.microsoftonline.com/common', ['user.read']]
];

@NgModule({
  declarations: [
    AppComponent,
    NavMenuComponent,
    HomeComponent,
    CounterComponent,
    EntitySignoffComponent
  ],
  imports: [
    BrowserModule.withServerTransition({ appId: 'ng-cli-universal' }),
    HttpClientModule,
    FormsModule,
    AgGridModule.withComponents([]),
    MsalModule.forRoot({
      clientID: [CLIENT_ID_FROM_AZURE_PORTAL],
      authority: "https://login.microsoftonline.com/common",
      redirectUri: "https://localhost:44321/",
      validateAuthority: true,
      cacheLocation: "localStorage",
      storeAuthStateInCookie: false, // dynamically set to true when IE11
      postLogoutRedirectUri: "https://localhost:44321/",
      navigateToLoginRequestUrl: true,
      popUp: false,
      unprotectedResources: [ "https://login.microsoftonline.com/common" ],
      protectedResourceMap: protectedResourceMap
    }
    ),
    RouterModule.forRoot([
      { path: '', component: HomeComponent, pathMatch: 'full', canActivate: [MsalGuard] }
      { path: 'counter', component: CounterComponent, canActivate: [MsalGuard] },
    ])
  ],
  providers: [NavMenuComponent, { provide: HTTP_INTERCEPTORS, useClass: MsalInterceptor, multi: true }],

package.json

{
  "name": "myapp",
  "version": "0.0.0",
  "scripts": {
    "ng": "ng",
    "start": "ng serve",
    "build": "ng build",
    "build:ssr": "ng run myapp:server:dev",
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e"
  },
  "private": true,
  "dependencies": {
    "@angular/animations": "8.2.14",
    "@angular/common": "^8.2.14",
    "@angular/compiler": "8.2.14",
    "@angular/core": "^8.2.14",
    "@angular/forms": "8.2.14",
    "@angular/platform-browser": "8.2.14",
    "@angular/platform-browser-dynamic": "8.2.14",
    "@angular/platform-server": "8.2.14",
    "@angular/router": "8.2.14",
    "@azure/msal-angular": "^0.1.4",
    "@nguniversal/module-map-ngfactory-loader": "8.2.6",
    "aspnet-prerendering": "^3.0.1",
    "bootstrap": "^4.4.1",
    "core-js": "^3.6.1",
    "jquery": "3.4.1",
    "oidc-client": "^1.10.1",
    "popper.js": "^1.16.0",
    "rxjs": "^6.5.4",
    "rxjs-compat": "^6.5.4",
    "zone.js": "0.10.2"
  },
  "devDependencies": {
    "@angular-devkit/build-angular": "^0.803.21",
    "@angular/cli": "8.3.21",
    "@angular/compiler-cli": "8.2.14",
    "@angular/language-service": "8.2.14",
    "@types/jasmine": "^3.5.0",
    "@types/jasminewd2": "~2.0.8",
    "@types/node": "~13.1.2",
    "codelyzer": "^5.2.1",
    "jasmine-core": "~3.5.0",
    "jasmine-spec-reporter": "~4.2.1",
    "karma": "^4.4.1",
    "karma-chrome-launcher": "~3.1.0",
    "karma-coverage-istanbul-reporter": "^2.1.1",
    "karma-jasmine": "~2.0.1",
    "karma-jasmine-html-reporter": "^1.5.1",
    "typescript": "3.5.3"
  },
  "optionalDependencies": {
    "node-sass": "^4.13.0",
    "protractor": "~5.4.2",
    "ts-node": "~8.5.4",
    "tslint": "~5.20.1"
  }
}

Error in Chrome after login

1
Please check if the issue is caused by same one discussed in this github issue.Fei Han
I've updated to those line services.AddAuthentication(AzureADDefaults.BearerAuthenticationScheme) .AddAzureADBearer(options => Configuration.Bind("AzureAd", options)); But now I get a 401, and it still load the webpage twice after login HttpErrorResponse {headers: HttpHeaders, status: 401, statusText: "OK", url: "localhost:44321/Environment/GetUserInfo", ok: false, …}D2O
Hi, has your issue been solved now ?Stanley Gong
Hi Stanley, no I have the same issue, check the image above "Error in Chrome after login"D2O

1 Answers

3
votes

Per my understanding, these two official samples will meet your requirement perfectly.

I have modified the official demo and integrated it for you. Pls follow the steps below to make them work :

  1. For API side .You can download my code here.

1)Got to Azure portal and register an Azure AD app for it : enter image description here

2)Note its application id and created an client secret for it. Type them into appsettings.json : enter image description here

3) Back to Azure portal, expose an API so that client-side could request permission of it : enter image description here

4) API side's work is done , you can run this project directly.

  1. For angular side, you can get my code here .

1) Register an Azure AD app for this client-side : enter image description here

2) Add a permission to this app so that it could access your backend : enter image description here enter image description here Click grant permission button to finish the process : enter image description here These two demo all are based on Azure AD V2.0 endpoint which allows personal accounts and school work accounts to log in and do some actions.

3) Do some config on Auth blade so that it could get tokens needed as a public client : enter image description here

4) All steps are done for angular side , npm start to run it , access it on : http://localhost:44302/ I have tested on my local and it works perfectly for me: enter image description here

If you dont know how to combine these two app, pls let me know .