Get certificates from Azure KeyVault to Keystore in Kotlin/SpringBoot for outgoing requests - spring-boot

For some API requests I need in my backend a certificate. Currently the .p12 is versioned in the repository and loaded into the WebClient when its initialized like this:
private fun getWebClient(): WebClient {
val ks: KeyStore = KeyStore.getInstance("PKCS12")
ks.load(ClassPathResource("keystore.p12").inputStream, config.trustStorePassword.toCharArray())
val kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
kmf.init(ks, config.trustStorePassword.toCharArray())
val sslContext = SslContextBuilder
.forClient()
.keyManager(kmf)
.build()
val httpClient: HttpClient = HttpClient.create().secure { sslSpec -> sslSpec.sslContext(sslContext) }
return WebClient
.builder()
.baseUrl(config.BaseUrl)
.clientConnector(ReactorClientHttpConnector(httpClient))
.build()
}
I want to change it since the backend is deployed to Azure AppService. I already created a KeyVault and imported the certificate and granted access via a managed identity to the AppService.
I currently struggle to load the keystore in Spring Boot from the KeyVault. For reference I am trying to follow https://learn.microsoft.com/en-us/azure/developer/java/spring-framework/configure-spring-boot-starter-java-app-with-azure-key-vault-certificates.
It uses the azure.keyvault.uri property which is apparently deprecated, so I am using spring.cloud.azure.keyvault.certificate.endpoint.
Also documentation states:
KeyStore azureKeyVaultKeyStore = KeyStore.getInstance("AzureKeyVault");
KeyVaultLoadStoreParameter parameter = new KeyVaultLoadStoreParameter(
System.getProperty("azure.keyvault.uri"));
azureKeyVaultKeyStore.load(parameter);
SSLContext sslContext = SSLContexts.custom()
.loadTrustMaterial(azureKeyVaultKeyStore, null)
.build();
However, I am not able to resolve the class KeyVautLoadStoreParameter.
I am using spring boot 2.7.7 and
implementation("com.azure.spring:spring-cloud-azure-starter:4.5.0")
implementation("com.azure.spring:spring-cloud-azure-starter-keyvault:4.5.0")
Any help towards loading the certificates and configuring the WebClient would be greatly appreciated.

What you can do is create an app registration in azure AD and then grant access to this app registration through access policy in the azure keyvault.
Now collect the clientId , Tenant Id and client Secret from the app registration
Create the secret in certificates&secret tab and click on new registration.
Now create the application.yaml file in the following format
server:
ssl:
key-alias: TestCertificate
key-store-type: AzureKeyVault
trust-store-type: AzureKeyVault
port: 8443
azure:
keyvault:
uri: <KEYVAULT URL>
client-id: <CLIENT_ID FROM APP REGISTRATIOM>
client-secret: <CLIENT_SECRET FROM APP REGISTRATIOM>
enabled: true
tenant-id: <TENANT_ID FROM APP REGISTRATIOM>
Now you can import all the credentials from the azure key vault using the following program.
KeyStore azureKeyVaultKeyStore = KeyStore.getInstance("AzureKeyVault");
KeyVaultLoadStoreParameter parameter = new KeyVaultLoadStoreParameter(
System.getProperty("azure.keyvault.uri"),
System.getProperty("azure.keyvault.tenant-id"),
System.getProperty("azure.keyvault.client-id"),
System.getProperty("azure.keyvault.client-secret")
);
azureKeyVaultKeyStore.load(parameter);
SSLContext sslContext = SSLContexts.custom()
.loadTrustMaterial(azureKeyVaultKeyStore, null)
.build();
// Here I am cross check wether we have imported the certificate name " TestCertificate"
if(azureKeyVaultKeyStore.containsAlias("TestCertificate"))
{
System.out.println("The Certificate Exists ");
}
else {
System.out.println("Error Has Occured");
}
Output:

Related

Unable to get access_token, Refresh token using client_assertion in Spring oauth2 Server

