I am currently working on a Spring Boot application and I have the task to do the security of the application. They suggested to use OAuth2 token authentification even thought in other applications I manage to create the security with other spring security tutorial.
This are created based on tutorials I found on different sources:
public class OAuthPermissionConfig extends ResourceServerConfigurerAdapter
#Override
public void configure(HttpSecurity http) throws Exception {
http.anonymous().disable()
.authorizeRequests()
.antMatchers("/pim/oauth/token").permitAll().and().formLogin()
.and().authorizeRequests().antMatchers("/actuator/**", "/v2/api-docs", "/webjars/**",
"/swagger-resources/configuration/ui", "/swagger-resources", "/swagger-ui.html",
"/swagger-resources/configuration/security").hasAnyAuthority("ADMIN")
.anyRequest().authenticated();
}
public class CustomAuthenticationProvider implements AuthenticationProvider
#Autowired
private ADService adService;
#Autowired
private UserService userService;
#Override
#Transactional
public Authentication authenticate(Authentication authentication) {
try {
String username = authentication.getName();
String password = authentication.getCredentials().toString();
User user = userService.getUserByUsername(username);
userService.isUserAllowedToUseTheApplication(user);
if (adService.isUserNearlyBlockedInAD(user)) {
throw new BadCredentialsException(CustomMessages.TOO_MANY_LOGIN_FAILED);
} else {
adService.login(username, password);
}
List<GrantedAuthority> userAuthority = user.getRoles().stream()
.map(p -> new SimpleGrantedAuthority(p.getId())).collect(Collectors.toList());
return new LoginToken(user, password, userAuthority);
} catch (NoSuchDatabaseEntryException | NullArgumentException | NamingException | EmptyUserRolesException e) {
throw new BadCredentialsException(CustomMessages.INVALID_CREDENTIALS + " or " + CustomMessages.UNAUTHORIZED);
}
}
#Override
public boolean supports(Class<?> authentication) {
return authentication.equals(
UsernamePasswordAuthenticationToken.class);
}
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Bean
public PasswordEncoder getPasswordEncoder() {
return new BCryptPasswordEncoder();
}
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
public class OAuthServerConfig extends AuthorizationServerConfigurerAdapter
#Autowired
private AuthenticationManager authenticationManager;
#Autowired
private UserService userService;
#Autowired
private PasswordEncoder passwordEncoder;
#Bean
public TokenEnhancer tokenEnhancer() {
return new CustomTokenEnhancer();
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints.authenticationManager(authenticationManager).tokenEnhancer(tokenEnhancer());
}
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients
.inMemory()
.withClient("pfjA#Dmin")
.secret(passwordEncoder.encode("4gM~$laY{gnfShpa%8Pcjwcz-J.NVS"))
.authorizedGrantTypes("password")
.accessTokenValiditySeconds(UTILS.convertMinutesToSeconds(1440))
.scopes("read", "write", "trust")
.resourceIds("oauth2-resource");
}
#Override
public void configure(AuthorizationServerSecurityConfigurer security) {
security.checkTokenAccess("isAuthenticated()").allowFormAuthenticationForClients();
}
When testing the login, I use postman with this parameters :
http://localhost:8080/oauth/token?grant_type=password
Headers: Basic btoa(pfjA#Dmin,4gM~$laY{gnfShpa%8Pcjwcz-J.NVS)
Content-Type : application/x-www-form-urlencoded
Body: form-data -> username and pass
that should be a valid user credentials from the database.
And the user will respond if the credentials are correct
"access_token": "f0dd6eee-7a64-4079-bb1e-e2cbcca6d7bf",
"token_type": "bearer",
"expires_in": 86399,
"scope": "read write trust"
Now I have to use this token for all the other requests otherwise I dont have any permision to use the application.
My question: Is this other version of Spring Security or what? I read about OAuth2 authentication but I read that an application can have BOTH Spring Security and OAuth2. Can someone please explain me if there is something wrong with the way we decided to implement the app security?
Thank you very much!
Yes,you can think it's a different version of spring security,it replaces some strategies of standard spring security,such as the authorization checking of requests.
Related
I'm making a spring boot webserver which has spring security and jwt for user authentication/authorization via username and password. But seems like spring recognize /api/users/signup and /api/users/signin
as must-be-authenticated URL.
UserController.java:
#PostMapping("/signin")
public ResponseEntity<String> login(#ApiParam("Username") #RequestParam String username, //
#ApiParam("Password") #RequestParam String password) {
return ResponseEntity.ok(userService.signin(username, password));
}
#PostMapping("/signup")
public void signUp(#ApiParam("SignUp User") #RequestBody SignUpRequest request) {
User user = User.of(request.getUsername(), bCryptPasswordEncoder.encode(request.getPassword()), request.getEmail());
Role userRole = roleRepository.findByName(RoleName.ROLE_MEMBER).orElse(null);
user.setRoles(Collections.singleton(userRole));
userRepository.save(user);
}
WebSecurityConfig.java
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(
prePostEnabled = true
)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private final JwtTokenProvider jwtTokenProvider;
public WebSecurityConfig(JwtTokenProvider jwtTokenProvider) {
this.jwtTokenProvider = jwtTokenProvider;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
// Disable CSRF (cross site request forgery)
http.csrf().disable();
// No session will be created or used by spring security
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
// Entry points
http.authorizeRequests()//
.antMatchers("/api/users/signin").permitAll()//
.antMatchers("/api/users/signup").permitAll()//
.antMatchers("/api/test/**").permitAll()
.antMatchers("/h2-console/**/**").permitAll()
// Disallow everything else..
.anyRequest().authenticated();
// If a user try to access a resource without having enough permissions
http.exceptionHandling().accessDeniedPage("/login");
// Apply JWT
http.apply(new JwtTokenFilterConfigurer(jwtTokenProvider));
http.cors().disable();
// Optional, if you want to test the API from a browser
// http.httpBasic();
super.configure(http);
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(10);
}
#Override
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
JwtTokenFilter.java:
public class JwtTokenFilter extends OncePerRequestFilter {
private JwtTokenProvider jwtTokenProvider;
public JwtTokenFilter(JwtTokenProvider jwtTokenProvider) {
this.jwtTokenProvider = jwtTokenProvider;
}
#Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
String token = jwtTokenProvider.resolveToken(httpServletRequest);
try {
if (token != null && jwtTokenProvider.validateToken(token)) {
Authentication auth = jwtTokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(auth);
}
} catch (CustomException ex) {
//this is very important, since it guarantees the user is not authenticated at all
SecurityContextHolder.clearContext();
httpServletResponse.sendError(ex.getHttpStatus().value(), ex.getMessage());
return;
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}
MyUserDetailsService.java:
#Service
public class MyUserDetailsService implements UserDetailsService {
#Autowired
private UserRepository userRepository;
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
final User user = userRepository.findByUsername(username).orElseThrow(() -> new CustomException("User doesn't exist", HttpStatus.NOT_FOUND));
List<GrantedAuthority> authorities = user.getRoles().stream().map(role ->
new SimpleGrantedAuthority(role.getName().getAuthority())
).collect(Collectors.toList());
if (user == null) {
throw new UsernameNotFoundException("User '" + username + "' not found");
}
return org.springframework.security.core.userdetails.User//
.withUsername(username)
.password(user.getPassword())
.authorities(authorities)
.accountExpired(false)
.accountLocked(false)
.credentialsExpired(false)
.disabled(false)
.build();
}
}
When I request to both of these links as I told above. It's done quickly by giving me 401 HTTP error code while testing on postman.
Both this link and this link are not helpful at all.
You might want to try excluding these URLs from the WebSecurity section, instead, so that they do not get processed by Spring Security and your JwtTokenFilter at all.
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
public void configure(final WebSecurity web) throws Exception {
web.ignoring()
.antMatchers("/api/users/signin").antMatchers("/api/users/signup");
}
}
I configured resource and authorization servers in one application. I use spring-security oauth2, with Resource Owner Password Credentials. Can I set up basic auth on the server side? I don't want to do it on the front-end.
I don't know what a part of the code I need to show...
When I want to receive a token I need to enter this in postman:
Can I configure it on the server side?
Authorization Server:
#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)
.pathMapping("/oauth/token", "/login");
}
}
Resource Server:
#Configuration
#EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
private static final String RESOURCE_ID = "resource_id";
#Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.resourceId(RESOURCE_ID).stateless(false);
}
#Override
public void configure(HttpSecurity http) throws Exception {
http
.cors().and().csrf().disable()
.authorizeRequests()
.antMatchers("/swagger-ui.html#").permitAll()
.antMatchers("/").authenticated()
.and().exceptionHandling().accessDeniedHandler(new OAuth2AccessDeniedHandler());
}
}
Security config:
#Configuration
#EnableWebSecurity(debug = true)
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private ClientDetailsService clientDetailsService;
#Autowired
private CustomAuthenticationProvider customAuthenticationProvider;
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(10);
}
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Override
public void configure(final AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(customAuthenticationProvider);
}
#Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtTokenEnhancer());
}
#Bean
protected JwtAccessTokenConverter jwtTokenEnhancer() {
converter.setSigningKey("Demo-Key-1");
return converter;
}
#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;
}
#Override
#Order(Ordered.HIGHEST_PRECEDENCE)
protected void configure(final HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/resources/**").permitAll()
.antMatchers(HttpMethod.OPTIONS, "/oauth/token").permitAll()
.and().cors().and().csrf().disable();
}
}
This answer is accompanied by a complete and working sample.
Maybe you are biting off more than you can chew here?
For example:
.antMatchers(HttpMethod.OPTIONS, "/oauth/token").permitAll()
The /oauth/token endpoint must remain protected. This is the endpoint on the authorization server that issues tokens to authenticated clients. The system will probably fail with NullpointerException or other exceptions if you open it, however, the above configuration option indicate that maybe you're a bit confused about how OAuth2 works.
What I would recommend is to first fully understand authorization server versus resource server. You can definitely combine the two, but they would have very different endpoints.
Authorization Server - typical endpoints
/oauth/token - issues tokens
/oauth/authorize - issues authorization codes
/introspect - validates a token and returns token claims in a known format
Resource Server - These would be your application endpoints, requiring Bearer tokens, for example
/account/123/debit
and these endpoints expect a stateless request that has an authorization header
Authorization: Bearer <token value here>
A controller for a resource server would look like this:
#PreAuthorize("hasRole('your-scope-role')")
#RequestMapping(value = "/hello")
#ResponseBody
public String hello(Principal principal) {
return "Hello to " + principal.getName();
}
Feel free to review the simple project that I have created for you.
In addition to that, I also recommend this video on OAuth2 and OpenID Connect
In my sample, I have configured the clients like this:
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
InMemoryClientDetailsService clientDetails = new InMemoryClientDetailsService();
BaseClientDetails client = new BaseClientDetails(
"testclient",
null,
"testscope,USER,ADMIN",
"password",
null
);
client.setClientSecret(passwordEncoder.encode("secret"));
clientDetails.setClientDetailsStore(
Collections.singletonMap(
client.getClientId(),
client
)
);
clients.withClientDetails(clientDetails);
}
Take a look at this simple test case, the client is using http-basic authentication:
mvc.perform(
post("/oauth/token")
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
.header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
.param("username", "admin")
.param("password", "password")
.param("grant_type", "password")
.param("response_type", "token")
.param("client_id", "testclient")
.header("Authorization", "Basic " + Base64.encodeBase64String("testclient:secret".getBytes()))
This is client authentication, using the http-basic method:
.header("Authorization", "Basic " + Base64.encodeBase64String("testclient:secret".getBytes()))
I am trying to front or "wrap" a RESTful API service application in front of a backend HTTP service. The backend service maintains authentication state via proprietary sessions. Authentication with the backend server is achieved through a convoluted conversation between the authenticating client and the server. In general, interaction with this server is non-standard and not good enough to expose to our customers as an API. However there is too much legacy to overhaul and re-engineed the entire HTTP interface.
My objective is to abstract the interface to this backend server by fronting this server with a RESTful server that implements OAuth2 authentication via password grant so that 3rd party APIs can interface with the backend server in a standard and well understood way.
I have built a simple REST SpringBoot application that can authenticate with the backend. The part I am battling with is figuring out how best to map authenticated tokens/requests to the backend's session IDs.
In essence I am trying to associate a bit of state on each authenticated authorization.
As a solution I've thought about implementing my own TokenStore which will maintain the mapping. Alternatively I have thought about implementing a "Custom Token Granter". However I have a strong intuition that there is a better and easier way to achieve my objective.
Additional information:
#EnableAuthorizationServer and #EnableResourceServer are in the same application.
Version information:
org.springframework.security.oauth:spring-security-oauth2 2.0.13.RELEASE
org.springframework.security:spring-security-config 4.2.2.RELEASE
org.springframework.boot:spring-boot-starter-security 1.5.3.RELEASE
This is my Resource Server Config:
#Configuration
#Profile(Common.CONST_EXTAPI_PROFILE)
#EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
#Override
public void configure(HttpSecurity http) throws Exception {
http
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED).and()
.headers().frameOptions().disable().and()
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/", "/home","/register","/login", "/auth").permitAll()
.antMatchers(HttpMethod.OPTIONS,"/oauth/token").permitAll()
.antMatchers("/extapi/agent", "/extapi/agent/**", "/extapi/account/**", "/extapi/sale/**").authenticated();
}
}
This is my Authentication Server Config:
#Configuration
#Profile(Common.CONST_EXTAPI_PROFILE)
#EnableAuthorizationServer
public class OAuth2AuthorizationConfig extends AuthorizationServerConfigurerAdapter {
#Autowired
private AuthenticationManager authenticationManager;
#Order(-1)
public class MyWebSecurity extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS, "/oauth/token").permitAll();
}
}
#Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
KeyPair keyPair = new KeyStoreKeyFactory(new ClassPathResource("keystore.jks"), "foobar".toCharArray())
.getKeyPair("test");
converter.setKeyPair(keyPair);
return converter;
}
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception
{
clients.inMemory()
.withClient("acme")
.autoApprove(true)
.secret("acmesecret")
.authorizedGrantTypes("password", "authorization_code", "refresh_token").scopes("openid");
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager)
.accessTokenConverter(jwtAccessTokenConverter());
}
#Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer)
throws Exception {
oauthServer.tokenKeyAccess("permitAll()").checkTokenAccess(
"isAuthenticated()");
}
#Bean
public TokenStore tokenStore() {
return new InMemoryTokenStore();
}
}
And the Custom Authentication Provider
#Component
#Profile(Common.CONST_EXTAPI_PROFILE)
public class CustomAuthenticationProvider implements AuthenticationProvider
{
#Autowired
private LoginSessionData sessionData;
#Autowired
private GuiAuthenticationService authService;
#Autowired
private ContextService contextService;
#Autowired
private RSAEncryptionService rsaService;
#Autowired
private ObjectMapper mapper;
#Override
public Authentication authenticate(Authentication auth) throws AuthenticationException {
final String username = auth.getName();
final String password = auth.getCredentials().toString();
Authentication result = null;
DataWrapper oauthResponse = new DataWrapper();
try
{
BaseResponse resp = authService.authenticate(contextService.getCompanyID(), false, null);
oauthResponse.setAuth(rsaService.getPublicKey());
oauthResponse.setData(mapper.writeValueAsString(resp));
if(resp.getState().equals("REQUIRE_UTF8_USERNAME")){
BaseRequest request = new BaseRequest();
request.setData(username);
request.setCid(Integer.toString(contextService.getCompanyID()));
sessionData.captureData(request);
resp = authService.process(request);
}
if(resp.getState().equals("REQUIRE_RSA_PASSWORD"))
{
BaseRequest request = new BaseRequest();
request.setData(password);
request.setCid(Integer.toString(contextService.getCompanyID()));
resp = authService.process(request);
}
if(resp.getState().equals("AUTHENTICATED"))
{
UsernamePasswordAuthenticationToken token =
new UsernamePasswordAuthenticationToken(username, password, Collections.emptyList());
result = token;
}
}
catch (Exception e)
{
sessionData.setCurrentState(AuthenticationState.INVALID);
oauthResponse.setError(e.getLocalizedMessage());
oauthResponse.setData(Common.CONST_FATAL_ERROR);
e.printStackTrace();
}
if(result == null)
throw new BadCredentialsException("External system authentication failed");
return result;
}
#Override
public boolean supports(Class<?> auth) {
return auth.equals(UsernamePasswordAuthenticationToken.class);
}
}
I am trying to protect my microservices on Spring Boot using Oath2 with Client Credentials flow.
By the way, those microservices will only talk each other over the middleware layer, I mean no user credentials are needed to allow the authorization (user login process as Facebook).
I have looked for samples on the Internet showing how to create an authorization and resource server to manage this communication. However I just found examples explaining how to do it using user credentials (three legs).
Does anyone have any sample how to do it in Spring Boot and Oauth2? If it is possible give further details about the scopes used, token exchanging would be grateful.
We have REST services protected with Oauth2 Client credentials scheme. The Resource and authorization service are running in the same app, but can be split into different apps.
#Configuration
public class SecurityConfig {
#Configuration
#EnableResourceServer
protected static class ResourceServer extends ResourceServerConfigurerAdapter {
// Identifies this resource server. Usefull if the AuthorisationServer authorises multiple Resource servers
private static final String RESOURCE_ID = "*****";
#Resource(name = "OAuth")
#Autowired
DataSource dataSource;
#Override
public void configure(HttpSecurity http) throws Exception {
// #formatter:off
http
.authorizeRequests().anyRequest().authenticated();
// #formatter:on
}
#Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId(RESOURCE_ID);
resources.tokenStore(tokenStore());
}
#Bean
public TokenStore tokenStore() {
return new JdbcTokenStore(dataSource);
}
}
#Configuration
#EnableAuthorizationServer
protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
#Resource(name = "OAuth")
#Autowired
DataSource dataSource;
#Bean
public TokenStore tokenStore() {
return new JdbcTokenStore(dataSource);
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(tokenStore());
}
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.jdbc(dataSource);
}
}
}
Datasource config for the Oauth2 tables:
#Bean(name = "OAuth")
#ConfigurationProperties(prefix="datasource.oauth")
public DataSource secondaryDataSource() {
return DataSourceBuilder.create().build();
}
Communicating with authentication & resource server goes as followed
curl -H "Accept: application/json" user:password#localhost:8080/oauth/token -d grant_type=client_credentials
curl -H "Authorization: Bearer token" localhost:8080/...
The following record is present in the Oauth2 Database:
client_id resource_ids client_secret scope authorized_grant_types web_server_redirect_uri authorities access_token_validity refresh_token_validity additional_information autoapprove
user **** password NULL client_credentials NULL X NULL NULL NULL NULL
Resttemplate configuration in client application
#Configuration
#EnableOAuth2Client
public class OAuthConfig {
#Value("${OAuth2ClientId}")
private String oAuth2ClientId;
#Value("${OAuth2ClientSecret}")
private String oAuth2ClientSecret;
#Value("${Oauth2AccesTokenUri}")
private String accessTokenUri;
#Bean
public RestTemplate oAuthRestTemplate() {
ClientCredentialsResourceDetails resourceDetails = new ClientCredentialsResourceDetails();
resourceDetails.setId("1");
resourceDetails.setClientId(oAuth2ClientId);
resourceDetails.setClientSecret(oAuth2ClientSecret);
resourceDetails.setAccessTokenUri(accessTokenUri);
/*
When using #EnableOAuth2Client spring creates a OAuth2ClientContext for us:
"The OAuth2ClientContext is placed (for you) in session scope to keep the state for different users separate.
Without that you would have to manage the equivalent data structure yourself on the server,
mapping incoming requests to users, and associating each user with a separate instance of the OAuth2ClientContext."
(http://projects.spring.io/spring-security-oauth/docs/oauth2.html#client-configuration)
Internally the SessionScope works with a threadlocal to store variables, hence a new thread cannot access those.
Therefore we can not use #Async
Solution: create a new OAuth2ClientContext that has no scope.
*Note: this is only safe when using client_credentials as OAuth grant type!
*/
// OAuth2RestTemplate restTemplate = new OAuth2RestTemplate(resourceDetails, oauth2ClientContext);
OAuth2RestTemplate restTemplate = new OAuth2RestTemplate(resourceDetails, new DefaultOAuth2ClientContext());
return restTemplate;
}
}
You can inject the restTemplate to talk (Asynchronously) to the Oauth2 secured service.
We do not use scope at the moment.
Update with Spring-boot-2.7 and Java 17.
https://chuangtc.com/Java/spring-boot-27-security-social-login.php
public class SecurityConfig {
#Value("${spring.social.facebook.appSecret}")
String appSecret;
#Value("${spring.social.facebook.appId}")
String appId;
#Autowired
private UserDetailsService userDetailsService;
#Autowired
private FacebookConnectionSignup facebookConnectionSignup;
#Bean
public AuthenticationManager authManager(HttpSecurity http) throws Exception {
return http.getSharedObject(AuthenticationManagerBuilder.class)
.userDetailsService(userDetailsService)
.and()
.build();
}
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf()
.disable()
.authorizeRequests()
.antMatchers("/login*", "/signin/**", "/signup/**")
.permitAll()
.anyRequest()
.authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.logout();
return http.build();
}
#Bean
// #Primary
public ProviderSignInController providerSignInController() {
ConnectionFactoryLocator connectionFactoryLocator = connectionFactoryLocator();
UsersConnectionRepository usersConnectionRepository = getUsersConnectionRepository(connectionFactoryLocator);
((InMemoryUsersConnectionRepository) usersConnectionRepository).setConnectionSignUp(facebookConnectionSignup);
return new ProviderSignInController(connectionFactoryLocator, usersConnectionRepository, new FacebookSignInAdapter());
}
private ConnectionFactoryLocator connectionFactoryLocator() {
ConnectionFactoryRegistry registry = new ConnectionFactoryRegistry();
registry.addConnectionFactory(new FacebookConnectionFactory(appId, appSecret));
return registry;
}
private UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
return new InMemoryUsersConnectionRepository(connectionFactoryLocator);
}
}
FacebookSignInAdapter
#Service
public class FacebookSignInAdapter implements SignInAdapter {
#Override
public String signIn(String localUserId, Connection<?> connection, NativeWebRequest request) {
System.out.println(" ====== Sign In adapter");
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(connection.getDisplayName(), null, Arrays.asList(new SimpleGrantedAuthority("FACEBOOK_USER"))));
return null;
}
}
I have a working Spring Boot 1.3.1 application with an AngularJS client using the "password" grant type with OAuth2. I know this is not very good as the client_id and client_secret are visible in the AngularJS code by everybody.
I would like to change to the "implicit" grant. My authorisation server and resource server are running in the same application.
This is my current configuration of the authorisation server:
#Configuration
#EnableAuthorizationServer
protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
#Autowired
private MyApplicationSecuritySettings securitySettings;
#Autowired
private AuthenticationManager authenticationManager;
#Autowired
private UserDetailsService userDetailsService;
#Autowired
private PasswordEncoder passwordEncoder;
#Autowired
private DataSource dataSource;
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient(securitySettings.getAngularClientId())
.authorizedGrantTypes("password","refresh_token")
.scopes("read", "write")
.resourceIds(RESOURCE_ID)
.secret(passwordEncoder.encode(securitySettings.getAngularClientSecret()));
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(tokenStore())
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService);
}
#Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.passwordEncoder(passwordEncoder);
}
#Bean
#Primary
public DefaultTokenServices tokenServices() {
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setSupportRefreshToken(true);
tokenServices.setTokenStore(tokenStore());
return tokenServices;
}
#Bean
public TokenStore tokenStore() {
return new JdbcTokenStore(dataSource) {
// Workaround for https://github.com/spring-projects/spring-boot/issues/5071
#Override
protected OAuth2Authentication deserializeAuthentication(byte[] authentication) {
ObjectInputStream input = null;
try {
input = new ConfigurableObjectInputStream(
new ByteArrayInputStream(authentication),
Thread.currentThread().getContextClassLoader());
return (OAuth2Authentication) input.readObject();
} catch (Exception ex) {
throw new IllegalArgumentException(ex);
} finally {
if (input != null) {
try {
input.close();
} catch (IOException ex) {
// Continue
}
}
}
}
};
}
}
I also have this WebSecurityConfigurerAdapter subclass:
#Configuration
#EnableWebSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
private UserDetailsService userDetailsService;
#Autowired
private PasswordEncoder passwordEncoder;
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder);
}
#Override
public void configure(WebSecurity web) throws Exception {
// Disable security for bower components
web.ignoring().antMatchers("/components/**");
}
#Override
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
and this is the configuration of the resource server:
#Configuration
#EnableResourceServer
#EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
#Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId(RESOURCE_ID);
}
#Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/admin", "/admin/", "/index.html", "/home.html", "/", "/logout",
"/partials/login.html"
)
.permitAll()
.authorizeRequests()
.antMatchers("/management/**").hasRole("ADMIN")
.anyRequest().authenticated();
}
}
With this configuration, a token can be requested via /oauth/token. This is implemented in AngularJS when the user visits http://localhost:8080/admin/#/login and types his username and password in that HTML form.
For the implicit grant, I am changing the ClientDetailsServiceConfigurer to:
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient(securitySettings.getAngularClientId())
.authorizedGrantTypes("implicit")
.scopes("read", "write")
.resourceIds(RESOURCE_ID);
}
If I then try the following url: http://localhost:8080/oauth/authorize?client_id=angularClient&response_type=token
The browser redirects to http://localhost:8080/login and shows:
<oauth>
<error_description>Full authentication is required to access this resource</error_description>
<error>unauthorized</error>
</oauth>
Am I testing this implicit flow wrongly?
Do I need to change something in the configuration so that the redirect goes to http://localhost:8080/admin/#/login instead? Or does the angular app need to go from http://localhost:8080/admin/#/login to http://localhost:8080/oauth/authorize passing in the user's username and password somewhere?