I use spring-security-oauth2.
I would like to custom configure the /oauth/token end-point.
After registration a new client, I need to use login and password from this client and create a token for him and return this token.
Can I custom configure this /oauth/token endpoint?
Can I return a custom error if I receive incorrect information for this end-point (incorrect login or password)?
Can I put here a client-id and client-secret and don`t enter this in front-end?
#Configuration
#EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
#Autowired
private DataSource dataSource;
#Autowired
private TokenStore tokenStore;
#Autowired
private JwtAccessTokenConverter jwtTokenEnhancer;
#Autowired
private UserApprovalHandler userApprovalHandler;
#Autowired
private AuthenticationManager authenticationManager;
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.jdbc(dataSource);
}
#Override
public void configure(AuthorizationServerSecurityConfigurer security) {
security.checkTokenAccess("isAuthenticated()");
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(tokenStore).tokenEnhancer(jwtTokenEnhancer).userApprovalHandler(userApprovalHandler)
.authenticationManager(authenticationManager);
}
}
Related
I had Oauth implemented in Spring Boot 1.5.7 but when I switched to 2 it showed me error "java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null".
From some research, I found that this may be an issue about password storage and password encoding.
What I've tried - I tried encoding the client secret in the authorization server file but that doesn't do anything and the error remains.
I've also tried saving the password with {bcrypt} as a prefix as spring security 5 looks for ann {id} during the password search.
I'm not able to fetch the access token and the above error doesn't go. Can someone help me figure this out? I've read and implemented almost everything and it doesn't seem to work.
Update: I was able to solve the above error by saving the password with {bcrypt} format. Similarly applying passwordEncoder in other required places.
Issue: I'm now facing an error with bad credentials. I've debugged and figured that its not getting the username we're trying to pass in the api and receiving null parameter. The flow reaches the userDetailservice but with an epmty parameter. I've attached my UserDetailsService along with this.
SecurityConfig.java
#Configuration
#EnableWebSecurity
public class OAuth2SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
private ClientDetailsService clientDetailsService;
#Autowired
private UserDetailsService userDetailsService;
#Autowired
private CustomPasswordEncoder customPasswordEncoder;
#Autowired
public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(customPasswordEncoder);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.anonymous().disable()
.authorizeRequests()
.antMatchers("/oauth/token").permitAll();
}
#Override
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Bean
#Autowired
public TokenStoreUserApprovalHandler userApprovalHandler(TokenStore tokenStore) {
TokenStoreUserApprovalHandler handler = new TokenStoreUserApprovalHandler();
handler.setTokenStore(tokenStore);
handler.setRequestFactory(new DefaultOAuth2RequestFactory(clientDetailsService));
handler.setClientDetailsService(clientDetailsService);
return handler;
}
#Bean
#Autowired
public ApprovalStore approvalStore(TokenStore tokenStore) throws Exception {
TokenApprovalStore store = new TokenApprovalStore();
store.setTokenStore(tokenStore);
return store;
}
#Bean
public BCryptPasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
AuthorizationServerConfig.java
#Configuration
#EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
private static String REALM = "api-security";
#Value("${app.oauth.client-id}")
private String CLIENT_ID;
#Value("${app.oauth.client-secret}")
private String CLIENT_SECRET;
#Value("${app.oauth.access-token-validity}")
private int accessTokenValidity;
#Value("${app.oauth.refresh-token-validity}")
private int refreshTokenValidity;
#Autowired
#Qualifier("tokenStore")
private TokenStoreService tokenStore;
#Autowired
private UserApprovalHandler userApprovalHandler;
#Autowired
private BCryptPasswordEncoder passwordEncoder;
#Autowired
#Qualifier("authenticationManagerBean")
private AuthenticationManager authenticationManager;
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory().withClient(CLIENT_ID)
.authorizedGrantTypes("password", "authorization_code", "refresh_token", "implicit")
.authorities("ROLE_ADMIN").scopes("read", "write", "trust").secret(passwordEncoder.encode(CLIENT_SECRET))
.accessTokenValiditySeconds(accessTokenValidity).refreshTokenValiditySeconds(refreshTokenValidity);
System.out.println(passwordEncoder.encode(CLIENT_SECRET));
System.out.println(CLIENT_SECRET);
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(tokenStore).userApprovalHandler(userApprovalHandler)
.authenticationManager(authenticationManager);
}
#Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer.realm(REALM + "/client");
}
}
UserDetailsService.java
#Configuration
#EnableWebSecurity
public class OAuth2SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
private ClientDetailsService clientDetailsService;
#Autowired
#Qualifier("userDetailsService")
private UserDetailsService userDetailsService;
#Autowired
public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.anonymous().disable()
.authorizeRequests()
.antMatchers("/oauth/token").permitAll();
}
#Override
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Bean
#Autowired
public TokenStoreUserApprovalHandler userApprovalHandler(TokenStore tokenStore) {
TokenStoreUserApprovalHandler handler = new TokenStoreUserApprovalHandler();
handler.setTokenStore(tokenStore);
handler.setRequestFactory(new DefaultOAuth2RequestFactory(clientDetailsService));
handler.setClientDetailsService(clientDetailsService);
return handler;
}
#Bean
#Autowired
public ApprovalStore approvalStore(TokenStore tokenStore) throws Exception {
TokenApprovalStore store = new TokenApprovalStore();
store.setTokenStore(tokenStore);
return store;
}
#Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
// #Bean
// #Override
// public UserDetailsService userDetailsServiceBean() throws Exception {
// return super.userDetailsServiceBean();
// }
// #Bean
// public UserDetailsService userDetailsService() {
// return super.userDetailsService();
// }
}
For whoever finds this useful, I was able to solve this by the following points:
If you clear your access token collection or table, you'll be able to get the access toke once but that's it. Every request you do after that will go with "500 error - Internal server error".
This happens because spring boot wasn't able to understand the access token from the DB when making other requests, for which you can use "org.springframework.util.SerializationUtils" package. You can search about this, it serializes and deserializes the access tokens and refresh token when requests are made.
I'm currently developing a React WebApp with a Spring Boot REST API in the backend. I want to use OAuth2 to secure the API (and to improve my knowledge). I have however some questions regarding the correct authentication flow to use.
Since the frontend is using JavaScript I should not use flows that require a client secret.
The only flow that does not require a client secret in Spring is the Implicit Flow. With this flow however, Spring does not support refresh tokens. That means that after some time the user would get automatically logged out and needs to authorize the WebApp again.
Another option I saw was creating a client without a secret and then use the Authorization Code flow. But I have some doubts if this is the right way to go.
So my question basically: Which is the best OAuth2 flow to use with a Javascript frontend, when I don't want the user to be logged out after some time?
WebSecurityConfig
#Configuration
#EnableWebSecurity
#Import(Encoders.class)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private UserDetailsService userDetailsServiceImpl;
#Autowired
private PasswordEncoder userPasswordEncoder;
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsServiceImpl).passwordEncoder(userPasswordEncoder);
}
#Override
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.formLogin().permitAll()
.and()
.authorizeRequests().antMatchers("/login", "/error**").permitAll()
.anyRequest().authenticated();
}
}
ResourceServerConfig
#Configuration
#EnableResourceServer
#EnableGlobalMethodSecurity
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
private static final String RESOURCE_ID = "resource-server-rest-api";
private static final String SECURED_READ_SCOPE = "#oauth2.hasScope('read')";
private static final String SECURED_WRITE_SCOPE = "#oauth2.hasScope('write')";
private static final String SECURED_PATTERN = "/api/**";
#Autowired
private DefaultTokenServices tokenServices;
#Autowired
private TokenStore tokenStore;
#Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId(RESOURCE_ID)
.tokenServices(tokenServices)
.tokenStore(tokenStore);
}
#Override
public void configure(HttpSecurity http) throws Exception {
http.antMatcher(SECURED_PATTERN).authorizeRequests().anyRequest().authenticated();
}
}
OAuth2Config
#Configuration
#PropertySource({"classpath:persistence.properties"})
#EnableAuthorizationServer
#Import(WebSecurityConfig.class)
public class OAuth2Config extends AuthorizationServerConfigurerAdapter {
#Autowired
#Qualifier("dataSource")
private DataSource dataSource;
#Autowired
private UserDetailsService userDetailsServiceImpl;
#Autowired
private PasswordEncoder oauthClientPasswordEncoder;
#Autowired
private AuthenticationManager authenticationManager;
#Bean
public OAuth2AccessDeniedHandler oauthAccessDeniedHandler() {
return new OAuth2AccessDeniedHandler();
}
#Bean
public JwtAccessTokenConverter accessTokenConverter() {
final JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(
new ClassPathResource("mykeys.jks"),
"mypass".toCharArray());
converter.setKeyPair(keyStoreKeyFactory.getKeyPair("mykeys"));
return converter;
}
#Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
#Bean
#Primary
public DefaultTokenServices tokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
defaultTokenServices.setSupportRefreshToken(true);
defaultTokenServices.setTokenEnhancer(accessTokenConverter());
return defaultTokenServices;
}
#Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.allowFormAuthenticationForClients().tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()")
.passwordEncoder(oauthClientPasswordEncoder);
}
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.jdbc(dataSource);
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
final TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(accessTokenConverter()));
endpoints
.tokenStore(tokenStore())
.tokenEnhancer(tokenEnhancerChain)
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsServiceImpl);
}
}
The Implicit flow is meant for JavaScript implementations according to the OAuth2 spec
Refresh tokens are not supported with the Implicit flow. The spring implementation is following the Oauth2 spec.
In case of a Javascript client implementations, tokens are stored on the client. When using refresh tokens, the refresh token needs to be persisted on the client in order to obtain a new access token in the future. If this is the case, you might as wel issue a long(er) lasting access token instead of a refresh token. When working client side, there is no advantage in using refresh token.
I'm facing an issue authenticating my app under junit-test.
I've got a CustomAuthenticationProvider
#Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
#Autowired
private UserRepository userRepository;
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
//businness logic
return auth;
}
}
A SecurityConfig, that uses it
#Configuration
#EnableWebSecurity
#Import(CustomAuthenticationProvider.class)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private CustomAuthenticationProvider customAuthenticationProvider;
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(customAuthenticationProvider);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
//some permission filters here
}
}
And my test, that is supposed to call on a Rest API and make sure, that answer is Ok.
#RunWith(SpringJUnit4ClassRunner.class)
#TestExecutionListeners({DependencyInjectionTestExecutionListener.class,
TransactionalTestExecutionListener.class,
DbUnitTestExecutionListener.class})
#SpringApplicationConfiguration(classes = {MyApplication.class},locations = {"/dbContext.xml"})
#TestPropertySource("/application.properties")
#WebIntegrationTest
public class SimpleTest {
#Autowired
protected WebApplicationContext webAppContext;
#Autowired
private CustomAuthenticationProvider customAuthenticationProvider;
#Before
public void setup() {
RestAssuredMockMvc.webAppContextSetup(webAppContext);
SecurityContext context = SecurityContextHolder.createEmptyContext();
Authentication user = customAuthenticationProvider.authenticate(new UsernamePasswordAuthenticationToken("admin", "123"));
context.setAuthentication(user);
SecurityContextHolder.setContext(context);
}
#Test
public void makeSureLoginIsOk() {
given().when().get("/myurl").then().statusCode(200);
}
}
Well, the test always failes, because GET returns 401, instead of 200.
Can anyone help, what it wrong with SecurityContext?
Finally found an answer:
This post How to Mock the security context in Spring MVC for testing was helpfull
#Before
public void setup() {
RestAssuredMockMvc.webAppContextSetup(webAppContext);
RestAssuredMockMvc.enableLoggingOfRequestAndResponseIfValidationFails();
Authentication user = customAuthenticationProvider.authenticate(new UsernamePasswordAuthenticationToken("admin", "123"));
RestAssuredMockMvc.authentication(user); //add this
SecurityContextHolder.getContext().setAuthentication(user);
}
#Test
public void makeSureLoginIsOk() {
RestAssuredMockMvc.get("/myurl").then().statusCode(200); //change given to RestAssured
}
I'm slowly moving into understanding Spring Cloud Security. I've created an authorization service and it works when authorizing and returning a token, but doesn't return any current user details when using that token, when getting these from OAuth2Authentication. This two lines return an NPE:
userInfo.put("user", user.getUserAuthentication().getPrincipal());
userInfo.put("authorities", AuthorityUtils.authorityListToSet(user.getUserAuthentication().getAuthorities()));
OAuth2Authentication user isn't instantiated and is null, while I understood that it should be by default instantiated by Spring Security. Maybe I am missing some configuration beans? Thanks in advance!
Application.class
#SpringBootApplication
#RestController
#EnableResourceServer
#EnableAuthorizationServer
public class AuthorizationServiceApplication {
#RequestMapping(value = {"/user"}, produces = "application/json")
public Map <String, Object> user (OAuth2Authentication user) {
Map <String, Object> userInfo = new HashMap <>();
userInfo.put("user", user.getUserAuthentication().getPrincipal());
userInfo.put("authorities", AuthorityUtils.authorityListToSet(user.getUserAuthentication().getAuthorities()));
return userInfo;
}
public static void main (String[] args) {
SpringApplication.run(AuthorizationServiceApplication.class, args);
}
}
OAuth2Config.class
#Configuration
public class OAuth2Config extends AuthorizationServerConfigurerAdapter {
#Value("${token.secret}")
private String secret;
private AuthenticationManager authenticationManager;
private UserDetailsService userDetailsService;
public OAuth2Config (AuthenticationManager authenticationManager, UserDetailsService userDetailsService) {
this.authenticationManager = authenticationManager;
this.userDetailsService = userDetailsService;
}
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("eagleeye")
.secret(secret)
.authorizedGrantTypes("refresh_token", "password", "client_credentials")
.scopes("webclient", "mobileclient");
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService);
}
}
WebSecurityConfigurer.class
#Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
#Override
#Bean
public AuthenticationManager authenticationManagerBean () throws Exception {
return super.authenticationManagerBean();
}
#Override
#Bean
public UserDetailsService userDetailsServiceBean() throws Exception {
return super.userDetailsServiceBean();
}
// TODO: implemented DB stuff
#Override
protected void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder
.inMemoryAuthentication()
.withUser("deniss").password("deniss1").roles("USER")
.and()
.withUser("oksana").password("oksana").roles("USER, ADMIN");
}
private CsrfTokenRepository csrfTokenRepository() {
HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository();
repository.setSessionAttributeName("_csrf");
return repository;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().csrfTokenRepository(csrfTokenRepository());
}
}
In the end I got it working like this:
Application.class
#SpringBootApplication
#RestController
#EnableResourceServer
public class AuthorizationServiceApplication {
private final Logger log = LoggerFactory.getLogger(this.getClass());
#RequestMapping("/user")
public Principal user(Principal user) {
log.info("User information display for User: " + user.getName());
return user;
}
#Bean
UserDetailsService userDetailsService() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("deniss").password("deniss").roles("USER").build());
return manager;
}
public static void main (String[] args) {
SpringApplication.run(AuthorizationServiceApplication.class, args);
}
}
OAuth2Config.java
#Configuration
#EnableAuthorizationServer
public class OAuth2Config extends AuthorizationServerConfigurerAdapter {
//TODO: refactor to recieve this info from config server
#Value("${token.secret}")
private String secret;
#Autowired
private AuthenticationManager authenticationManager;
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager);
}
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("eagleeye")
.secret(secret)
.authorizedGrantTypes("refresh_token", "password", "client_credentials")
.scopes("webclient", "mobileclient");
}
}
SecurityConfigurer.class
#Configuration
#EnableGlobalAuthentication
public class SecurityConfigurer extends GlobalAuthenticationConfigurerAdapter {
#Autowired
private UserDetailsService userDetailsService;
// TODO: implemented DB stuff
#Override
public void init(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder.userDetailsService(this.userDetailsService);
}
}
I met the same problem, it seems a bug of new version. I changed Spring Boot 1.5.9.RELEASEļ¼Spring Cloud Edgware.RELEASE backed to Spring Boot 1.4.4.RELEASEļ¼Spring Cloud Camden.SR5, the problem disappeared.
Set security.oauth2.resource.filter-order=3 configuration property to restore the ordering used in previous versions. See enter link description here for more details.
Im trying to split the resource server from the authorization server in spring-boot. I have two different applications that i'm running separately. In the authorization server i can get the bearer token from oauth/token but when i'm trying to get access to the resource(sending the token in header) i'm getting an invalid token error. My intention is to use the InMemoryTokenStore and the bearer token. Can anyone tell me what is wrong in my code?
Authorization Server:
#SpringBootApplication
public class AuthorizationServer extends WebMvcConfigurerAdapter {
public static void main(String[] args) {
SpringApplication.run(AuthorizationServer.class, args);
}
#Configuration
#EnableAuthorizationServer
protected static class OAuth2Config extends AuthorizationServerConfigurerAdapter {
private TokenStore tokenStore = new InMemoryTokenStore();
#Autowired
private AuthenticationManager authenticationManager;
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager)
.tokenStore(tokenStore);
}
#Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.checkTokenAccess("hasAuthority('ROLE_USER')");
}
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients
.inMemory()
.withClient("user")
.secret("password")
.authorities("ROLE_USER")
.authorizedGrantTypes("password")
.scopes("read", "write")
.accessTokenValiditySeconds(1800);
}
}
Resource Server:
#SpringBootApplication
#RestController
#EnableOAuth2Resource
#EnableWebSecurity
#Configuration
public class ResourceServer extends WebSecurityConfigurerAdapter {
public static void main(String[] args){
SpringApplication.run(ResourceServer.class, args);
}
#RequestMapping("/")
public String home(){
return "Hello Resource World!";
}
#Bean
public ResourceServerTokenServices tokenService() {
RemoteTokenServices tokenServices = new RemoteTokenServices();
tokenServices.setClientId("user");
tokenServices.setClientSecret("password");
tokenServices.setTokenName("tokenName");
tokenServices.setCheckTokenEndpointUrl("http://localhost:8080/oauth/check_token");
return tokenServices;
}
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
OAuth2AuthenticationManager authenticationManager = new OAuth2AuthenticationManager();
authenticationManager.setTokenServices(tokenService());
return authenticationManager;
}
#Configuration
#EnableResourceServer
protected static class ResourceServerConfig extends ResourceServerConfigurerAdapter {
#Override
public void configure(HttpSecurity http) throws Exception {
http
.requestMatchers()
.antMatchers("/","/home")
.and()
.authorizeRequests()
.anyRequest().access("#oauth2.hasScope('read')");
}
#Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
TokenStore tokenStore = new InMemoryTokenStore();
resources.resourceId("Resource Server");
resources.tokenStore(tokenStore);
}
}
You have created 2 instances of InMemoryTokenStore. If you want to share tokens between the auth server and resource server they need the same store.