generated client assertion like below using PKCS12, still I get invalid client error. Need your help to resolve this issue. Looks like authorization server is not compatible with certificate generated RSAkey. I am badly stuck here. base branch used to create project is https://github.com/jgrandja/spring-authorization-server/tree/jwt-client-authn/samples/default-authorizationserver
InputStream fm = new FileInputStream(pathPKCS12);
KeyStore keystore = KeyStore.getInstance("PKCS12");
keystore.load(fm, pwdPKCS12.toCharArray());
Key key = keystore.getKey(keystore.aliases().nextElement(), pwdPKCS12.toCharArray());
Certificate cert = keystore.getCertificate(keystore.aliases().nextElement());
PublicKey publicKey = cert.getPublicKey();
KeyPair keyPair = new KeyPair(publicKey, (PrivateKey) key);
RSAPublicKey rsaPublicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) keyPair.getPrivate();
Instant now = Instant.now();
//The JWT signature algorithm we will be using to sign the token
//SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.RS256;
String jwt= Jwts.builder()
.setAudience("https://localhost:9000")
.setIssuedAt(Date.from(now))
.setExpiration(Date.from(now.plus(5L, ChronoUnit.DAYS)))
.setIssuer("profinch")
.setSubject("profinch")
.setId(UUID.randomUUID().toString())
.signWith(rsaPrivateKey)
.compact();
System.out.println(jwt);
error is below -
postman printscreen where error is displayed
post man request x-www-form-URL encoded is -
client_assertion_type:urn:ietf:params:oauth:client-assertion-type:jwt-bearer
client_assertion:eyJraWQiOiJKWHYwMXhuNW83eDFuTXB1SVV1Q1gyNDdPbnhMMGdMeGV2VUFnLW0xbjdvIiwidHlwIjoiSldUIiwiYWxnIjoiUlMyNTYifQ.eyJhdWQiOiJodHRwczovL2xvY2FsaG9zdDo5MDAwL29hdXRoMi90b2tlbiIsImlhdCI6MTY3NTY3NDU4NywiZXhwIjoxNjc2MTA2NTg3LCJpc3MiOiJwcm9maW5jaCIsInN1YiI6InByb2ZpbmNoIiwianRpIjoiYTFiYzdiZTktZGY3Zi00NjhkLTkwMzktNTZjOTE4NmMwNDkwIn0.LjyF122kDSTlkUzVomOpnqwpHxNEdeq73fVx4Zzcu1dEmChWpk2LB1NbDhOPTIDNpGPF4aB8RkHkZfTmbA9TFYrm20MdHYFt5fRliBILUdBuNjRok3EEmNmWnjFTyfwoufMGT-sX6orLWkNUmsOkaywGIKCgHgR-MfEpTZ_v0cNL3uSuWnDQAy6yZgm9kWt_jNZdPGouBFHGsoZxhr2XAFrwhlQVq0VDOTELX7ylDcqOR5phHxAfubx_mW79ns1Que3jO-o_mvYtXTamoOq_DiJD-xpRnAQEq36czoSHvPYVrjlLhCloBtuedzD0gODmypVc_0Dq2dPp4-y9pCiV-g
Auth-server client registration as below-
RegisteredClient registeredClient2 = RegisteredClient.withId(UUID.randomUUID().toString()) .clientId("profinch") .clientAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT) .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) .redirectUri(clientModel.getRedirectUri()) .scope(OidcScopes.OPENID).scope(OidcScopes.PROFILE).scope("message.read").scope("message.write") .clientSettings(ClientSettings.builder() .requireAuthorizationConsent(false) .jwkSetUrl("http://localhost:8080/jwks") .tokenEndpointAuthenticationSigningAlgorithm(SignatureAlgorithm.RS256) .build()) .build();
Should be able to get acces_token, refresh_oken using client_assertion in postman as per printscreen shared.

How to configure sslContext with p12 store (and password) for client authentication with https endpoint in Spring/Kotlin

I want to call a https protected endpoint using a certificate and private key. I received a .p12 keystore which is protected with a password.
For testing purposes I extracted the .cer file and private key using openssl.
I could verify locally that communication works by setting the ssl context like this:
fun test(): WebClient.RequestHeadersSpec<*> {
val sslContext = SslContextBuilder
.forClient()
.keyManager(ClassPathResource("cert").inputStream, ClassPathResource("key").inputStream)
.build()
val httpClient: HttpClient = HttpClient.create().secure { sslSpec -> sslSpec.sslContext(sslContext) }
val webClient = WebClient
.builder()
.baseUrl("baseUrl")
.clientConnector(ReactorClientHttpConnector(httpClient))
.build()
return webClient
.post()
.uri("uri")
.contentType(MediaType.APPLICATION_JSON)
.bodyValue("test")
}
However, I do not want to version the cert and private key in my repository. How would I set the sslContext with the .p12 keystore and the password? I did not find any examples for this scenario.
load file from disc
this is java but it should be easy to adapt to kotlin
KeyStore keyStore = KeyStore.getInstance("pkcs12");
keyStore.load(new FileInputStream(keyStoreLocation), keyStorePassword.toCharArray());
SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(
new SSLContextBuilder()
.loadKeyMaterial(keyStore, keyStorePassword.toCharArray())
.build(),
NoopHostnameVerifier.INSTANCE);
HttpClient httpClient = HttpClients.custom().setSSLSocketFactory(socketFactory).build();
where file location and password can provided from configuration
#Value("${keyStore.location}")
private String keyStoreLocation;
#Value("${keyStore.password}")
private String keyStorePassword;

