0

I am trying to write unit tests for authentication logic implemented using Azure AD client credentials flow using MOQ.

The first test case is to check that if the "Audience" is valid. I am trying to mock claim to set up the "aud" or "appId" claims using ClaimTypes but not able to find anything like ClaimTypes.Aud

var identity = new ClaimsIdentity(new Claim[] {
    new Claim(ClaimTypes.Name, "Sahil")
});
var mockPrincipal = new Mock<ClaimsPrincipal>(identity);
mockPrincipal.Setup(x => x.Identity).Returns(identity);
mockPrincipal.Setup(x => x.IsInRole(It.IsAny<string>())).Returns(true);

How can I set up the "aud" and "appId" claims in C# OR Just setup mockPrincipal so that when it tries to check if "aud" is valid it returs false.

I am trying to write unit tests for the below code.

public void Authenticate(JwtBearerOptions options)
{
    _configuration.Bind("AzureAD", options);
    options.TokenValidationParameters.ValidateAudience = true;
    options.TokenValidationParameters.ValidateIssuerSigningKey = true;
    options.TokenValidationParameters.ValidateIssuer = true;

    options.Events ??= new JwtBearerEvents();
    var existingHandlers = options.Events.OnTokenValidated;

    options.Events.OnTokenValidated = async context =>
    {
        string appId = GetAppIdFromToken(context);
        bool isAllowed = await CheckAppIdIsAllowedAsync(context, appId);

        if (isAllowed)
        {
            _logger.LogInformation($"[{nameof(Authenticate)}] AppId in allow list");
        }
        else
        {
            _logger.LogError($"[{nameof(Authenticate)}] AppId {appId} not in allowed list");
        }

        await Task.CompletedTask.ConfigureAwait(false);
    };
    options.Events.OnTokenValidated += existingHandlers;
}

private string GetAppIdFromToken(TokenValidatedContext context)
{
    string appId = context.Principal.Claims.FirstOrDefault(x => x.Type == "appid" || x.Type == "azp")?.Value;
    return appId;
}

private async Task<bool> CheckAppIdIsAllowedAsync(TokenValidatedContext context, string appId)
{
    IEnumerable<string> AllowedApps = _configuration.GetSection("AllowedAppPrincipals").Get<string[]>();
    var FoundAppId = AllowedApps.FirstOrDefault(a => a == appId);
    if (FoundAppId == null)
    {
        context.Response.StatusCode = (int)HttpStatusCode.Forbidden;
        context.Response.ContentType = "application/json";
        const string message = "{\"error\" : \"Unacceptable app principal\"}";
        byte[] arr = Encoding.ASCII.GetBytes(message);
        await context.Response.BodyWriter.WriteAsync(arr);
        context.Fail(message);
        return false;
    }
    return true;
}

How to mock aud and appId claims with Moq?

Peter Csala
  • 17,736
  • 16
  • 35
  • 75
Aastha
  • 15
  • 3
  • Please bear in mind that you can provide [any string as type for Claim constructor](https://learn.microsoft.com/en-us/dotnet/api/system.security.claims.claim.-ctor?view=net-7.0#system-security-claims-claim-ctor(system-string-system-string)). So, `new Claim("appid", "ExpectedValue")` would be enough – Peter Csala Jan 24 '23 at 12:32

1 Answers1

0

I tried to reproduce how to get audience invalid in my environment.

It usually happens when the issuer endpoint is different or scope is not given correctly or different scope than what is intended is mentioned.

https://login.microsoftonline.com/xx/oauth2/v2.0/token

Here i gave scope for api other than microsoft graph.

enter image description here

But next tep i am calling graph endpoint and so i am getting invalid audience error .

https://graph.microsoft.com/v1.0/users/xxx

enter image description here

To check for mocking test , you can consider below point as direct claims for "aud" audience in c# couldn't be obtained AFAIK.

Aud claim is Application ID URI or GUID Identifies the intended audience of the token. In v2.0 tokens, audience must be client ID of the API whereas in v1.0 tokens, it can be the client ID or the resource URI used in the request.

enter image description here

enter image description here

One way to validate it is to check following way with issuer/audience

You can give custom values according to the authorization endpoint

Code:

string AUDIENCE = "<GUID of your Audience according to the app>";
 string TENANT = "<GUID of your Tenant>";

private static async Task<SecurityToken> validateJwtTokenAsync(string token)
{
    //  URL based on your AAD-TenantId
    var stsDiscoveryEndpoint = String.Format(CultureInfo.InvariantCulture, "https://login.microsoftonline.com/{0}/.well-known/openid-configuration", TENANT);
    //To Get tenant information
    var configManager = new ConfigurationManager<OpenIdConnectConfiguration>(stsDiscoveryEndpoint)
    // Get Config from AAD:
    var config = await configManager.GetConfigurationAsync();

    // Validate token:
    var tokenHandler = new JwtSecurityTokenHandler();
    var validationParameters = new TokenValidationParameters
    {
        ValidAudience = AUDIENCE,
        ValidIssuer = config.Issuer,
        IssuerSigningTokens = config.SigningTokens,
        CertificateValidator = X509CertificateValidator.ChainTrust,
    };

    var validatedToken = (SecurityToken)new JwtSecurityToken();

    tokenHandler.ValidateToken(token, validationParameters, out validatedToken);

    return validatedToken;
}

Or

var TokenValidationParameters = new TokenValidationParameters
{
    ValidateIssuerSigningKey = true,
    IssuerSigningKey = key,
    ValidateIssuer = true,
    ValidateAudience = true,
    ValidateLifetime = false,
    ....
    ValidIssuer = configuration["JwtAuthentication:Issuer"],
    ValidAudience = configuration["JwtAuthentication:Audience"]
};

So from validation parameters, you can get if that we that audience was valid or not, it majorly occurs when issuer is different from what we expected or when scopes are not correct.

You can try get those validate as claims to check for mocking

Snippent below taken from TokenValidationParameters.AudienceValidator, System.IdentityModel.Tokens C# (CSharp) Code Examples - HotExamples

public virtual ClaimsPrincipal ValidateToken(string securityToken, TokenValidationParameters validationParameters, out SecurityToken
validatedToken)
        {
            if (string.IsNullOrWhiteSpace(securityToken))
            {
                throw new ArgumentNullException("securityToken");
            }

            if (validationParameters == null)
            {
                throw new ArgumentNullException("validationParameters");
            }

            

            if (validationParameters.ValidateAudience)
            {
                if (validationParameters.AudienceValidator != null)
                {
                    if   
(!validationParameters.AudienceValidator(jwt.Audiences, jwt,
validationParameters))
                    {
                        throw new SecurityTokenInvalidAudienceException(string.Format(CultureInfo.InvariantCulture,
 ErrorMessages.IDX10231, jwt.ToString()));
                    }
                }
                else
                {
                    this.ValidateAudience(jwt.Audiences, jwt, 
 validationParameters);
                }
            }


ClaimsIdentity identity = this.CreateClaimsIdentity(jwt, issuer, 
   validationParameters);
            if (validationParameters.SaveSigninToken)
            {
                identity.BootstrapContext = new 
 BootstrapContext(securityToken);
            }

            validatedToken = jwt;
            return new ClaimsPrincipal(identity);
        }

Also check Reference :c# - How to mock ConfigurationManager.AppSettings with moq - Stack Overflow

kavyaS
  • 8,026
  • 1
  • 7
  • 19