Why is #AuthenticationPrincipal null in spring-native image? - spring-security

I'm injecting #AuthenticationPrincipal in a #RestController method. It works as expected in the JVM, but I get a NPE at security SpEL evaluation when running native image.
Here is the method:
#PutMapping("/{proxiedUserSubject}/proxies/{grantedUserSubject}")
#PreAuthorize("#token.subject == #proxiedUserSubject")
public ResponseEntity<?> editUserProxy(
#PathVariable(name = "proxiedUserSubject") #NotEmpty String proxiedUserSubject,
#PathVariable(name = "grantedUserSubject") #NotEmpty String grantedUserSubject,
#RequestBody Collection<Long> grantIds,
#AuthenticationPrincipal Object token) {
final var proxiedUser = getOrCreateUser(proxiedUserSubject);
final var grantedUser = getOrCreateUser(grantedUserSubject);
final var grants = grantRepo.findAllById(grantIds);
grantedUser.setGrantsOn(proxiedUser, grants);
userRepo.save(grantedUser);
return ResponseEntity.accepted().build();
}
Any idea why token is null in native image only?
I suspect something with AOT plugin configuration, but could not isolate the issue yet.

If your principal is a custom type you will need to add a reflection hint so that it can be used in a SpEL expression.
#TypeHint(types = CustomToken.class)
The default Spring Security types already have reflection hints as part of Spring Native.

Related

Spring security saml2 provider: InResponseTo validation for saml2 executed even if saved request is not found

With spring security saml2 provider version 5.7.x mandatory validation of InResponseTo was introduced if it is provided in the authentication response.
Validation logic expects to find saved Saml2AuthenticationRequest in HttpSession. However that is only possible if SameSite attribute is not set.
According security requirements of current project I'm working on it is set to Lax or Strict. This configuration is done outside of the application. This causes loss of the session and request data.
Maybe someone already have dealed with an issue and knows how to deal with it? I don't see any way to disable validation or alternative way of saving request available in the library.
While upgrading spring security from 5.x to 6, I ran into the same issue with spring session cookie. There doesn't seem to be any standard solution for this. For now, I have provided an implementation of Saml2AuthenticationRequestRepository which saves the SAML request in database based on RelayState parameter. Following is the sample implementation using Mongo. Any backend (In-memory cache, Redis, MySQL etc.) can be used -
import java.util.Optional;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.security.saml2.core.Saml2ParameterNames;
import org.springframework.security.saml2.provider.service.authentication.AbstractSaml2AuthenticationRequest;
import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationRequestRepository;
import org.springframework.stereotype.Repository;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
#Repository
#RequiredArgsConstructor
#Slf4j
public class MongoSaml2AuthenticationRequestRepository implements Saml2AuthenticationRequestRepository<AbstractSaml2AuthenticationRequest> {
public static final String SAML2_REQUEST_COLLECTION = "saml2RequestsRepository";
private final #NonNull MongoTemplate mongoTemplate;
#Override
public AbstractSaml2AuthenticationRequest loadAuthenticationRequest(HttpServletRequest request) {
String relayState = request.getParameter(Saml2ParameterNames.RELAY_STATE);
if (relayState == null) {
return null;
}
log.debug("Fetching SAML2 Authentication Request by relay state : {}", relayState.get());
Query query = Query.query(Criteria.where("relayState").is(relayState.get()));
AbstractSaml2AuthenticationRequest authenticationRequest = mongoTemplate.findOne(query, AbstractSaml2AuthenticationRequest.class, SAML2_REQUEST_COLLECTION);
if (!authenticationRequest.getRelayState().equals(relayState.get())) {
log.error("Relay State received from request '{}' is different from saved request '{}'.", relayState.get(), authenticationRequest.getRelayState());
return null;
}
log.debug("SAML2 Request retrieved : {}", authenticationRequest);
return authenticationRequest;
}
#Override
public void saveAuthenticationRequest(AbstractSaml2AuthenticationRequest authenticationRequest,
HttpServletRequest request, HttpServletResponse response) {
//As per OpenSamlAuthenticationRequestResolver, it will always have value. However, one validation can be added to check for null and regenerate.
String relayState = authenticationRequest.getRelayState();
log.debug("Relay State Received: {}", relayState);
mongoTemplate.save(authenticationRequest, SAML2_REQUEST_COLLECTION);
}
#Override
public AbstractSaml2AuthenticationRequest removeAuthenticationRequest(HttpServletRequest request,
HttpServletResponse response) {
AbstractSaml2AuthenticationRequest authenticationRequest = loadAuthenticationRequest(request);
if (authenticationRequest == null) {
return null;
}
mongoTemplate.remove(authenticationRequest, SAML2_REQUEST_COLLECTION);
return authenticationRequest;
}
}
Post adding this, all the SAML validations including InResponseTo validation are passed successfully. Since RelayState is not supposed to change between SAML requests as per specifications, this seemed like reasonable alternative (in absence of any standard solution so far). I am running this through standard security testing and will update the solution with my findings if any.