Spring OAuth 401 exception

I'm building a Spring Boot application with OAuth 2.0 and OpenID. Application is an API client. When the user opens the app, he is redirected to Authorization URI (that works). After login and permission grants, he should be redirected to a callback URI (that works).
Before the next page app should take some info from API, but I get org.springframework.web.reactive.function.client.WebClientResponseException$Unauthorized: 401 Unauthorized from GET https(URL of resource).
My application yaml.
spring:
security:
oauth2:
client:
registration:
localhost-for-development-3:
provider: spring
client-id: myId
client-secret: mySecret
client-authentication-method: basic
authorization-grant-type: authorization_code
scope:
- pr.pro
- pr.act
- openid
- offline
redirect-uri: http://localhost:8080/login/oauth2/code/{registrationId}
client-name: localhost-for-development-3
provider:
spring:
authorization-uri: https://someurl.com/oauth2/auth
token-uri: https://someurl.com/oauth2/token
issuer-uri: https://someurl.com/
logging:
level:
'[org.springframework.web]': DEBUG
OAuthClientConfiguration class
#Configuration
public class OAuthClientConfiguration
{
#Bean
ReactiveClientRegistrationRepository clientRegistrations(
#Value(value = "${spring.security.oauth2.client.provider.spring.token-uri}") String tokenUri,
#Value(value = "${spring.security.oauth2.client.registration.localhost-for-development-3.client-id}") String clientId,
#Value(value = "${spring.security.oauth2.client.registration.localhost-for-development-3.client-secret}") String clientSecret,
#Value(value = "${spring.security.oauth2.client.registration.localhost-for-development-3.authorization-grant-type}") String authorizationGrantType,
#Value(value = "${spring.security.oauth2.client.registration.localhost-for-development-3.redirect-uri}") String redirectUri,
#Value(value = "${spring.security.oauth2.client.provider.spring.authorization-uri}") String authorizationUri
)
// #Value(value = "${spring.security.oauth2.client.provider.spring.issuer-uri}") String issuerUri
{
ClientRegistration registration = ClientRegistration
.withRegistrationId("localhost-for-development-3")
.tokenUri(tokenUri)
.clientId(clientId)
.clientSecret(clientSecret)
.scope("m3p.f.pr.pro", "m3p.f.pr.act", "openid", "offline")
.authorizationGrantType(new AuthorizationGrantType(authorizationGrantType))
.redirectUri(redirectUri)
.authorizationUri(authorizationUri)
// .issuerUri(issuerUri)
// .jwkSetUri("https://someurl.com")
.build();
return new InMemoryReactiveClientRegistrationRepository(registration);
}
#Bean
WebClient webClient(ReactiveClientRegistrationRepository clientRegistrations)
{
InMemoryReactiveOAuth2AuthorizedClientService clientService = new InMemoryReactiveOAuth2AuthorizedClientService(clientRegistrations);
AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager authorizedClientManager = new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(clientRegistrations, clientService);
ServerOAuth2AuthorizedClientExchangeFilterFunction oauth = new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
oauth.setDefaultClientRegistrationId("localhost-for-development-3");
return WebClient.builder().filter(oauth).build();
}
}
I'm not sure what I need to look for in the Google console network tab. I see I got the "code" parameter. It's probably an authorization code. I don't see that I've received a token.
Please, if I need to provide you with more details, inform me what I need to look for, and I'll edit the question.
Also, the API provider mentioned that for issuer-uri I MUST write URL WITHOUT trailing "/" at the end. But when I do so, Spring Security will throw an exception. Is that connected to my problem maybe? I've already mentioned this problem in more detail in this post:
ClientRegistration.Builder adds trailing "/" at the end of issuer uri. How to prevent that?

SSL context in mongo:client attribute using spring xml config

