How to override AbpAuthorizationResources localization in AbpAuthorizationModule - abp

I've successfully overridden few localization resources but for some reason cant do it for the Authorization localization.
as u can see from my code below in my domain shared module if successfully added localization from different modules but for authorization its not working.
for example
this is just a part of response
"code": "Volo.Authorization:010002",
"message": "Authorization failed! Given policy has not granted: SimplyAir.TimeTracks",
in my sr.json localization file ive added this localization string and translated it like the rest of modules. but localization from auth module is not working
{
[DependsOn(
typeof(AbpAuditLoggingDomainSharedModule),
typeof(AbpBackgroundJobsDomainSharedModule),
typeof(AbpFeatureManagementDomainSharedModule),
typeof(AbpIdentityDomainSharedModule),
typeof(AbpIdentityServerDomainSharedModule),
typeof(AbpPermissionManagementDomainSharedModule),
typeof(AbpSettingManagementDomainSharedModule),
typeof(AbpTenantManagementDomainSharedModule),
typeof(AbpAccountApplicationContractsModule),
typeof(AbpAuthorizationModule), //i have added auth module
typeof(AbpUiModule)
)]
public class SimplyAirDomainSharedModule : AbpModule
{
public override void PreConfigureServices(ServiceConfigurationContext context)
{
SimplyAirGlobalFeatureConfigurator.Configure();
SimplyAirModuleExtensionConfigurator.Configure();
}
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpVirtualFileSystemOptions>(options =>
{
options.FileSets.AddEmbedded<SimplyAirDomainSharedModule>();
});
Configure<AbpLocalizationOptions>(options =>
{
options.Resources
.Add<SimplyAirResource>("sr")
.AddBaseTypes(typeof(AbpValidationResource))
.AddVirtualJson("/Localization/SimplyAir");
options.Resources
.Get<IdentityResource>()
.AddVirtualJson("/Localization/SimplyAir");
options.Resources
.Get<AbpAuthorizationResource>() //ive included auth resource
.AddVirtualJson("/Localization/SimplyAir");
options.Resources
.Get<AccountResource>()
.AddVirtualJson("/Localization/SimplyAir");
options.Resources
.Get<AbpUiResource>()
.AddVirtualJson("/Localization/SimplyAir");
options.DefaultResourceType = typeof(SimplyAirResource);
});
Configure<AbpExceptionLocalizationOptions>(options =>
{
options.MapCodeNamespace("SimplyAir", typeof(SimplyAirResource));
});
}
}
}

First, check you database table [AbpPermissionGrants], check whether permission SimplyAir.TimeTracks has been granted. If not, I think you can update a new database and seed grant data again, check the permission grant data.

Related

.NET Core 3.1 web application with React - how to prevent access based on Active Directory group