How to get the id_token outside a controller in Asp.net Core MVC?

How can I get the id_token returned by OpenIdConnect provider outside of a controller.
Inside the controller, this gives me the token:
string idToken = await HttpContext.GetTokenAsync("id_token");
I need to get the token inside a service in a similar fashion. How can I do this?
P.S: I have already tried to use HttpContextAccessor but it does not return anything.
Using Session to store data/value, which can be used in different place within your project.
//store vaule
Session["idToken"] = await HttpContext.GetTokenAsync("id_token");
//Call session
var x = Session["idToken"];
//empty or clean Session value
Session["idToken"] = null;
It seems GetTokenAsync() is an extension method to the httpContext. If this is true, then you could add the HttpContextAccessor in your startup / top level program.cs (depending if you are on .net 5 or 6)
builder.Services.AddHttpContextAccessor();
//or
services.AddHttpContext();
Then in your class, you can go the well known DI route:
public class MyService : IMyService
{
private readonly IHttpContextAccessor _httpContextAccessor;
public MyService(IHttpContextAccessor httpContextAccessor) =>
_httpContextAccessor = httpContextAccessor;
public async Task DoSomething() {
var idtoken = await _httpContextAccessor.HttpContext.GetTokenAsync("id_token");
}
}
Documentation: https://learn.microsoft.com/de-de/aspnet/core/fundamentals/http-context?view=aspnetcore-6.0
As a variation on Marco's answer, you usually work with tokens in a filter / middleware, then inject claims into your service classes:
A website would use the ID token claims as the Claims Principal
A web API would use the access token claims as the Claims Principal
You can make the ClaimsPrincipal injectable at startup, and note that this is a per-request dependency:
this.services.AddScoped(ctx => ctx.GetService<IHttpContextAccessor>().HttpContext.User);
This enables you to inject the ClaimsPrincipal class into a Service class as in this API code of mine, so that it doesn't need to know about HTTP, just the claims. In my example the service class is also request scoped, and applies some claims based authorization rules.

Smallrye open api interceptor

