36

Please help me to understand the difference between JWT token validation from the ASP netcore application and the netcore Kestrel hosted application.

There are two applications that verifies token using the source code like below:

public static IServiceCollection AddJwtToken(this IServiceCollection services, OAuthConfig config)
{
    services.AddMvc();
    services.AddAuthorization();

    Logger.DebugFormat("AddJwtBearer authority:{0} audience:{1}", config.GetAuthority(), config.Resource);

    services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer(options => new JwtBearerOptions
        {
            Authority = config.GetAuthority(),
            Audience = config.Resource,
    });

    return services;
}

it is pretty simple and it works well if token is being validated from the asp net core 2.2 application

// in the asp.net core
var builder = WebHost.CreateDefaultBuilder(args);
builder
        .UseStartup<Startup>()
        .ConfigureKestrel(_ => _.ConfigureEndpoints())
        .UseSerilog();

And there is another application (console) that starts the same rest service host using the UseKestrel

//in the console app
var builder = WebHost.CreateDefaultBuilder()
    .UseNLog()
    .UseKestrel(_ => _.ConfigureEndpoints())
    .UseStartup<Startup>();

the only one significant difference is that there is UseKestrel in the console via ConfigureKestrel for asp.net core.

The same source code (and configuration) is used to get token from the Azure AD. Please find it as the gist here. It is configured to get token from the https://login.microsoftonline.com/{tenant}/v2.0 provider. The same token endpoint, clientid, secret and scope values are used for both cases.

The problem is that AddJwtBearer validates the token in the asp.net core and does not in the console app. the error is

Microsoft.IdentityModel.Tokens.SecurityTokenSignatureKeyNotFoundException: IDX10501: Signature validation failed. Unable to match keys:
kid: 'BB8CeFVqyaGrGNuehJIiL4dfjzw',
token: '{"typ":"JWT","alg":"RS256","kid":"BB8CeFVqyaGrGNuehJIiL4dfjzw"}.{"aud":"2c163c99-935b-4362-ae0d-657f589f5565","iss":"https://login.microsoftonline.com/{tenantidhere}/v2.0

Why asp.net core host validates the token (for the first AddJwtBearer implementation) and console host fails?

Thank you

No Refunds No Returns
  • 8,092
  • 4
  • 32
  • 43
oleksa
  • 3,688
  • 1
  • 29
  • 54

4 Answers4

37

to solve this error I've to load keys from the openid provider as below:

Logger.DebugFormat("AddJwtBearer authority:{0} audience:{1}", config.GetAuthority(), config.Resource);

IList<string> validissuers = new List<string>()
{
    config.GetAuthority(),
};

var configManager = new ConfigurationManager<OpenIdConnectConfiguration>($"{validissuers.Last()}/.well-known/openid-configuration", new OpenIdConnectConfigurationRetriever());

var openidconfig = configManager.GetConfigurationAsync().Result;

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, _ =>
    {
        _.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters()
        {
            ValidateAudience = true,
            ValidAudience = config.Resource,

            ValidateIssuer = true,
            ValidIssuers = new[] { config.GetAuthority() },

            ValidateIssuerSigningKey = true,
            IssuerSigningKeys = openidconfig.SigningKeys,

            RequireExpirationTime = true,
            ValidateLifetime = true,
            RequireSignedTokens = true,
        };

        _.RequireHttpsMetadata = false;

    });

And it started to work for both cases. But what is the difference with the old AddJwtBearer implementation and the new one (related to the keys validation)? Keys where downloaded and supplied using the IssuerSigningKeys = openidconfig.SigningKeys but why it is not loaded automatically using the .well-known/openid-configuration by the AddJwtBearer middleware ?

