I'm using Spring Boot 2.3.4 and I need to call an external web service that needs the oauth2 authentication.
Currently I've achieved that in this way using feign
Client
#FeignClient(name = "myClient", value = "myClient", url = "${app.my.client.apiUrl}", configuration = MyClientConfiguration.class)
public interface MyClient {
#GetMapping(value = "/api/my-url", consumes = "application/json")
String getSomeData();
}
Client Configuration
public class MyClientConfiguration {
private final OAuth2AuthorizedClientService oAuth2AuthorizedClientService;
private final ClientRegistrationRepository clientRegistrationRepository;
public MyClientConfiguration(OAuth2AuthorizedClientService oAuth2AuthorizedClientService, ClientRegistrationRepository clientRegistrationRepository) {
this.oAuth2AuthorizedClientService = oAuth2AuthorizedClientService;
this.clientRegistrationRepository = clientRegistrationRepository;
}
#Bean
public RequestInterceptor requestInterceptor() {
ClientRegistration clientRegistration = clientRegistrationRepository.findByRegistrationId("my-client");
AuthorizedClientServiceOAuth2AuthorizedClientManager authorizedClientManager = new AuthorizedClientServiceOAuth2AuthorizedClientManager(clientRegistrationRepository, oAuth2AuthorizedClientService);
authorizedClientManager.setAuthorizedClientProvider(OAuth2AuthorizedClientProviderBuilder.builder().clientCredentials().build());
return new OAuthClientCredentialsRestTemplateInterceptor(authorizedClientManager, clientRegistration);
}
}
OAuth Interceptor
public class OAuthClientCredentialsRestTemplateInterceptor implements RequestInterceptor {
private static final String BEARER_HEADER_NAME = "Bearer";
private final OAuth2AuthorizedClientManager manager;
private final Authentication emptyPrincipal;
private final ClientRegistration clientRegistration;
public OAuthClientCredentialsRestTemplateInterceptor(OAuth2AuthorizedClientManager manager, ClientRegistration clientRegistration) {
this.manager = manager;
this.clientRegistration = clientRegistration;
this.emptyPrincipal = createEmptyPrincipal();
}
#Override
public void apply(RequestTemplate requestTemplate) {
OAuth2AuthorizeRequest oAuth2AuthorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId(clientRegistration.getRegistrationId()).principal(emptyPrincipal).build();
OAuth2AuthorizedClient client = manager.authorize(oAuth2AuthorizeRequest);
if (client == null)
throw new IllegalStateException("Cannot retrieve a valid client for registration " + clientRegistration.getRegistrationId());
requestTemplate.header(HttpHeaders.AUTHORIZATION, BEARER_HEADER_NAME + " " + client.getAccessToken().getTokenValue());
}
private Authentication createEmptyPrincipal() {
return new Authentication() {
#Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Collections.emptySet();
}
#Override
public Object getCredentials() {
return null;
}
#Override
public Object getDetails() {
return null;
}
#Override
public Object getPrincipal() {
return this;
}
#Override
public boolean isAuthenticated() {
return false;
}
#Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
}
#Override
public String getName() {
return clientRegistration.getClientId();
}
};
}
}
properties
spring:
security:
oauth2:
client:
registration:
microsoft:
client-id: ******
client-secret: ******
scope: ******
authorization-grant-type: client_credentials
provider: my-client
provider:
my-client:
token-uri: ******
app:
my-client:
apiUrl: https://my-url.com
feign:
hystrix:
enabled: false
client:
config:
default:
connect-timeout: 3000
In another project I need the same BUT it's a spring boot application without the web environment, and I've the following error
bean of type 'org.springframework.security.oauth2.client.OAuth2AuthorizedClientService' that could not be found.
How can I solve this situation?
Is it possible to use the oauth2 auto-configuration in an environment without a tomcat (or similar)?
You need to configure a separate class and tell spring IOC that this is stateless session.
#Configuration
#EnableWebSecurity
public class SecurityConfigForOauth extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().httpBasic().disable().formLogin().disable().logout().disable().sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
Check my response on how to get client_credentials working on https://stackoverflow.com/a/65741386/698471, there is no direct dependency on spring-boot-starter-web
Not the solution, only a workaround to convert the microservice into a micro-application: let the job run at configuration time during application startup by either using no batch property or setting spring.batch.job.enabled=true and then terminating the process.
#SpringBootApplication
public class MyBootApp implements CommandLineRunner {
public static void main(String[] args) {
SpringApplication.run(MyBootApp.class, args);
}
//Runs after #Configuration classes are finished synchronously including batch jobs.
#Override
public void run(String... args) throws Exception {
System.exit(0);
}
}
It seems that RestTemplate and WebClient are using spring-web configuration files for autowiring stuff needed for OAuth interceptors.
Related
I have a Spring boot project which runs authentication with spring oauth2 token provider.
Now there is a idea to support a autentication with Keycloak, so that username and password will be stored in Keycloak and it will provide the access token.
Idea is to keep the oauth token store and provider, and as well to have a keycloak one, but to keep the roles and acces right part in spring. Keycloak will only be used for some users as a token provider instead of Spring one and to have a refresh token. So all the user data and access rights and roles are still be done by spring, from database, only the part where it authenticates username and password will be in Keycloak which provides a token.
#EnableWebSecurity
#Configuration
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private ClientDetailsService clientDetailsService;
private AccessDecisionManager accessDecisionManager;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().accessDecisionManager(accessDecisionManager)
.antMatchers("/service/*").fullyAuthenticated()
.anyRequest().permitAll().and().httpBasic().and().csrf().disable();
}
#Override
#Bean(name = "authenticationManagerBean")
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 AffirmativeBased accessDecisionManager() {
List<AccessDecisionVoter<?>> accessDecisionVoters = new ArrayList<>();
accessDecisionVoters.add(new ScopeVoter());
accessDecisionVoters.add(new RoleVoter());
accessDecisionVoters.add(new AuthenticatedVoter());
AffirmativeBased accessDecisionManager = new AffirmativeBased(accessDecisionVoters);
return accessDecisionManager;
}
And there is a Custom client service which grants access rights:
#Component
public class CustomClientService implements ClientDetailsService {
private static Map<String, BaseClientDetails> cache = new ConcurrentHashMap<>();
#Autowired
UserService userService;
#Autowired
AccessRightsService accessRightsService;
#Override
public ClientDetails loadClientByClientId(String paramString) throws ClientRegistrationException {
...
Also there is a custom TokenStore class:
public class MyTokenServices extends DefaultTokenServices {
private static Logger log = LoggerFactory.getLogger(MyTokenServices.class);
public UserService userService;
public AccessRightsService accessRightService;
private TokenStore my_tokenStore;
#Override
public void setTokenStore(TokenStore tokenStore) {
// TODO Auto-generated method stub
super.setTokenStore(tokenStore);
my_tokenStore = tokenStore;
}
#Override
#Transactional
public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
OAuth2AccessToken retVal= super.createAccessToken(authentication);
if(retVal instanceof DefaultOAuth2AccessToken) {
DefaultOAuth2AccessToken defRetVal = (DefaultOAuth2AccessToken)retVal;
log.info("New loging request"+ defRetVal.toString());
// defRetVal.setExpiration( Date.from(LocalDateTime.now().plus(8,ChronoUnit.HOURS).atZone(ZoneId.systemDefault()).toInstant()));
my_tokenStore.storeAccessToken(defRetVal, authentication);
}
return retVal;
}
#Override
public OAuth2Authentication loadAuthentication(String accessTokenValue)
throws AuthenticationException, InvalidTokenException {
OAuth2Authentication retVal = super.loadAuthentication(accessTokenValue);
OAuth2Request oldRequest = retVal.getOAuth2Request();
User user = userService.getUserByUsername(oldRequest.getClientId());
if(changeAutheticator(retVal, user)) {
HashSet<GrantedAuthority> authorities = new HashSet<>();
user.getRoles().forEach(a->authorities.add(new SimpleGrantedAuthority(a.getRoleName())));
Set<String> accessRights = accessRightService.getUserAccessRights(user);
if(accessRights != null) {
accessRights.forEach(rihgt->{
authorities.add(new SimpleGrantedAuthority(rihgt));
});
}
OAuth2Request newRequest = new OAuth2Request(retVal.getOAuth2Request().getRequestParameters(),
oldRequest.getClientId(), authorities, oldRequest.isApproved(), oldRequest.getScope(),
oldRequest.getResourceIds(), oldRequest.getRedirectUri(), oldRequest.getResponseTypes(), oldRequest.getExtensions());
retVal = new OAuth2Authentication(newRequest, retVal.getUserAuthentication());
}
return retVal;
}
/**
* Method that check do we need to change authenticator
* #param retVal
* #param user
* #return
*/
private boolean changeAutheticator(OAuth2Authentication auth, User user) {
if(user == null) return false;
if(user != null ) {
if(user.getRoles() != null) {
if(auth.getOAuth2Request()!=null && auth.getOAuth2Request().getAuthorities() != null){
for(Role role:user.getRoles()){
if(!auth.getOAuth2Request().getAuthorities().stream().anyMatch(a->a.getAuthority().equals(role.getRoleName()))){
return true;
}
}
for(GrantedAuthority ga : auth.getOAuth2Request().getAuthorities()) {
if(!user.getRoles().stream().anyMatch(a->a.getRoleName().equals(ga.getAuthority()))){
return true;
}
}
}
}
}
return false;
}
I was trying to implement a multiple authentiaction like the one from other stackoverflow but that was not a solution. Thinking that I should provide a custom authentication provider with Keycloak or still like not having a solution in head.
I use Keycloak to federate other identities: the only issuers that clients and resource-servers trust are Keycloak instances or realms. Other identity sources are hidden behind it.
With that config, roles are put into tokens by Keycloak, just as any other claims (for the last project I worked on, roles referential is LDAP but it could be a custom database table or Keycloak default one).
It's pretty easy to connect Keycloak to your user database and it comes with many features that I don't want to code and maintain (multi-factor authentication, social login, users, clients and resource-servers management screens,...)
I have a Web Filter that sets an object in a ThreadLocal attribute and I'm trying to understand how/when this Thread local should be cleaned-up (ThreadLocal.remove()) to avoid the exception "User context already initiated." that happens because it is being retrieved from the Spring Boot Thread Pool with the previous values set.
I'm using Spring Webflux.
Where can I hook this SecurityAuthorizationContext.clean() call?
public class SecurityAuthorizationContext
{
private static final ThreadLocal<PrivilegeHolder> userContext = new ThreadLocal<>();
private final List<String> roles;
private SecurityAuthorizationContext(List<String> roles)
{
this.roles = roles;
}
public static void create(List<String> roles)
{
if (nonNull(userContext.get()))
{
log.error("User context already initiated.");
throw new AuthorizationException("User context already initiated.");
}
PrivilegeHolder privilegeHolder = new PrivilegeHolder();
userContext.set(privilegeHolder);
// example of privileges retrieved from database by the user roles
privilegeHolder.add(INSERT);
privilegeHolder.add(DELETE);
}
public static void clean()
{
userContext.remove();
}
public static boolean hasInsertPrivilege()
{
return userContext.get().hasPrivilege(INSERT);
}
public static boolean hasDeletePrivilege()
{
return userContext.get().hasPrivilege(DELETE);
}
}
public class AuthorizationFilter implements OrderedWebFilter
{
private static final String USER_ROLES = "user-roles";
#Override
public int getOrder()
{
return SecurityWebFiltersOrder.AUTHORIZATION.getOrder();
}
#Override
public Mono<Void> filter(ServerWebExchange serverWebExchange, WebFilterChain webFilterChain)
{
ServerHttpRequest request = serverWebExchange.getRequest();
HttpHeaders headers = request.getHeaders();
List<String> roles = headers.get(USER_ROLES);
SecurityAuthorizationContext.create(roles);
return webFilterChain.filter(serverWebExchange);
}
}
#Configuration
#EnableWebFluxSecurity
#EnableTransactionManagement
public class ApplicationConfiguration
{
#Autowired
private AuthorizationFilter authorizationFilter;
#Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http)
{
return http
.csrf().disable()
.authorizeExchange()
.pathMatchers("/**").permitAll()
.and()
.addFilterAt(authorizationFilter, AUTHORIZATION)
.build();
}
}
UPDATE: Long story short ... I just want to extract something from request headers and make it available to all the stack without passing it as parameter.
So, better to use reactor context instead of ThreadLocal, here you can read about: https://projectreactor.io/docs/core/release/reference/#context
Question: is my implementation secure (by API Key standards) OR as secure as using Spring Boot Security?
I have produced a Spring Boot API, but rather than using Spring Boot Security to implement Api Key security, I have written my own API key implementation. The API Key is passed as a #RequestHeader in each 'secured' request (see /booking/cancel below).
Controller:
#RequestMapping(value = "/booking/cancel",
consumes = { "application/json" },
method = RequestMethod.POST)
public ResponseEntity<Void> cancelOrder(#RequestBody Cancellation cancellation,
#RequestHeader String apiKey) {
if(apiKey == null) {
return new ResponseEntity<Void>(HttpStatus.NOT_ACCEPTABLE);
}
long bookingProviderId;
try {
bookingProviderId = bookingService.getIdFromApiKey(apiKey);
if (bookingProviderId < 0) {
return new ResponseEntity<Void>(HttpStatus.NOT_ACCEPTABLE);
}
} catch (ApplicationException e) {
e.printStackTrace();
return new ResponseEntity<Void>(HttpStatus.INTERNAL_SERVER_ERROR);
}
//More code here...
}
Service layer:
The getIdFromApiKey function exists in my service layer and calls the Dao object. It returns a long (Id) which I can subsequently use to manage access in the controller (e.g. prevent a user from cancelling someone else's order).
public long getIdFromApiKey(String apiKey) throws ApplicationException {
return apiKeyDao.selectId(apiKey);
}
Dao Layer:
public long getApiKey (String apiKey) throws DataAccessException {
BookingProvider bp = jdbcTemplate.queryForObject("SELECT * FROM BookingProvider WHERE apiKey = ?", BeanPropertyRowMapper.newInstance(BookingProvider.class), apiKey);
if(bp == null)
return -1;
else
return bp.getId();
}
Late answer: The approach behind your code is good, but you don't need to write so much code. It is better to configure this in one place with Spring Security. It will allow to add security on only on your controllers as their are the entry point of your application. See this link for more details and below is the code I am using:
application.yml
application:
http:
authentication:
header-name: X-API-KEY-TEST
api-key: X-API-KEY-VALUE-TEST
ApiKeyProperties.java
#Getter
#Setter
#NoArgsConstructor
#Configuration
#ConfigurationProperties(prefix = "application.http.authentication")
public class ApiKeyProperties {
private String headerName;
private String apiKey;
}
ApiKeyAuthenticationFilter.java
public class ApiKeyAuthenticationFilter extends AbstractPreAuthenticatedProcessingFilter {
private final String headerName;
public ApiKeyAuthenticationFilter(String headerName) {
this.headerName = headerName;
}
#Override
protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
return request.getHeader(headerName);
}
#Override
protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
return null;
}
}
ApiSecurityConfiguration.java
#Configuration
#EnableWebSecurity
public class ApiSecurityConfiguration {
private final ApiKeyProperties apiKeyProperties;
#Autowired
public ApiSecurityConfiguration(ApiKeyProperties apiKeyProperties) {
this.apiKeyProperties = apiKeyProperties;
}
#Bean
public SecurityFilterChain securityFilterChain(HttpSecurity security) throws Exception {
var filter = new ApiKeyAuthenticationFilter(apiKeyProperties.getHeaderName());
filter.setAuthenticationManager(authentication -> {
var principal = (String) authentication.getPrincipal();
if (!apiKeyProperties.getApiKey().equals(principal)) {
throw new UnauthorizedException("The API key was not found or not the expected value");
}
authentication.setAuthenticated(true);
return authentication;
});
security.antMatcher("/**")
.csrf().disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.addFilter(filter)
.authorizeRequests()
.anyRequest()
.authenticated();
return security.build();
}
}
Postman
I have a application RESTful API. Currently, my resource validated from a authorization servers. (Figure 1). I want my resource must be validated against distinct remote multiple authorization servers. (Figure 2).
How can i implement ResourceServerTokenServices to do this?
My currently setting follow figure (1)
WebSecurityConfigurerAdapter:
#Configuration
#EnableResourceServer
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
#Order(Ordered.HIGHEST_PRECEDENCE)
public class OAuth2ClientConfig extends WebSecurityConfigurerAdapter {
#Autowired
private ResourceServerProperties sso;
#Bean
#Primary
public ResourceServerTokenServices userInfoTokenServices() {
CustomUserInfoTokenServices serv = new CustomUserInfoTokenServices(sso.getUserInfoUri(), sso.getClientId());
return serv;
}
}
CustomUserInfoTokenServices:
public class CustomUserInfoTokenServices implements ResourceServerTokenServices {
private final String userInfoEndpointUrl;
private final String clientId;
public CustomUserInfoTokenServices(String userInfoEndpointUrl, String clientId) {
this.userInfoEndpointUrl = userInfoEndpointUrl;
this.clientId = clientId;
}
#Override
public OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException {
// Call API from userInfoEndpointUrl
// Extract result to get OAuth2Authentication
return //OAuth2Authentication;
}
#Override
public OAuth2AccessToken readAccessToken(String accessToken) {
throw new UnsupportedOperationException("Not supported: read access token");
}
My application properties:
#Resource port
server.port = 8081
# Server oauth configuare
security.oauth2.client.clientId = CLIENT_ID
security.oauth2.client.clientSecret = CLIENT_SECRET
# Authorization server
security.oauth2.resource.user-info-uri = http://127.0.0.1:8080/oauth/user/me
I have my authorization server working with the autoconfiguration of spring-oauth2 #EnableAuthorizationServer in which I have my own custom ClientDetailService service, I am doing very well but I have a problem, it turns out that said service runs 6 times when invoking the endpoint [ oauth / token], which I am not sure is normal behavior but I want it to be executed only once because in it I call my database. Please your support.
My configuration:
#Configuration
#EnableAuthorizationServer
#Slf4j
public class AuthorizationServerConfigurer extends AuthorizationServerConfigurerAdapter {
#Autowired
private ApplicationJwtAccessTokenConverter applicationJwtAccessTokenConverter;
#Autowired
private ApplicationOauth2Service applicationOauth2Service;
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(applicationOauth2Service);
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
.tokenEnhancer(applicationJwtAccessTokenConverter)
.tokenStore(jwtTokenStore());
}
#Bean
public JwtTokenStore jwtTokenStore() {
return new JwtTokenStore(applicationJwtAccessTokenConverter);
}
}
My ClientDetailService:
#Service
#Slf4j
public class ApplicationOauth2Service implements ClientDetailsService {
#Autowired
UserRepository userRepository;
User user;
public ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException {
BaseClientDetails details = new BaseClientDetails();
details.setAuthorizedGrantTypes(Collections.singletonList("client_credentials"));
user = userRepository.findByEmail(clientId);
if (null == user) {
throw new NoSuchClientException("No client with requested id: " + clientId);
}
details.setClientId(user.getAuth().getEmail());
details.setClientSecret(user.getAuth().getPassword());
return details;
}
}
My console output: