首页 > 解决方案 > JWT 身份验证令牌在 Angular 和 .Net Core 中具有无效的 Audience 和 Issuer 以及缺少范围

问题描述

我一直致力于将我的 Angular 和 .Net Core 应用程序发布到 OpenShift Container Platform。我已将 JWT 授权令牌配置为对用户进行身份验证并获取 User.Identity.Name 信息。

我使用 Angular 作为前端,使用 .Net Core 3.1 作为后端。在本地一切正常,但是当我尝试将我的应用程序发布到 OpenShift 时,它给了我错误的受众和发行人信息。它也没有给我令牌的任何范围。

我对其进行了故障排除,发现我从部署到 OpenShift 的应用程序中获得的令牌是 v2.0,而它在本地为我提供了 v1.0 令牌。

任何帮助将不胜感激。

这是我的 app.componenet.ts 文件。

import { Component, OnInit, OnDestroy } from '@angular/core';
import { BroadcastService, MsalService } from '@azure/msal-angular';
import { Subscription } from 'rxjs/Subscription';

@Component
({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit, OnDestroy
{
loggedIn: boolean;
private subscription: Subscription;
public isIframe: boolean;
token: string;
// --------------------------------------------------------------------

constructor(private broadcastService: BroadcastService, private authService: MsalService) { 

}

// --------------------------------------------------------------------

ngOnInit() 
{

    this.broadcastService.subscribe("msal:loginSuccess", (payload) => {      
        localStorage.setItem('token', JSON.stringify(payload));      
        this.token = JSON.parse(localStorage.getItem('token'))['_token'];
        this.loggedIn = true;
        this.checkoutAccount();
      }); 
}

// --------------------------------------------------------------------

checkoutAccount() {
    this.loggedIn = !!this.authService.getAccount();
    if (this.m_router.url == "/")
    {
        if (!this.m_dbService.isAuthorizedUser())
        {
            this.setUserAndRoles();
        }
    }
}

// --------------------------------------------------------------------

login() {
    const isIE = window.navigator.userAgent.indexOf('MSIE ') > -1 || window.navigator.userAgent.indexOf('Trident/') > -1;

    if (isIE) {
        this.authService.loginRedirect();
    } else {
        this.authService.loginPopup();
    }
}

// --------------------------------------------------------------------

logout() {
    this.authService.logout();
}

// --------------------------------------------------------------------

ngOnDestroy() {
    this.broadcastService.getMSALSubject().next(1);
    if (this.subscription) {
        this.subscription.unsubscribe();
    }
}

}

这是我的 app.module.ts 文件。

// Modules
import { NgModule, ErrorHandler, forwardRef } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; // new
import { HttpModule } from '@angular/http';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';

// Routing
import { HashLocationStrategy, LocationStrategy, Location } from '@angular/common';

// Components
import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';

// Authentication
import { MsalModule, MsalInterceptor, MsalAngularConfiguration, MSAL_CONFIG, MSAL_CONFIG_ANGULAR, MsalService } from '@azure/msal-angular';
import { Logger, Configuration } from 'msal';
import { environment } from '../environments/environment';

export function loggerCallback(logLevel, message, piiEnabled) {
  console.log('client logging' + message);
}

export const protectedResourceMap: Map<string, string[]> = new Map([
  [environment.apiUrl, ['api://azure-app-client-id/api_consume']]
]);

const isIE = window.navigator.userAgent.indexOf('MSIE ') > -1 || window.navigator.userAgent.indexOf('Trident/') > -1;

@NgModule
({
  imports: 
  [
     BrowserModule
    ,BrowserAnimationsModule
    ,ToastrModule.forRoot()
    ,AppRoutingModule
    ,SharedModule
    ,CoreModule.forRoot()
    ,FwModule
    ,HttpModule
    ,HttpClientModule
    ,MsalModule.forRoot({
      auth: {
        clientId: environment.clientId,
        authority: environment.authority,
        redirectUri: environment.redirectUrl
      },
      framework: {
        protectedResourceMap: protectedResourceMap
      }
    },
    {
      consentScopes: ['openid', 'profile', 'api://azure-app-client-id/api_consume'],
    }
    )
  ]
  ,declarations: 
  [
     AppComponent
  ]
  ,exports: 
  [
  ]
  ,providers: 
  [    
     Globals
    ,CookieService 
    ,AcErrorService
    ,{provide: ErrorHandler, useClass: GlobalErrorHandlerService}
    ,Location, {provide: LocationStrategy, useClass: HashLocationStrategy}, 
    {
      provide: HTTP_INTERCEPTORS,
      useClass: MsalInterceptor,
      multi: true
    } 
  ]
  ,bootstrap: [AppComponent]
})
export class AppModule { }

这是我的 environment.ts 文件。

export const environment = {
  production: false,
  clientId: 'my-app-registration-client-id',
  authority: 'https://login.microsoftonline.com/my-app-registration-tenant-id',
  redirectUrl: 'http://localhost:4200/',
  apiUrl: 'http://localhost:5000/'
};

这是我的 environment.prod.ts 文件。

export const environment = {
  production: true,
  clientId: 'my-app-registration-client-id',
  authority: 'https://login.microsoftonline.com/my-app-registration-tenant-id',
  redirectUrl: 'my-openshift-app-url',
  apiUrl: 'my-openshift-app-url'
};

这是我的 database.service.ts 文件。

export class DatabaseService
{

constructor
    (
        public m_httpClient: HttpClient
    )
    {}

public get(NetCoreApiPath): Observable<any>
    {
        var strUrl;

        try
        {
            strUrl = this.m_globals.getBaseUrl() + p_strApiPath;
            return this.m_httpClient.get<any>(strUrl, { withCredentials: true })
            .pipe(map(data => data), catchError(this.handleError));
        }
        catch(err)
        {
            console.error(err);
            this.sendErrorEmail(err.message, "1");
        }
    }

}

这是我的 .Net Core appsettings.json 文件。

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=my-db-server,1433;Initial Catalog=my-db;Integrated Security=true;"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",

  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "Domain": "https://MyOrg.onmicrosoft.com/",
    "TenantId": "my-app-registration-tenant-id",
    "Authority": "https://sts.windows.net/my-app-registration-tenant-id/",
    "Audience": "api://my-app-registration-client-id",
    "ClientId": "my-app-registration-client-id",

  }

}

