I have integrated Spring Cloud Gateway with OAuth2 server. It works well with single instance gateway. here is my security config.
#EnableWebFluxSecurity
public class GatewaySecurityConfiguration {
#Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange().pathMatchers("/user/v3/api-docs", "/actuator/**").permitAll()
.anyExchange().authenticated()
.and()
.oauth2Login()
.and()
.csrf().disable();
return http.build();
}
But, When I scale gateway to 2 instances, some requests works expected, however some requests return 401.
load balancer (kubernetes nodeport service)
/ \
gateway gateway
\ /
(microservice clusters)
When I logged in first instance of gateway, the principal object is created successfully and also assign session to redis. If next request comes to second instance, it returns 401 because it has not principal.
how can i solve this problem?
ps: i am using redis for web sessions to commonize session informations between gateways.
TL;DR
You can share session principal information on Redis through WebSession. But you can't share access token(JWT), because they are stored in-memory at servers.
Solution-1: Your requests should always go to the server where you are logged in. (details below)
Solution-2: Implement new ReactiveOAuth2AuthorizedClientService bean that stores sessions in redis. (details below too)
Long Answer
From Spring Cloud documentation (https://cloud.spring.io/spring-cloud-static/Greenwich.SR5/multi/multi__more_detail.html);
The default implementation of ReactiveOAuth2AuthorizedClientService
used by TokenRelayGatewayFilterFactory uses an in-memory data store.
You will need to provide your own implementation
ReactiveOAuth2AuthorizedClientService if you need a more robust
solution.
The first thing you know: When you login successfully, the access token(as jwt) is returned by oauth2 server, and server creates session and this session is mapped to access token on ConcurrentHashMap (authorizedClients instance InMemoryReactiveOAuth2AuthorizedClientService class).
When you request API Gateway to access microservices with your session id, the access token(jwt) is resolved by TokenRelayGatewayFilterFactory in gateway, and this access token is set in Authorization header, and the request is forwarding to microservices.
So, let me explain how TokenRelayGatewayFilterFactory works (assume that you use WebSession through Redis and you have 2 gateway instances and you logged in at instance-1.)
If your request goes to instance-1, the principal is get back by session id from redis, then authorizedClientRepository.loadAuthorizedClient(..) is called in filter. This repository is instance of AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository object. The isPrincipalAuthenticated() method returns true, so the flow goes on authorizedClientService.loadAuthorizedClient(). this service is defined as ReactiveOAuth2AuthorizedClientService interface, and it has only one implementation ( InMemoryReactiveOAuth2AuthorizedClientService). This implementation has ConcurrentHashMap(key: principal object, value: JWT)
If your request goes to instance-2, all flow above are valid. But reminder that ConcurrentHashMap no access token to principal, because the access token is stored in instance-1's ConcurrentHashMap. So, the access token is empty, then your request downstreams without Authorization header. You will get 401 Unauthorized.
Solution-1
So, your requests should always go to the server where you are logged in to get valid access token.
If you use NGINX as load balancer, then use ip_hash in upstream.
If you use kubernetes service as load balancer, then use ClientIP in session affinity.
Solution-2
InMemoryReactiveOAuth2AuthorizedClientService is only implementation of ReactiveOAuth2AuthorizedClientService. So, create new implementation that uses Redis, and then do it primary bean.
#RequiredArgsConstructor
#Slf4j
#Component
#Primary
public class AccessTokenRedisConfiguration implements ReactiveOAuth2AuthorizedClientService {
private final SessionService sessionService;
#Override
#SuppressWarnings("unchecked")
public <T extends OAuth2AuthorizedClient> Mono<T> loadAuthorizedClient(String clientRegistrationId, String principalName) {
log.info("loadAuthorizedClient for user {}", principalName);
Assert.hasText(clientRegistrationId, "clientRegistrationId cannot be empty");
Assert.hasText(principalName, "principalName cannot be empty");
// TODO: When changed immutability of OAuth2AuthorizedClient, return directly object without map.
return (Mono<T>) sessionService.getSessionRecord(principalName, "accessToken").cast(String.class)
.map(mapper -> {
return new OAuth2AuthorizedClient(clientRegistration(), principalName, accessToken(mapper));
});
}
#Override
public Mono<Void> saveAuthorizedClient(OAuth2AuthorizedClient authorizedClient, Authentication principal) {
log.info("saveAuthorizedClient for user {}", principal.getName());
Assert.notNull(authorizedClient, "authorizedClient cannot be null");
Assert.notNull(principal, "principal cannot be null");
return Mono.fromRunnable(() -> {
// TODO: When changed immutability of OAuth2AuthorizedClient , persist OAuthorizedClient instead of access token.
sessionService.addSessionRecord(principal.getName(), "accessToken", authorizedClient.getAccessToken().getTokenValue());
});
}
#Override
public Mono<Void> removeAuthorizedClient(String clientRegistrationId, String principalName) {
log.info("removeAuthorizedClient for user {}", principalName);
Assert.hasText(clientRegistrationId, "clientRegistrationId cannot be empty");
Assert.hasText(principalName, "principalName cannot be empty");
return null;
}
private static ClientRegistration clientRegistration() {
return ClientRegistration.withRegistrationId("login-client")
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.clientId("dummy").registrationId("dummy")
.redirectUriTemplate("dummy")
.authorizationUri("dummy").tokenUri("dummy")
.build();
}
private static OAuth2AccessToken accessToken(String value) {
return new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, value, null, null);
}
}
Notes:
SessionService is my custom class that interacts redis with reactivehashoperations instance.
The best way is to store OAuth2AuthorizedClient instead of access token. But, it is too hard now (https://github.com/spring-projects/spring-security/issues/8905)
The TokenRelayGatewayFilterFactory uses an in-memory data store to store the OAuth2AuthorizedClient which includes the (JWT) access token. This data store is not shared between multiple gateways.
To share the OAuth2AuthorizedClient information with Spring Session through Redis provide the following configuration:
#Bean
public OAuth2AuthorizedClientRepository authorizedClientRepository() {
return new HttpSessionOAuth2AuthorizedClientRepository();
}
For reactive WebSessions:
#Bean
public ServerOAuth2AuthorizedClientRepository authorizedClientRepository() {
return new WebSessionServerOAuth2AuthorizedClientRepository();
}
Further information for this configuration can be found at https://github.com/spring-projects/spring-security/issues/7889
Related
I have two projects
WebApp(SpringMVC)
Microservice
The idea is that I have a page in which I list all the users from DB, so basically I need a listener on WebApp side and a producer in the microservice side, typically the flow is as follow
Whitout rabbitmq(synchronous)
Click page "List Users"
UserController that redirect me to a specific service
public List<User>getUsers(){//no args!!
service.getUsers();//no args
}
UserService with logic to access DB and retrieve all users
public List<User>getUsers(){//no args!!
//connect to DB and retrieve all users
return users
}
Render users on jsp
With RabbitMQ and assuming users has already been produced the list of users on the microservice's side
My question is about if I introduce rabbitmq then I need a method in which I listen a message(List of products as a JSON) but now the flow change a little bit comparing with the first one because
Click button "List Users"
Controller need a method findAll(Message message), here I need pass a message because the service is expecting one as the service is Listener
public List<User>getUsers(Message message){
service.getAllUsers(**String message**);
}
The service as right now is listen a message, I need to pass a Message
arg in which I will be listen the queues
#RabbitListener(queues = "${queue}", containerFactory = "fac")
public List<User> getUsers(String message){
//Transform JSON to POJO
//some logic...
return users;
}
So basically my question is the second flow is correct?
If so how I have to pass the Message object from controller to service
because in controller I do not needed a Message, but in order to
listen I have, is this correct?
If so how pass the message arg
There is a better way to achieve this?
Regards
Use a Jackson2JsonMessageConverter and the framework will do all the conversion for you.
Controller:
List<User> users = template.convertSendAndReceiveAsType("someExchange", "someRoutingKey",
myRequestPojo, new ParameterizedTypeReference<List<User>>() {});
Server:
#RabbitListener(queues = "${queue}", containerFactory = "fac")
public List<User> getUsers(SomeRequestPojo request){
//some logic...
return users;
}
Wire the converter into both the listener container factory and the RabbitTemplate.
Request Pojo and Users have to be Jackson-friendly (no-arg constructor, setters).
I got an app and I wanna create a connection to my rest-api.
Each user will get a "token" which will automatically be refreshed by google and co. In my requests, I will send the token and if it can be resolved to the user, the request should be answered, else if it is not up to date, I just wanna drop the request and return an error.
Are there still some possibilities?
Thanks for your help!
Current starting:
https://gist.github.com/PascalKu/97bca9506ad4f31c9e13f8fe8973d75b
You need to implement custom authentication in spring. I did the same thing but I had a db like:
fb_email_address | user_id | other_fields...
You must create these classes:
#Component
class TokenAuthenticationFilter extends OncePerRequestFilter {
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain) {
String theToken = request.getParameter('theToken');
TokenAuthentication tokenAuth = new TokenAuthentication(theToken)
SecurityContextHolder.getContext().setAuthentication(tokenAuth)
}
}
You need to add the authentication provider to spring's security system:
#Configuration
#EnableWebSecurity
class WebConfigHolder extends WebSecurityConfigurerAdapter implements WebMvcConfigurer {
#Autowired private TokenAuthenticationProvider tokenAuthenticationProvider
#Override
#Autowired
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(tokenAuthenticationProvider)
}
}
Implement authentication provider which actually checks to see if the token is valid.
#Component
class TokenAuthenticationProvider implements AuthenticationProvider {
//called by provider manager at some point during execution of security filters, I think
//it's the security api's job to call this
//the fbauthentication we create in our fbauthenticationfilter gets passed into this
#Override
#Transactional
Authentication authenticate(Authentication auth) {
TokenAuthentication tokenAuthentication = (TokenAuthentication) auth;
String theToken = auth.getThetoken();
boolean theTokenIsInDB = ///CHECK TO SEE IF TOKEN IS IN DB
if(theTokenIsInDB) {
TokenAuthentication t = new TokenAuthentication();
t.setAuthenticated(true);
return t;
} else {
throw new BadCredentialsException("Could not find user");
}
}
#Override
boolean supports(Class<?> authentication) {
boolean ret = TokenAuthentication.isAssignableFrom(authentication)
return TokenAuthentication.isAssignableFrom(authentication)
}
}
You need a simple Authentication Class that is just the object that's used to store the credentials while spring is waiting for the thread to get to the spring security filter; once it gets to that filter it passes authentication objects to the providers that support them. This allows you to have multiple authentication methods like FB, Google, custom tokens, etc... In my app I use FB tokens and in my provider, I check to see if the FB token corresponds to an authorized email address on my whitelist of email addresses. If it does, the user gets access to my app.
public class TokenAuthentication extends Authentication{
String token;
boolean isAuthenticated = false;
public TokenAuthentication(String theToken) { this.token = theToken;}
//getters and setters
}
What this code all does is, whenever someone accesses your API such as /api/person/get?theToken=132x8591dkkad8FjajamM9
The filter you created is run on every request. It checks to see if theToken was passed in and adds the TokenAuthentication to spring security.
At some point in the filter chain, spring security filter will run, and it will see that a TokenAuthentication has been created, and will search for a provider that can perform authentication on that. That happens to be your TokenAuthenticationProvider.
TokenAuthenticationProvider does the actual authentication. If it returns an authentication object that has isAuthenticated set to true, then the user will be allowed to access that api call.
Once authenticated, a user doesn't need to pass theToken again until his cookies are cleared or you invalidate his session. So he can call /api/person without the query parameters for the rest of his interactions. That's because the authentication is stored as a session-scoped data in spring.
Hope that helps. Let me know if anything's missing.
It is possible to have host address in app services?
For example we want send email to customer with specific link point to site address. How is this possible?
This came up via Google & the existing answer didn't really help. I don't necessarily agree that app services are the wrong domain for this; in ABP for example, a service is closely connected to a controller and services usually only exist in order to service web requests there. They often execute code in an authorised state that requires a signed-in user, so the whole thing is happening in the implicit domain context of an HTTP request/response cycle.
Accordingly - https://learn.microsoft.com/en-us/aspnet/core/fundamentals/http-context?view=aspnetcore-2.2#use-httpcontext-from-custom-components
Add services.AddHttpContextAccessor(); to something like your Startup.cs - just after wherever you already call services.AddMvc().
Use dependency injection to get hold of an IHttpContextAccessor in your service - see below.
Using constructor-based dependency injection, we add a private instance variable to store the injected reference to the context accessor and a constructor parameter where the reference is provided. The example below has a constructor with just that one parameter, but in your code you probably already have a few in there - just add another parameter and set _httpContextAccessor inside the constructor along with whatever else you're already doing.
using HttpContext = Microsoft.AspNetCore.Http.HttpContext;
using IHttpContextAccessor = Microsoft.AspNetCore.Http.IHttpContextAccessor;
// ...
public class SomeService : ApplicationService
{
private readonly IHttpContextAccessor _httpContextAccessor;
public SomeService(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
}
Now service code can read the HTTP context and from there things like the HTTP request's host and port.
public async Task<string> SomeServiceMethod()
{
HttpContext context = _httpContextAccessor.HttpContext;
string domain = context.Request.Host.Host.ToLowerInvariant();
int? port = context.Request.Host.Port;
// ...
}
HttpContext.Current.Request.Url
can get you all the info on the URL. And can break down the url into its fragments.
My WCF Webservice provide all data manipulation operations and my ASP .Net Web application present the user interface.
I need to pass user information with many wcf methods from ASP .Net app to WCF app.
Which one in is better approach regarding passing user info from web app to web service?
1) Pass user information with SOAP header?
ASP .Net Application has to maintain the number of instances of WCF Webservice client as the number of user logged in with the web application. Suppose 4000 user are concurrently active, Web app has to maintain the 4000 instances of WCF webserice client.
Is it has any performance issue?
2) Pass user information with each method call as an additional parameter?
Every method has to add this addtional paramter to pas the user info which does not seems a elegant solution.
Please suggest.
regards,
Dharmendra
I believe it's better to pass some kind of user ID in a header of every message you send to your WCF service. It's pretty easy to do, and it's a good way to get info about user + authorize users on service-side if needed. And you don't need 4000 instances of webservice client for this.
You just need to create Behavior with Client Message Inspector on client side(and register it in your config). For example:
public class AuthClientMessageInspector: IClientMessageInspector
{
public void AfterReceiveReply(ref Message reply, object correlationState)
{
}
public object BeforeSendRequest(ref Message request, IClientChannel channel)
{
request.Headers.Add(MessageHeader.CreateHeader("User", "app", "John"));
return null;
}
}
public class ClientBehavior : IEndpointBehavior
{
public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
{
}
public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
{
foreach (var operation in endpoint.Contract.Operations)
{
operation.Behaviors.Find<DataContractSerializerOperationBehavior>().MaxItemsInObjectGraph = Int32.MaxValue;
}
var inspector = new AuthClientMessageInspector();
clientRuntime.MessageInspectors.Add(inspector);
}
public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
{
}
public void Validate(ServiceEndpoint endpoint)
{
}
}
And extract it from your service-side:
var headers = OperationContext.Current.IncomingMessageHeaders;
var identity = headers.GetHeader<string>("User", "app");
I'm using the SignalR Javascript client and ASP.NET ServiceHost. I need the SignalR hubs and callbacks to only be accessible to logged in users. I also need to be able to get the identity of the currently logged in user from the Hub using the FormsIdentity from HttpContext.Current.User.
How do I secure the hub's so that only authenticated users can use SignalR?
How do I get the identity of the currently logged in user from the Hub?
You should use the this.Context.User.Identity that is available from the Hub. See a related question
EDIT: To stop unauthenticated users:
public void ThisMethodRequiresAuthentication()
{
if(!this.Context.User.Identity.IsAuthenticated)
{
// possible send a message back to the client (and show the result to the user)
this.Clients.SendUnauthenticatedMessage("You don't have the correct permissions for this action.");
return;
}
// user is authenticated continue
}
EDIT #2:
This might be better, just return a message
public string ThisMethodRequiresAuthentication()
{
if(!this.Context.User.Identity.IsAuthenticated)
{
// possible send a message back to the client (and show the result to the user)
return "You don't have the correct permissions for this action.");
// EDIT: or throw the 403 exception (like in the answer from Jared Kells (+1 from me for his answer), which I actually like better than the string)
throw new HttpException(403, "Forbidden");
}
// user is authenticated continue
return "success";
}
You can lock down the SignalR URL's using the PostAuthenticateRequest event on your HttpApplication. Add the following to your Global.asax.cs
This will block requests that don't use "https" or aren't authenticated.
public override void Init()
{
PostAuthenticateRequest += OnPostAuthenticateRequest;
}
private void OnPostAuthenticateRequest(object sender, EventArgs eventArgs)
{
if (Context.Request.Path.StartsWith("/signalr", StringComparison.OrdinalIgnoreCase))
{
if(Context.Request.Url.Scheme != "https")
{
throw new HttpException(403, "Forbidden");
}
if (!Context.User.Identity.IsAuthenticated)
{
throw new HttpException(403, "Forbidden");
}
}
}
Inside your hub you can access the current user through the Context object.
Context.User.Identity.Name
For part 1. of your question you could use annotations like below (This worked with SignalR 1.1):
[Authorize]
public class MyHub : Hub
{
public void MarkFilled(int id)
{
Clients.All.Filled(id);
}
public void MarkUnFilled(int id)
{
Clients.All.UnFilled(id);
}
}
Something missing from the other answers is the ability to use SignalR's built in custom auth classes. The actual SignalR documentation on the topic is terrible, but I left a comment at the bottom of the page detailing how to actually do it (Authentication and Authorization for SignalR Hubs).
Basically you override the Provided SignalR AuthorizeAttribute class
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public class CustomAuthAttribute : AuthorizeAttribute
Then you decorate your hubs with [CustomAuth] above the class declaration. You can then override the following methods to handle auth:
bool AuthorizeHubConnection(HubDescriptor hubDesc, IRequest request);
bool AuthorizeHubMethodInvocation(IHubIncomingInvokerContext hubContext, bool appliesToMethod);
Since I'm on IIS servers and have a custom auth scheme, I simply return true from the AuthorizeHubConnection method, because in my Auth HttpModule I already authenicate the /signalr/connect and /signalr/reconnect calls and save user data in an HttpContext item. So the module handles authenticating on the initial SignalR connection call (a standard HTTP call that initiates the web socket connection).
To authorize calls on specific hub methods I check method names against permissions saved in the HttpContext (it is the same HttpContext saved from the initial connect request) and return true or false based on whether the user has permission to call a certain method.
In your case you might be able to actually use the AuthorizeHubConnection method and decorate your hub methods with specific roles, because it looks like you are using a standardized identity system, but if something isn't working right you can always revert to brute force with HttpModule (or OWIN) middle-ware and looking up context data in on subsequent websocket calls with AuthorizeHubMethodInvocation.