how to send tenantid to downstream services when using ocelot with abp - abp

I am currently using ABP and ocelot to build a api gateway for microservices. Everything works fine but now I want to use api gateway as a domain resolver. ABP's Domain Resolver works to resolve tenantid from different domains but i want ocelot to able to send the resolved tenant id to the downstream services(Include __tenant header in the headers of the request to the downstream service).

You can create a Middleware for adding ResolvedId to the request headers. So the ocelot will send the edited request to downstream with the tenantId.
Example:
public class MyMiddleware : IMiddleware, ITransientDependency
{
private readonly ICurrentTenant CurrentTenant;
public MyMiddleware(ICurrentTenant currentTenant)
{
CurrentTenant = currentTenant;
}
public Task InvokeAsync(HttpContext context, RequestDelegate next)
{
context.Request.Headers.Add("__tenant", CurrentTenant.Id.ToString());
return next(context);
}
}
As you see, the middleware manupilates the request, it's adding the CurrentTenantId to the request.
The CurrentTenant is found before by your "domain tenant resolver".
You can use this middleware between app.UseMultiTenancy() & app.UseOcelot()
...
app.UseMultiTenancy();
app.UseMiddleware<MyMiddleware>(); // it must be here, between them
app.UseOcelot();
...

Related

Azure Authentication from client app to another API

I'm trying to get azure AD authentication working between a Blazor WASM app, and another API that I have running locally but on a different port. I need both applications to use the Azure login, but I only want the user to have to log in once on the Blazor app which should then pass those credentials through to the API.
I've set up app registrations for both apps in the portal, created the redirect url, exposed the API with a scope and I can successfully log into the blazor app and see my name using #context.User.Identity.Name.
When it then tries to call the API though, I get a 401 error back and it doesn't hit any breakpoints in the API (presumably because there is no authentication being passed across in the http request).
My code in the Blazor app sets up a http client with the base address set to the API:
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.Services.AddHttpClient("APIClient", client => client.BaseAddress = new Uri("https://localhost:11001"))
.AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();
builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("APIClient"));
builder.Services.AddMsalAuthentication<RemoteAuthenticationState, CustomUserAccount>(options =>
{
builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
options.ProviderOptions.DefaultAccessTokenScopes.Add("api://d3152e51-9f5e-4ff7-85f2-8df5df5e2b2e/MyAPI");
//options.UserOptions.RoleClaim = "appRole";
});
await builder.Build().RunAsync();
}
In my API, I just have the Authorise attribute set on the class, and eventually will need roles in there too:
[Authorize]
[Route("api/[controller]")]
[ApiController]
public class CarController
Then, in my Blazor component, I then inject the http factory and try to make a request:
#inject IHttpClientFactory _factory
...
private async Task RetrieveCars()
{
var httpClient = _factory.CreateClient("APIClient");
HttpResponseMessage response = await httpClient.GetAsync("https://localhost:11001/api/cars");
var resp = await response.Content.ReadAsStringAsync();
cars = JsonSerializer.Deserialize<List<Car>>(resp);
}
but this returns the 401 error. I've also tried a few different variations like just injecting a http client (#inject HttpClient Http) but nothing seems to be adding my authorisation into the API calls. The options.UserOptions.RoleClaim is also commented out in the AddMsalAuthentication section as I wasn't sure if it was needed, but it doesn't work with or without it in there.
Can anyone explain what I'm doing wrong and what code I should be using?
Common causes.
Most cases ,we tend to forget to grant consent after giving API
permissions in the app registration portal,after exposing the api
which may lead to unauthorized error.
Other thing is when Audience doesn’t match the “aud” claim when we
track the token in jwt.io .Make sure ,Audience=clientId is configured
in the code in authentication scheme or Token validation parameters
by giving ValidAudiences.And also try with and without api:// prefix
in client id parameter.
Sometimes aud claim doesn’t match as we mistakenly send ID token
instead of Access tokens as access tokens are meant to call APIs .So
make sure you check mark both ID Token and access token in portal
while app registration.
While Enabling the authentication by injecting the [Authorize]
attribute to the Razor pages.Also add reference
Microsoft.AspNetCore.Authorization as(#using
Microsoft.AspNetCore.Authorization)
Please see the note in MS docs and some common-errors
If above are not the cases, please provide with additional error details and startup configurations or any link that you are following to investigate further.

How to perform external auth request from Identity Server app?

