Spring Security SAML2 multiple IDPs configuration - spring

I am trying to configure multiple idps via RelyingPartyRegistrationRepository using spring security 5.3
This is my application.yaml config
spring:
security:
saml2:
relyingparty:
registration:
idpokta:
identityprovider:
entity-id: http://<url>
sso-url: https://<url>
verification:
credentials:
- certificate-location: "classpath:saml/okta.cert"
signing:
credentials:
certificate: |
-----BEGIN CERTIFICATE-----
MIIDpDCC...
-----END CERTIFICATE-----
private-key: |
-----BEGIN PRIVATE KEY-----
MIIEvQIBA....
-----END PRIVATE KEY-----
idponelogin:
identityprovider:
entity-id: https://<url>
sso-url: https://<url>
verification:
credentials:
- certificate-location: "classpath:saml/onelogin.cert"
signing:
credentials:
certificate: |
-----BEGIN CERTIFICATE-----
MIID/z...
-----END CERTIFICATE-----
private-key: |
-----BEGIN PRIVATE KEY-----
MIpoi...
-----END PRIVATE KEY-----
my login controller is defined as follows:
#Controller
public class LoginController {
private final RelyingPartyRegistrationRepository relyingParties;
// ...
#GetMapping("/login")
public void login(HttpServletRequest request, HttpServletResponse response) throws IOException {
String registrationId = request.getParameter("idp");
RelyingPartyRegistration relyingParty = this.relyingParties
.findByRegistrationId(registrationId);
if (relyingParty == null) {
response.setStatus(401);
} else {
response.sendRedirect("/saml2/authenticate/" + registrationId);
}
}
PROBLEM
my relyingParty has the provider details but I think the fact that my assertionConsumerServiceUrl is defaulting to {baseUrl}/login/saml2/sso/{registrationId} and my localEntityIdTemplate = {baseUrl}/saml2/service-provider-metadata/{registrationId}` is causing it the problem. How do I add the sp info in my yaml file? or Am I doing this completely wrong?
Screenshot

I've been trying to do the same thing. The API doesn't seem to be well thought through. If you try to use auto configuration spring boot feature the only way I found is to exclude this configuration
Saml2RelyingPartyRegistrationConfiguration
and provide your own class for that.
Since all of that is package local, you have to bring pretty much entire package
org.springframework.boot.autoconfigure.security.saml2
to your own application first disabling the one from spring altogether.
You will end up with
CustomRegistrationConfiguredCondition
CustomSaml2LoginConfiguration
CustomSaml2RelyingPartyAutoConfiguration
CustomSaml2RelyingPartyRegistrationConfiguration
at the very least.
Make sure the references between the classes are updated too.
Now, you need to update the following method in the CustomSaml2RelyingPartyRegistrationConfiguration:
private RelyingPartyRegistration asRegistration(String id, Registration properties) {
RelyingPartyRegistration.Builder builder = RelyingPartyRegistration.withRegistrationId(id);
builder.assertionConsumerServiceUrlTemplate(
"{baseUrl}" + Saml2WebSsoAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI);
builder.idpWebSsoUrl(properties.getIdentityprovider().getSsoUrl());
builder.remoteIdpEntityId(properties.getIdentityprovider().getEntityId());
builder.localEntityIdTemplate("template_you_like");
builder.credentials((credentials) -> credentials.addAll(asCredentials(properties)));
return builder.build();
}
Alternatively, you can also copy Saml2RelyingPartyProperties to your project and add all necessary fields there. This way you'll be able to set properties in yaml or properties file. Don't forget to use those values in asRegistration method mentioned above.

From the screenshot you posted, it seems that assertionConsumerServiceUrl="{baseUrl}/login/saml2/sso/{registrationId}" at runtime (so the bindings are not working). In my experience, I had it working by replacing baseUrl and registrationId with the actual values (bypassing the placeholders).
E.g.: http://localhost:8080/login/saml2/sso/idpokta
The same goes for localEntityIdTemplate: http://localhost:8080/saml2/service-provider-metadata/idpokta

Related

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?

Avoid basic auth when using x.509 authentication

I have created an REST API based on Spring WebFlux that is protected through X.509 authentication. I followed this guide https://www.baeldung.com/x-509-authentication-in-spring-security to create all the certificates.
The router implementation:
#Configuration
class LogRouter {
#Bean
fun functionalRoutes(handler: LogHandler): RouterFunction<ServerResponse> =
route()
.route(RequestPredicates.path("/")) {
ServerResponse.ok().body(Mono.just("I am alive"))
}
.nest(RequestPredicates.path("/api").and(RequestPredicates.accept(MediaType.APPLICATION_JSON))) { builder ->
builder.GET("/fn/mono", handler::monoMessage)
.POST("/fn/mono", handler::monoPostMessage)
}
.build()
}
and app implementation:
#SpringBootApplication
#EnableWebFluxSecurity
class RestplayApplication {
#Bean
fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain? {
val principalExtractor = SubjectDnX509PrincipalExtractor()
principalExtractor.setSubjectDnRegex("OU=(.*?)(?:,|$)")
val authenticationManager = ReactiveAuthenticationManager { authentication: Authentication ->
authentication.isAuthenticated = "Trusted Org Unit" == authentication.name
Mono.just(authentication)
}
http
.x509 { x509 ->
x509
.principalExtractor(principalExtractor)
.authenticationManager(authenticationManager)
}
.authorizeExchange { exchanges ->
exchanges
.anyExchange().authenticated()
}
return http.build()
}
}
fun main(args: Array<String>) {
runApplication<RestplayApplication>( *args)
}
I use Firefox browser to test the x.509 authentication and I have added the self signed certificate(rootCA.crt) to the Firefox:
included client certificate(clientBob.p12).
When calling the link in the browser it shows basic authentication form:
However, I expect the authentication form not to be appeared because I have provided a valid client certificate in the browser.
Why the basic form appears every time?
The code is hosted on https://github.com/softshipper/restplay. The password for certificates are always changeit.
I debugged it and the problem seems to be that your client certificate clientBob.crt does not contain the field for Organization Unit of the subject and your principalExtractor is set to extract this field. As a result, your principalExtractor fails, and so it calls authenticationFailureHandler, which is set to prompt for basic authentication by default.
Possible solutions could be:
Use a client certificate that includes the Organization Unit of the subject, and set it to "Trusted Org Unit".
Alter the principalExtractor regex so that it uses a different field. The default one uses the common name (CN). If you do edit this, then remember to also update your authenticationManager to check for "Bob" instead of "Trusted Org Unit"

Retrieve private key from Azure Key Vault in Spring Boot app

I have a .pfx certificate in Azure Key Vault. I need to retrieve the private key from this to decrypt a string value in my Spring Boot application.
I have used the azure-spring-boot-starter-keyvault-certificates library to load the certificate to java key store, this seems to be working ok.
What I don't understand is how to retrieve the private key part. Any clues to what I am doing wrong?
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);
// returns null!
PrivateKey privateKey = (PrivateKey) azureKeyVaultKeyStore.getKey(environment.getProperty("azure.keyvault.alias"), "".toCharArray());
// decrypt value
Cipher c = Cipher.getInstance(privateKey.getAlgorithm());
c.init(Cipher.DECRYPT_MODE, privateKey);
c.update(DatatypeConverter.parseBase64Binary(cryptedMsg));
String decryptedMessage = new String(c.doFinal());
Testing with the same certificate on my machine works doing like this:
KeyStore keyStore = KeyStore.getInstance("pkcs12");
keyStore.load(new FileInputStream(filename), password.toCharArray());
PrivateKey privateKey = (PrivateKey) keyStore.getKey(alias, password.toCharArray());
The private key can be retrieved using the GetSecret() method, otherwise you only get the public part.
See this article for details (Even tough its for .NET, I hope you can figure out how to do it in Java) or see the Java samples here

Spring JAVA SSL - No name matching localhost found

I have two SSL end-points that share the same application.properties and key store file.
I want to have one end-point call the other, but getting an error No name matching localhost found
How can I adjust this to allow one microservice to call the other(s) as intended below?
I have played with the following to attempt a solution to no avail:
javax.net.ssl.HostnameVerifier()
Created a localhost certificate and added it to the keystore
#CrossOrigin(origins = "*", maxAge = 3600)
#RestController
public class submitapplicationcontroller {
#Bean
public WebClient.Builder getWebClientBuilder(){
return WebClient.builder();
}
#Autowired private WebClient.Builder webClientBuilder;
#PostMapping("/submitapplication")
public String submitapplication() {
/*** Returns Error Found Below ***/
String response = webClientBuilder.build()
.post()
.uri("https://localhost:8080/validateaddress")
.retrieve()
.bodyToMono(String.class)
.block();
return response;
}
}
javax.net.ssl.SSLHandshakeException: No name matching localhost found
at java.base/sun.security.ssl.Alert.createSSLException
Error has been observed at the following site(s):
|_ checkpoint ⇢ Request to POST https://localhost:8080/v1/validateaddress
#CrossOrigin(origins = "*", maxAge = 3600)
#RestController
public class validateaddresscontroller {
#PostMapping("/validateaddress")
public String validateaddress() {
return "response";
}
}
server.ssl.key-alias=server
server.ssl.key-password=asensitivesecret
server.ssl.key-store=classpath:server.jks
server.ssl.key-store-provider=SUN
server.ssl.key-store-type=JKS
server.ssl.key-store-password=asensitivesecret
The problem here was the way I went about creating and implementing the certificates. I had 2 separate keystores and certificates; one named "server", and one named "localhost". I added the localhost certificate to the server keystore, and applied the server keystore and certificate to the springboot application / application.properties.
What you have to do is create just one certificate and keystore dubbed "localhost" and you have to use that to apply to the application / application.properties.
What you should have after creating the localhost JKS and certificate
server.ssl.key-alias=localhost
server.ssl.key-password=asensitivesecret
server.ssl.key-store=classpath:localhost.jks
server.ssl.key-store-provider=SUN
server.ssl.key-store-type=JKS
server.ssl.key-store-password=asensitivesecret
Note: I don't believe you actually have to create a JKS named "localhost", just the certificate. I just did for testing purposes.

How to get the certificate into the X509 filter (Spring Security)?

I need to extract more information than just the CN of the certificate. Currently, I only get the standard UserDetails loadUserByUsername(String arg) where arg is the CN of the certificate. I need to get the X509Certificate object. Is it possible?
on spring security xml file :
<x509 subject-principal-regex="CN=(.*?)," user-service-ref="myUserDetailsService" />
No you can't get it that way. You need to grab it from the HttpServletRequest:
X509Certificate[] certs = (X509Certificate[])HttpServletRequest.getAttribute("javax.servlet.request.X509Certificate");
It is also worth noting that once you are authorized by the in-built X509AuthenticationFilter of Spring Security as it has accepted your certificate, then you can access the X509Certificate as
Object object = SecurityContextHolder.getContext().getAuthentication().getCredentials();
if (object instanceof X509Certificate)
{
X509Certificate x509Certificate = (X509Certificate) object;
//convert to bouncycastle if you want
X509CertificateHolder x509CertificateHolder =
new X509CertificateHolder(x509Certificate.getEncoded());
...

Resources