How to add SSL key store and trust store file path and password in mongo:client options using spring xml to connect mongo db on TLS . Also need to know how to add ssl invalid host name allowed in mongo:client attribute in xml. I am using spring data mongo db 2.2.3 .
This is not XML solution, but via Bean.
This is what I did for 2.2.5.RELEASE. Note that for 2.3.0, there is no MongoClientOptions
#Value("classpath:truststore/mongoserver-truststore.p12")
private Resource trustStore;
#Value("${ssl.truststore.mongodb.password}")
private String mongoTrustStorePassword;
#Bean
public MongoClientOptions mongoClientOptions() throws Exception {
TrustManagerFactory trustManagerFactory = TrustManagerFactory
.getInstance(TrustManagerFactory.getDefaultAlgorithm());
KeyStore keyStore = KeyStore.getInstance("PKCS12");
keyStore.load(trustStore.getInputStream(), mongoTrustStorePassword.toCharArray());
trustManagerFactory.init(keyStore);
SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
sslContext.init(null, trustManagerFactory.getTrustManagers(), null);
return MongoClientOptions.builder()
.sslEnabled(true)
.sslContext(sslContext)
.sslInvalidHostNameAllowed(true)
.build();
}

How to use Spring WebSocketClient with SSL?

I connect with my websocket client to non-SSL endpoint without any problem. But I cannot find any way how to connect to wss (SSL) endpoint. Where can I define the SSL factory etc. No object seem to have related set method.
WebSocketClient transport = new StandardWebSocketClient();
WebSocketStompClient stompClient = new WebSocketStompClient(transport);
stompClient.setMessageConverter(new MappingJackson2MessageConverter());
String url = cfg.getWebsocketEndpoint();
StompSessionHandler handler = new MySessionHandler();
WebSocketHttpHeaders headers = new WebSocketHttpHeaders();
stompClient.connect(url, handler);
I am using wss:// url and on the other side I have a server with self-signed certificate. However, this code does not throw any exception while connecting, but the session is not established.
EDIT: After enabling tracing for web.* I got a standard error, with
sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
It occurs when connecting to server with self-signed certificate. However, for RestTemplate I already updated SSLContext with this code and REST calls are fine now, but I do not know why, StandardWebSocketClient is IGNORING the SSLContext. Why?
String keystoreType = "JKS";
InputStream keystoreLocation = new FileInputStream("src/main/resources/aaa.jks");
char [] keystorePassword = "zzz".toCharArray();
char [] keyPassword = "zzz".toCharArray();
KeyStore keystore = KeyStore.getInstance(keystoreType);
keystore.load(keystoreLocation, keystorePassword);
KeyManagerFactory kmfactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmfactory.init(keystore, keyPassword);
InputStream truststoreLocation = new FileInputStream("src/main/resources/aaa.jks");
char [] truststorePassword = "zzz".toCharArray();
String truststoreType = "JKS";
KeyStore truststore = KeyStore.getInstance(truststoreType);
truststore.load(truststoreLocation, truststorePassword);
TrustManagerFactory tmfactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmfactory.init(truststore);
KeyManager[] keymanagers = kmfactory.getKeyManagers();
TrustManager[] trustmanagers = tmfactory.getTrustManagers();
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(keymanagers, trustmanagers, new SecureRandom());
SSLContext.setDefault(sslContext);
UPDATE: Unfortunately, I did not managed to do this with custom truststore. I installed the certificate with InstallCert.java.
I think that each websocket container implementation provides ways to do this.
You have to set this configuration using the StandardWebSocketClient .setUserProperties. All those properties are internally set in the ClientEndpointConfig used by the client.
Here's an example with Tomcat as a provider:
StandardWebSocketClient wsClient = //...;
SSLContext sslContext = //...;
wsClient.setUserProperties(WsWebSocketContainer.SSL_CONTEXT_PROPERTY, sslContext);
In any case you should refer to your provider reference documentation to know which configuration keys you should use.
I've faced a similar issue, where I need to use a different ssl context instead of the default one and I could not use the Tomcat provider version solution. I've met a lot of resources and examples about this solution which does not fit my case, and for sure I landed here too on this question.
When you need to use a specific/custom SSLContext to set and you don't need the 'tomcat version', you can go for the Jetty version because it allows you to set which trust store and/or key store you want to use. In this case my Spring application was on the 'client' side and not the server one, but Jetty SslContextFactory provides the same functionalities for the 'Server' case.
The short example code below is for the client side, but it shares methods and signatures with the server side (check the jetty documentation about)
final SslContextFactory.Client factory = new SslContextFactory.Client();
factory.setSslContext(sslContext); // a different loaded java SslContext instead of the default one
//Create the web socket client with jetty client factory
final JettyWebSocketClient client =
new JettyWebSocketClient(new WebSocketClient(new HttpClient(factory)));
client.start();
final WebSocketStompClient stomp = new WebSocketStompClient(client);
There're also method for setting the trust store or key store instead a fully loaded SslContext.

Resources