I have a .NET Core 3.1 web application with React using windows authentication.
When a user enters their Active Directory credentials i would like to verify they belong to a particular Active Directory group before allowing access to the React app.
I have tried setting the default endpoint to a Login Controller to verify the user's groups but i don't know how to redirect to the React app if they do have the valid group.
Startup.cs:
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller}/{action=Index}/{id?}",
defaults: new { Controller = "Login", action = "Index" });
});
LoginController:
public IActionResult Index()
{
if (HttpContext.User.Identity.IsAuthenticated)
{
string[] domainAndUserName = HttpContext.User.Identity.Name.Split('\\');
//AuthenticateUser verifies if the user is in the correct Active Directory group
if (AuthenticateUser(domainAndUserName[0], domainAndUserName[1]))
{
//This is where i would like to redirect to the React app
return Ok(); //This does not go to the react app
return LocalRedirect("http://localhost:50296/"); //This will keep coming back to this method
}
return BadRequest();
}
}
Is it possible to redirect to the React app from the controller?
Is there a better way to verify an active directory group, possibly through authorizationService.js?
I've been in this situation before, and solved it with custom implementation of IClaimsTransformation. This approach may also be used with OpenId Connect and other authentication systems that requires additional authorization.
With this approach, you can use authorize attribute on controller that serves your React app
[Authorize(Roles = "HasAccessToThisApp")]
and
User.IsInRole("HasAccessToThisApp")
elsewhere in code.
Implementation. Please note that TransformAsync will be called on every request, some caching is recommended if any time-consuming calls.
public class YourClaimsTransformer : IClaimsTransformation
{
private readonly IMemoryCache _cache;
public YourClaimsTransformer(IMemoryCache cache)
{
_cache = cache;
}
public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal incomingPrincipal)
{
if (!incomingPrincipal.Identity.IsAuthenticated)
{
return Task.FromResult(incomingPrincipal);
}
var principal = new ClaimsPrincipal();
if (!string.IsNullOrEmpty(incomingPrincipal.Identity.Name)
&& _cache.TryGetValue(incomingPrincipal.Identity.Name, out ClaimsIdentity claimsIdentity))
{
principal.AddIdentity(claimsIdentity);
return Task.FromResult(principal);
}
// verifies that the user is in the correct Active Directory group
var domainAndUserName = incomingPrincipal.Identity.Name?.Split('\\');
if (!(domainAndUserName?.Length > 1 && AuthenticateUser(domainAndUserName[0], domainAndUserName[1])))
{
return Task.FromResult(incomingPrincipal);
}
var newClaimsIdentity = new ClaimsIdentity(
new[]
{
new Claim(ClaimTypes.Role, "HasAccessToThisApp", ClaimValueTypes.String)
// copy other claims from incoming if required
}, "Windows");
_cache.Set(incomingPrincipal.Identity.Name, newClaimsIdentity,
DateTime.Now.AddHours(1));
principal.AddIdentity(newClaimsIdentity);
return Task.FromResult(principal);
}
}
In Startup#ConfigureServices
services.AddSingleton<IClaimsTransformation, YourClaimsTransformer>();

Identity server 4: intercept 302 and replace it with 401