I am developing a rest application.
Some endpoints require a custom header parameter, not related to authorisation. I created a custom annotation using jax-rs NameBinding. Here is an usage example:
#GET
#RequiresBankHeader
public int get(
#HeaderParam("bank")
#Parameter(ref = "#/components/parameters/banks")
String bank) {
return someService.getSomeInformation();
}
There is a provider that intercepts this call and do some routine using the information in the header parameter.
The problem is that I have to repeat '#HeaderParam("bank") #Parameter(ref = "#/components/parameters/banks") String bank' everywhere, just so it appears in Swagger, even though the service classes do not need it. I was able to at least reuse the parameter definition with ref = "#/components/parameters/banks", and declaring it in the OpenAPI.yml file, that Quarkus merges with generated code very nicely.
But I also want to create and interceptor to dynamically add this do the OpenApi definition whenever RequiresBankHeader annotation is present.
Is there a way to do it?
I dont think you can use interceptors to modify the generated Openapi schema output.
If all methods on a given endpoint require some parameter, you can specify it on class level like so:
#Path("/someendpoint")
public class MyEndpoint {
#HeaderParam("bank")
#Parameter(name = "bank")
String bank;
#GET
public Response getAll() {return Response.ok().build()}
#GET
#Path("{id}")
public Response someMethod(#PathParam("id") String id) {return Response.ok().build();}
}
As mentioned by Roberto Cortez, the MP OpenAPI spec provides a programmatic way to contribute metadata to the openapi.yml file.
It is not possible to detect an annotation in the JAX-RS endpoint definition, but it was good enough to automate what I needed. Since all methods that had the RequiresBankHeader return the same Schema, I was able to hack it like this:
public class OpenApiConfigurator implements OASFilter {
#Override
public Operation filterOperation(Operation operation) {
operation.getResponses().getAPIResponses().values().stream().
map(APIResponse::getContent).
filter(Objects::nonNull).
map(Content::getMediaTypes).
flatMap(mediaTypes -> mediaTypes.values().stream()).
map(MediaType::getSchema).
filter(Objects::nonNull).
map(Schema::getRef).
filter(Objects::nonNull).
filter(ref -> ref.contains("the common response schema")).
findAny().
ifPresent(schema -> {
ParameterImpl parameter = new ParameterImpl();
parameter.setRef("#/components/parameters/banks");
operation.addParameter(parameter);
});
return operation;
}
OpenApiConfigurator should be configure in the application properties, using mp.openapi.filter=com.yourcompany.OpenApiConfigurator

How #PreAuthorize is working in an Reactive Application or how to live without ThreadLocal?

Can you explain where the advice handling #PreAuthorize("hasRole('ADMIN')") retrieves the SecurityContext in a Reactive application?
The following Spring Security example is a good illustration of this kind of usage: https://github.com/spring-projects/spring-security/tree/5.0.0.M4/samples/javaconfig/hellowebflux-method
After checking the Spring Security Webflux source code, I've found some implementations of SecurityContextRepository but the load method needs the ServerWebExchange as a parameter.
I'm trying to understand how to replace SecurityContextHolder.getContext().getAuthentication() call in a standard service (because ThreadLocal is no longer an option in a Reactive Application), but I don't understand how to replace this with a call to a SecurityContextRepository without a reference on the ServerWebExchange.
The ReactiveSecurityContextHolder provides the authentication in a reactive way, and is analogous to SecurityContextHolder.
Its getContext() method provides a Mono<SecurityContext>, just like SecurityContextHolder.getContext() provides a SecurityContext.
ReactiveSecurityContextHolder
.getContext()
.map(context ->
context.getAuthentication()
You're right, ThreadLocal is no longer an option because the processing of a request is not tied to a particular thread.
Currently, Spring Security is storing the authentication information as a ServerWebExchange attribute, so tied to the current request/response pair. But you still need that information when you don't have direct access to the current exchange, like #PreAuthorize.
The authentication information is stored in the Reactive pipeline itself (so accessible from your Mono or Flux), which is a very interesting Reactor feature - managing a context tied to a particular Subscriber (in a web application, the HTTP client is pulling data from the server and acts as such).
I'm not aware of an equivalent of SecurityContextHolder, or some shortcut method to get the Authentication information from the context.
See more about Reactor Context feature in the reference documentation.
You can also see an example of that being used in Spring Security here.
I implemented a JwtAuthenticationConverter (kotlin):
#Component
class JwtAuthenticationConverter : Function<ServerWebExchange,
Mono<Authentication>> {
#Autowired
lateinit var jwtTokenUtil: JwtTokenUtil
#Autowired
lateinit var userDetailsService: ReactiveUserDetailsService
private val log = LogFactory.getLog(this::class.java)
override fun apply(exchange: ServerWebExchange): Mono<Authentication> {
val request = exchange.request
val token = getJwtFromRequest(request)
if ( token != null )
try {
return userDetailsService.findByUsername(jwtTokenUtil.getUsernameFromToken(token))
.map { UsernamePasswordAuthenticationToken(it, null, it.authorities) }
} catch ( e: Exception ) {
exchange.response.statusCode = HttpStatus.UNAUTHORIZED
exchange.response.headers["internal-message"] = e.message
log.error(e)
}
return Mono.empty()
}
private fun getJwtFromRequest(request: ServerHttpRequest): String? {
val bearerToken = request.headers[SecurityConstants.TOKEN_HEADER]?.first {
it.startsWith(SecurityConstants.TOKEN_PREFIX, true)}
return if (bearerToken.isNullOrBlank()) null else bearerToken?.substring(7, bearerToken.length)
}
And then I set a SecurityConfig like this:
val authFilter = AuthenticationWebFilter(ReactiveAuthenticationManager {
authentication: Authentication -> Mono.just(authentication)
})
authFilter.setAuthenticationConverter(jwtAuthenticationConverter)
http.addFilterAt( authFilter, SecurityWebFiltersOrder.AUTHENTICATION)
You can use this approach to customize your AuthenticationConverter as I did to jwt based authentication to set the desired authentication object.

How do I automatically pick the configured SAML Identity provider in a multi-tenant environment to do SSO using Spring SAML

I am using Spring SAML in a multi-tenant application to provide SSO. Different tenants use different urls to access the application, and each has a separate Identity Provider configured. How do I automatically assign the correct Identity Provider given the url used to access the application?
Example:
Tenant 1: http://tenant1.myapp.com
Tenant 2: http://tenant2.myapp.com
I saw that I can add a parameter idp to the url (http://tenant1.myapp.com?idp=my.idp.entityid.com) and the SAMLContextProvider will pick the identity provider with that entity id. I developed a database-backed MetadataProvider that takes the tenant hostname as initialisation parameter to fetch the metadata for that tenant form the database linked to that hostname. Now I think I need some way to iterate over the metadata providers to link entityId of the metadata to the hostname. I don't see how I can fetch the entityId of the metadata, though. That would solve my problem.
You can see how to parse available entityIDs out of a MetadataProvider in method MetadataManager#parseProvider. Note that generally each provider can supply multiple IDP and SP definitions, not just one.
Alternatively, you could further extend the ExtendedMetadataDelegate with your own class, include whatever additional metadata (like entityId) you wish, and then simply retype MetadataProvider to your customized class and get information from there when iterating data through the MetadataManager.
If I were you, I'd take a little bit different approach though. I would extend SAMLContextProviderImpl, override method populatePeerEntityId and perform all the matching of hostname/IDP there. See the original method for details.
At the time of writing, Spring SAML is at version 1.0.1.FINAL. It does not support multi-tenancy cleanly out of the box. I found another way to achieve multi-tenancy apart from the suggestions given by Vladimir above. It's very simple and straight-forward and does not require extension of any Spring SAML classes. Furthermore, it utilizes Spring SAML's in-built handling of aliases in CachingMetadataManager.
In your controller, capture the tenant name from the request and create an ExtendedMetadata object using the tenant name as the alias. Next create an ExtendedMetadataDelegate out of the ExtendedMetadata and initialize it. Parse the entity ids out of it and check if they exist in MetadataManager. If they don't exist, add the provider and refresh metadata. Then get the entity id from MetadataManager using getEntityIdForAlias().
Here is the code for the controller. There are comments inline explaining some caveats:
#Controller
public class SAMLController {
#Autowired
MetadataManager metadataManager;
#Autowired
ParserPool parserPool;
#RequestMapping(value = "/login.do", method = RequestMethod.GET)
public ModelAndView login(HttpServletRequest request, HttpServletResponse response, #RequestParam String tenantName)
throws MetadataProviderException, ServletException, IOException{
//load metadata url using tenant name
String tenantMetadataURL = loadTenantMetadataURL(tenantName);
//Deprecated constructor, needs to change
HTTPMetadataProvider httpMetadataProvider = new HTTPMetadataProvider(tenantMetadataURL, 15000);
httpMetadataProvider.setParserPool(parserPool);
//Create extended metadata using tenant name as the alias
ExtendedMetadata metadata = new ExtendedMetadata();
metadata.setLocal(true);
metadata.setAlias(tenantName);
//Create metadata provider and initialize it
ExtendedMetadataDelegate metadataDelegate = new ExtendedMetadataDelegate(httpMetadataProvider, metadata);
metadataDelegate.initialize();
//getEntityIdForAlias() in MetadataManager must only be called after the metadata provider
//is added and the metadata is refreshed. Otherwise, the alias will be mapped to a null
//value. The following code is a roundabout way to figure out whether the provider has already
//been added or not.
//The method parseProvider() has protected scope in MetadataManager so it was copied here
Set<String> newEntityIds = parseProvider(metadataDelegate);
Set<String> existingEntityIds = metadataManager.getIDPEntityNames();
//If one or more IDP entity ids do not exist in metadata manager, assume it's a new provider.
//If we always add a provider without this check, the initialize methods in refreshMetadata()
//ignore the provider in case of a duplicate but the duplicate still gets added to the list
//of providers because of the call to the superclass method addMetadataProvider(). Might be a bug.
if(!existingEntityIds.containsAll(newEntityIds)) {
metadataManager.addMetadataProvider(metadataDelegate);
metadataManager.refreshMetadata();
}
String entityId = metadataManager.getEntityIdForAlias(tenantName);
return new ModelAndView("redirect:/saml/login?idp=" + URLEncoder.encode(entityId, "UTF-8"));
}
private Set<String> parseProvider(MetadataProvider provider) throws MetadataProviderException {
Set<String> result = new HashSet<String>();
XMLObject object = provider.getMetadata();
if (object instanceof EntityDescriptor) {
addDescriptor(result, (EntityDescriptor) object);
} else if (object instanceof EntitiesDescriptor) {
addDescriptors(result, (EntitiesDescriptor) object);
}
return result;
}
private void addDescriptors(Set<String> result, EntitiesDescriptor descriptors) throws MetadataProviderException {
if (descriptors.getEntitiesDescriptors() != null) {
for (EntitiesDescriptor descriptor : descriptors.getEntitiesDescriptors()) {
addDescriptors(result, descriptor);
}
}
if (descriptors.getEntityDescriptors() != null) {
for (EntityDescriptor descriptor : descriptors.getEntityDescriptors()) {
addDescriptor(result, descriptor);
}
}
}
private void addDescriptor(Set<String> result, EntityDescriptor descriptor) throws MetadataProviderException {
String entityID = descriptor.getEntityID();
result.add(entityID);
}
}
I believe this directly solves the OP's problem of figuring out how to get the IDP for a given tenant. But this will work only for IDPs with a single entity id.

Resources