ASP.Net Web Api 4.5.2 using UseJwtBearerAuthentication, the Roles are ignored - asp.net

I would like to use Roles in Azure AD.
I have almost everything working, including Roles configured in the AppRegistration Manifest, etc. On breakpoint, I do see a claim "roles":["MyAdminRole"]
I am trying to apply authorization on the ApiController method:
[System.Web.Http.Authorize("Roles="MyAdminRole")]
public void PostMyMethod(){}
The owin startup looks something like this:
var issuer = "https://sts.windows.net/MyAzureAppServiceGuidFromAuthProperties/";
var secret = TextEncodings.Base64Url.Decode("AzureAppServiceAuthSecret"); //have tried using the encoded value of my Azure Secret here as well
app.UseJwtBearerAuthentication(
new JwtBearerAuthenticationOptions
{
AuthenticationMode = AuthenticationMode.Active,
AllowedAudiences = new[] { "MyAppIdGUID"},
IssuerSecurityTokenProviders = new IIssuerSecurityTokenProvider[]
{
new SymmetricKeyIssuerSecurityTokenProvider(issuer, secret)
},
TokenValidationParameters = new TokenValidationParameters
{
//this should set ClaimsIdentity.RoleClaimType to "roles", so it finds the "roles" claim during ClaimsPrincipal.UserInRole("")
//in the Mvc Controller example, it behaves this way, but in my ApiController, this is being ignored.
RoleClaimType = "roles"
}
});
The request is coming from an Angular-adal app, and includes a Authorization: Bearer JWT_TOKEN. The token decodes on https://jwt.io, although it says "Invalid Signature" even when I enter my Azure App's Secret from AppService/Auth/AzureAD properties. In the decoded token I do see the audience and issuer are what I expect, and I see the "roles" claim having ["MyAdminRole"].
On a breakpoint in the method, if I inspect the private m_identities
((List<ClaimsIdentity>)typeof(System.Security.Claims.ClaimsPrincipal).GetField("m_identities", System.Reflection.BindingFlags.NonPublic|System.Reflection.BindingFlags.GetField|System.Reflection.BindingFlags.Instance).GetValue(User))[0]
the ClaimIdentity.RoleClaimType values equals System.Security.Claims.ClaimTypes.Role, ie: "http://schemas.microsoft.com/ws/2008/06/identity/claims/role", and User.IsInrole("MyAdminRole") returns false, presumably because the User.Claims role key is "roles", ie: they don't match.
To contrast, from working with jmprieur's azure sample active-directory-dotnet-webapp-roleclaims(https://azure.microsoft.com/en-us/resources/samples/active-directory-dotnet-webapp-roleclaims/), which is using OpenIdConnectAuthenticationOptions, at breakpoints in the Controller methods, the ClaimsIdentity.RoleClaimType has the value "roles", and User.IsInRole("MyAdminRole") returns true, presumably the ClaimsPrincipal.IsInRole is finding the "roles" claim because the ClaimsIdentity.RoleClaimType "roles" matches the User.Claims key of "roles".
How to I get the ClaimsIdentity.RoleClaimType to be "roles" so that ClaimsIdentity.IsInRole("MyAdminRole") to return true so that Authorize("MyAdminRole") will pass? Is it that JwtBearerAuthentication is not constructing the ClaimsPrinciple, something else is, and that something is setting ClaimsIdentity.RoleClaimType to "http://schemas.microsoft.com/ws/2008/06/identity/claims/role"? And, how do I know if the app.useJwtBearerAthentication is even being applied to the request? I don't see any event where I could log that it is being invoked.

Related

When using Global Query Filters, tenant Id is null when I try to get roles to update the claims

I'm trying to set up multitenancy in the application that I'm working on. I added the Global Query Filters and implemented the TenantProvider as suggested here. Note this block of code in the example that I listed:
public interface ITenantProvider
{
Guid GetTenantId();
}
public class DummyTenantProvider : ITenantProvider
{
public Guid GetTenantId()
{
return Guid.Parse("069b57ab-6ec7-479c-b6d4-a61ba3001c86");
}
}
In my case, instead of the DummyTenantProvider, I have implemented the tenant provider that gets tenantId from the HttpContextAccessor.HttpContext.User.GetTenantId(). I understand that the type of HttpContextAccessor.HttpContext.User is of ClaimsPrincipal, so I added the additional method that accepts this ClaimsPrincipal as parameter and returns tenantId:
public static string GetTenantId(this ClaimsPrincipal principal)
{
return principal.FindFirstValue("tenant");
}
Also, I've implemented the Api Authentication with JWT token. In Startup.cs, I added the registration of the authentication like this:
services.AddAuthentication(options =>
{
// some code that is not relevant
}).AddJwtBearer(options =>
{
// some code that is not relevant
options.Events = new JwtBearerEvents
{
OnTokenValidated = async context =>
{
// here I get the needed service and from that service, I get the data that I need for Claims (Roles are needed)
var claims = new List<Claim>
{
new Claim(ClaimTypes.Role, JsonConvert.SerializeObject(roles)),
};
var appIdentity = new ClaimsIdentity(claims);
context.Principal.AddIdentity(appIdentity);
},
};
});
Now, the issue that I'm having is when I'm making an HTTP request that's targeting the method in the controller that gets roles for the user (the method is using the same service I'm using in the code above) when debugging the code, first the OnTokenValidated is called and the roles for the user should be populated and added to claims, and then the method in the controller is called. When the OnTokenValidated calls the service and when the request to the database is executed (simple "dbContext.Roles.ToListAsync()" in the repository), the global query filter is applied and tenantId should be added to that database request. But when the global filter is applied, tenantId is null in the GetTenantId method and the code throws ArgumentNullException error. What I can't figure out is why is the tenantId null. My assumption is that when the service is called from OnTokenValidated, that is not the part of the HTTP request, and then the HttpContextAccessor.HttpContext doesn't have the needed value, but I'm not sure if I'm right about that.
I would appreciate some additional insight into what I'm doing wrong here.
If anything else is needed to make things clearer, I'm happy to edit the question.
Thank you all.
For anyone in need of a solution, I was able to resolve this issue by adding the Tenant Id as a new claim in Startup.cs using the TokenValidatedContext (because I have that information in TokenValidatedContext at that moment). Looks something like this:
ctx.HttpContext.User.Identity?.AddClaim(New Claim('tenant'));
Later in the Tenant Provider I have access to this claim.

Azure Active Directory SSO with MSAL and openID Connect

I was tasked with writing an ASP.NET website that uses Azure Active Directory. I went with the route of OAuth and OpenID Connect. I am not able to use implicit flow and therefore must set the ResponseType to be code.
Using MSAL code samples I got most of it working but the problem is that all the samples are using a response type that returns tokens. I think I need to do it in 2 separate steps, first get the authorization code and then get the id token. I'm not exactly sure how to do this and would much appreciate some guidance here.
I have a startup class that look like this:
public void Configuration(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions { });
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
Authority = authority,
ClientId = clientId,
RedirectUri = redirectUri,
Scope = "openid profile email offline_access user.readbasic.all", // a basic set of permissions for user sign in & profile access
ResponseType = OpenIdConnectResponseType.Code,
ClientSecret = clientSecret,
TokenValidationParameters = new TokenValidationParameters
{
// In a real application you would use ValidateIssuer = true for additional checks and security.
ValidateIssuer = false,
NameClaimType = "name",
},
Notifications = new OpenIdConnectAuthenticationNotifications()
{
AuthorizationCodeReceived = OnAuthorizationCodeReceived,
AuthenticationFailed = OnAuthenticationFailed,
}
});
}
private Task OnAuthenticationFailed(AuthenticationFailedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> context)
{
// Handle any unexpected errors during sign in
context.OwinContext.Response.Redirect("/Error?message=" + context.Exception.Message);
context.HandleResponse(); // Suppress the exception
return Task.FromResult(0);
}
private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedNotification context)
{
/*
The `MSALPerUserMemoryTokenCache` is created and hooked in the `UserTokenCache` used by `IConfidentialClientApplication`.
At this point, if you inspect `ClaimsPrinciple.Current` you will notice that the Identity is still unauthenticated and it has no claims,
but `MSALPerUserMemoryTokenCache` needs the claims to work properly. Because of this sync problem, we are using the constructor that
receives `ClaimsPrincipal` as argument and we are getting the claims from the object `AuthorizationCodeReceivedNotification context`.
This object contains the property `AuthenticationTicket.Identity`, which is a `ClaimsIdentity`, created from the token received from
Azure AD and has a full set of claims.
*/
IConfidentialClientApplication confidentialClient = GroupManager.Utils.MsalAppBuilder.BuildConfidentialClientApplication(null);
// Upon successful sign in, get & cache a token using MSAL
AuthenticationResult result = await confidentialClient.AcquireTokenByAuthorizationCode(new[] { "openid profile email offline_access user.readbasic.all" }, context.Code).ExecuteAsync();
}
How do I take the information from the result's tokens and create a claims identity for the AuthenticationTicket.Identity and access the user info?
Please note that this is an ASP.NET application. Not MVC and not Core.
If you use MSAL, you don't need to handle the code yourself. MSAL will return the token to you after you log in interactively, please see:Overview of Microsoft Authentication Library (MSAL).
Before that, you need to take a look at Add sign-in to Microsoft to an ASP.NET web app,the workflow is:
Code example please check: https://github.com/AzureAdQuickstarts/AppModelv2-WebApp-OpenIDConnect-DotNet
Update:
Try to enable ID token