I've got an app which is hosting simultaneously Identity Server 4 and a client app (Vue) which uses a couple of rest services defined in an area for managing the site. The idea is that users associated with a specific role can access the client app and call the rest services for performing the actions.
Currently, my problem is that when the api return 302 when the user doesn't belong to the admin role. I'd like to change this to a 401, but I'm having some problems with it.
If this was a simple aspnet core app, then I'd simply pass a lambda to the OnRedirectToLogin property of the cookie handler that takes care of the request. Unfortunately, IS4 will only allow me to set a couple of basic settings of the cookie (expiration and sliding). The same docs say that I can override the cookie handler. So, I've tried doing the following:
services.AddIdentityServer()
... // other configurations
services.AddAuthentication(sharedOptions => {
sharedOptions.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;//IdentityServerConstants.ExternalCookieAuthenticationScheme;
sharedOptions.DefaultChallengeScheme = IdentityServerConstants.SignoutScheme;
})
... //other external providers...
.AddCookie( CookieAuthenticationDefaults.AuthenticationScheme, options => {
options.Events = new CookieAuthenticationEvents {
OnRedirectToLogin = ctx => {
if (ctx.Request.Path.StartsWithSegments("/Admin", StringComparison.OrdinalIgnoreCase)) {
ctx.Response.StatusCode = (int) HttpStatusCode.Unauthorized;
}
return Task.CompletedTask;
};
});
I expected to seem my handler being called whenever a request is redirected to the login page, but it never happens. Can anyone help?
Thanks
EDIT: just to add that I'm also using aspnet identity for managing the user accounts...
Posting the answer here in case anyone is interested...
After some digging, I've found that using identity means that you can't customize the cookie handler by doing what I was doing. Fortunately, the ConfigureAuthenticationEvent that can be configured by the ConfigureApplicationCookie extension method already does the right thing: if it detects that the current request is an AJAX call, it will return 401; if not, it will return 302. And here was the problem: the request made from the vue client wasn't being considered an AJAX request because it wasn't setting the X-Request-With header to XMLHttpRequest.
So, all it was required was to configure axios to set the header in all the calls:
axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
I wrote a middleware sometime ago for this exact purpose and never looked back so if you don't find better solution, perhaps the solution can help you as well:
public class RedirectHandlingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RedirectHandlingMiddleware> _logger;
public RedirectHandlingMiddleware(RequestDelegate next, ILogger<RedirectHandlingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task Invoke(HttpContext context)
{
await HandleRedirect(context, ex);
await _next(context);
}
private Task HandleRedirect(HttpContext context)
{
if (context.Request.Path.StartsWithSegments("/Admin", StringComparison.OrdinalIgnoreCase) && context.Response.StatusCode == 302)
{
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
}
return Task.CompletedTask;
}
}
Just need to register in Startup.cs:
app.UseAuthentication();
app.UseMiddleware<RedirectHandlingMiddleware>();

How to set acr-values for Sustainsys external provider in identity server 3

I have Idsvr3 with local user accounts in SQL. In addition i have also configured external identity provider which support SAML2 using https://github.com/Sustainsys/Saml2 I followed the sample here
Now when user access the client application he gets redirected to login page which presents userid/password textboxes for local login and also a button to redirect to external provider.
I want to change this behavior. I want user directly goto external login based on some condition. I've read that I can pass the required login provider to the acr_values and IdSvr3 will directly go to external provider.
Here is how i registered external provider with IdSvr3 (Note some code is removed for brevity)
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.Map("/identity", idsrvApp =>
{
var identityServerOptions = new IdentityServerOptions
{
AuthenticationOptions = new AuthenticationOptions()
{
}
.Configure(ConfigureIdentityProviders),
};
idsrvApp.UseIdentityServer(identityServerOptions);
});
}
private void ConfigureIdentityProviders(IAppBuilder app, string signInAsType)
{
// SAML2
var options = new Saml2AuthenticationOptions(false)
{
SPOptions = new SPOptions
{
EntityId = new EntityId("https://localhost:44300/IdSrv3/Saml2"),
},
SignInAsAuthenticationType = signInAsType,
Caption = "SAML2p"
};
UseIdSrv3LogoutOnFederatedLogout(app, options);
options.SPOptions.ServiceCertificates.Add(new X509Certificate2(
AppDomain.CurrentDomain.SetupInformation.ApplicationBase + "/App_Data/Sustainsys.Saml2.Tests.pfx"));
options.IdentityProviders.Add(new IdentityProvider(
new EntityId("https://stubidp.sustainsys.com/Metadata"),
options.SPOptions)
{
LoadMetadata = true
});
app.UseSaml2Authentication(options);
}
}
and here is client application startup
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.UseCookieAuthentication(CK);
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
Authority = "https://localhost:44300/identity",
Scope = "openid profile email",
ClientId = "XXXXXXXXXXXXXXX",
RedirectUri = "http://localhost:36102/",
ResponseType = "id_token",
SignInAsAuthenticationType = "Cookies",
Notifications = new OpenIdConnectAuthenticationNotifications
{
RedirectToIdentityProvider = (n) =>
{
if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.AuthenticationRequest)
{
if(SomeCondition == true)
{
n.ProtocolMessage.AcrValues = "idp:saml2";
}
}
return Task.FromResult(0);
}
}
});
}
}
However identity server throws error External login error: provider requested saml2 is not a configured external provider
What is the valid name for Sustainsys/Saml2 provider and where is it configured?
I think i found it. The idp is actually the value of AuthenticationType property.
During external provider setup in IdentityServer3, the Saml2AuthenticationOptions by default sets the AutheticationType to Saml2.
So in client application i have to use exact same value as acr-values, it is case-sensitive. I was using small s instead of capital S. When i changed to Saml2 it worked.
I can also override AutheticationType to any string i want, and that is good because now i can setup multiple external IdP that supports SAML2 protocol and differentiate them by their AutheticationType
Also i found this documentation helpful
https://media.readthedocs.org/pdf/saml2/latest/saml2.pdf
Take a look how okta is configured with IdentityServer3 in section 2.5.4 Step 3: Configure your identity server with the new identity provider
Also from IdentityServer documentation
AuthenticationType must be a unique value to identify the external
identity provider. This value will also be used for the idp claim in
the resulting tokens. Furthermore the same value can be used to
pre-select identity providers during authorization/authentication
requests using the acr_values parameter (see this for more
information). This value is also used to restrict the allowed identity
providers on the Client configuration.

Custom endpoint for authorized clients on Identity Server 4

