I'm making a Slack app with Spring Boot and have created an endpoint for Slack to send payloads to. Since Slack runs over HTTPS I have had to enable HTTPS by generating a self-signed certificate purely for testing purposes. I updated my application.yml with the following:
server:
port: 3000
ssl:
enabled: true
key-store: classpath:cert.p12
key-store-password: my_password
key-store-type: pkcs12
When I try to access the endpoint in my browser I get the error NET::ERR_CERT_INVALID. I then try curl on my endpoint and get the error:
curl: (60) SSL certificate problem: self signed certificate
curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it.
My Slack app doesn't seem to be able to reach the endpoint either, giving an error /hello failed with the error "ssl_cacert" when I try a slash command.
Is there something else I need to do when setting up a self signed certificate? I'm aware this is unsafe in practice and therefore may be the root of these errors.
Many thanks for the help!
First of all, you have to set up your backend correctly:
Add dependency:
implementation 'org.apache.httpcomponents:httpclient:4.5'
Provide RestTemplate bean:
#Bean
private RestTemplate restTemplate() {
SSLContext sslContext = buildSslContext();
SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(sslContext);
HttpClient httpClient = HttpClients.custom()
.setSSLSocketFactory(socketFactory)
.build();
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(httpClient);
return new RestTemplate(factory);
}
private SSLContext buildSslContext() {
try {
char[] keyStorePassword = sslProperties.getKeyStorePassword();
return new SSLContextBuilder()
.loadKeyMaterial(
KeyStore.getInstance(new File(sslProperties.getKeyStore()), keyStorePassword),
keyStorePassword
).build();
} catch (Exception ex) {
throw new IllegalStateException("Unable to instantiate SSL context", ex);
} finally {
sslProperties.setKeyStorePassword(null);
sslProperties.setTrustStorePassword(null);
}
}
Provide required SSL properties in your application.properties or application.yaml file:
server:
ssl:
enabled: true
key-store: /path/to/key.keystore
key-store-password: password
key-alias: alias
trust-store: /path/to/truststore
trust-store-password: password
That's it. Now you can see your Tomcat is starting on 8080 (or another port) (https).
Alternatively, you can use my spring boot starter
Related
I have couple of microservices running behind the Spring cloud gateway and all microservices run behind are protected by OAuth2 protocol using spring authorization server. Everything is working fine but few of the microservices would like to exchange data with each other but these resource servers are protected.
I have created the registered client (for each microservice with their own client id & client secret) at authorization server and as well as respective OAuth2 client configuration at each service. I have configured this setup with Webclient to fetch data from other services needed as follows
#Configuration
#EnableWebSecurity
public class SecurityConfig {
#Bean
SecurityFilterChain defaultFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
)
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
return http.build();
}
#Bean
public WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
oauth2Client.setDefaultClientRegistrationId("server");
return WebClient.builder()
.apply(oauth2Client.oauth2Configuration())
.build();
}
#Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.clientCredentials()
.build();
DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
....
....
application.yml (similar config. for resource-client2)
spring:
security:
oauth2:
client:
registration:
server:
scope: ROLE_RESOURCE_CLIENT1
client-id: resource-client1
client-secret: '{noop}secret1'
client-authentication-method: basic
authorization-grant-type: client_credentials
client-name: resource-client1-client-credentials
provider:
server:
token-uri: http://localhost:9001/oauth2/token
resourceserver:
jwt:
issuer-uri: http://localhost:9001
and at authorization server, the client configuration is as follows
private final static RegisteredClient resource1ServiceClient = RegisteredClient.withId("2")
.clientId("resource-client1")
.clientSecret("{noop}secret1")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.build();
private final static RegisteredClient resource2ServiceClient = RegisteredClient.withId("3")
.clientId("resource-client2")
.clientSecret("{noop}secret2")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.build();
when I'm trying to do rest call using webclient from resource client1 to resource client2, I'm getting NPE error because the spring authorization server is not providing the access token.
error is
java.lang.NullPointerException: Cannot invoke "org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse.getAccessToken()" because "tokenResponse" is null
at org.springframework.security.oauth2.client.ClientCredentialsOAuth2AuthorizedClientProvider.authorize(ClientCredentialsOAuth2AuthorizedClientProvider.java:87) ~[spring-security-oauth2-client-6.0.1.jar:6.0.1]
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Error has been observed at the following site(s):
*__checkpoint ⇢ Request to GET http://localhost:8082/hello2 [DefaultWebClient]
Original Stack Trace:
at org.springframework.security.oauth2.client.ClientCredentialsOAuth2AuthorizedClientProvider.authorize(ClientCredentialsOAuth2AuthorizedClientProvider.java:87) ~[spring-security-oauth2-client-6.0.1.jar:6.0.1]
at org.springframework.security.oauth2.client.DelegatingOAuth2AuthorizedClientProvider.authorize(DelegatingOAuth2AuthorizedClientProvider.java:71) ~[spring-security-oauth2-client-6.0.1.jar:6.0.1]
at org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager.authorize(DefaultOAuth2AuthorizedClientManager.java:176) ~[spring-security-oauth2-client-6.0.1.jar:6.0.1]
at org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction.lambda$authorizeClient$22(ServletOAuth2AuthorizedClientExchangeFilterFunction.java:485) ~[spring-security-oauth2-client-6.0.1.jar:6.0.1]
at reactor.core.publisher.MonoSupplier.call(MonoSupplier.java:67) ~[reactor-core-3.5.1.jar:3.5.1]
Not sure what went wrong with my setup or missing the logic to get it done. using latest spring version & spring authorization server version 1.0.0 . kindly help
I was going through the Spring Security Oauth 2.x migration to Spring security 5.2 and encountered the following blocker.
As I can see OAuth2RestTemplate is not used anymore, instead WebClient is recommended. So i was making changes to my codebase to make webclient work.
In Oauth2 we have an option to get token straight from RestTemplate using oAuth2RestTemplate.getAccessToken(), i couldn't find anything similar in WebClient. This is to call an external microservice.
Is there any option to get the accesstoken from webclient? Or is it handled in a different way there?
As usual when working with spring security, a lot of stuff happens automagically with configuration by convention. Meaning: you should get familiar with the oauth-related spring security configuration.
A good starting point for your studies will be the spring security docu or one of the many good Baeldung articles on this topic (maybe this one helps: https://www.baeldung.com/spring-oauth-login-webflux).
With the correct configuration in place, the following method will create a WebClient that has the proper oauth token automatically created on demand in the filter method.
In this case a Bean of type ReactiveOAuth2AuthorizedClientManager takes care of this. That bean is created in listing no. 2.
Furthermore, you need to configure the oauth server url and the credentials. See Listing 3 for a simple example.
This example works without you needing to handle the access token.
#Bean
public WebClient oauthWebClient(
final WebClient.Builder webClientBuilder,
#Qualifier("authorizedClientManager") final ReactiveOAuth2AuthorizedClientManager manager) {
final ExchangeStrategies exchangeStrategies =
ExchangeStrategies.builder()
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(EXCHANGE_BYTE_COUNT))
.build();
final ServerOAuth2AuthorizedClientExchangeFilterFunction oauth = new ServerOAuth2AuthorizedClientExchangeFilterFunction(manager);
oauth.setDefaultClientRegistrationId(authenticationProperties.getClientId());
// set more properties if needed
// oauth.set ...
return webClientBuilder
.exchangeStrategies(exchangeStrategies)
.baseUrl(apiProperties.getBaseUrl())
.filter(oauth)
.build();
}
Listing 1: create a WebClient bean
#Bean
public ReactiveOAuth2AuthorizedClientManager authorizedClientManager(
final ReactiveClientRegistrationRepository clientRegistrationRepository,
final ReactiveOAuth2AuthorizedClientService authorizedClientService) {
final ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
ReactiveOAuth2AuthorizedClientProviderBuilder
.builder()
.clientCredentials()
.build();
final AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager authorizedClientManager =
new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(clientRegistrationRepository, authorizedClientService);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
Listing 2: create an OAuth2 authorized client manager bean
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://your-oauth-server.com/auth/realms/your-realm
client:
provider:
your-provider:
issuer-uri: https://your-oauth-server.com/auth/realms/your-realm
registration:
your-provider:
client-id: your-client-id
client-secret: ${your_client_secret} # from an environment variable
scope: openid
authorization-grant-type: client_credentials
Listing 3: one of many possible ways to configure your spring web client
Check if this helps
public String getToken() {
return webClient
.post()
.uri(uri)
.body(
BodyInserters.fromFormData(GRANT_TYPE, PASSWORD)
.with(USERNAME, userName)
.with(PASSWORD, password)
.with(CLIENT_ID, clientId)
.with(CLIENT_SECRET, clientSecret))
.retrieve()
.bodyToMono(JsonNode.class)
.map(tokenResponse -> tokenResponse.get(ACCESS_TOKEN).textValue())
.cache(Duration.ofMinutes(30))
.block();
}
I have an API that is authenticated by x509. However I need my actuator health check end point to be bypassed from authentication, so that I can do health check without client certificates.
I realized this can be done by using different ports for server and management where actuator endpoints will hit.
My application.yml config is as below:
server:
port: 9000
ssl:
key-store: classpath:keystore.jks
key-store-password: password
key-password: password
enabled: true
trust-store: classpath:truststore.jks
trust-store-password: password
client-auth: need
management:
server:
port: 9010
ssl:
enabled: false
security:
enabled: false
My problem is that even with this configuration, management endpoint is not where I can access the health endpoint.
http://localhost:9010/health --> this doesn't work
I am still able to access it with the server port and certificates.
https://localhost:9000/health --> this works
My security configuration is as below :
#Configuration
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.x509()
.subjectPrincipalRegex("CN=(.*?)(?:,|$)")
.userDetailsService(userDetailsService())
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.NEVER)
.and()
.csrf().disable();
}
#Bean
public UserDetailsService userDetailsService() {
return new UserDetailsService() {
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if (username.equals("user")) {
return new User(username, "", AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER"));
}
throw new UsernameNotFoundException("User not found!");
}
};
}
I have the spring-boot-actuator dependency in my pom. I don't understand why my management port doesn't work.
Any suggestions?
I figured out what was wrong here. It was spring boot dependencies. I was using spring-boot-actuator, when I changed it to spring-boot-starter-actuator, the management port started listening to my configured port. I then disabled the SSL authentication on that port. Providing my config below so that it would be helpful for someone else:
management:
server:
port: 8000
ssl:
enabled: false
server:
port: 9000
ssl:
key-store: classpath:keystore.jks
key-store-password: password
key-alias: alias
key-password: password
enabled: true
trust-store: classpath:truststore.jks
trust-store-password: password
client-auth: need
Now I can access my actuator endpoints like health via http on port 8000 and my application rest APIs on port 9000 via https with valid client certificate.
I am using a setup with Keycloak as Identity Provider, Spring Cloud Gateway as API Gateway and multiple Microservices.
I can receive a JWT via my Gateway (redirecting to Keycloak) via http://localhost:8050/auth/realms/dev/protocol/openid-connect/token.
I can use the JWT to access a resource directly located at the Keycloak server (e.g. http://localhost:8080/auth/admin/realms/dev/users).
But when I want to use the Gateway to relay me to the same resource (http://localhost:8050/auth/admin/realms/dev/users) I get the Keycloak Login form as response.
My conclusion is that there must me a misconfiguration in my Spring Cloud Gateway application.
This is the Security Configuration in the Gateway:
#Configuration
#EnableWebFluxSecurity
#EnableReactiveMethodSecurity
public class SecurityConfiguration {
#Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http, ReactiveClientRegistrationRepository clientRegistrationRepository) {
// Authenticate through configured OpenID Provider
http.oauth2Login();
// Also logout at the OpenID Connect provider
http.logout(logout -> logout.logoutSuccessHandler(
new OidcClientInitiatedServerLogoutSuccessHandler(clientRegistrationRepository)));
//Exclude /auth from authentication
http.authorizeExchange().pathMatchers("/auth/realms/ahearo/protocol/openid-connect/token").permitAll();
// Require authentication for all requests
http.authorizeExchange().anyExchange().authenticated();
// Allow showing /home within a frame
http.headers().frameOptions().mode(Mode.SAMEORIGIN);
// Disable CSRF in the gateway to prevent conflicts with proxied service CSRF
http.csrf().disable();
return http.build();
}
}
This is my application.yaml in the Gateway:
spring:
application:
name: gw-service
cloud:
gateway:
default-filters:
- TokenRelay
discovery:
locator:
lower-case-service-id: true
enabled: true
routes:
- id: auth
uri: http://localhost:8080
predicates:
- Path=/auth/**
security:
oauth2:
client:
registration:
keycloak:
client-id: 'api-gw'
client-secret: 'not-relevant-but-correct'
authorizationGrantType: authorization_code
redirect-uri: '{baseUrl}/login/oauth2/code/{registrationId}'
scope: openid,profile,email,resource.read
provider:
keycloak:
issuerUri: http://localhost:8080/auth/realms/dev
user-name-attribute: preferred_username
server:
port: 8050
eureka:
client:
service-url:
default-zone: http://localhost:8761/eureka
register-with-eureka: true
fetch-registry: true
How can I make the Gateway able to know that the user is authenticated (using the JWT) and not redirect me to the login page?
If you want to make requests to Spring Gateway with access token you need to make it a resource server. Add the following:
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
application.yml
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://.../auth/realms/...
SecurityConfiguration.java
#Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http,
ReactiveClientRegistrationRepository clientRegistrationRepository) {
// Authenticate through configured OpenID Provider
http.oauth2Login();
// Also logout at the OpenID Connect provider
http.logout(logout -> logout.logoutSuccessHandler(
new OidcClientInitiatedServerLogoutSuccessHandler(clientRegistrationRepository)));
// Require authentication for all requests
http.authorizeExchange().anyExchange().authenticated();
http.oauth2ResourceServer().jwt();
// Allow showing /home within a frame
http.headers().frameOptions().mode(Mode.SAMEORIGIN);
// Disable CSRF in the gateway to prevent conflicts with proxied service CSRF
http.csrf().disable();
return http.build();
}
I bypassed the problem by communicating directly with Keycloak without relaying requests to it via Spring Cloud Gateway.
That's actually not a workaround but actually best practice/totally ok as far as I understand.
This code is for Client_credentials grant_type. if you use other grant type you need to add client_id and client_secret in request parameters.
public class MyFilter2 extends OncePerRequestFilter {
private final ObjectMapper mapper = new ObjectMapper();
#Value("${auth.server.uri}")
private String authServerUri;
#Value("${client_id}")
private String clientId;
#Value("${client_secret}")
private String clientSecret;
#Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
FilterChain filterChain) throws IOException {
try {
String token = httpServletRequest.getHeader("Authorization");
HttpHeaders headers = new HttpHeaders();
headers.set("Content-Type","application/x-www-form-urlencoded");
headers.set("Authorization",token);
final HttpEntity finalRequest = new HttpEntity("{}", headers);
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> response = restTemplate.postForEntity(authServerUri,finalRequest,String.class);
if (!HttpStatus.OK.equals(response.getStatusCode())) {
Map<String, Object> errorDetails = new HashMap<>();
errorDetails.put("status", HttpStatus.UNAUTHORIZED.value());
errorDetails.put("message", "Invalid or empty token");
httpServletResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);
mapper.writeValue(httpServletResponse.getWriter(), errorDetails);
} else {
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}catch(HttpClientErrorException he) {
Map<String, Object> errorDetails = new HashMap<>();
errorDetails.put("status", HttpStatus.UNAUTHORIZED.value());
errorDetails.put("message", "Invalid or empty token");
httpServletResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);
mapper.writeValue(httpServletResponse.getWriter(), errorDetails);
}catch (Exception exception) {
}
}
could somebody please help me with the following questions:
I have a REST server that has SSL enabled and a REST client that are on 2 different computers. Both are built with Spring boot. The server will have a .p12 or .pfx certificate.
If the REST client wants to make a request to the server, does it need to provide a certificate or can it make a request with a simple RestTemplate even if the server is secure? Does the same rules apply for Postman or can Postman send a request without a certificate as well?
I tried to create a request from the REST client, using a REST template with the certificate. But I am not sure, which certificate should I provide. Should it be the same certificate that is on the server or another one? And does the certificate from the server need to have a rule for the ip of the REST client to allow the requests?
The ssl server configuration:
ssl:
key-store-type: PKCS12
key-store: ${MY_DIR}/config/ssl/myCert.pfx
key-store-password: 123456
The rest template from the client:
RestTemplate restTemplate = null;
try {
SSLContext sslContext = SSLContextBuilder
.create()
.loadTrustMaterial(ResourceUtils.getFile("classpath:config/ssl/myCert.pfx"), password.toCharArray())
.build();
HttpClient client = HttpClients.custom()
.setSSLContext(sslContext)
.build();
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory();
requestFactory.setHttpClient(client);
restTemplate = new RestTemplate(requestFactory);
} catch (KeyStoreException | IOException | NoSuchAlgorithmException | CertificateException | KeyManagementException ex) {
LOGGER.error("Error getting the RestTemplate with ssl certificate", ex);
}
If a client certificate is required or not depends fully on the server configuration. Some servers require client certificates, some do not. Please refer to the documentation of your specific REST API to find out what the requirements on the client are.
#Bean
public RestTemplate restTemplate() throws KeyStoreException, NoSuchAlgorithmException, KeyManagementException {
TrustStrategy acceptingTrustStrategy = (X509Certificate[] chain, String authType) -> true;
SSLContext sslContext = SSLContexts.custom().loadTrustMaterial(null, acceptingTrustStrategy).build();
SSLConnectionSocketFactory csf = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
CloseableHttpClient httpClient = HttpClients.custom()
.setSSLSocketFactory(csf)
.build();
HttpComponentsClientHttpRequestFactory requestFactory =
new HttpComponentsClientHttpRequestFactory();
requestFactory.setHttpClient(httpClient);
RestTemplate restTemplate = new RestTemplate(requestFactory);
return restTemplate;
}
This works for me.
please use this below import packages.
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.X509Certificate;
import javax.net.ssl.SSLContext;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.ssl.SSLContexts;
import org.apache.http.ssl.TrustStrategy;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;