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.
Related
I'm trying to build an Identity Provider using Spring authorization-server that third party applications are going to use for FIM (federated identity management).
We want each OAuth client to require authentication (if a user tries to login with a different client they would need to authenticate for each client).
Out of the box the flow looks like this:
So there's 2 issues.
The /oauth2/authorize endpoint just checks whether or not the sessions principal is authenticated, it doesn't care or know which client the principal was meant for.
There's just a single /login endpoint, so during authentication it doesn't know which client is used.
My best bet here is that I should:
Make the oauth2/authorize endpoint redirection to /login include the query parameter client_id
Create a custom AuthenticationFilter that also adds the client_id to the User principal
Override the authorizationRequestConverter for the oauth2/authorize endpoint and validate that the client in the request is the same as the client stored on the authenticated principal
Am I missing anything or do anyone know of a simpler way of doing this?
Based on your last comment, it seems one possibility is to simply require authentication every time, or at least every time an authorization is requested. In that case, you could clear out the authentication after the authorization code is issued to the client, using a Filter. This doesn't seem ideal and will result in a poor user experience, but may achieve your requirement.
#Configuration
#EnableWebSecurity
public class SecurityConfig {
#Bean
#Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)
throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
// ...
// Add filter to remove the SecurityContext after successful authorization
http.addFilterAfter(new RemoveSecurityContextOnAuthorizationFilter(), LogoutFilter.class);
return http.build();
}
private static final class RemoveSecurityContextOnAuthorizationFilter extends OncePerRequestFilter {
private SecurityContextHolderStrategy securityContextHolderStrategy =
SecurityContextHolder.getContextHolderStrategy();
private final LogoutHandler logoutHandler = new CompositeLogoutHandler(
new CookieClearingLogoutHandler("JSESSIONID"),
new SecurityContextLogoutHandler()
);
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
filterChain.doFilter(request, response);
} finally {
String locationHeader = response.getHeader(HttpHeaders.LOCATION);
if (locationHeader != null) {
UriComponents uriComponents = UriComponentsBuilder.fromUriString(locationHeader).build();
if (uriComponents.getQueryParams().containsKey("code")) {
Authentication authentication = this.securityContextHolderStrategy.getContext().getAuthentication();
this.logoutHandler.logout(request, response, authentication);
}
}
}
}
}
// ...
}
I want to build a spring cloud infrastructure with several oauth2 resource servers supporting multiple identity provider (like google, facebook, github, ....) and also one self implemented authorization mechanism.
Oauth2 Resource Server example
Security config
#Configuration
#EnableWebSecurity
#EnableResourceServer
public class Oauth2ResourceServerConfiguration extends WebSecurityConfigurerAdapter {
#Override
public void configure(final HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated();
}
}
application.yml
security:
oauth2:
resource:
user-info-uri: https://api.github.com/user
As you can see this example is using github as identity provider and its working fine.
Oauth2 Authorization Server example:
Config
#Configuration
#EnableAuthorizationServer
public class Oauth2AuthorizationServerConfiguration extends GlobalAuthenticationConfigurerAdapter {
#Override
public void init(final AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("user").password("{noop}user").roles("USER")
.and()
.withUser("admin").password("{noop}admin").roles("USER", "ADMIN");
}
#Override
public void configure(final AuthenticationManagerBuilder auth) throws Exception {
super.configure(auth);
}
}
application.yml
security:
oauth2:
client:
client-id: user
client-secret: user
authorized-grant-types: password,client_credentials,authorization_code,refresh_token
scope: read,write
I can change my oauth2 resource server to use the authorization server:
security:
oauth2:
resource:
#user-info-uri: https://api.github.com/user
user-info-uri: http://${AUTH_HOST:localhost}:${AUTH_PORT:9000}/user
And it works just fine.
But what do I have to do if I now want to use both of them, github and my own authorization server?
Do I simply need a different configuration in my Oauth2 resource server to provide multiple user-info-uri or do I have to do more?
Can I extend my own authorization service to support github & co?
I would prefer not to use Auth0 and Co because I really dont like to outsource the most important part of my application: security. Even though I would like to try it. But I could not find any working example so far for auth0 + spring cloud gateway + token authentication for all underlying services.
I am stuck with the implementation of spring boot as a resource server for multiple authorization servers for validating the access/id tokens provided by authorization servers (such as google, facebook via front end libraries). Here is the architecture I am looking for the below desired flow as a working model.
Desired Architecture Image - Click here
what I implemented so far: I used a library angularx-social-login on angular 8 to get the required tokens google. I was able to pass the token back to the backend resource server to validate the token with google and authorize. Below is the code snippets.
Property File:
google:
client-id: xyz
iss: accounts.google.com
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://accounts.google.com
jwk-set-uri: https://accounts.google.com/.well-known/openid-configuration
Security Config Snippet
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
private String issuer;
#Value("${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}")
private String jwkSetUri;
#Value("${google.client-id}")
private String clientId;
#Value("${google.iss}")
private String iss;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
.anyRequest().authenticated().and()
.oauth2ResourceServer()
.jwt().decoder(jwtDecoder());
}
#Bean
JwtDecoder jwtDecoder() {
NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder)
JwtDecoders.fromOidcIssuerLocation(issuer);
OAuth2TokenValidator<Jwt> audienceValidator = new AudienceValidator(clientId);
OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(iss);
OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>
(withIssuer, audienceValidator);
jwtDecoder.setJwtValidator(withAudience);
return jwtDecoder;
}
}
The above snippet works for one authorization server (google in this case) but
below are my issues
How do Intercept the code to validate if the user exists in our DB first?
How do I add support for another authorization server like facebook?
I've set up 2 oauth2 client Spring Boot 2 web applications running on different ports and an authorization server. If I authenticate on one web application, I can then go and access a secured resource on the other. Brilliant, SSO in action!
But looking at the network traffic, I can't see any Bearer tokens on headers, only some SESSIONID related cookies. The use of HTTP sessions got me worrying that horizontal scaling could be an issue. Then I realized that the second application is working and authenticating somehow.
So what is the browser passing on the call to the 2nd web application that enables it to use the existing authentication? Are there any scaling concerns due to the use of http session by spring security.
thanks
ui oauth2 client application.yml (using spring boot 2 security's oauth2 client implementation)
spring:
profiles: oauth2-security
security:
oauth2:
client:
registration:
myoauth:
client-id: myoauth-trusted-client
client-secret: ...
authorization-grant-type: authorization_code
redirect-uri-template: http://localhost:${server.port}/ui/login/oauth2/code/myoauth
provider:
myoauth:
authorization-uri: http://localhost:8081/auth/oauth/authorize
token-uri: http://localhost:8081/auth/oauth/token
user-info-uri: http://localhost:8081/auth/user_info
user-info-authentication-method: header
user-name-attribute: name
The authz server. using the shim jar spring-security-oauth2-autoconfigure
#Configuration
#EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
private AuthenticationManager authenticationManager;
public AuthorizationServerConfig(AuthenticationConfiguration authenticationConfiguration) throws Exception {
this.authenticationManager = authenticationConfiguration.getAuthenticationManager();
}
#Override
public void configure(AuthorizationServerSecurityConfigurer security) {
security.tokenKeyAccess("permitAll()").checkTokenAccess("isAuthenticated()");
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
.authenticationManager(this.authenticationManager) //for use with password grant type
.authorizationCodeServices(new InMemoryAuthorizationCodeServices()); //for use with authorization_code grant type
}
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("myoauth-trusted-client")
.authorizedGrantTypes("authorization_code")
.authorities("ROLE_CLIENT", "ROLE_TRUSTED_CLIENT")
.secret("{bcrypt}" + new BCryptPasswordEncoder().encode("..."))
.scopes("all")
.autoApprove(true)
.redirectUris("http://localhost:8082/ui/login/oauth2/code/myoauth", "http://localhost:8083/ui/login/oauth2/code/myoauth").and()
.withClient("myoauth-client-with-secret")
.authorizedGrantTypes("password", "client_credentials")
.authorities("ROLE_CLIENT")
.scopes("read")
.secret("{bcrypt}" + new BCryptPasswordEncoder().encode("..."))
;
}
}
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;
}
}