I want my Identity Server 4 server to offer an additional service (e.g., "MyAdditionalService") for SOME of the registered clients. That service will be consumed by them through a custom endpoint to be defined on the server.
I am thinking of defining an API for my that service (e.g., named "myAdditionalService") so that the access to such service can be granted to clients according to their configuration. However I am not sure how to restrict the access to the Endpoint (MVC - Action method) allowing only the clients (potentially on behalf of a user) that are allowed to consume the API.
I found out that I can do:
services.AddAuthorization(options =>
{
options.AddPolicy("MyAdditionalServicePolicy",
policy => policy.RequireClaim("scope",
"myAdditionalService"));
});
and use the attribute [Authorize("MyAdditionalServicePolicy")] to decorate the action method that is used to access such service. However, I don't know can the server be the API at the same time or even if it is possible.
How can I implement this? It is confusing that the token service plays the role of the API as well, since it protects access to an action method or endpoint.
Thanks.
UPDATE:
My web app is an IdentityServerWithAspNetIdentity which already use the Authentication mechanism of Asp.net core Identity. For the sake of the example, the additional service my web app if offering to some registered clients is the list of Twitter friends of a user (Modeled on a controller called Twitter, action called ImportFriends) the api is consequently called "TwitterFriends"
As per suggestion in response below, I modified my Configure() method to have app.UseJwtBearerAuthentication(). I already had app.UseIdentity() and app.UseIdentityServer() as shown below:
app.UseIdentity();
app.UseIdentityServer();
app.UseJwtBearerAuthentication(new JwtBearerOptions
{
AuthenticationScheme = "Bearer",
Authority = Configuration["BaseUrl"],
Audience = "TwitterFriends",
RequireHttpsMetadata = false //TODO: make true, it is false for development only
});
// Add external authentication middleware below. To configure them please see http://go.microsoft.com/fwlink/?LinkID=532715
app.UseGoogleAuthentication(new GoogleOptions
{
AuthenticationScheme = "Google",
SignInScheme = "Identity.External", // this is the name of the cookie middleware registered by UseIdentity()
And on a dedicated controller:
[Authorize(ActiveAuthenticationSchemes = "Identity.Application,Bearer")]
//[Authorize(ActiveAuthenticationSchemes = "Identity.Application")]
//[Authorize(ActiveAuthenticationSchemes = "Bearer")]
[SecurityHeaders]
public class TwitterController : Controller
{...
but I am getting this in the log:
info: Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware
[7]
Identity.Application was not authenticated. Failure message: Unprotect tic
ket failed
info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2]
Authorization failed for user: (null).
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[1]
Authorization failed for the request at filter 'Microsoft.AspNetCore.Mvc.A
uthorization.AuthorizeFilter'.
info: Microsoft.AspNetCore.Mvc.ChallengeResult[1]
Executing ChallengeResult with authentication schemes (Identity.Applicatio
n, Bearer).
info: Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware
[12]
AuthenticationScheme: Identity.Application was challenged.
info: Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerMiddleware[12]
AuthenticationScheme: Bearer was challenged.
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[2]
Executed action IdentityServerWithAspNetIdentity.Controllers.TwitterContro
ller.ImportFriends (IdentityServerWithAspNetIdentity) in 86.255ms
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
Request finished in 105.2844ms 401
I have tried different combinations of the attribute but it seems that Identity.Application and Bearer don't get along in this scenario: getting 401.
any help is appreciated.
Thanks..
See this example on how to host an API in the same web app as IdentityServer.
https://github.com/brockallen/IdentityServerAndApi
In essence you need to add the JWT token validation handler:
services.AddAuthentication()
.AddJwtBearer(jwt =>
{
jwt.Authority = "base_address_of_identityserver";
jwt.Audience = "name of api";
});
On the API itself you must select the JWT authentication scheme:
public class TestController : ControllerBase
{
[Route("test")]
[Authorize(AuthenticationSchemes = "Bearer")]
public IActionResult Get()
{
var claims = User.Claims.Select(c => new { c.Type, c.Value }).ToArray();
return Ok(new { message = "Hello API", claims });
}
}
If you want to enforce an additional authorization policy, you can either pass that into the [Authorize] attribute or call it imperatively.
To achieve this, first you have to write some policy. Policy will define the boundry of accessibility of that specific api.
So you will assign the some scope to registered clients. let's say scope name is "ApiOnlyForRegisteredClients".
So we will create the policy as below:
services.AddAuthorization(options =>
{
options.SetRegisteredClientsPolicy();
}
and
private static void RequireScope(this AuthorizationPolicyBuilder authorizationPolicyBuilder, string[] values)
{
authorizationPolicyBuilder.RequireClaim("scope", values);
}
private static void SetRegisteredClientsPolicy(this AuthorizationOptions options)
{
options.AddPolicy(
OpenIdPolicies.Clients.RegisteredClients,
policyBuilder =>
{
policyBuilder.RequireAuthenticatedUser();
policyBuilder.RequireScope(new string[] { "ApiOnlyForRegisteredClients" });
});
}
Once it done, you are done with policy creation.
Make sure while creating the access token, you are put the same value "ApiOnlyForRegisteredClients" in scope claim.
Now we have to add one api and label it with [Authorize] attribute.
[Authorize(AuthenticationSchemes = "Bearer", Policy = OpenIdPolicies.Clients.RegisteredClients)]
public async Task<ActionResult<T>> Post(int userId, [FromBody] List<int> simRoleIds)
{
}
Now we have to add jwt authentication middleware.
.AddJwtBearer("Bearer", options =>
{
options.Authority = configuration["AuthorityAddresses"];
options.RequireHttpsMetadata = Convert.ToBoolean(configuration["RequireHttpsMetadata"]);
options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
TokenDecryptionKey = new X509SecurityKey()
ValidAudiences = apiResources.Select(x => x.ResourceName).ToList(),
ValidIssuers = new List<string> { authorityAddressWithHttps.Uri.OriginalString, authorityAddressWithBasePathHttps.Uri.OriginalString, configuration["AuthorityAddresses"] }
};
})

Getting a XMLHttpRequest: Network Error 0x80070005, Access is denied

I have a token endpoint that is passed a username and password grant type to authenticate users. This token endpoint is called from an AngularJS service that is part of my MVC web front end. When I call the service I get the following error
XMLHttpRequest: Network Error 0x80070005, Access is denied.
This seems to be a CORS problem. I did the following to resolve this problem with no luck thus far
I added the app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll); to my Startup.cs file for my token web service.
public class Startup
{
public void Configuration
(
IAppBuilder app
)
{
ConfigureOAuth(app);
var config = new HttpConfiguration();
WebApiConfig.Register(config);
config.Filters.Add(new AuthorizeAttribute());
app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
app.UseWebApi(config);
}
private void ConfigureOAuth
(
IAppBuilder app
)
{
app.UseOAuthAuthorizationServer(new OAuthServerOptionsProvider().Provide());
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
}
I also have a class that is overriding the OAuthAuthorizationServerProvider, within the GrantResourceOwnerCredentials method I have the following code to add all origins to the response headers
public override async Task GrantResourceOwnerCredentials
(
OAuthGrantResourceOwnerCredentialsContext context
)
{
System.Web.HttpContext.Current.Response.Headers.Add("Access-Control-Allow-Origin", "*");
// Other code left out to keep this short
}
In fiddler I can see that the response header was successfully added
Is there something I'm missing here?
Update
Here is my request headers
First of all I have to point out that the comments made by #maurycy helped me find the solution in the comments of this stackoverflow post.
This post explains that the app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll); in the Startup.cs file should be moved to the top of the method and that the System.Web.HttpContext.Current.Response.Headers.Add("Access-Control-Allow-Origin", "*"); should be removed from the GrantResourceOwnerCredentials class. So I changed the code in my question to look something like this,
public class Startup
{
public void Configuration(IAppBuilder app)
{
ConfigureOAuth(app);
var config = new HttpConfiguration();
WebApiConfig.Register(config);
config.Filters.Add(new AuthorizeAttribute());
app.UseWebApi(config);
}
private void ConfigureOAuth(IAppBuilder app)
{
app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
app.UseOAuthAuthorizationServer(new OAuthServerOptionsProvider().Provide());
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
}
}
I also have changed the class that is overriding the OAuthAuthorizationServerProvider
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
// Remove the response header that was added
// Other code left out to keep this short
}

Resources