这是我的 .Net Core startup.cs 文件。

using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.EntityFrameworkCore;
using API.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.Authorization;
using Newtonsoft.Json;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;

namespace API
{
    public class Startup
    {
        private readonly IWebHostEnvironment _environment;

        public Startup(IConfiguration configuration, IWebHostEnvironment env)
        {
            Configuration = configuration;
            _environment = env;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {

            services.AddControllers();

            services.AddAuthentication(sharedOptions =>
            {
                sharedOptions.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
            })
            .AddJwtBearer(options =>
            {
                Configuration.Bind("AzureAd", options);
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateIssuer = true,
                    ValidateAudience = true
                };
            });

            services.AddCors(options =>
            {
                options.AddPolicy("AllowCors",
                    builder => builder
                                .WithOrigins("http://localhost:4200", "my-openshift-app-url")
                                .AllowAnyHeader()
                                .AllowAnyMethod()
                                .AllowCredentials()
                    );
            });

            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_3_0);

            services
                .AddMvc(options =>
                {
                    var policy = new AuthorizationPolicyBuilder()
                                     .RequireAuthenticatedUser()
                                     .Build();
                    options.Filters.Add(new AuthorizeFilter(policy));
                    options.MaxIAsyncEnumerableBufferLimit = int.MaxValue;
                })
                .AddNewtonsoftJson(options =>
                    options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore
                );

            var connectionString = Configuration["ConnectionStrings:DefaultConnection"];

            if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Production")
                services.AddDbContext<AppDB>(options =>
                        options.UseSqlServer(connectionString));
            else
                services.AddDbContext<AppDB>(options =>
                        options.UseSqlServer(connectionString));

        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/error");
            }

            app.UseDefaultFiles();

            app.UseStaticFiles();       

            app.UseHttpsRedirection();

