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.
Related
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.
I separate my application into 2 parts:
Front end : Vue js and connected with AWS congnito for login feature (email/pw or google social login).
Back end : Spring boot Restful. User information stored in database (a unique id from congnito as primary key.)
My flow of authentication
User redirected to congnito and login. congnito will return a unique id and JWT.
Front end passes the unique id and JWT to back end controller.
backend validate JWT and return user information from DB
My question is:
Is this a bad practice to authenticate on front end and pass data to back end for spring security? If so, may I have any suggestion to change my implementation flow?
To call AuthenticationProvider.authenticate, a Authentication consist username (in my case, the unique id from cognito) and password is needed (UsernamePasswordAuthenticationToken). Are there any implementation to set only username? or it is fine to set password as empty string?
// controller
public String login(HttpServletRequest req, String cognitoId, String jwt) {
// check JWT with AWS
if(!AwsJwtChecker(cognitoId, jwt))
return createErrorResponseJson("invalid jwt");
UsernamePasswordAuthenticationToken authReq
= new UsernamePasswordAuthenticationToken(cognitoId, "");
Authentication auth = authManager.authenticate(authReq);
SecurityContext sc = SecurityContextHolder.getContext();
sc.setAuthentication(auth);
HttpSession session = req.getSession(true);
session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, sc);
MyUser user = userRepository.selectUserByCognitoId(cognitoId);
return createLoginSuccessResponse(user);
}
// web config
#Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String cognitoId = authentication.getName();
// check user exist in db or not
MyUser user = userRepository.selectUserByCognitoId(cognitoId);
if (user != null) {
return new UsernamePasswordAuthenticationToken(username, "", user.getRoles());
} else {
throw new BadCredentialsException("Authentication failed");
}
}
#Override
public boolean supports(Class<?>aClass) {
return aClass.equals(UsernamePasswordAuthenticationToken.class);
}
}
Is this a bad practice to authenticate on front end and pass data to back end for spring security? If so, may I have any suggestion to change my implementation flow?
No, in fact it's best practice. JWT is exactly for that purpose: You can store information about the user and because of the signature of the token, you can be certain, that the information is trustworthy.
You don't describe what you are saving in the database, but from my perspective, you are mixing two authentication methods. While it's not forbidden, it might be unnecessary. Have you analysed your token with jwt.io? There are many information about the user within the token and more can be added.
Cognito is limited in some ways, like number of groups, but for a basic application it might be enough. It has a great API to manage users from within your application, like adding groups or settings properties.
You don't describe what you do with the information that is returned with 3). Vue can too use the information stored in the jwt to display a username or something like that. You can decode the token with the jwt-decode library, eg, and get an object with all information.
To call AuthenticationProvider.authenticate...
Having said that, my answer to your second question is: You don't need the whole authentication part in you login method.
// controller
public String login(HttpServletRequest req, String cognitoId, String jwt) {
// check JWT with AWS
if(!AwsJwtChecker(cognitoId, jwt))
return createErrorResponseJson("invalid jwt");
return userRepository.selectUserByCognitoId(cognitoId);
}
This should be completely enough, since you already validate the token. No need to authenticate the user again. When spring security is set up correctly, the jwt will be set in the SecurityContext automatically.
The problem I see with your implementation is that anyone could send a valid jwt and a random cognitoId and receive user information from the database. So it would be better to parse the jwt and use something from within the jwt, like username, as identifier in the database. The token can't be manipulated, otherwise the validation fails.
public String login(String jwt) {
// check JWT with AWS
if(!AwsJwtChecker(jwt))
return createErrorResponseJson("invalid jwt");
String identifier = getIdentifier(jwt);
return userRepository.selectUserByIdentifier(identifier);
}
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.
My ASP.NET MVC application uses Dependency Injection to inject services to the controllers.
I need to find some way of passing run-time data to the services, because as far as I know it's anti-pattern to send run-time data to the constructors using DI.
In my case I have four different services that all rely on access tokens, which can be re-used between the services. However, that access token can expire so something needs to take care of issuing new access token when it expires.
The services (independent NuGet packages) are all clients for various services, that require access token for every request made. One example would be the AddUserAsync method in the IUserServiceBusiness, it basically POSTs to an endpoint with JSON data and adds Authorization header with bearer access token.
My current solution is to accept access token as a parameter in all of the methods in the services, which means that the web application takes care of handling the access tokens and passing them when needed.
But this solution smells, there has to be a better way of doing this.
Here's an example on how it's done currently.
The RegisterContainer method where all of the implementations are registered.
public static void RegisterContainers()
{
// Create a new Simple Injector container
var container = new Container();
container.Options.DefaultScopedLifestyle = new WebRequestLifestyle();
SSOSettings ssoSettings = new SSOSettings(
new Uri(ConfigConstants.SSO.FrontendService),
ConfigConstants.SSO.CallbackUrl,
ConfigConstants.SSO.ClientId,
ConfigConstants.SSO.ClientSecret,
ConfigConstants.SSO.ScopesService);
UserSettings userSettings = new UserSettings(
new Uri(ConfigConstants.UserService.Url));
ICacheManager<object> cacheManager = CacheFactory.Build<object>(settings => settings.WithSystemRuntimeCacheHandle());
container.Register<IUserBusiness>(() => new UserServiceBusiness(userSettings));
container.Register<IAccessTokenBusiness>(() => new AccessTokenBusiness(ssoSettings, cacheManager));
container.RegisterMvcControllers(Assembly.GetExecutingAssembly());
container.RegisterMvcIntegratedFilterProvider();
container.Verify();
DependencyResolver.SetResolver(new SimpleInjectorDependencyResolver(container));
}
Implementation of IUserBusiness and IAccessTokenBusiness are injected to AccountController.
private readonly IUserBusiness _userBusiness;
private readonly IAccessTokenBusiness _accessTokenBusiness;
public AccountController(IUserBusiness userBusiness, IAccessTokenBusiness accessTokenBusiness)
{
_userBusiness = userBusiness;
_accessTokenBusiness = accessTokenBusiness;
}
Example endpoint in AccountController that updates the user's age:
public ActionResult UpdateUserAge(int age)
{
// Get accessToken from the Single Sign On service
string accessToken = _accessTokenBusiness.GetSSOAccessToken();
bool ageUpdated = _userBusiness.UpdateAge(age, accessToken);
return View(ageUpdated);
}
And here are some ideas that I've thought of:
Pass the access token to the services with a setter, in the constructor of the controllers. For example:
public HomeController(IUserBusiness userBusiness, IAccessTokenBusiness accessTokenBusiness)
{
_userBusiness = userBusiness;
_accessTokenBusiness = accessTokenBusiness;
string accessToken = _accessTokenBusiness.GetAccessToken();
_userBusiness.setAccessToken(accessToken);
}
I donĀ“t like this idea because then I would have to duplicate this code in every controller.
Pass the access token with every method on the services (currently doing this). For example:
public ActionResult UpdateUser(int newAge)
{
string accessToken = _accessTokenBusiness.GetAccessToken();
_userBusiness.UpdateAge(newAge, accessToken);
}
Works, but I don't like it.
Pass implementation of IAccessTokenBusiness to the constructor of the services. For example:
IAccessTokenBusiness accessTokenBusiness = new AccessTokenBusiness();
container.Register<IUserBusiness>(() => new IUserBusiness(accessTokenBusiness));
But I'm unsure how I would handle caching for the access tokens. Perhaps I can have the constructor of AccessTokenBusiness accept some generic ICache implementation, so that I'm not stuck with one caching framework.
I would love to hear how this could be solved in a clean and clever way.
Thanks!
As I see it, the requirement of having this access token for communication with external services is an implementation detail to the class that actually is responsible of calling that service. In your current solution you are leaking these implementation details, since the IUserBusiness abstraction exposes that token. This is a violation of the Dependency Inversion Principle that states:
Abstractions should not depend on details.
In case you ever change this IUserBusiness implementation to one that doesn't require an access token, it would mean you will have to make sweeping changes through your code base, which basically means you voilated the Open/close Principle.
The solution is to let the IUserBusiness implementation take the dependency on IAccessTokenBusiness itself. This means your code would look as follows:
// HomeController:
public HomeController(IUserBusiness userBusiness)
{
_userBusiness = userBusiness;
}
public ActionResult UpdateUser(int newAge)
{
bool ageUpdated = _userBusiness.UpdateAge(newAge);
return View(ageUpdated);
}
// UserBusiness
public UserBusiness(IAccessTokenBusiness accessTokenBusiness)
{
_accessTokenBusiness = accessTokenBusiness;
}
public bool UpdateAge(int age)
{
// Get accessToken from the Single Sign On service
string accessToken = _accessTokenBusiness.GetSSOAccessToken();
// Call external service using the access token
}
But I'm unsure how I would handle caching for the access tokens.
This is neither a concern of the controller nor the business logic. This is either a concern of the AccessTokenBusiness implementation or a decorator around IAccessTokenBusiness. Having a decorator is the most obvious solution, since that allows you to change caching independently of generation of access tokens.
Do note that you can simplify your configuration a bit by making use of the container's auto-wiring abilities. Instead of registering your classes using a delegate, you can let the container analyse the type's constructor and find out itself what to inject. Such registration looks as follows:
container.Register<IUserBusiness, UserServiceBusiness>();
container.Register<IAccessTokenBusiness, AccessTokenBusiness>();
ICacheManager<object> cacheManager =
CacheFactory.Build<object>(settings => settings.WithSystemRuntimeCacheHandle());
container.RegisterSingleton<ICacheManager<object>>(cacheManager);
Further more, a decorator for IAccessTokenBusiness can be added as follows:
container.RegisterDecorator<IAccessTokenBusiness, CachingAccessTokenBusinessDecorator>();
I'm having a set of Sping Data Repositories which are all exposed over Rest by using Spring-data-rest project. Now I want to secure the HTTP, so that only registered users can access the http://localhost:8080/rest/ So for this purpose I add #Secured(value = { "ROLE_ADMIN" }) to all the repositories and I also enable the security by specifying the
#EnableGlobalMethodSecurity(securedEnabled = true, jsr250Enabled = true, prePostEnabled = true)
So now what happens is I go to the rest and it's all good - i'm asked to authenticate. Next thing I do is I go to my website (which uses all the repositories to access the database) but my request fails with
nested exception is org.springframework.security.authentication.AuthenticationCredentialsNotFoundException: An Authentication object was not found in the SecurityContext
which is correct because i'm browsing my website as anonymous user.
So my question is: is there a way to provide method authentication for the REST layer only? To me it sounds like a new annotation is needed (something like #EnableRestGlobalMethodSecurity or #EnableRestSecurity)
I don't know if this will solve your problem, however I managed to get something similar, working for me by creating an event handler for my specific repository, and then used the #PreAuthorize annotation to check for permissions, say on beforeCreate. For example:
#RepositoryEventHandler(Account.class)
public class AccountEventHandler {
private final Logger logger = LoggerFactory.getLogger(getClass());
#PreAuthorize("isAuthenticated() and (hasRole('ROLE_USER'))")
#HandleBeforeCreate
public void beforeAccountCreate(Account account) {
logger.debug(String.format("In before create for account '%s'", account.getName()));
}
#PreAuthorize("isAuthenticated() and (hasRole('ROLE_ADMIN'))")
#HandleBeforeSave
public void beforeAccountUpdate(Account account) {
logger.debug(String.format("In before update for account '%s'", account.getName()));
//Don't need to add anything to this method, the #PreAuthorize does the job.
}
}