How do I configure ASP.NET WebApi to validate bearer tokens against an OpenID Connect server?

I am writing a service which receives POSTs from another service, which includes an Authorization header containing a bearer token. This token is obtained independently from an OpenID Connect server (Keycloak in our dev environment, but not necessarily in production). Our service does not need to obtain or issue tokens; it merely needs to be able to validate them.
We are using .NET Framework 4.8 with self-hosted ASP.NET WebApi (OWIN 4, etc).
Configuration-wise, the information we have is:
the URL of the OpenID Connect service, eg. 'http://keycloak:8080/auth/realms/demo/'
the client ID, eg. 'js-client'.
The intent is that we obtain the issuer public key dynamically, from the OpenID server's metadata endpoint 'http://keycloak:8080/auth/realms/demo/.well-known/openid-configuration'. Currently I have something like:
WebApp.Start(startOptions, builder => {
var config = ...
// ... Set up routes etc ...
config.Filters.Add(new HostAuthenticationFilter("Bearer"));
builder.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
ClientId = "js-client",
Authority = "http://keycloak:8080/auth/realms/demo/",
RequireHttpsMetadata = false,
SignInAsAuthenticationType = "Bearer",
});
builder.UseWebApi(config);
}));
The controller action looks like:
[HttpGet]
[HttpPost]
[Authorize]
public IHttpActionResult Receive([FromBody] string dto) => Ok();
Currently, it always returns 401 Unauthorized with a message 'Authorization has been denied for this
request' irrespective of the validity of the token.
Wireshark reveals that our service never tries to contact the Keycloak server for OIDC metadata, so I guess that the authorisation handler is not even finding the token.
I've looked at UseJwtBearerAuthentication and UseOAuthAuthorizationServer too, but those seem to want more information than just an OIDC endpoint (unsurprising, really) or they need custom provider implementations.
This does not seem to be such an unusual use case that I need to implement my own validator, so presumably I'm missing something? Google searches turn up hundreds of examples which seem to relate only to ASP.NET Core or don't cover non-interactive use cases.
I managed to make progress on this by inspecting the source of OpenIdConnectAuthenticationMiddleware.
The JwtBearer middleware handles validation of the issuer, but needs to know the public key. Since I need to avoid configuring this directly, I need to ask the OIDC server for it.
This can be accomplished using a ConfigurationManager, which should deal with caching, etc for us:
private JwtBearerAuthenticationOptions GetJwtBearerTokenAuthenticationOptions(string issuer, IConfigurationManager<OpenIdConnectConfiguration> configurationManager)
{
return new JwtBearerAuthenticationOptions
{
Realm = "demo",
TokenValidationParameters = new TokenValidationParameters
{
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
// ... etc ...
IssuerSigningKeyResolver = (token, securitytoken, kid, validationparameters) =>
configurationManager.GetConfigurationAsync(CancellationToken.None).GetAwaiter().GetResult().SigningKeys,
ValidIssuer = issuer.TrimEnd('/'),
}
};
}
(The resolver delegate can't be async unfortunately, so I can't await this properly.)
The ConfigurationManager can be constructed like this (based on the internals of OpenIdConnectAuthenticationMiddleware):
private IConfigurationManager<OpenIdConnectConfiguration> GetOIDCConfigurationManager(string issuer)
{
var httpClient = new HttpClient(new WebRequestHandler());
httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("Demo OpenIdConnect middleware");
httpClient.Timeout = TimeSpan.FromMinutes(1);
httpClient.MaxResponseContentBufferSize = 10485760L;
var httpRetriever = new HttpDocumentRetriever(httpClient) { RequireHttps = false };
return new ConfigurationManager<OpenIdConnectConfiguration>($"{issuer}.well-known/openid-configuration", new OpenIdConnectConfigurationRetriever(), httpRetriever);
}
These can then be used as follows:
const string issuer = "http://keycloak:8080/auth/realms/demo/";
var configurationManager = GetOIDCConfigurationManager(issuer);
builder.UseJwtBearerAuthentication(GetJwtBearerTokenAuthenticationOptions(issuer, configurationManager));
It all seems to work, although I'd very much like to know if there's a simpler way...?
Obviously, anyone using this in production should RequireHttps = true instead.