oleksa
  • 3,688
  • 1
  • 29
  • 54
  • I solved my issue thanks to your answer. As for your question why the signing keys not loaded automatically, I can only assumes the id server is not configured properly. In my case, the issuer uri is `http` instead of `https`, which is weird. – Rosdi Kasim Dec 23 '19 at 08:00
  • @RosdiKasim i'm loading tokens from the `https://login.microsoftonline.com` do you think they have `http://` under the hood ? – oleksa Jan 10 '20 at 07:59
  • 13
    No. After I commented on this, I totally fixed this issue by explicitly setting the MetaDataAddress like this: `options.MetadataAddress = Config.OidcAuthority + "/.well-known/openid-configuration"`. Then the validation works automatically. – Rosdi Kasim Jan 10 '20 at 08:41
  • I used Rosdi's comment. https://stackoverflow.com/questions/49694383/use-multiple-jwt-bearer-authentication/49706390#49706390 is related to what I'm trying to setup and both of these Q's and A's were key to working solution. FWIW YMMV. Comments should be taken as of date of this writing. Thank you future readers. – No Refunds No Returns Feb 12 '20 at 02:33
  • What does "config.Resource" from the OAuthConfig Type represent in your code? Is this "ValidAudience" it hydrates the user's client browser, the machine running your code, the OpenID Connect server, or something else? – bopapa_1979 Nov 11 '20 at 20:11
  • @bopapa_1979 `config.Resource` is a resource id that token sould be issued for. The `aud` token value – oleksa Nov 12 '20 at 14:22
  • @oleksa I'm sorry, but I'm still not clear on what you mean by resource. Resource as in "restful API resource," resource, resource as in "the client that was issued an OpenID Connect client_id," or what? What kind of data goes here? A URL, a unique identifier of some kind, what? I did read the documentation here as to what the "aud" represents, but it still isn't clear. The term "audience" is not defined: https://openid.net/specs/openid-connect-core-1_0.html – bopapa_1979 Nov 17 '20 at 20:00
  • @bopapa_1979 from the link you have provided `aud REQUIRED. Audience(s) that this ID Token is intended for. It MUST contain the OAuth 2.0 client_id of the Relying Party as an audience value. It MAY also contain identifiers for other audiences. In the general case, the aud value is an array of case sensitive strings` – oleksa Nov 18 '20 at 09:42
  • @bopapa_1979 Client asks to issue token for certain resource (unique string identifier) Then service can verify that this token was issued for that resource as far as I understood – oleksa Nov 18 '20 at 09:48
  • Microsoft recommends caching the keys for 24 hours. See https://learn.microsoft.com/en-us/azure/active-directory/develop/access-tokens#validating-the-signature. Does ConfigurationManager do this automatically, or does it always retrieve the latest config without caching? – Florian Winter Jan 12 '21 at 11:54
  • @FlorianWinter I do not think that `ConfigurationManager` can do such caching. However it may be done like storing `TokenValidationParameters` instance and renewing the `IssuerSigningKeys` by schedule. However it looks like overkill so please try with @RosdiKasim solution first. – oleksa Jan 12 '21 at 12:24
21

