I have a Spring Cloud Gateway and one Microservice
I am using OAuth2 with Keycloak
API Gateway is my OAuth2 Client and it serves secure pages and redirect request to microservice
I need to authorize access to renderer one page "/backoffice", but when i use hasRole method to authorize the user a receive Access Denied.
It occurs because keycloak generates access token with the pattern:
Access Token from Keycloak
It is my Gateway SecurityWebFilterChain
#Configuration
#EnableWebFluxSecurity
public class SecurityConfig {
#Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http.csrf().disable();
http.
authorizeExchange()
.pathMatchers("/backoffice/**").hasRole("approver")
.anyExchange().permitAll()
.and()
.oauth2Login()
.and()
.oauth2Client();
return http.build();
}
}
I tried to use the same approach used by Microservice but I receive 401 Unauthorized... I tried to use #Order to have two filter but not works
// Microservice's SecurityWebFilterChain
#Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
http
.csrf().disable()
.authorizeExchange(exchanges ->
exchanges
.pathMatchers("/{id}/decision").hasRole("ROLE_user")
.anyExchange().permitAll())
.oauth2ResourceServer(oauth2 ->
oauth2.jwt(jwt -> jwt.jwtAuthenticationConverter(grantedAuthoritiesExtractor())));
return http.build();
}
Converter<Jwt, Mono<AbstractAuthenticationToken>> grantedAuthoritiesExtractor() {
JwtAuthenticationConverter jwtAuthenticationConverter =
new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(new GrantedAuthoritiesExtractor());
return new ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter);
}
static class GrantedAuthoritiesExtractor implements Converter<Jwt, Collection<GrantedAuthority>> {
#Override
public Collection<GrantedAuthority> convert(Jwt jwt) {
var realm_access = (Map<String, List<String>>) jwt.getClaims().getOrDefault("realm_access", Collections.emptyMap());
return realm_access.get("roles").stream()
.map(role -> "ROLE_".concat(role))
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
}
Can I have both in same application, is there conflict?
Related
We currently have 2 web applications that authenticate against a CAS. The communication between them is done via Basic Auth.
For security reasons we want to switch to OAuth2 to get rid of the Basic Auth. At the same time we want to get rid of the CAS because it does not meet our requirements.
So the goal is something along those lines:
I am at the point that I can log in to the user management and manipulate data. The 2nd application also correctly redirects me to the 1st application to authenticate me. Authentication also works. And the 1st application responds correctly to the 2nd application. The 2nd application then executes the expected POST to the url "oauth2/token". But the 1st application replies with a 302 to "/login" instead of a token.
Which point am I missing or do I still have to configure?
I have orientated myself on the following documentation:
https://docs.spring.io/spring-authorization-server/docs/current/reference/html/getting-started.html
My AuthorizationServerConfig looks like this:
#Configuration
public class AuthorizationServerConfig {
private final SecurityApp app;
public AuthorizationServerConfig(SecurityApp app) {
this.app = app;
}
#Bean
#Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)
throws Exception {
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
new OAuth2AuthorizationServerConfigurer();
authorizationServerConfigurer
.authorizationEndpoint(authorizationEndpoint ->
authorizationEndpoint.consentPage("/oauth2/authorize"))
.oidc(Customizer.withDefaults()); // Enable OpenID Connect 1.0
RequestMatcher endpointsMatcher = authorizationServerConfigurer
.getEndpointsMatcher();
http
.securityMatcher(endpointsMatcher)
.authorizeHttpRequests(authorize ->
authorize.anyRequest().authenticated()
)
.csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
.exceptionHandling(exceptions ->
exceptions.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login"))
)
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
.apply(authorizationServerConfigurer);
return http.build();
}
#Bean
#Order(2)
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
)
// Form login handles the redirect to the login page from the
// authorization server filter chain
.formLogin(Customizer.withDefaults());
return http.build();
}
#Bean
UserDetailsService users() {
return app::findUserByLogin;
}
// OVERWATCH
#Bean
public RegisteredClientRepository registeredClientRepository() {
return new RegisteredClientRepository() {
#Override
public void save(RegisteredClient registeredClient) {
throw new NotImplementedException();
}
#Override
public RegisteredClient findById(String id) {
return app.findByClientId(id);
}
#Override
public RegisteredClient findByClientId(String clientId) {
return app.findByClientId(clientId);
}
};
}
#Bean
public JWKSource<SecurityContext> jwkSource() {
RSAPublicKey publicKey = app.getPublicKey();
RSAPrivateKey privateKey = (RSAPrivateKey) app.getPrivateKey();
RSAKey rsaKey = new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
JWKSet jwkSet = new JWKSet(rsaKey);
return new ImmutableJWKSet<>(jwkSet);
}
#Bean
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}
#Bean
public AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder().build();
}
// region Password Authenticator
#Bean
public PasswordEncoder passwordEncoder() {
return app.passwordEncoder();
}
// endregion
}
And the Configuration from App 2 looks like this:
#Configuration
#EnableWebSecurity
public class AceSecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/**")
.authorizeRequests()
.antMatchers("/oauth/authorize**", "/login**", "/error**")
.permitAll()
.and()
.authorizeRequests()
.anyRequest().authenticated()
.and()
.oauth2Login( oauth2Login -> oauth2Login.defaultSuccessUrl("/index.html") );
}
}
application.properties from App2 is:
spring.security.oauth2.client.registration.<provider-name>.client-id=${app.uuid}
spring.security.oauth2.client.registration.<provider-name>.client-secret=${app.secret}
spring.security.oauth2.client.registration.<provider-name>.scope=openid
spring.security.oauth2.client.registration.<provider-name>.redirect-uri=http://127.0.0.1:8088/login/oauth2/code/<provider-name>
spring.security.oauth2.client.registration.<provider-name>.client-name=${app.name}
spring.security.oauth2.client.registration.<provider-name>.provider=${provider.name}
spring.security.oauth2.client.registration.<provider-name>.client-authentication-method=code
spring.security.oauth2.client.registration.<provider-name>.authorization-grant type=authorization_code
spring.security.oauth2.client.provider.<provider-name>.authorization-uri=http://localhost:8086/oauth2/authorize
spring.security.oauth2.client.provider.<provider-name>.token-uri=http://localhost:8086/oauth2/token
spring.security.oauth2.client.provider.<provider-name>.user-info-uri=http://localhost:8086/oauth2/userinfo?schema=openid
spring.security.oauth2.client.provider.<provider-name>.user-name-attribute=name
spring.security.oauth2.client.provider.<provider-name>.user-info-authentication-method=header
spring.security.oauth2.client.provider.<provider-name>.jwk-set-uri=http://localhost:8086/jwks
The request from App2 to App 1 that causes the redirect is:
POST http://localhost:8086/oauth2/token
Body:
grant_type=authorization_code,
code=Zls0ppjnS_RXyMVPB8fg_eQQgoiUAxRguOMsdyVYQpgd8eDkUDzgz813L0ybovTL7sNj0TDRUHibPfek9NzwULND1mty5WPW2DOtQjTAaEROL3qP7RvyTWXTEzzYe-o,
redirect_uri=[http://127.0.0.1:8088/login/oauth2/code/<provider-name>],
client_id=<app.uuid>
Header:
Accept:"application/json;charset=UTF-8",
Content-Type:"application/x-www-form-urlencoded;charset=UTF-8"
I'm struggling to use the very same Spring Boot 3.0 application as both authentication server and resource server, but until now, I've not been able to make the whole thing working.
First, I defined a very simple RestController:
#RestController
#RequestMapping("api")
public class PublicAPI {
#GetMapping("/apitest")
public String test(Principal principal) {
return " This is a test ==>";
}
}
Then, essentially following the code found in a Sample project of Spring, I managed to setup my boot app as Spring Authorization Server. I'm able to use Postman to get the authentication token using Oauth2 flow: I'm redirected to Spring's standard login page, I log in with credentials, and I get the Token.
Problem is, if I try to GET http://localhost:9000/api/apitest` using provided token, I get a 401 response from Spring boot.
This is my Security Configuration:
#Bean
#Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http, CorsConfiguration configCors) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class).oidc(Customizer.withDefaults());
http
.exceptionHandling((exceptions) -> exceptions
.authenticationEntryPoint(
new LoginUrlAuthenticationEntryPoint("/login"))
);
http.cors().configurationSource(request -> configCors);
return http.build();
}
#Bean
#Order(2)
SecurityFilterChain apiFilter(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**")
.authorizeHttpRequests()
.requestMatchers("/api/**").authenticated()
.and()
.oauth2ResourceServer()
.jwt();
return http.build();
}
#Bean
#Order(3)
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http, CorsConfiguration configCors) throws Exception {
http
.securityMatcher("/oauth2/**", "/login")
.authorizeHttpRequests()
.requestMatchers("/login", "/oauth2/**")
.authenticated()
.and()
.formLogin(Customizer.withDefaults());
http.cors().configurationSource(request -> configCors);
return http.build();
}
#Bean
public CorsConfiguration corsConfiguration() throws Exception {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowCredentials(true);
configuration.setAllowedOriginPatterns(List.of("*"));
configuration.setAllowedMethods(List.of("*"));
configuration.setAllowedHeaders(List.of("*"));
return configuration;
}
If I try to access another Spring API in a different Spring Boot application which uses the first one as Authentication Server I get no errors.
Pretty sure that there's something wrong my configuration... any hint will be greatly appreciated !
At the very end, it turned out that another filter has been configured:
#Component
#Order(Ordered.HIGHEST_PRECEDENCE)
public class LoopbackIpRedirectFilter extends OncePerRequestFilter {
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (request.getServerName().equals("localhost") && request.getHeader("host") != null) {
UriComponents uri = UriComponentsBuilder.fromHttpRequest(new ServletServerHttpRequest(request))
.host("127.0.0.1").build();
response.sendRedirect(uri.toUriString());
return;
}
filterChain.doFilter(request, response);
}
}
Removing the LoopbackIpRedirectFilter problem was fixed
I'm trying to make security using oauth2 resource server with google as authentication provider and add custom roles from database by getting email from JWT token and searching for it in database.
This is my configuration
#Bean
public SecurityFilterChain filterChain(final HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/csrf").permitAll()
.anyRequest().authenticated()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.oauth2ResourceServer().jwt().decoder(jwtDecoder()).jwtAuthenticationConverter(jwtAuthenticationConverter())
.and()
.and()
.cors().and()
.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
return http.build();
}
JWT decoder and authentication converter
#Bean
JwtDecoder jwtDecoder() {
NimbusJwtDecoder jwtDecoder = JwtDecoders.fromOidcIssuerLocation(issuerUri);
OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuerUri);
MappedJwtClaimSetConverter converter = MappedJwtClaimSetConverter.withDefaults(Collections.singletonMap("roles", customClaim -> getUserRolesFromDatabase()));
jwtDecoder.setClaimSetConverter(converter);
jwtDecoder.setJwtValidator(withIssuer);
return jwtDecoder;
}
#Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");
grantedAuthoritiesConverter.setAuthoritiesClaimName("roles");
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
return jwtAuthenticationConverter;
}
public List<String> getUserRolesFromDatabase() {
return Collections.singletonList("USER");
}
How do I get email claim from bearer token to use it in getUserRolesFromDatabase() and search for roles in database?
I have the following services:
Eureka server
Auth server
Spring Gateway
This will pass the login request to the login service
All other requests will be authenticated(using jwt token and secret key) and passed to other services
Login Service
To validate login and issue jwt token
Below is my config. The flow is not even reaching to this code..
#Configuration
#EnableWebFluxSecurity
#EnableReactiveMethodSecurity
public class SecurityConfig {
private static final Logger LOGGER = LoggerFactory.getLogger(SecurityConfig.class);
#Autowired
private AuthenticationManager authenticationManager;
#Autowired
private SecurityContextRepository securityContextRepository;
#Bean(value="org.springframework.security.config.annotation.web.reactive.WebFluxSecurityConfiguration.WebFilterChainFilter")
public SecurityWebFilterChain springSecurityWebFilterChainFilter(ServerHttpSecurity http) {
LOGGER.info("In the securiry config..................");
return http
.csrf().disable()
.exceptionHandling()
.authenticationEntryPoint((swe, e) -> {
return Mono.fromRunnable(() -> {
swe.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
});
}).accessDeniedHandler((swe, e) -> {
return Mono.fromRunnable(() -> {
swe.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
});
})
.and()
.authenticationManager(authenticationManager)
.securityContextRepository(securityContextRepository)
.authorizeExchange()
.pathMatchers(HttpMethod.OPTIONS).permitAll()
.pathMatchers("/login-service/api/login").permitAll()
.anyExchange().authenticated()
.and().build();
}
}
I always get 403 with An expected CSRF token cannot be found this error even though I have disabled csrf.
Can anyone help what can be wrong here?
Have you tried adding your role:
.authorizeExchange()
.pathMatchers("/login-service/api/login").hasAuthority("ROLE_ADMIN")
.anyExchange().authenticated()
.and().build();
I am trying to implement Single Sign On with Spring Security OAuth2 and JWT.
I use two separate applications:
An Authorization Server – which is the central authentication mechanism
Client Application: the applications using SSO
When a user tries to access a secured page in the client app, they’ll be redirected to authenticate first, via the Authentication Server.
And I am using the Authorization Code grant type out of OAuth2 to drive the delegation of authentication.
Authorization server:
#Configuration
#EnableAuthorizationServer
public class OAuth2AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
public static final Logger LOGGER = LoggerFactory.getLogger(AuthorizationServerConfigurerAdapter.class);
#Autowired
private AuthenticationManager authenticationManager;
#Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
#Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("abcd");
return converter;
}
#Bean
#Primary
public DefaultTokenServices tokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
defaultTokenServices.setSupportRefreshToken(true);
defaultTokenServices.setTokenEnhancer(accessTokenConverter());
return defaultTokenServices;
}
#Override
public void configure(ClientDetailsServiceConfigurer clientDetailsServiceConfigurer) throws Exception {
clientDetailsServiceConfigurer
.inMemory()
.withClient("webapp")
.secret("Pass")
.authorizedGrantTypes("implicit", "refresh_token", "password", "authorization_code")
.scopes("user_info")
.autoApprove(true);
}
#Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()");
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager);
}
}
Security Configuration on Authorization Server
#Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
#Value("${ldap.url}")
private String ldapUrl;
#Value("${ldap.userDnPatterns}")
private String ldapUserDnPatterns;
#Autowired
private PersonService personService;
#Autowired
private RoleService roleService;
#Override
protected void configure(HttpSecurity http) throws Exception { // #formatter:off
http.requestMatchers()
.antMatchers("/login", "/oauth/authorize")
.and()
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.formLogin()
.permitAll();
} // #formatter:on
#Bean(name = "authenticationManager")
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.authenticationProvider(this.ldapAndDatabaseAuthenticationProvider());
}
#Bean(name="ldapAuthenticationProvider")
public AuthenticationProvider ldapAndDatabaseAuthenticationProvider(){
LdapUserDetailsMapper userDetailsMapper = new LdapUserDetailsMapper();
userDetailsMapper.setRoleAttributes(new String[]{"groupMembership"});
LdapAndDatabaseAuthenticationProvider provider =
new LdapAndDatabaseAuthenticationProvider(
this.ldapAuthenticator(),
this.ldapAuthoritiesPopulator(),
this.personService);
provider.setUserDetailsContextMapper(userDetailsMapper);
return provider;
}
#Bean( name = "ldapAuthoritiesPopulator" )
public LdapAndDatabaseAuthoritiesPopulator ldapAuthoritiesPopulator(){
return new LdapAndDatabaseAuthoritiesPopulator(this.contextSource(), "", roleService);
}
#Bean( name = "ldapAuthenticator" )
public LdapAuthenticator ldapAuthenticator() {
BindAuthenticator authenticator = new BindAuthenticator( this.contextSource() );
authenticator.setUserDnPatterns(new String[]{"cn={0},ou=prod,o=TEMP"});
return authenticator;
}
#Bean( name = "contextSource" )
public DefaultSpringSecurityContextSource contextSource() {
DefaultSpringSecurityContextSource contextSource =
new DefaultSpringSecurityContextSource( ldapUrl );
return contextSource;
}
}
application.properties:
server.port=8888
server.context-path=/auth
security.basic.enabled=false
When I login the client application, It correctly forwards to Authorization Server for Single Sign On.
I enter the user credentials. User successfully get authenticated, but then I see the below error on browser:
OAuth Error
error="invalid_grant", error_description="A redirect_uri can only be
used by implicit or authorization_code grant types."
URL Shows:
http://localhost:8888/auth/oauth/authorize?client_id=webapp&redirect_uri=http://localhost:8080/jwt/webapp&response_type=code&state=LGvAzj
I also see the below at the log:
02:14:43.610 [http-nio-8888-exec-6] DEBUG o.s.s.o.p.e.FrameworkEndpointHandlerMapping/getHandlerInternal Looking up handler method for path /oauth/authorize
02:14:43.614 [http-nio-8888-exec-6] DEBUG o.s.s.o.p.e.FrameworkEndpointHandlerMapping/getHandlerInternal Returning handler method [public org.springframework.web.servlet.ModelAndView org.springframework.security.oauth2.provider.endpoint.AuthorizationEndpoint.authorize(java.util.Map<java.lang.String, java.lang.Object>,java.util.Map<java.lang.String, java.lang.Str ing>,org.springframework.web.bind.support.SessionStatus,java.security.Principal)]
02:14:43.849 [http-nio-8888-exec-6] INFO o.s.s.o.p.e.AuthorizationEndpoint/handleOAuth2Exception Handling OAuth2 error: error="invalid_grant", error_description="A redirect_uri can only be used by implicit or authorization_code grant types."
Can you please help me to find the problem?
UPDATE
Actually, Dur is right. This configuration is correct and works fine. I had another configuration file which configures JdbcClientDetails and it was overwriting the clientDetailsService created with inmemory in this configuration.