I implemented a login with this
https://vaadin.com/docs/latest/tutorial/login-and-authentication
docs.
I also added a view with RouteAlias("") but after login I got redirected to
/sw-runtime-resources-precache.js
This looks really weird. I tried to find a place to manually set the redirect to a path or route but I did not find a solutions yet. Does anybody know why I did not get redirected to the home path?
This is my Security Config
#EnableWebSecurity
#Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter
{
#Resource
private UserDetailsService userDetailsService;
private static final String LOGIN_PROCESSING_URL = "/login";
private static final String LOGIN_FAILURE_URL = "/login?error";
private static final String LOGIN_URL = "/login";
private static final String LOGOUT_SUCCESS_URL = "/login";
#Override
protected void configure(HttpSecurity http) throws Exception
{
// Vaadin handles CSRF internally
http.csrf().disable()
// Register our CustomRequestCache, which saves unauthorized access attempts, so the user is redirected after login.
.requestCache().requestCache(new CustomRequestCache())
// Restrict access to our application.
.and().authorizeRequests()
// Allow all Vaadin internal requests.
.requestMatchers(SecurityUtils::isFrameworkInternalRequest).permitAll()
// Allow all requests by logged-in users.
.anyRequest().authenticated()
// Configure the login page.
.and().formLogin()
.loginPage(LOGIN_URL).permitAll()
.loginProcessingUrl(LOGIN_PROCESSING_URL)
.failureUrl(LOGIN_FAILURE_URL)
// Configure logout
.and().logout().logoutSuccessUrl(LOGOUT_SUCCESS_URL);
}
#Bean
public DaoAuthenticationProvider authProvider()
{
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
#Bean
public PasswordEncoder passwordEncoder()
{
return new BCryptPasswordEncoder();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception
{
auth.authenticationProvider(authProvider());
}
/*#Bean
#Override
public UserDetailsService userDetailsService()
{
UserDetails user = User.withUsername("user")
.password("{noop}userpass")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}*/
/**
* Allows access to static resources, bypassing Spring Security.
*/
#Override
public void configure(WebSecurity web)
{
web.ignoring().antMatchers(
// Client-side JS
"/VAADIN/**",
// the standard favicon URI
"/favicon.ico",
// the robots exclusion standard
"/robots.txt",
// web application manifest
"/manifest.webmanifest",
"/sw.js",
"/offline.html",
// icons and images
"/icons/**",
"/images/**",
"/styles/**",
// (development mode) H2 debugging console
"/h2-console/**");
}
}
I currently have a backend application that implements a very simple Spring security based on login / password that must be added in the http headers.
I also have a front end that uses OKTA as a provider and works with JWT tokens.
I now want to make the end points dedicated to the front end applications use the JWT token system and all the others use the current login/password system.
I can make my application work with an OKTA configuration or with a login / password configuration but I can't make both work together.
Looking at the different messages on stack overflow I have implemented a double configuration but it is always the first one that is applied. The second one is simply ignored and the endpoints of the perimeter are allowed without any token or login / password
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Configuration
#Order(1)
public static class OauthOktaConfigurationAdapter extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
http.cors();
http.csrf().disable();
http
.authorizeRequests().antMatchers("/api/v1/end-point/**").authenticated()
.and().oauth2ResourceServer().jwt();
Okta.configureResourceServer401ResponseBody(http);
}
}
#Configuration
#Order(2)
public static class StandardSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
#Value("${http.auth-app-id-header-name}")
private String appIdRequestHeaderName;
#Value("${http.auth-api-key-header-name}")
private String apiKeyRequestHeaderName;
private final AuthenticationManager authenticationManager;
#Autowired
public StandardSecurityConfigurationAdapter(AuthenticationManager authenticationManager) {
super();
this.authenticationManager = authenticationManager;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors();
http.csrf().disable();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().addFilter(initAuthenticationFilter())
.antMatcher("/api/v1/tools/**")
.authorizeRequests().anyRequest().authenticated();
}
private RequestHeaderAuthenticationFilter initAuthenticationFilter() {
RequestHeaderAuthenticationFilter requestHeaderAuthenticationFilter = new RequestHeaderAuthenticationFilter(appIdRequestHeaderName,
apiKeyRequestHeaderName);
requestHeaderAuthenticationFilter.setContinueFilterChainOnUnsuccessfulAuthentication(false);
requestHeaderAuthenticationFilter.setAuthenticationManager(authenticationManager);
return requestHeaderAuthenticationFilter;
}
}
#Override
#Bean
#Primary
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
In this code, configuration 2 is never used even if I make a call to /api/v1/tools
If I remove configuration 1, configuration 2 is applied.
Can you help me to understand what I am doing wrong?
EDIT 1 :
With the help and suggestion of Eleftheria Stein-Kousathana, i change my configuration (and i add Swagger white list configuration)
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private static final String[] AUTH_WHITELIST = {
"/v2/api-docs",
"/swagger-resources/configuration/ui",
"/swagger-resources",
"/swagger-resources/configuration/security",
"/swagger-ui.html",
"/webjars/**"
};
#Configuration
#Order(1)
public static class SwaggerConfigurationAdapter extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
System.out.println("Loading configuration 1");
http.cors();
http.csrf().disable();
http
.requestMatchers(matchers -> matchers.antMatchers(AUTH_WHITELIST))
.authorizeRequests(authz -> {
authz.anyRequest().permitAll();
});
}
}
#Configuration
#Order(2)
public static class OauthOktaConfigurationAdapter extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
System.out.println("Loading configuration 2");
http.cors();
http.csrf().disable();
http
.requestMatchers(matchers -> matchers.antMatchers("/api/v1/end-point/**"))
.authorizeRequests(authz -> {
try {
authz.anyRequest().authenticated().and().oauth2ResourceServer().jwt();
} catch (Exception e) {
e.printStackTrace();
}
});
Okta.configureResourceServer401ResponseBody(http);
}
}
#Configuration
#Order(3)
public static class StandardSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
#Value("${algo.http.auth-app-id-header-name}")
private String appIdRequestHeaderName;
#Value("${algo.http.auth-api-key-header-name}")
private String apiKeyRequestHeaderName;
private final AuthenticationManager authenticationManager;
#Autowired
public StandardSecurityConfigurationAdapter(AuthenticationManager authenticationManager) {
super();
this.authenticationManager = authenticationManager;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
System.out.println("Loading configuration 3");
http.cors();
http.csrf().disable();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().addFilter(initAuthenticationFilter())
.requestMatchers(matchers -> matchers.antMatchers("/api/**"))
.authorizeRequests(authz -> {
try {
authz.anyRequest().authenticated();
} catch (Exception e) {
e.printStackTrace();
}
});
}
private RequestHeaderAuthenticationFilter initAuthenticationFilter() {
RequestHeaderAuthenticationFilter requestHeaderAuthenticationFilter = new RequestHeaderAuthenticationFilter(appIdRequestHeaderName,
apiKeyRequestHeaderName);
requestHeaderAuthenticationFilter.setContinueFilterChainOnUnsuccessfulAuthentication(false);
requestHeaderAuthenticationFilter.setAuthenticationManager(authenticationManager);
return requestHeaderAuthenticationFilter;
}
}
#Override
#Bean
#Primary
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
I feel that I am very close to succeeding
Swaggers is accessible when not authenticated
The routes corresponding to "/api/v1/end-point/**" need a JWT token otherwise I get a 401 error
The routes corresponding to "/api/** " need a login / password otherwise I get a 401 error
But now I have the following error:
Every time I request a page under swagger or make a call to my api, my web browser asks me for a login / password.
If I cancel I can still navigate on Swagger UI and make call to "/api/v1/end-point/**".
Every Login / password are rejected even they are valid in configuration 3.
If I don't fill the login / password and make a call to any route of "/api/**" i got the following error :
2021-07-23 14:49:16.642 [http-nio-8081-exec-9] INFO c.c.a.a.c.CorrelationIdLoggingAspect - Calling api.controller.endpoint.getActivities executed in 197ms.
2021-07-23 14:49:22.247 [http-nio-8081-exec-1] ERROR o.a.c.c.C.[.[.[.[dispatcherServlet] - Servlet.service() for servlet [dispatcherServlet] in context with path [/secret] threw exception [Filter execution threw an exception] with root cause
java.lang.StackOverflowError: null
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:344)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:205)
at com.sun.proxy.$Proxy236.authenticate(Unknown Source)
at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:195)
at org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter$AuthenticationManagerDelegator.authenticate(WebSecurityConfigurerAdapter.java:501)
at jdk.internal.reflect.GeneratedMethodAccessor220.invoke(Unknown Source)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:344)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:205)
at com.sun.proxy.$Proxy236.authenticate(Unknown Source)
at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:195)
at org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter$AuthenticationManagerDelegator.authenticate(WebSecurityConfigurerAdapter.java:501)
If I'm understanding your program sketch and description correctly, let me attempt to summarize. Your application seeks to support the following:
Serve up swagger UI to the public and allow browsing of API definitions.
Use authenticated API endpoints (prefixed by /api/v1/end-point) with an Okta-provided JWT from another client (not swagger).
Use authenticated API endpoints (prefixed by /api, but not /api/v1/end-point) via swagger with username/password as headers.
Note: I'm not going to cover how to configure Okta as a provider here, nor configuring swagger. If those steps are not done correctly, you may still have issues.
As far as Spring Security, I think your main issue is due to the fact that you don't appear to have configured an authentication provider for your header-based configuration. This is typically done via a UserDetailsService (see section on UserDetailsService):
#Bean
public UserDetailsService userDetailsService() {
// #formatter:off
UserDetails userDetails = User.builder()
.username("api-client")
.password("{noop}my-api-key")
.roles("USER")
.build();
// #formatter:on
return new InMemoryUserDetailsManager(userDetails);
}
This is obviously an example not meant for production. But the important point is that you have to provide a way for Spring Security to determine that the credentials are valid. Whether it's a user's username/password, or an API client's appId/apiKey, the principal (see Authentication) is looked up through a UserDetailsService, and then the credentials are validated by the AuthenticationProvider.
Unfortunately, the built-in RequestHeaderAuthenticationFilter is built on top of a different type of provider that assumes you are pre-authenticated, and is therefore incompatible with username/password authentication. While you could work around this by adapting one type of provider to another, it's more straight forward (at least for example purposes) to adapt the UsernamePasswordAuthenticationFilter to your use case. For example:
private UsernamePasswordAuthenticationFilter usernamePasswordAuthenticationFilter() throws Exception {
UsernamePasswordAuthenticationFilter usernamePasswordAuthenticationFilter = new UsernamePasswordAuthenticationFilter() {
#Override
protected String obtainUsername(HttpServletRequest request) {
return request.getHeader(getUsernameParameter());
}
#Override
protected String obtainPassword(HttpServletRequest request) {
return request.getHeader(getPasswordParameter());
}
#Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
super.successfulAuthentication(request, response, chain, authResult);
chain.doFilter(request, response);
}
};
usernamePasswordAuthenticationFilter.setAuthenticationManager(authenticationManager());
usernamePasswordAuthenticationFilter.setUsernameParameter(appIdRequestHeaderName);
usernamePasswordAuthenticationFilter.setPasswordParameter(apiKeyRequestHeaderName);
usernamePasswordAuthenticationFilter.setRequiresAuthenticationRequestMatcher(AnyRequestMatcher.INSTANCE);
usernamePasswordAuthenticationFilter.setPostOnly(false);
usernamePasswordAuthenticationFilter.setAuthenticationSuccessHandler((request, response, authentication) -> {
// Do nothing
});
return usernamePasswordAuthenticationFilter;
}
If you're interested in making this feel more built-in, check out the section of the docs on custom DSLs.
I would also suggest you override the configure(WebSecurity web) method in WebSecurityConfigurerAdapter to perform your permitAll and condense the configuration down to two, as well as eliminating the /api/** pattern so your entire application is secure by default. Here's a full example (omitting any Okta-specific code) that also demonstrates correct usage of the Spring Security lambda DSL:
#Configuration
public class SecurityConfiguration {
private static final String[] AUTH_WHITELIST = {
"/v2/api-docs",
"/swagger-resources/configuration/ui",
"/swagger-resources",
"/swagger-resources/configuration/security",
"/swagger-ui.html",
"/webjars/**"
};
#Order(1)
#EnableWebSecurity
public static class OauthOktaConfigurationAdapter extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
// #formatter:off
http
.antMatcher("/api/v1/end-point/**")
.authorizeRequests((authorizeRequests) ->
authorizeRequests
.anyRequest().authenticated()
)
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
.sessionManagement((sessionManagement) ->
sessionManagement
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.cors(withDefaults())
.csrf(CsrfConfigurer::disable);
// #formatter:on
}
}
#Order(2)
#EnableWebSecurity
public static class StandardSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
#Value("${algo.http.auth-app-id-header-name}")
private String appIdRequestHeaderName;
#Value("${algo.http.auth-api-key-header-name}")
private String apiKeyRequestHeaderName;
#Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers(AUTH_WHITELIST);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
// #formatter:off
http
.addFilterAt(usernamePasswordAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.authorizeRequests((authorizeRequests) ->
authorizeRequests
.anyRequest().authenticated()
)
.sessionManagement((sessionManagement) ->
sessionManagement
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.cors(withDefaults())
.csrf(CsrfConfigurer::disable);
// #formatter:on
}
private UsernamePasswordAuthenticationFilter usernamePasswordAuthenticationFilter() throws Exception {
UsernamePasswordAuthenticationFilter usernamePasswordAuthenticationFilter = new UsernamePasswordAuthenticationFilter() {
#Override
protected String obtainUsername(HttpServletRequest request) {
return request.getHeader(getUsernameParameter());
}
#Override
protected String obtainPassword(HttpServletRequest request) {
return request.getHeader(getPasswordParameter());
}
#Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
super.successfulAuthentication(request, response, chain, authResult);
chain.doFilter(request, response);
}
};
usernamePasswordAuthenticationFilter.setAuthenticationManager(authenticationManager());
usernamePasswordAuthenticationFilter.setUsernameParameter(appIdRequestHeaderName);
usernamePasswordAuthenticationFilter.setPasswordParameter(apiKeyRequestHeaderName);
usernamePasswordAuthenticationFilter.setRequiresAuthenticationRequestMatcher(AnyRequestMatcher.INSTANCE);
usernamePasswordAuthenticationFilter.setPostOnly(false);
usernamePasswordAuthenticationFilter.setAuthenticationSuccessHandler((request, response, authentication) -> {
// Do nothing
});
return usernamePasswordAuthenticationFilter;
}
#Bean
public UserDetailsService userDetailsService() {
// #formatter:off
UserDetails userDetails = User.builder()
.username("api-client")
.password("{noop}my-api-key")
.roles("USER")
.build();
// #formatter:on
return new InMemoryUserDetailsManager(userDetails);
}
}
}
Final note: One caveat is that I included disabling CSRF, which you have done. This is only a reasonable thing to do if you don't intend to use this application in a web browser with sessions. Since I marked both configurations as stateless (your Okta+JWT example was not), this seems reasonable. Most of the time, however, you really don't want to disable CSRF protection, especially if the reason is "I can't figure out how to make my UI application work with CSRF enabled."
First of all, thank you very much for your help.
I took the time to respond because I wanted to understand your answer.
You are right about the description of the sketch I am trying to implement.
With your configuration I can now access Swagger without any login/password.
The first configuration (OKTA) works fine and I think the last one (login / password) does too.
I now face one last error when I try to access the routes
protected by login and password.
I am facing an issue where Spring throws an "org.springframework.security.authentication.ProviderNotFoundException: No AuthenticationProvider found for org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken" exception.
I am looking to resolve this issue and I think everything will work after that.
Let me modestly point out that the setter methods :
requestHeaderAuthenticationFilter.setPrincipalRequestHeader(appIdRequestHeaderName);
requestHeaderAuthenticationFilter.setCredentialsRequestHeader(apiKeyRequestHeaderName);
are not accessible and I keep setting them by constructor.
RequestHeaderAuthenticationFilter requestHeaderAuthenticationFilter = new RequestHeaderAuthenticationFilter(appIdRequestHeaderName, apiKeyRequestHeaderName)
Thank you very much for all the answers.
We found the solution thanks your help.
Here the final code for helping everyone who needs to do the same things as us.
Security configuration :
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private static final String[] AUTH_WHITELIST = {
"/v2/api-docs",
"/swagger-resources/configuration/ui",
"/swagger-resources",
"/swagger-resources/configuration/security",
"/swagger-ui.html",
"/webjars/**"
};
#Order(1)
#Configuration
public static class OauthOktaConfigurationAdapter extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/api/v1/end-point/**")
.authorizeRequests((authz) -> authz.anyRequest().authenticated())
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
.sessionManagement((sessionManagement) ->
sessionManagement
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.cors(withDefaults())
.csrf(CsrfConfigurer::disable);
Okta.configureResourceServer401ResponseBody(http);
}
}
#Order(2)
#Configuration
public static class StandardSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
#Value("${http.app-id-header-name}")
private String appIdRequestHeaderName;
#Value("${http.api-key-header-name}")
private String apiKeyRequestHeaderName;
private final AuthenticationManager authenticationManager;
#Autowired
public StandardSecurityConfigurationAdapter(AuthenticationManager authenticationManager) {
super();
this.authenticationManager = authenticationManager;
}
#Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers(AUTH_WHITELIST);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilterAt(initAuthenticationFilter(), UsernameRequestHeaderAuthenticationFilter.class)
.authorizeRequests((authorizeRequests) ->
authorizeRequests
.anyRequest().authenticated()
)
.sessionManagement((sessionManagement) ->
sessionManagement
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.cors(withDefaults())
.csrf(CsrfConfigurer::disable);
}
private UsernameRequestHeaderAuthenticationFilter initAuthenticationFilter() throws Exception {
UsernameRequestHeaderAuthenticationFilter usernameRequestHeaderAuthenticationFilter = new UsernameRequestHeaderAuthenticationFilter();
usernameRequestHeaderAuthenticationFilter.setAuthenticationManager(authenticationManager);
usernameRequestHeaderAuthenticationFilter.setUsernameParameter(appIdRequestHeaderName);
usernameRequestHeaderAuthenticationFilter.setPasswordParameter(apiKeyRequestHeaderName);
usernameRequestHeaderAuthenticationFilter.setRequiresAuthenticationRequestMatcher(AnyRequestMatcher.INSTANCE);
usernameRequestHeaderAuthenticationFilter.setPostOnly(false);
usernameRequestHeaderAuthenticationFilter.setAuthenticationSuccessHandler((request, response, authentication) -> {
// Do nothing
});
return usernameRequestHeaderAuthenticationFilter;
}
}
}
Authentication filter :
public class UsernameRequestHeaderAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
#Override
protected String obtainUsername(HttpServletRequest request) {
return request.getHeader(getUsernameParameter());
}
#Override
protected String obtainPassword(HttpServletRequest request) {
return request.getHeader(getPasswordParameter());
}
#Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
super.successfulAuthentication(request, response, chain, authResult);
chain.doFilter(request, response);
}
#Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
super.unsuccessfulAuthentication(request, response, failed);
}
}
We have also adapted our AuthenticationManager to use UserAuthorities
Thanks again to all
I have an Angular 7 app with a spring API. I use spring security, JWT tokens, and https://github.com/manfredsteyer/angular-oauth2-oidc to handle login.
I'm able to login, but when I logout and clic on login once again, I immediately get a new token, without having to connect.
I'm new to spring security and JWT tokens, but from what I know, I cannot properly logout from the back, the classic way is to remove token from the front. That's what the library do, and to be sure I tried by emptying manualy the localStorage (where token is stored), but even with localStorage cleared, when I try to login, I get a new token immediately.
Any idea about what happengin and how can I logout?
Authorization server config :
#Configuration
#EnableAuthorizationServer
public class OAuth2AuthorizationServerConfigJwt extends AuthorizationServerConfigurerAdapter {
#Override
public void configure(final ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory().withClient("sampleClientId")
.authorizedGrantTypes("implicit")
.scopes("read", "write", "foo", "bar")
.autoApprove(true)/*.accessTokenValiditySeconds(3600)*/
.redirectUris("http://localhost:4200");
}
#Override
public void configure(final AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
final TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(tokenEnhancer(), accessTokenConverter()));
endpoints.tokenStore(tokenStore()).tokenEnhancer(tokenEnhancerChain);
}
#Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
#Bean
public JwtAccessTokenConverter accessTokenConverter() {
final JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("123");
return converter;
}
#Bean
public TokenEnhancer tokenEnhancer() {
return new CustomTokenEnhancer();
}
}
Edit :
It seems that this was due to the lack of
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
But now that I've added it, I'm stuck on the login form, seems that there's still something missing. Here my config :
#Override
protected void configure(final HttpSecurity http) throws Exception {
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/login").permitAll()
.antMatchers("/user").permitAll()
.antMatchers("/registrationConfirm").permitAll()
.antMatchers("/user/confirm/**").permitAll()
.antMatchers("/user/registration/**").permitAll()
.antMatchers("/oauth/token/revokeById/**").permitAll()
.antMatchers("/tokens/**").permitAll()
.anyRequest().authenticated()
.and().formLogin().permitAll();
}
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.
I have a Spring-boot app that is using Spring-security, configured with Java-config. Ideally, I will have a customer UserDetailsService so I can add/modify users. Until then I am failing to configure this correctly.
I am using the following dependencies:
compile("org.springframework.boot:spring-boot-starter-web:1.1.1.RELEASE")
compile("org.springframework.boot:spring-boot:1.0.1.RELEASE")
compile("org.springframework.boot:spring-boot-starter-thymeleaf")
compile("org.springframework.boot:spring-boot-starter-data-jpa:1.1.1.RELEASE")
compile("org.springframework.security:spring-security-web:4.0.0.M1")
compile("org.springframework.security:spring-security-config:4.0.0.M1")
I have the following Configurations
#Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
#Configuration
#EnableWebMvcSecurity
public class ApplicationSecurity extends WebSecurityConfigurerAdapter {
#Autowired
private DataSource datasource;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/resources/**").permitAll()
.antMatchers("/css/**").permitAll();
http
.formLogin().failureUrl("/login?error")
.defaultSuccessUrl("/")
.loginPage("/login")
.permitAll()
.and()
.logout().logoutRequestMatcher(new AntPathRequestMatcher("/logout")).logoutSuccessUrl("/")
.permitAll();
http
.authorizeRequests().anyRequest().authenticated();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
JdbcUserDetailsManager userDetailsService = jdbcUserService();
// userDetailsService.setDataSource(datasource);
// PasswordEncoder encoder = new BCryptPasswordEncoder();
auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
auth.jdbcAuthentication().dataSource(datasource);
}
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Bean
public org.springframework.security.provisioning.JdbcUserDetailsManager jdbcUserService() throws Exception {
JdbcUserDetailsManager jdbcUserDetailsManager = new JdbcUserDetailsManager();
jdbcUserDetailsManager.setDataSource(datasource);
jdbcUserDetailsManager.setAuthenticationManager(authenticationManagerBean());
return jdbcUserDetailsManager;
}
}
#Order(Ordered.HIGHEST_PRECEDENCE)
#Configuration
public class AuthenticationSecurity extends GlobalAuthenticationConfigurerAdapter {
#Autowired
private DataSource dataSource;
#Override
public void init(AuthenticationManagerBuilder auth) throws Exception {
auth
.jdbcAuthentication()
.dataSource( dataSource );
}
}
So, I realize that my configurations are wrong but not really sure how to best fix them. The symptoms are that when I log into my Thymeleaf UI, the session never exires.
I have used various online resources for my spring-security learning & implementation. Unfortunately, I am still not grasping why this is not correct.
You appear to be configuring 3 filter chains (3 WebSecurityConfigurerAdapters) but only one of them configures the HttpSecurity. That's probably not what you intended to do. Maybe consolidate down to WebSecurityConfigurerAdapter and one GlobalAuthenticationConfigurerAdapter and see where it gets you.