I am using Identity Server 4 and I need to interact with external authorization API.
The authorization process must be like that:
Client sends generated token based on user's data to IdentityServer
IdentityServer creates POST request with specific header and body.
IdentityServer send this request to ExternalAuthApi and gets a response containing token
IdentityServer returning that token to the Client (and caching it)
I looked through docs about External Identity Provider, but it requires interaction between Client and ExternalAuthApi in some way, which I need to avoid.
How to implement direct interaction between IdentityServer and ExternalAuthApi? Is it possible?
I've achieved this using ITokenCreationService.
I've created my own implementation and added to services: services.AddTransient<ITokenCreationService, ProviderBasedTokenCreationService >();
3rd party services are now called in CreateTokenAsync like
public override async Task<string> CreateTokenAsync(Token token)
{
var provider = token.GetProviderClaim();
switch (provider)
{
case "3rdPartySystem_A":
return this._systemAClient.RequestToken(token.TransformToSystemAFormat());
case "anotherSystem":
return this._anotherSystemClient.RequestToken(token.TransformToAnotherSystemFormat());
default:
return await base.CreateTokenAsync(token);
}
}

How to use Spring Boot CAS authentication with React frontend

My application has an API server (Spring Boot) and a client-facing server (React), the React server will make API requests on behalf of an authenticated user. For authentication, I use a CAS server provided by a third-party.
I used the Java Apereo CAS Client (version 3.6.2), and add the following API endpoints for the React app to call:
#RestController
public class CASController {
#Value("${cas.server-url-prefix}")
private String CASUrl;
#GetMapping("/login")
public String getCASUserId(HttpServletRequest request){
return request.getRemoteUser();
}
#GetMapping("/logout")
public void logout(HttpServletRequest request, HttpServletResponse response) {
HttpSession session = request.getSession(false);
if(session != null || !request.isRequestedSessionIdValid()) {
String id = session.getId();
session.invalidate();
log.info("JSESSIONID: " + id + " is valid: " + request.isRequestedSessionIdValid());
}
try{
response.sendRedirect(CASUrl + "/logout");
}
catch (IOException | IllegalStateException e){
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, e.getLocalizedMessage());
}
}
}
The above endpoints work if the Spring Boot app interacts with the client directly but it doesn't, it accepts requests from the React server. When the React app tries to call: localhost:8080/login, it failed because of the redirection.
A way to work around this is to call the CAS server from the React app, then send the required info (service URL & ticket issued by the CAS server) to the backend, and let the backend make the ticket validation call, if the ticket validation success, then the user is authenticated and a session id is sent back to the React app for subsequent requests.
My questions are:
Does the workaround even make sense? Authentification is split between the frontend (get a ticket from CAS server) and the backend (validate the issued ticket with CAS server and issue session). If it doesn't, what is a better solution?
For the workaround, I think I will need to use Spring Security for this instead of Java Apereo CAS Client? If that's the case, how do I do it?
I tried something like this:
Ticket validation endpoint:
#GetMapping("/login")
public String getCASUserId(#RequestParam #NotBlank String URL,
#RequestParam #NotBlank String ticket,
HttpServletRequest request){
// make the call the the CAS server for ticket validation
String userId = userService.getUserId(url, ticket);
HttpSession session = request.getSession();
session.setAttribute("userId", userId);
return userId;
}
The ticket is validated, session issued but no endpoint is actually protected since neither spring-security nor Java Apereo CAS Client is enabled.

Hosting asp.net core + ReactJS web app with SSL containing multiple CN or domain names is causing invalid issuer error