In my case, the same error was because of inadvertent use of the token received from one environment (https://dev/identity) and validated in another environment (i.e. http://local/identity).

Donut
  • 110,061
  • 20
  • 134
  • 146
2

In my case it never was an issue with my code, but with the default SignIn-SignUp User Flow from Azure AD B2C.

I've gone through a whole rabbit hole of potential fixes before I realized it could actually be an issue that only exists with the User Flow.

If you're using a User Flow, I would recommend trying to create a Custom Policy instead and see if the issues persist.

I followed the documentation for creating my OpenId connect SignIn-SignUp Custom Policy: https://learn.microsoft.com/en-us/azure/active-directory-b2c/identity-provider-generic-openid-connect?pivots=b2c-custom-policy

I've included my own code below

B2C_1A_signup_signin_SSO

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<TrustFrameworkPolicy
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:xsd="http://www.w3.org/2001/XMLSchema"
  xmlns="http://schemas.microsoft.com/online/cpim/schemas/2013/06"
  PolicySchemaVersion="0.3.0.0"
  TenantId="yourtenant.onmicrosoft.com"
  PolicyId="B2C_1A_signup_signin_SSO"
  PublicPolicyUri="http://yourtenant.onmicrosoft.com/B2C_1A_signup_signin_SSO">

  <BasePolicy>
    <TenantId>yourtenant.onmicrosoft.com</TenantId>
    <PolicyId>B2C_1A_TrustFrameworkExtensions_SSO</PolicyId>
  </BasePolicy>

  <RelyingParty>
    <DefaultUserJourney ReferenceId="SignUpOrSignInSSO" />
    <Endpoints>
      <!--points to refresh token journey when app makes refresh token request-->
      <Endpoint Id="Token" UserJourneyReferenceId="RedeemRefreshToken" />

    </Endpoints>
    <TechnicalProfile Id="PolicyProfile">
      <DisplayName>PolicyProfile</DisplayName>
      <Protocol Name="OpenIdConnect" />
      <OutputClaims>
        <OutputClaim ClaimTypeReferenceId="displayName" />
        <OutputClaim ClaimTypeReferenceId="givenName" />
        <OutputClaim ClaimTypeReferenceId="surname" />
        <OutputClaim ClaimTypeReferenceId="email" />
        <OutputClaim ClaimTypeReferenceId="objectId" PartnerClaimType="sub"/>
        <OutputClaim ClaimTypeReferenceId="identityProvider" />
        <OutputClaim ClaimTypeReferenceId="tenantId" AlwaysUseDefaultValue="true" DefaultValue="{Policy:TenantObjectId}" />

      </OutputClaims>
      <SubjectNamingInfo ClaimType="sub" />
    </TechnicalProfile>
  </RelyingParty>
</TrustFrameworkPolicy>

B2C_1A_TrustFrameworkExtensions_SSO

<?xml version="1.0" encoding="utf-8" ?>
<TrustFrameworkPolicy xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:xsd="http://www.w3.org/2001/XMLSchema"
  xmlns="http://schemas.microsoft.com/online/cpim/schemas/2013/06" PolicySchemaVersion="0.3.0.0" TenantId="yourtenant.onmicrosoft.com" PolicyId="B2C_1A_TrustFrameworkExtensions_SSO" PublicPolicyUri="http://yourtenant.onmicrosoft.com/B2C_1A_TrustFrameworkExtensions_SSO">

  <BasePolicy>
    <TenantId>yourtenant.onmicrosoft.com</TenantId>
    <PolicyId>B2C_1A_TrustFrameworkLocalization</PolicyId>
  </BasePolicy>
  <BuildingBlocks>
    <ClaimsSchema>
      <ClaimType Id="issuerUserId">
        <DisplayName>Username</DisplayName>
        <DataType>string</DataType>
        <UserHelpText/>
        <UserInputType>TextBox</UserInputType>
        <Restriction>
          <Pattern RegularExpression="^[a-zA-Z0-9]+[a-zA-Z0-9_-]*$" HelpText="The username you provided is not valid. It must begin with an alphabet or number and can contain alphabets, numbers and the following symbols: _ -" />
        </Restriction>
      </ClaimType>
      <ClaimType Id="alternativeSecurityId">
        <DisplayName>AlternativeSecurityId</DisplayName>
        <DataType>string</DataType>
        <UserHelpText/>
      </ClaimType>

      <ClaimType Id="identityProvider">
        <DisplayName>Identity Provider</DisplayName>
        <DataType>string</DataType>
        <DefaultPartnerClaimTypes>
          <Protocol Name="OAuth2" PartnerClaimType="idp" />
          <Protocol Name="OpenIdConnect" PartnerClaimType="idp" />
          <Protocol Name="SAML2" PartnerClaimType="http://schemas.microsoft.com/identity/claims/identityprovider" />
        </DefaultPartnerClaimTypes>
        <UserHelpText/>
      </ClaimType>


      <ClaimType Id="upnUserName">
        <DisplayName>UPN User Name</DisplayName>
        <DataType>string</DataType>
        <UserHelpText>The user name for creating user principal name.</UserHelpText>
      </ClaimType>
    </ClaimsSchema>
    <ClaimsTransformations>
      <ClaimsTransformation Id="CreateRandomUPNUserName" TransformationMethod="CreateRandomString">
        <InputParameters>
          <InputParameter Id="randomGeneratorType" DataType="string" Value="GUID" />
        </InputParameters>
        <OutputClaims>
          <OutputClaim ClaimTypeReferenceId="upnUserName" TransformationClaimType="outputClaim" />
        </OutputClaims>
      </ClaimsTransformation>

      <ClaimsTransformation Id="CreateUserPrincipalName" TransformationMethod="FormatStringClaim">
        <InputClaims>
          <InputClaim ClaimTypeReferenceId="upnUserName" TransformationClaimType="inputClaim" />
        </InputClaims>
        <InputParameters>
          <InputParameter Id="stringFormat" DataType="string" Value="cpim_{0}@{RelyingPartyTenantId}" />
        </InputParameters>
        <OutputClaims>
          <OutputClaim ClaimTypeReferenceId="userPrincipalName" TransformationClaimType="outputClaim" />
        </OutputClaims>
      </ClaimsTransformation>

      <ClaimsTransformation Id="CreateAlternativeSecurityId" TransformationMethod="CreateAlternativeSecurityId">
        <InputClaims>
          <InputClaim ClaimTypeReferenceId="issuerUserId" TransformationClaimType="key" />
          <InputClaim ClaimTypeReferenceId="identityProvider" TransformationClaimType="identityProvider" />
        </InputClaims>
        <OutputClaims>
          <OutputClaim ClaimTypeReferenceId="alternativeSecurityId" TransformationClaimType="alternativeSecurityId" />
        </OutputClaims>
      </ClaimsTransformation>

      <ClaimsTransformation Id="CreateSubjectClaimFromAlternativeSecurityId" TransformationMethod="CreateStringClaim">
        <InputParameters>
          <InputParameter Id="value" DataType="string" Value="Not supported currently. Use oid claim." />
        </InputParameters>
        <OutputClaims>
          <OutputClaim ClaimTypeReferenceId="sub" TransformationClaimType="createdClaim" />
        </OutputClaims>
      </ClaimsTransformation>
    </ClaimsTransformations>
  </BuildingBlocks>

  <ClaimsProviders>
    <ClaimsProvider>
      <Domain>contoso.com</Domain>
      <DisplayName>Login with Contoso SSO</DisplayName>
      <TechnicalProfiles>
        <TechnicalProfile Id="contoso-OpenIdConnect">
          <DisplayName>Contoso SSO</DisplayName>
          <Description>Login with your Contoso account</Description>
          <Protocol Name="OpenIdConnect"/>
          <Metadata>
            <Item Key="METADATA">https://login.microsoftonline.com/contoso.com/v2.0/.well-known/openid-configuration</Item>
            <Item Key="client_id">your-client-id</Item>
            <Item Key="response_types">code</Item>
            <Item Key="scope">openid profile</Item>
            <Item Key="response_mode">form_post</Item>
            <Item Key="HttpBinding">POST</Item>
            <Item Key="UsePolicyInRedirectUri">false</Item>
          </Metadata>
          <CryptographicKeys>
            <Key Id="client_secret" StorageReferenceId="B2C_1A_ContosoOidcSecret"/>
          </CryptographicKeys>
          <OutputClaims>
            <OutputClaim ClaimTypeReferenceId="issuerUserId" PartnerClaimType="oid"/>
            <OutputClaim ClaimTypeReferenceId="tenantId" PartnerClaimType="tid"/>
            <OutputClaim ClaimTypeReferenceId="givenName" PartnerClaimType="given_name" />
            <OutputClaim ClaimTypeReferenceId="surName" PartnerClaimType="family_name" />
            <OutputClaim ClaimTypeReferenceId="displayName" PartnerClaimType="name" />
            <OutputClaim ClaimTypeReferenceId="email" PartnerClaimType="email" />
            <OutputClaim ClaimTypeReferenceId="authenticationSource" DefaultValue="socialIdpAuthentication" AlwaysUseDefaultValue="true" />
            <OutputClaim ClaimTypeReferenceId="identityProvider" PartnerClaimType="iss" />
            <OutputClaim ClaimTypeReferenceId="objectId" />
          </OutputClaims>
          <OutputClaimsTransformations>
            <OutputClaimsTransformation ReferenceId="CreateRandomUPNUserName"/>
            <OutputClaimsTransformation ReferenceId="CreateUserPrincipalName"/>
            <OutputClaimsTransformation ReferenceId="CreateAlternativeSecurityId"/>
            <OutputClaimsTransformation ReferenceId="CreateSubjectClaimFromAlternativeSecurityId"/>
          </OutputClaimsTransformations>
          <UseTechnicalProfileForSessionManagement ReferenceId="SM-SocialLogin"/>
        </TechnicalProfile>
      </TechnicalProfiles>
    </ClaimsProvider>

    <ClaimsProvider>
      <DisplayName>Session Management</DisplayName>
      <TechnicalProfiles>
        <TechnicalProfile Id="SM-Noop">
          <DisplayName>Noop Session Management Provider</DisplayName>
          <Protocol Name="Proprietary" Handler="Web.TPEngine.SSO.NoopSSOSessionProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
        </TechnicalProfile>

        <TechnicalProfile Id="SM-AAD">
          <DisplayName>Session Mananagement Provider</DisplayName>
          <Protocol Name="Proprietary" Handler="Web.TPEngine.SSO.DefaultSSOSessionProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
          <PersistedClaims>
            <PersistedClaim ClaimTypeReferenceId="objectId" />

            <PersistedClaim ClaimTypeReferenceId="authenticationSource" />
            <PersistedClaim ClaimTypeReferenceId="identityProvider" />
            <PersistedClaim ClaimTypeReferenceId="newUser" />
            <PersistedClaim ClaimTypeReferenceId="executed-SelfAsserted-Input" />
          </PersistedClaims>
          <OutputClaims>
            <OutputClaim ClaimTypeReferenceId="objectIdFromSession" DefaultValue="true" />
          </OutputClaims>
        </TechnicalProfile>

        <!-- Profile name is being used to disambiguate AAD session between sign up and sign in -->
        <TechnicalProfile Id="SM-SocialSignup">
          <IncludeTechnicalProfile ReferenceId="SM-AAD" />
        </TechnicalProfile>

        <TechnicalProfile Id="SM-SocialLogin">
          <DisplayName>Session Mananagement Provider</DisplayName>
          <Protocol Name="Proprietary" Handler="Web.TPEngine.SSO.ExternalLoginSSOSessionProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
          <Metadata>
            <Item Key="AlwaysFetchClaimsFromProvider">true</Item>
          </Metadata>
          <PersistedClaims>
            <PersistedClaim ClaimTypeReferenceId="alternativeSecurityId" />
          </PersistedClaims>
        </TechnicalProfile>

        <!-- Session management technical profile for OIDC based tokens -->
        <TechnicalProfile Id="SM-jwt-issuer">
          <DisplayName>Session Management Provider</DisplayName>
          <Protocol Name="Proprietary" Handler="Web.TPEngine.SSO.OAuthSSOSessionProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
        </TechnicalProfile>
      </TechnicalProfiles>
    </ClaimsProvider>
  </ClaimsProviders>

  <UserJourneys>
    <UserJourney Id="SignUpOrSignInSSO">
      <OrchestrationSteps>

        <OrchestrationStep Order="1" Type="CombinedSignInAndSignUp" ContentDefinitionReferenceId="api.signuporsignin">
          <ClaimsProviderSelections>
            <ClaimsProviderSelection TargetClaimsExchangeId="ContosoOpenIdExchange" />
          </ClaimsProviderSelections>
        </OrchestrationStep>

        <OrchestrationStep Order="2" Type="ClaimsExchange">
          <ClaimsExchanges>
            <ClaimsExchange Id="ContosoOpenIdExchange" TechnicalProfileReferenceId="contoso-OpenIdConnect" />
          </ClaimsExchanges>
        </OrchestrationStep>

        <OrchestrationStep Order="3" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="JwtIssuer" />

      </OrchestrationSteps>
      <ClientDefinition ReferenceId="DefaultWeb" />
    </UserJourney>

  </UserJourneys>

</TrustFrameworkPolicy>

Anh-Duc
  • 154
  • 1
  • 12
  • 1
    I can confirm that changing from pre-built user flow to custom policy fixed issue in my case. Thanks! – rosko Mar 27 '23 at 09:23
  • 2
    Out of interest, when getting the error were you referencing both User Flows *and* Custom Policies in your application (e.g. a flow for sign in but a custom policy for profile edit)? The two methods will have different signing keys, so I wonder if the issue is caused by an implicit assumption that you'll do either one approach or the other, but not both in the same application. – meulop Apr 26 '23 at 13:23
  • 1
    @meulop I'm wondering the same thing. I am trying that exact scenario and running into this issue. – Ageonix Jun 12 '23 at 17:35
0

This is very weird, but I got the same error in a small Azure web app because my deployed configuration parameters were not updated as I expected but remained and pointed to a previous authority, issuer and audience. After I manually updated these configuration values, everything worked as expected.

Papa Stahl
  • 687
  • 8
  • 7