            app.UseRouting();

            app.UseCors("AllowCors");

            app.UseAuthentication();        

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllerRoute(
                name: "default",
                pattern: "{controller=Home}/{action=Index}/{id?}");
                endpoints.MapRazorPages();
            });

        }
    }

}

这是我的控制器类。

using System;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Web.Http;
using Microsoft.AspNetCore.Http.Extensions;
using System.Net;
using Microsoft.AspNetCore.Authorization;
using API.Helpers;

namespace API.Controllers
{

    [Route("api/[controller]")]
    [ApiController]
    public class EmployeeStatusController : ControllerBase
    {
        private string m_strUserName;
        private readonly AppDB db;

        public EmployeeStatusController(AppDB context)
        {
            db = context;
        }

        [Authorize]
        [HttpGet]
        public IQueryable<EmployeeStatus> GetEmployeeStatus()
        {
            try
            {
                this.m_strUserName = UserHelper.getUserNameOrAlias(User);

                IQueryable<EmployeeStatus> data =

                    from tbl in db.EmployeeStatus

                    select tbl;
                    // ----- /get -----

                    return data;

        }
            catch (Exception ex)
            {
                if (m_strError.Length == 0)
                {
                    if (this.showException())
                    {
                        m_strError = ex.ToString();
                    }
                }

                sendEmail(this.m_strError + ":  " + ex.ToString());

                throw new System.Web.Http.HttpResponseException(HttpStatusCode.Forbidden);
            }
        }

}
}

这是我的 UserHelper.cs 类。

namespace API.Helpers
{
    public static class UserHelper
    {
        public static string getUserName(IPrincipal user)
        {

            return user.Identity.IsAuthenticated ? user.Identity.Name.ToLower() : "";

        }


    }
}

这是我在本地得到的令牌结果。

{
  "aud": "api://client-id",
  "iss": "https://sts.windows.net/tenant-id/",
  "iat": ,
  "nbf": ,
  "exp": ,
  "acr": "1",
  "aio": "",
  "amr": [
    ""
  ],
  "appid": "client-id",
  "appidacr": "0",
  "family_name": "",
  "given_name": "",
  "in_corp": "true",
  "ipaddr": "",
  "name": "",
  "oid": "",
  "onprem_sid": "",
  "rh": "",
  "scp": "api_consume",
  "sub": "",
  "tid": "tenant-id",
  "unique_name": "",
  "upn": "",
  "uti": "",
  "ver": "1.0"
}

这是我将应用程序发布到 OpenShift 后得到的令牌。

{
  "aud": "client-id",
  "iss": "https://login.microsoftonline.com/tenant-id/v2.0",
  "iat": ,
  "nbf": ,
  "exp": ,
  "acct": 0,
  "aio": "",
  "auth_time": 
  "ctry": "",
  "email": "",
  "family_name": "",
  "given_name": "",
  "in_corp": "true",
  "ipaddr": "",
  "name": "",
  "nonce": "",
  "oid": "",
  "onprem_sid": "",
  "preferred_username": "",
  "rh": "",
  "sid": "",
  "sub": "",
  "tenant_ctry": "",
  "tenant_region_scope": "",
  "tid": "tenant-id",
  "upn": "",
  "uti": "",
  "ver": "2.0",
  "verified_primary_email": [
    ""
  ],
  "verified_secondary_email": [
    "",
    ""
  ],
  "xms_tpl": "en"
}

你可以看到我得到的两个令牌有不同的受众、发行者和版本。可能是我缺少一些小配置,但在此阶段的任何帮助将不胜感激。

标签: angularazure.net-corejwtazure-authentication

解决方案


对于本地和生产,您是否有不同的租户 ID?如果是,那么检查应用程序的清单(portal.azure.com -> Azure Active Directory -> 应用程序注册 -> 搜索您的应用程序 -> 清单)并在第 4 行查看属性可能是值得的accessTokenAcceptedVersion。如果值为null1,则令牌将由 sts.windows.net 颁发。如果值为2,则将由 login.microsoftonline.com 发出。

改变可能需要时间,虽然......


推荐阅读