Validating Node.Js JWT token in asp.net/Authorize

I am in the process of splitting up my asp.net service to multiple micro services. As a process, I have created my identity service using Node.Js and it uses JWT for tokens.
Now i want to use this token in C# so that all my [Authorise] attributes use this token and allow access.
I have looked at many implementations, but could not get this to work. Since JWT is a standard impementation, i do not understand a reason why this would not work.
This is my C# code
public void ConfigureAuth(IAppBuilder app)
{
var issuer = "myorg/identity2";
string audienceId = ConfigurationManager.AppSettings["as:AudienceId"];
byte[] audienceSecret = TextEncodings.Base64Url.Decode
("xfecrrt7CV");
// Api controllers with an [Authorize] attribute will be validated with JWT
app.UseJwtBearerAuthentication(
new JwtBearerAuthenticationOptions
{
AuthenticationMode = AuthenticationMode.Active,
AllowedAudiences = new[] { audienceId },
IssuerSecurityTokenProviders = new IIssuerSecurityTokenProvider[]
{
new SymmetricKeyIssuerSecurityTokenProvider(issuer, audienceSecret)
}
});
However, I get this error everytime i try to access a protected method.
{"Message":"Authorization has been denied for this request."}
Is there anything i am missing here? How do i add the claim identity to this?
Finally, it was resolved. One of my friends debugged the Identity source code and recommended to increased the key length. After increasing the key length, I was able to validate the token

How to re-validate token for multi-tenant ASP.NET Identity?

I have implemented a custom OAuthAuthorizationServerProvider to add a domain constraint for the account login. Everything was good. However, I met a problem that, once the user get the token, they can use it for whatever system they want. For example:
They request the TokenEndpointPath with proper username and password (assume it is the admin account of Tenant 1): http://localhost:40721/api/v1/account/auth and receive the Bearer Token.
Now they use it to access: http://localhost:40720/api/v1/info/admin, which is of Tenant 0. The request is considered Authorized.
I tried changing the CreateProperties method but it did not help:
public static AuthenticationProperties CreateProperties(string userName)
{
var tenant = DependencyUtils.Resolve<IdentityTenant>();
IDictionary<string, string> data = new Dictionary<string, string>
{
{ "userName", userName },
{ "tenantId", tenant.Tenant.Id.ToString() },
};
return new AuthenticationProperties(data);
}
I also tried overriding ValidateAuthorizeRequest, but it is never called in my debug.
Do I need to implement a check anywhere else, so the Token is only valid for a domain/correct tenant?
(NOTE: a tenant may have multiple domains, so it's great if I can manually perform an account check against correct tenant rather than sticking to a domain. However, it's a plus if I could do that, or else, simply limit the token to the domain is ok)
Not a direct answer to my question (since it's not inside ASP.NET Identity workflow), but the simplest fix I applied was to use ActionFilterAttribute instead.
public class DomainValidationFilter : ActionFilterAttribute
{
public override Task OnActionExecutingAsync(HttpActionContext actionContext, CancellationToken cancellationToken)
{
// Other Code...
// Validate if the logged in user is from correct tenant
var principal = actionContext.ControllerContext.RequestContext.Principal;
if (principal != null && principal.Identity != null && principal.Identity.IsAuthenticated)
{
var userId = int.Parse(principal.Identity.GetUserId());
// Validate against the tenant Id of your own storage, and use this code to invalidate the request if it is trying to exploit:
actionContext.Response = actionContext.Request.CreateResponse(System.Net.HttpStatusCode.Unauthorized, "Invalid Token");
}
return base.OnActionExecutingAsync(actionContext, cancellationToken);
}
}
Then applies the Filter to all actions by registering it in either FilterConfig or WebApiConfig:
config.Filters.Add(new DomainValidationFilter());

Resources