I am facing the following problem while hosting a web app built with asp.net core 3.1 and React.
We have used default visual studio template for React. ASP.NET Identity is used for authentication and authorization.
Authentication and Authorization work as expected as long as we host the website with an SSL certificate issued for single domain or CN. (e.g. example.com)
If we host he website with an SSL with multiple CNs (e.g. example.com, sub1.example.com, sub2.example.com), it works fine for any ONE of the domains. For the remaining domains we get the following behavior:
The login works as expected. The /connect/token path issues valid token. Once logged in, when we try to invoke any api (all apis are hosted under /api route), we get 401 unauthorized error. Error description in the header:
WWW-Authenticate: Bearer error="invalid_token", error_description="The issuer 'https://sub1.example.com' is invalid".
I also tried parsing the issued token on jwt.io. The iss field (issuer) is https://sub1.example.com which exactly matches the error description. I cannot fathom why identity engine refuses to identify the issuer for which it issued token for.
Here is relevant snippet from Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>();
services.AddAuthentication()
.AddIdentityServerJwt();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseAuthentication();
app.UseIdentityServer();
app.UseAuthorization();
}
Any ideas?
The new .Net (.net core) is highly configurable and modular. Usually the extension methods take a delegate which we can use to configure options. However, AddIdentityServerJwt method doesn't follow that convention.
I noticed long time ago that there is a property called ValidIssuers in TokenValidationParameters which can be configured with AddJwtBearer extension method. However, AddIdentityServerJwt extension method doesn't accept any options delegate as parameter.
It turns out that there is a special way to configure options.
services.AddAuthentication()
.AddIdentityServerJwt();
services.Configure<JwtBearerOptions>(IdentityServerJwtConstants.IdentityServerJwtBearerScheme, options =>
{
options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters()
{
ValidIssuers = new string[] { "https://sub1.example.com", "https://sub2.example.com", "https://sub3.example.com" }
};
});
Added this code and problem solved. Configuration can also be moved to appsettings.json.
This is probably happening as a result of receiving the token from an instance of IdentityServer4 on one CN, and trying to validate it with a request to IdentityServer4 using another CN. The IdentityServer component that's rejecting the token is TokenValidator's ValidateJwtAsync method. This method passes in the issuer into JwtSecurityTokenHandler's ValidateToken as a property of TokenValidationParameters. The issuer is retrieved from either the issuer configured on the IdentityServerOptions in the 'AddIdentityServer' extension method, or is dynamically generated from the request.
I can think of one way to resolve the validation problems, and that is to set the issuer on the IdentityServerOptions using the delegate passed into AddIdentityServer. This will result in the same issuer being set for all tokens issued, regardless of the CN it was accessed from. This would allow IdentityServer a single source of truth for issuer information, and will allow IdentityServer to know which issuer to verify against when a token comes in for validation.
Other solutions of trying to maintain the issuer are heavily restricted by the TokenValidator being an internal class that can't be inherited and easily replaced with an implementation that will validate against a list of valid issuers. Additionally, the IdentityServerOptions that's configured to have the issuer uri is registered as a singleton and cannot have its values changed. Other contrived implementation could be devised like attempting to dynamically change the host value on the HttpContext with a middleware (which I'm not sure is even possible since I've never tried), but anything that goes against IdentityServer4's design decision is not advised.
Please check url http://{url}/.well-known/openid-configuration
This url is should be true
Following codes are worked different domain.
Auth Startup
services.AddIdentityServer(options =>
{
options.IssuerUri = Configuration["ServerSettings:Authority"].ToString();
options.PublicOrigin = Configuration["ServerSettings:Authority"].ToString();
})
.AddDeveloperSigningCredential()
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddInMemoryClients(Config.GetClients())
.AddProfileService<ProfileService>();
Api Startup
services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(options =>
{
options.Authority = Configuration["ServerSettings:Authority"].ToString(); //"http://localhost:31864";
options.RequireHttpsMetadata = false;
options.ApiName = "api";
});
Works in the same domain but if different domain you should specify this

Intercepting cxf web service header with Apache Camel (Java DSL)

I created a web service client to handle cxf soap web services with apache camel.
String serviceUri = "cxf:http://localhost:10000/myservice?serviceClass=" +
MyRequest.class.getCanonicalName();
from(uri).to("mock:xyz");
The web service receives the soap call but throws an exception since the request requires a handling for wss.
org.apache.cxf.binding.soap.SoapFault: MustUnderstand headers: [{http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd}Security] are not understood.
The reason is, that the service requires ws security, which can be seen by lloking at the request.
<SOAP-ENV:Header><wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" SOAP-ENV:mustUnderstand="1">
I found out that I need to implement an interceptor to handle header properties.
My questions:
How can I add an interceptor to handle the header attributes with Camel Java-DSL?
Will this be sufficient to get rid of the SOAP Fault?
You can do it through
cxfEndpointConfigurer option #see: Camel-CXF configuration
(I use Spring (it is much easier)), but I guess for DSL URI will look like:
String serviceUri = "cxf:http://localhost:10000/myservice?serviceClass=" +
MyRequest.class.getCanonicalName() +
"&cxfEndpointConfigurer="+ MyConfigurer.class.getCanonicalName();
by implementing org.apache.camel.component.cxf.CxfEndpointConfigurer you have ability to add an Interceptor inside configureServer method
server.getEndpoint().getInInterceptors().add(new MyJAASLoginInterceptor());
if you run your Camel in container with JAAS (like JBOSS) you can use extension from
org.apache.cxf.interceptor.security.JAASLoginInterceptor
with needed callback handler.
Simple example which validates user/password from WSS header against JBOSS users:
public class MyJAASLoginInterceptor extends javax.security.auth.callback.JAASLoginInterceptor {
#Override
protected CallbackHandler getCallbackHandler(String name, String password) {
return new org.apache.cxf.interceptor.security.NamePasswordCallbackHandler(name, password, "setCredential");
}
}

Resources