ZUUL Dynamic Routing with Eureka - netflix-zuul

I have a zuul pre filter:
#Override
public Object run() {
// read authenticated User from session
RequestContext context = RequestContext.getCurrentContext();
AuthenticatedUser authUser = SessionUtil.getAuthUser();
if (authUser == null) {
// if not logged in
// route to login service
}
// if logged in
// add additional headers to requests
return null;
}
So in this filter, I first see if the request is authenticated. If not, I want to re-direct it to a login service in Eureka and then after the user logs in, he should be forwarded to the service he originally requested.
ZUUL config is like:
zuul:
ignoredServices: '*'
routes:
service1:
path: /s1/**
serviceId: ${env}.s1
stripPrefix: false
service2:
path: /s2/**
serviceId: ${env}.s2
stripPrefix: false
login:
path: /login/**
sensitiveHeaders:
serviceId: ${env}.login
eureka.client.serviceUrl.defaultZone=http://hostname:8761/eureka/
eureka.instance.non-secure-port-enabled=true
Basically, I'd like to know how I can do the routing with Eureka in the filter code.
EDIT:
I think a better design might be just to redirect to zuul itself: get the host and append /login. So the zuul routing settings in yml will take effect. I still wonder how to do it, and then forward the request to its original destination.

Related

Spring Authorization Server with custom login

I'm trying the new spring framework
<artifactId>spring-security-oauth2-authorization-server</artifactId>
I got the POC working perfectly from baeldung, but when I try go farther than the default config, I didn't managed to get things working.
I tried to configure a custom login page , with a custom path for POSTing user information, the login page is well displayed, but after POSTing the form (username/passwrod) I'm getting a 404 (NOT_FOUND)
Here is my config:
#Bean
#Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authServerSecurityFilterChain(HttpSecurity http) throws Exception {
// Authorization server Oauth2 default config commented
// OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
//Extracted from Oauth2 default config
OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer();
RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();
http
//Here is my custom form / post login config
.antMatcher("/**")
.formLogin()
.loginPage("/home")
.loginProcessingUrl("/mydomain/login")
.usernameParameter("identifier")
.permitAll()
.and()
.authenticationProvider(customAuthenticationProvider)
.requestMatcher(endpointsMatcher)
.authorizeRequests().antMatchers("/js/**","/assets/**", "/css/**","/home**", "/mydomain/**").permitAll()
.and()
//Extracted from Oauth2 default config``
.authorizeRequests((authorizeRequests) -> {
((ExpressionUrlAuthorizationConfigurer.AuthorizedUrl)authorizeRequests.anyRequest()).authenticated();
})
.csrf((csrf) -> {
csrf.ignoringRequestMatchers(new RequestMatcher[]{endpointsMatcher});
})
.apply(authorizationServerConfigurer);
return http.build();
thanks for help!
regards
Need to implement Controller like this:
#GetMapping("/login")
public String oauth2LoginPage(Model model,
#CurrentSecurityContext(expression = "authentication") Authentication authentication,
#Value("${spring.security.oauth2.server.login.captcha.enabled:true}") boolean enableCaptchaLogin,
#RequestAttribute(name = "org.springframework.security.web.csrf.CsrfToken", required = false) CsrfToken csrfToken) {
if (!(authentication instanceof AnonymousAuthenticationToken)){
return "redirect:/";
}
if (csrfToken != null) {
model.addAttribute("_csrfToken", csrfToken);
}
SystemSettings systemSettings = new SystemSettings();
model.addAttribute("enableCaptchaLogin",enableCaptchaLogin);
model.addAttribute("systemSettings", systemSettings);
return "oauth2_login";
}
There are a few key points about the default HTML form:
The form should perform a post to /login
The form will need to include a CSRF Token which is automatically
included by Thymeleaf.
The form should specify the username in a parameter named username
The form should specify the password in a parameter named password
If the HTTP parameter error is found, it indicates the user failed to
provide a valid username / password
If the HTTP parameter logout is found, it indicates the user has
logged out successfully
Reference link:https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/form.html

How do I dynamically specify OAuth2 resource details in Spring Security?

I'm creating an application integrating with Shopify's API, which uses OAuth2 for authentication and authorization. Using the tutorial for Spring Security OAuth2, and the tutorial for Shopify, I've been able to get integration working with a single shop. The YAML configuration looks like this:
shopify:
shop: myshop
scopes: read_customers,read_orders
security:
oauth2:
client:
clientId: myclientid
clientSecret: mysecret
tokenName: access_token
authenticationScheme: query
clientAuthenticationScheme: form
accessTokenUri: https://${shopify.shop}.myshopify.com/admin/oauth/access_token
userAuthorizationUri: https://${shopify.shop}.myshopify.com/admin/oauth/authorize?scope=${shopify.scopes}&grant_options[]=
pre-established-redirect-uri: https://myapp/login
registered-redirect-uri: https://myapp/login
use-current-uri: false
resource:
userInfoUri: https://${shopify.shop}.myshopify.com/admin/shop.json
However, this static configuration won't work for an app published in Shopify's App Store because the redirect, access token, user info, and user authorization URIs depend on the shop name. There are examples of using more than one provider, but they still have to be static.
To allow these URI's to be dynamic, I've come up with a few possible options:
Use a parameter in the /login path to identify the shop, then create a filter that adds the shop name to a ThreadLocal that runs before everything else, then dynamically create the AuthorizationCodeResourceDetails that is needed by the OAuth2 filter via a Spring proxied factory bean.
Use a sort of "metafilter" that dynamically recreates the OAuth2ClientAuthenticationProcessingFilter per request along with all of the resources that it needs.
Override OAuth2ClientAuthenticationProcessingFilter so that it can handle recreating the RestTemplate it needs to obtain the access token.
All of these options seem pretty difficult. What's a good way to handle dynamically-generated URI's for access tokens and user information in Spring Security OAuth2?
Also, since I'm new to OAuth2 in general, do I need to enable a Resource Server in my Spring configuration to protect my app with the access token?
a bit late but I did return a dynamic url for an oauth resource by overriding the getter for the Oauth2ProtectedResource
#Bean(name = "googleOauthResource")
public BaseOAuth2ProtectedResourceDetails getGoogleOauthResource() {
final AuthorizationCodeResourceDetails details = new AuthorizationCodeResourceDetails() {
#Override
public String getPreEstablishedRedirectUri() {
final RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (requestAttributes instanceof ServletRequestAttributes) {
final HttpServletRequest request = ((ServletRequestAttributes)requestAttributes).getRequest();
return request.getRequestURL() + "?" + request.getQueryString() + "&addStuff";
}
return super.getPreEstablishedRedirectUri();
}
};
details.setId("google-oauth-client");
details.setClientId("xxxxxxxxxxx");
details.setClientSecret("xxxxxxxx");
details.setAccessTokenUri("https://www.googleapis.com/oauth2/v4/token");
details.setUserAuthorizationUri("https://accounts.google.com/o/oauth2/v2/auth");
details.setTokenName("authorization_code");
details.setScope(Arrays.asList("https://mail.google.com/,https://www.googleapis.com/auth/gmail.modify"));
details.setPreEstablishedRedirectUri("http://localhost:8080/xxx-api-web/v2/gmail"); //TODO
details.setUseCurrentUri(false);
details.setAuthenticationScheme(AuthenticationScheme.query);
details.setClientAuthenticationScheme(AuthenticationScheme.form);
details.setGrantType("authorization_code");
return details;
}
I'm having the same problem you are and I'm leaning toward your first theory of using ThreadLocal storage. Here is how I'll probably go about my solution:
Set values from ServletRequest in LocalThread storage by overriding methods in OAuth2ClientAuthenticationProcessingFilter:
attemptAuthentication
successfulAuthentication
unsuccessfulAuthentication
requiresAuthentication
Then translate the URI's within the OAuth2RestTemplate by overriding the followign methods:
createRequest
doExecute
appendQueryParameter
I'll probably have to make my own #Bean for the RestTemplate that has an injected #Service that will lookup the dynamic Shopify domain.
I'll post my solution if it works.

How to trigger OAuth2 authentication in Zuul API gateway if a downstream service responds with a 401

I'm experimenting with three Spring cloud (boot) applications.
An Authentication Server on port 9999
A basic backend-sample that has secured and unsecured endpoints on port 9008
A basic Zuul API gateway with several routes (secured and unsecured) to the backend-sample on port 9000
The backend-sample boot application is annotated as a resource server (#EnableResourceServer) and secures some endpoints with a ResourceServerConfigurerAdapter
When I first call one of the routes that are secured on the Zuul API gateway, I get redirected to the authentication server's login page. After logging in there, I get redirected to the secured route I initially requested. Secured backend-sample endpoints behave as expected which means that the backend-sample does get the granted roles for the supplied token. If I hit a backend-sample endpoint I don't have the proper role for, I get an OAuth 403 response. Everything fine in this case.
We need to put legacy services behind the API gateway as well. These render html and should be able to trigger a login when the user hits a secured area there. We can't secure these areas on API gateway route level as the legacy backends have complicated (grown) permission models for many different sub URLs.
Does anyone know a good way to make a Spring-cloud API gateway redirect to an authentication server's login in such a downstream 401-response case? I tried a simple redirect in a ZuulFilter of type "post" but failed as the response is already committed there.
Backend-sample application.yml;
server:
port: 9008
security:
oauth2:
resource:
userInfoUri: http://localhost:9999/uaa/user
API gateway application.yml:
server:
port: 9008
zuul:
proxy:
addProxyHeaders: true
sensitive-headers:
routes:
unsecured-backend-sample:
path: /unsecured-backend-sample/**
url: http://localhost:9008
authorized-backend-sample:
path: /authorized-backend-sample/**
url: http://localhost:9008/
user-role-secured-backend-sample:
path: /user-role-secured-backend-sample/**
url: http://localhost:9008/
xxx-role-secured-backend-sample:
path: /xxx-role-secured-backend-sample/**
url: http://localhost:9008/
security:
oauth2:
client:
accessTokenUri: http://localhost:9999/uaa/oauth/token
userAuthorizationUri: http://localhost:9999/uaa/oauth/authorize
clientId: acme
clientSecret: acmesecret
resource:
userInfoUri: http://localhost:9999/uaa/user
I finally found a solution that works great for me. I wrote a ZuulFilter that handles only 401 responses and redirects to login. It also saves the rejected request in an HTTP session request cache so the SavedRequestAwareAuthenticationSuccessHandler can redirect you back to the initially requested downstream service URL.
#Component
public class LoginOnDownstreamUnauthorizedResponseFilter extends ZuulFilter {
private Logger logger = LoggerFactory.getLogger(getClass());
private AuthenticationEntryPoint authenticationEntryPoint = new LoginUrlAuthenticationEntryPoint("/login");
private RequestCache requestCache = new HttpSessionRequestCache();
#Override
public boolean shouldFilter() {
// Only handle downstream 401s
return RequestContext.getCurrentContext().getResponse().getStatus() == HttpStatus.SC_UNAUTHORIZED;
}
#Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
HttpServletResponse response = ctx.getResponse();
// We need to put the rejected request in the request cache for SavedRequestAwareAuthenticationSuccessHandler
// to find it's way back to the initial request URI after successful authentication.
requestCache.saveRequest(request, response);
String text = String.format("Downstream service %s responded with a status code 401.", request.getRequestURI());
logger.debug(text + " Calling Authentication entry point.");
try {
authenticationEntryPoint.commence(request, response, new InsufficientAuthenticationException(text));
} catch (IOException | ServletException e) {
logger.error("Failed to redirect to Authentication entry point", e);
}
return null;
}
#Override
public String filterType() {
return "post";
}
#Override
public int filterOrder() {
// make sure to run before SendResponseFilter
return 500;
}
}

Spring Oauth 2 SSO, Zuul and OpenAM integration

Introduction, Requirements:
right now i am writing a Single Page Application with AngularJS which talks to a Spring REST API. For security purposes I would like to setup a reverse proxy with zuul which proxies every request to the API and verifies that the user is authenticated. Also, if the user is not authenticated he should be redirected to an OpenAM instance (functioning as OAuth 2 Authorization Server). If the user is authenticated the request should be forwarded to the API with a Json Web Token (JWT) in the Header, containing at least the LDAP groups of the User.
In short I would like to have something like a API Gateway similar to the solution in this tutorial: https://spring.io/blog/2015/02/03/sso-with-oauth2-angular-js-and-spring-security-part-v
Status quo
I setup the Spring Cloud Security and Zuul with the following config:
server:
port: 9000
spring:
oauth2:
sso:
home:
secure: false
path: /,/**/*.html
client:
accessTokenUri: http://openam.example.org:8080/OpenAMTest/oauth2/access_token
userAuthorizationUri: http://openam.example.org:8080/OpenAMTest/oauth2/authorize
clientId: bearer-client
clientSecret: clientsecret
scope: openid profile
resource:
userInfoUri: http://openam.example.org:8080/OpenAMTest/oauth2/userinfo
zuul:
routes:
exampleApp:
path: /example-application/**
url: http://openam.example.org:8081/example-application
The Application class looks like the following:
#SpringBootApplication
#EnableZuulProxy
#EnableOAuth2Sso
public class TestZuulProxy extends SpringBootServletInitializer {
public static void main(String[] args){
SpringApplication.run(TestZuulProxy.class, args);
}
#Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(applicationClass);
}
private static Class<TestZuulProxy> applicationClass = TestZuulProxy.class;
#Configuration
#Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
protected static class SecurityConfiguration extends OAuth2SsoConfigurerAdapter {
#Override
public void match(RequestMatchers matchers) {
matchers.anyRequest();
}
#Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/index.html", "/home.html", "/")
.permitAll().anyRequest().authenticated().and().csrf()
.csrfTokenRepository(csrfTokenRepository()).and()
.addFilterAfter(new CsrfHeaderFilter(), CsrfFilter.class);
}
private CsrfTokenRepository csrfTokenRepository() {
HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository();
repository.setHeaderName("X-XSRF-TOKEN");
return repository;
}
public class CsrfHeaderFilter extends OncePerRequestFilter {
#Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
CsrfToken csrf = (CsrfToken) request.getAttribute(CsrfToken.class
.getName());
if (csrf != null) {
Cookie cookie = WebUtils.getCookie(request, "XSRF-TOKEN");
String token = csrf.getToken();
if (cookie==null || token!=null && !token.equals(cookie.getValue())) {
cookie = new Cookie("XSRF-TOKEN", token);
cookie.setPath("/");
response.addCookie(cookie);
}
}
filterChain.doFilter(request, response);
}
}
}
}
Now when i go to the "example-application" i get forwarded to the OpenAM authorization login screen. When I type in the credentials I can access the "example-application". Console log on the Gateway Service:
2015-06-22 17:14:10.911 INFO 6964 --- [nio-9000-exec-3] o.s.c.s.o.r.UserInfoTokenServices : Getting user info from: http://openam.example.org:8080/OpenAMTest/oauth2/userinfo
2015-06-22 17:14:10.953 INFO 6964 --- [nio-9000-exec-3] o.s.b.a.audit.listener.AuditListener : AuditEvent [timestamp=Mon Jun 22 17:14:10 CEST 2015, principal=Aaccf Amar, type=AUTHENTICATION_SUCCESS, data={details=remoteAddress=0:0:0:0:0:0:0:1, sessionId=<SESSION>, tokenType=BearertokenValue=<TOKEN>}]
Http-Header read by Zuul Filter:
authorization --- Bearer c2b75b5a-c026-4e07-b8b9-81e9162c9277
x-forwarded-host --- localhost:9000
x-forwarded-prefix --- /example-application
So something works! I have an access-token that gets forwarded to the REST-API.
Problem
1) This solution does not really meet my requirements, because I don't want the REST API to call the token-endpoint of OpenAM. I want that a JWT with the nessessary claims gets passed to the API in the Header. Should I create a JWT in the Gateway (e.g. Zuul Filter) manually or is there another solution?
2) In the solution above, when the access-token expires Zuul keeps forwarding me to the API. Why is this? Doesn't Spring Oauth2 checks if the access-token expires? how can I implement that?
3) I also tried to configure the tokenInfoUri in application.yml, but then I am getting a "405 Method Not Allowed" exception, because I think OpenAM expects a GET request on the tokeninfo-Endpoint. Can I customize this somehow? Which Classes do I need to override/customize to change the request.
If you have an advices, ideas or possible solutions, let me know!
Thank you!
If you want to use a JWT in your application, configure OpenAM as an OpenID Connect provider (OpenAM 12.0 or later). Once the user has authenticated OpenAM will issue a JWT with a number of claims about the user. Your SPA can pass that along in requests to your service tier.
If you want a gateway to enforce AuthN/ AuthZ on the users session, you can use something like ForgeRock's OpenIG. This can act as a policy enforcement point, and has the ability to introspect JWT tokens.

Using an MVC web api, can I find out the domain a cors request

So this is insecure and isn't my favorite option, but I just want to know how possible it is. Is there a way I could find the host in a cors request so that i could send back a Access-Control-Allow-Origin: "domain.com" where domain is any domain that uses the api. The reason being is I would like to be able to "use credentials" with what is essentially a wildcard.
I could have my end user send me their host name, but if possible i would rather just pick it up from the request itself.
Its possible, assuming you are using the Microsoft.AspNet.WebApi.Cors to enable CORS on your API, you need to define your own Cors Policy Provider:
public class MyApiCorsPolicy : Attribute, ICorsPolicyProvider
{
private System.Web.Cors.CorsPolicy _policy;
public MyApiCorsPolicy ()
{
// Create a CORS policy.
_policy = new System.Web.Cors.CorsPolicy
{
AllowAnyMethod = true,
AllowAnyHeader = true,
SupportsCredentials = true
};
}
public Task<System.Web.Cors.CorsPolicy> GetCorsPolicyAsync(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
{
// Add the Request origin to the response.
_policy.Origins.Add(request.GetCorsRequestContext().Origin);
return Task.FromResult<System.Web.Cors.CorsPolicy>(_policy);
}
}
Next, in your ASP.NET Web Api config code you need to pass this policy when enabling Cors:
config.EnableCors(new MyApiCorsPolicy());

Resources