Wanting a custom ExpressionInterceptURL to dynamically manage access to URLs - spring-security

We have information packs at URLs which need a user right to access, laid out as:
/InfoPacks/InfoPack1/
/InfoPacks/InfoPack2/
etc
A user need ROLE_INFOPACK1 to access the /InfoPacks/InfoPack1/ and ROLE_INFOPACK2 to access the /InfoPacks/InfoPack2/ etc.
We are adding packs all the time so putting adding to WebSecurityConfig() with
.antMatchers("/InfoPacks/InfoPack1/**/*").hasAuthority("ROLE_INFOPACK1)
isnt really a goer as it would imply modifying and re-deploying every time a new pack was created while the configure method in security config got larger and larger.
A custom evaluator would be better. eg something that could call a service with
like:
hasPermission(Authentication auth, String targetURL) {
// search auth.GrantAuthorities for a match to targetURL
}
I see this sort of custom permission expression examples for use with PreAuthorize but doesnt seem to be way to do this with URL authorizeRequests(). (at least in version 4).
Any pointers would be very welcome.

Got it figured out, thanks to http://www.baeldung.com/spring-security-custom-voter.
My web security config now look like:
protected void configure(HttpSecurity http) throws Exception {
http
.headers()
.frameOptions().sameOrigin()
.and()
.csrf().disable()
.exceptionHandling()
.and()
.authorizeRequests()
....
.antMatchers("/Infopacks/**/*").authenticated().accessDecisionManager(accessDecisionManager())
..... etc
}
#SuppressWarnings("unchecked")
#Bean
public AccessDecisionManager accessDecisionManager() {
System.out.println("Arrive AccessDecisionManager");
List<AccessDecisionVoter<? extends Object>> decisionVoters
= Arrays.asList(
new WebExpressionVoter(),
new RoleVoter(),
new AuthenticatedVoter(),
new DynamicVoter());
return new UnanimousBased(decisionVoters);
}
The vote method of DynamicVoter looks (in ugly first test - I know it needs work to plug holes) like:
#Override
public int vote(Authentication a, Object s, Collection clctn) {
String url = ((FilterInvocation) s).getRequestUrl();
int vote = ACCESS_ABSTAIN;
if (url.contains("/Infopack")) {
vote = ACCESS_DENIED;
for (GrantedAuthority ga:a.getAuthorities()) {
if (url.toUpperCase().contains(ga.getAuthority()) ) {
vote = ACCESS_GRANTED;
break;
}
}
}
return vote;
}
The user authorization system thus just needs to add the infopack name as a granted authority to allow user access to the infopack directory. You can add new infopack directories at will without changing the code.

Related

Altering URL for Spring Security SAML2 Login

I have an application with multiple authentication types (i.e. Basic and a special Preauthorized login). I am attempting to add a SAML2 RelyingParty registration in my security configuration, where I am attempting to change the default path from:
/login/saml2/sso/{registrationId}
to
/auth/saml2/{registrationId}
So, I have the following setup:
public RelyingPartyRegistration provder1RelyingPartyRegistration() {
RelyingPartyRegistration registration = RelyingPartyRegistrations
.fromMetadataLocation("classpath:provider1/metadata.xml")
.registrationId("provider1")
.assertionConsumerServiceLocation("{baseUrl}/auth/saml2/{registrationId}")
.build();
return registration;
}
// #Bean
public RelyingPartyRegistrationRepository relyingPartyRegistrationRepository() {
Collection<RelyingPartyRegistration> registrations = Collections.unmodifiableList(Arrays.asList(provider1RelyingPartyRegistration()));
InMemoryRelyingPartyRegistrationRepository repository = new InMemoryRelyingPartyRegistrationRepository(registrations);
return repository;
}
// fluff
#Override
protected void configure(HttpSecurity http) throws Exception {
final RequestMatcher filterRequestMatcher = new OrRequestMatcher(
new AntPathRequestMatcher("/auth/basic"),
new AntPathRequestMatcher("/auth/preauth")
);
ApplicationAuthenticationProcessingFilter filter = new ApplicationAuthenticationProcessingFilter(filterRequestMatcher, authenticationManagerBean());
filter.setAuthenticationSuccessHandler(successHandler());
filter.setAuthenticationFailureHandler(failureHandler());
http
.authorizeRequests()
.antMatchers("/**").permitAll()
.and()
.addFilterAfter(filter, LogoutFilter.class)
// fluff
.and()
.saml2Login()
.relyingPartyRegistrationRepository(relyingPartyRegistrationRepository())
.loginProcessingUrl("/auth/saml2/{registrationId}")
;
}
Unfortunately, I get this:
14 Dec 10:55:34 WARN [https-openssl-nio-127.0.0.1-444-exec-2] (DispatcherServlet.java:1278) - No mapping for POST /svc/auth/saml2/provider1
Can anyone tell me what I'm doing wrong trying to change that path? My application does NOT use Spring Boot, so I'm stuck with manual configuration.
EDIT
Some debugging has led to this hitting this line in the Saml2LoginConfigurer:
Map<String, String> providerUrlMap = getIdentityProviderUrlMap(
this.authenticationRequestEndpoint.filterProcessingUrl, this.relyingPartyRegistrationRepository);
Somehow, there's a default authenticationRequestEndpoint (since I didn't define one) setting the filterProcessingUrl to a value of /saml2/authenticate/{registrationId}. So, how do I override this?
The loginProcessingUrl is called by the asserting party after the authentication succeeds, which contains in the request the SAMLResponse parameter.
What you are trying to change is the URL to process an authentication request (create the SAMLRequest and send to the asserting party), this is done by the Saml2WebSsoAuthenticationRequestFilter class. To change the redirectMatcher you have to provide an ObjectPostProcessor, see this issue.
ObjectPostProcessor<Saml2WebSsoAuthenticationRequestFilter> processor = new ObjectPostProcessor<>() {
#Override
public <O extends Saml2WebSsoAuthenticationRequestFilter> O postProcess(O filter) {
filter.setRedirectMatcher(new AntPathRequestMatcher("/my/custom/url"));
return filter;
}
};
http.saml2Login().addObjectPostProcessor(processor);
Take a look at SAML 2.0 Login Overview for more detail about the flow.

"No KeyInfo Generator provided" for spring-security-saml2-service-provider

I have an application that connects to a SAML idP that only supports the POST Binding. After configuring my application which uses spring-security-saml2-service-provider to manually create a POST Authentication request, I looked at the XML that got generated and saw that it included the Signature information (which is expected) but not the Key Info. Then in the logs, I noticed it said:
No KeyInfoGenerator was supplied in parameters or resolveable for credential type org.opensaml.security.x509.X509Credential, No KeyInfo will be generated for Signature
This is what my code looks like to manually generate the POST Authentication request:
#Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic()
.disable()
.csrf()
.disable();
}
#Bean
public RelyingPartyRegistration nnanetRelyingPartyRegistration() {
SAMLMetadataSignatureSigningParametersResolver resolver = new SAMLMetadataSignatureSigningParametersResolver();
return RelyingPartyRegistrations
.fromMetadataLocation("https://example.com/metadata.xml")
.entityId("example")
.registrationId("nnanet")
.assertingPartyDetails(party -> {
party.wantAuthnRequestsSigned(true)
.singleSignOnServiceLocation("https://example.com/login")
.entityId("https://example.com/login");
.verificationX509Credentials(saml2X509Credentials -> {
saml2X509Credentials.add(getVerificationCertificate());
});
})
.signingX509Credentials(saml2X509Credentials -> {
saml2X509Credentials.add(getSigningCredential());
})
.decryptionX509Credentials(saml2X509Credentials -> {
saml2X509Credentials.add(getSigningCredential());
})
.build();
}
#Bean
public RelyingPartyRegistrationRepository relyingPartyRegistrationRepository() {
return new InMemoryRelyingPartyRegistrationRepository(relyingPartyRegistration());
}
#Bean
public Saml2PostAuthenticationRequest saml2PostAuthenticationRequest() {
Saml2AuthenticationRequestContext.Builder contextBuilder = Saml2AuthenticationRequestContext.builder();
contextBuilder.assertionConsumerServiceUrl("http://localhost:8080/saml/SSO");
contextBuilder.relyingPartyRegistration(relyingPartyRegistration());
contextBuilder.issuer("issuer");
OpenSamlAuthenticationRequestFactory factory = new OpenSamlAuthenticationRequestFactory();
return factory.createPostAuthenticationRequest(contextBuilder.build());
}
I just call the saml2PostAuthenticationRequest() method from my Controller and generate a form to submit automatically due to some reasons that are outside the scope of this question. After looking further into the OpenSamlAuthenticationRequestFactory, it looks like this is creating the SignatureSigningParameters specifically without including the KeyInfoGenerator as it's only being created in the private methods. Does anyone have an idea on how to get around this, or perhaps point out if I'm doing something wrong?
Thanks!
This will be addressed in a future version of Spring Security SAML 2
Please review the PR - https://github.com/spring-projects/spring-security/pull/9746

Spring Security 4 intercept anonymous user AccessDeniedException

My website allows unauthenticated (anonymous) users to perform a search of content which requires authentication to view. Currently, when they click on a link to view the details of one of the content items on the search results page Spring Security correctly identifies the user as unauthenticated and displays the login page. However, I would like to intervene and instead display a page to encourage the anonymous user to sign up for the website. I have traced what is happening in the filter chain but it's not clear to me whether I should extend an existing filter or handler or create a custom filter or handler. If it's the later I'm not sure where it should go.
When I run this through debug I can see the following happening:
ExceptionTranslationFilter.doFilter executes FilterSecurityInterceptor which determines that the detail page requires authentication (returns a -1 vote and throws an AccessDeniedException)
ExceptionTranslationFilter catches the exception, determines the user is anonymous and calls the authenticationEntryPoint, in this case LoginUrlAuthenticationEntryPoint
LoginUrlAuthenticationEntryPoint invokes the DefaultRedirectStrategy which redirects to the login page
So, basically I need to override the redirection to the login page for this one use case. My best guess is to create a custom filter that checks for the combination of an anonymous user accessing this specific detail page and forces a redirect to the join up page, inserting the filter in the chain after ExceptionTranslationFilter. Or is this total overkill for handling a single page redirect and there's an easier way to accomplish this?
For anyone interested, here's the code for the custom auth entry point, borrowing from LoginUrlAuthenticationEntryPoint and ExceptionTranslationFilter.
public class CustomAuthLoginEntryPoint extends LoginUrlAuthenticationEntryPoint {
private final Logger logger = LoggerFactory.getLogger(getClass());
private PortResolver portResolver = new PortResolverImpl();
private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();
private RequestCache requestCache = new HttpSessionRequestCache();
private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
private String joinPageUrl;
public CustomAuthLoginEntryPoint(String loginFormUrl) {
super(loginFormUrl);
}
#Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
logger.debug("commence");
String redirectUrl = null;
if (!StringUtils.isBlank(joinPageUrl)) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth == null || trustResolver.isAnonymous(auth)) {
SavedRequest savedRequest = requestCache.getRequest(request, response);
redirectUrl = savedRequest.getRedirectUrl();
if (redirectUrl.indexOf("viewDetail") > 0) {
String joinPageUrl = buildRedirectUrlToJoinPage(request);
logger.debug("Redirecting to '" + joinPageUrl + "'");
redirectStrategy.sendRedirect(request, response, joinPageUrl);
return;
}
}
}
super.commence(request, response, authException);
}
protected String buildRedirectUrlToJoinPage(HttpServletRequest request) {
int serverPort = portResolver.getServerPort(request);
String scheme = request.getScheme();
RedirectUrlBuilder urlBuilder = new RedirectUrlBuilder();
urlBuilder.setScheme(scheme);
urlBuilder.setServerName(request.getServerName());
urlBuilder.setPort(serverPort);
urlBuilder.setContextPath(request.getContextPath());
urlBuilder.setPathInfo(joinPageUrl);
return urlBuilder.getUrl();
}
public void setJoinPage(String joinPageUrl) {
this.joinPageUrl = joinPageUrl;
}
}
I added this to my WebSecurityConfigurerAdapter:
#Bean
public CustomAuthLoginEntryPoint customAuthLoginEntryPoint() {
CustomAuthLoginEntryPoint entryPoint = new CustomAuthLoginEntryPoint("/user/login");
entryPoint.setJoinPage("/user/join");
return entryPoint;
}
and the http configure:
.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler())
.authenticationEntryPoint(customAuthLoginEntryPoint())

Securing exclusively the REST access to a Spring Data Rest Repository

I'm using Spring Data Rest to expose a repository. I'm using #PreAuthorize and #PostFilter to restrict the access to the REST end points to exclusively admin users and filter the results.
#PreAuthorize("hasRole('ROLE_ADMIN')")
#PostFilter("hasPermission(filterObject, 'read')
public interface SomeRepository extends CrudRepository<SomeEntity, Long> {
}
At the same time I have another Controller that doesn't require any authentication but is using the repository.
#Controller
public class SomeController {
#Autowired
SomeRepository repository;
#RequestMapping(value = "/test")
public ResponseEntity test () {
// Do something
repository.findAll();
// Do something else
}
}
This doesn't work because the user that send the request to "/test" is not admin so it doesn't have access to the repository.
My question is, it is possible to add security exclusively to the REST interface of the repository and not when the repository is used internally in the application?
Thanks
Please evaluate these possibilities:
Security checks in REST event handlers
Adding custom repository methods for internal use
Using RunAsManager (or temporarily switching SecurityContext to perform a privileged operation)
Securing modifying requests using REST event handlers:
#Service
#RepositoryEventHandler
public class FooService {
/**
* Handles before-* events.
*/
#HandleBeforeCreate
#HandleBeforeSave
#HandleBeforeDelete
#PreAuthorize("hasRole('ADMIN')")
public void onBeforeModify(final Foo entity){
// noop
}
/**
* Handles before-* events.
*/
#HandleBeforeLinkSave
#HandleBeforeLinkDelete
#PreAuthorize("hasRole('ADMIN')")
public void onBeforeModifyLink(final Foo entity, final Object linked){
// noop
}
}
Securing standard CRUD methods while adding non-secure custom methods on repository for internal use:
public interface FooDao extends CrudRepository<Foo, Long> {
#Override
#PreAuthorize("hasRole('ADMIN')")
<S extends Foo> S save(final S entity);
/**
* Saves entity without security checks.
*/
#Transactional
#Modifying
default <S extends Foo> S saveInternal(final S entity) {
return save(entity);
}
}
One solution would be to remove the #PreAuthorize annotation from your repository interface, and in a configuration class, extend WebSecurityConfigAdaptor and override the configure(HttpSecurity security) method. From here you can use AntMatchers to impose access restrictions to the REST endpoints as required. For example:
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/someEntities/**").hasRole('ADMIN')
.anyRequest().permitAll();
}
See http://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#jc-httpsecurity for more details.
I ran into the same problem and came up with a workaround that doesn't feel completely right but does its job for the time being.
I basically created a security utils bean which can be used to check if a method was called internally or externally using the Spring Data REST API (remark: my repositories are prefixed /api/, if you have another prefix you need to change the regex accordingly).
#Component("securityUtils")
public class SecurityUtils {
public boolean isRestRequest(){
HttpServletRequest r = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
return Pattern.matches("^/api/", UrlUtils.buildRequestUrl(r));
}
}
To make this work, you need to add the following line to your listeners in the web.xml:
<listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>
And use the method in your expression based access control like so (where the last line in the expression allows you to use the save method from any controller methods that are mapped against URLs which do not start with /api/:
#Override
#PreAuthorize("hasRole('ROLE_ADMINISTRATOR') " +
"or hasPermission(#user, 'WRITE') " +
"or !#securityUtils.isRestRequest()")
<S extends User> S save(#P("user") S user);
Caveats:
You cannot use this when you want to expose custom functionality over the /api route as this is merely a simple regex check against the route
The check has to be explicitly added to each repository or repository method for which you want to omit the authorization check internally (might be an advantage as well)
In my opinion the right solution would be to have two Repositories, one that is called EntityRepository and one SecuredEntityRepository.
Example:
#RestResource(exported = false)
public abstract interface CustomerRepository extends JpaRepository<Customer, Long> {
}
and the secured version:
#RestResource(exported = true)
public abstract interface SecuredCustomerRepository extends CustomerRepository {
#Override
#PreAuthorize("#id == principal.customer.id or hasAuthority('ADMIN_CUSTOMER_ONE')")
public Customer findOne(#Param("id") Long id);
#Override
#Query("SELECT o FROM #{#entityName} o WHERE o.id = ?#{principal.customer.id} or 1 = ?#{ hasAuthority('ADMIN_CUSTOMER_LIST') ? 1 : 0 }")
public Page<Customer> findAll(Pageable pageable);
#Override
#SuppressWarnings("unchecked")
#PreAuthorize("#customer.id == principal.customer.id or hasAuthority('ADMIN_CUSTOMER_SAVE')")
public Customer save(#P("customer") Customer customer);
#Override
#PreAuthorize("hasAuthority('ADMIN_CUSTOMER_DELETE')")
public void delete(#Param("id") Long id);
#Override
#PreAuthorize("hasAuthority('ADMIN_CUSTOMER_DELETE')")
public void delete(Customer customer);
}
This is currently not possible due to an issue with the auto-wiring mechanism in SD REST: https://jira.spring.io/browse/DATAREST-923
Sure. Just change the location of the #PreAuthorize annotation. This annotation can be placed in classes or single methods.
For example
#Controller
public class SomeController {
#Autowired
SomeRepository repository;
#RequestMapping(value = "/test")
#PreAuthorize(....)
public ResponseEntity test () {
// Do something
repository.findAll();
// Do something else
}
}
is perfectly legit (note the annotation on the test() method.
I decorated the repository class with this:
#PreAuthorize("hasRole('admin')")
It locked down everything.
Then whatever I wanted to enable for internal use but not rest, I decorated like this:
#Transactional
#Modifying
#PreAuthorize("hasRole('user')")
#RestResource(exported = false)
default <S extends SomeEntity> S saveInternal(final S entity) {
return save(entity);
}
And whatever I wanted to expose via the Rest interface (handpicked few) I exposed with something like this:
#PreAuthorize("(hasRole('user')) and
(#entity.user.username == principal.name)")
#Override
<S extends SomeEntity> S save(#Param("entity") S entity);
Note that this also validates that you are saving a record you are authorized to save.
I solved this problem by adding my own check
I created my AbstractHttpConfigurer class with global security. I have declared methods that can be public.
public class CommonSpringKeycloakTutorialsSecurityAdapter extends AbstractHttpConfigurer<CommonSpringKeycloakTutorialsSecurityAdapter, HttpSecurity> {
public static String[] PERMIT_ALL_URL = {"/api/user/createUser"};
#Override
public void init(HttpSecurity http) throws Exception {
// any method that adds another configurer
// must be done in the init method
http
// disable csrf because of API mode
.csrf().disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
// manage routes securisation here
.authorizeRequests().antMatchers(HttpMethod.OPTIONS).permitAll()
// manage routes securisation here
.and()
.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS).permitAll()
.antMatchers("/swagger-ui.html*", "/swagger-ui/**", "/v3/api-docs/**").permitAll()
.antMatchers(PERMIT_ALL_URL).permitAll()
.anyRequest().authenticated();
}
Then I created my own check based on global permissions.
#Component("securityUtils")
public class SecurityUtils {
public boolean isPermitRestRequest(){
HttpServletRequest r = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
String currentUrl = UrlUtils.buildRequestUrl(r);
for(String url: CommonSpringKeycloakTutorialsSecurityAdapter.PERMIT_ALL_URL) {
if(currentUrl.equals(url)) {
return true;
}
}
return false;
}
}
For native validation to work, include a listener
#WebListener
public class MyRequestContextListener extends RequestContextListener {
}
In my team we evaluated several of the answers in this post and they didn't fit to our scenario.
A variation of Johannes Hiemer answer worked for us. We configured Spring Data REST to only expose annotated repositories:
data.rest:
detection-strategy: annotated
Then we defined 2 repositories without hierarchical relationship.
One of the repos will be exposed by adding the #RepositoryRestResource annotation to it. For this one, we deny access to every method by default so auth will have to be specified on a method level to reduce the chances of exposing methods by mistake. For example, initially we extended CrudRepository and didn't want to expose the deletion operation:
#RepositoryRestResource
#PreAuthorize("denyAll()")
interface SomeRestResourceRepository : Repository<SomeEntity, Long> {
}
The repository to be used for internal calls is defined as a regular Spring Data Repository:
interface SomeRepository : Repository<SomeEntity, Long> {
}
We are using spring-boot-starter-data-rest 2.6.3.

Extending AjaxAwareAuthenticationSuccessHandler, Remembering destination URL

Grails 1.3.7
Spring-Security-Core 1.1.2
I've implemented a custom class that extends AjaxAwareAuthenticationSuccessHandler so that specific roles can be taken to specific URL's after logging in which works great. However, if the session expires I need to be able to take the user to the requested URL when the session expired, overriding the Role based URL.
Here is a simplified version of my code
class MyAuthSuccessHandler extends AjaxAwareAuthenticationSuccessHandler {
#Override
public void onAuthenticationSuccess(final HttpServletRequest request, final HttpServletResponse response,
final Authentication authentication) throws ServletException, IOException {
def goAdmin = false
authentication.authorities.each { ga ->
if (ga.authority.equals('ROLE_ADMIN')) {
goAdmin = true
}
}
if (goAdmin) {
response.sendRedirect(request.contextPath + '/admin/index')
}else{
super.onAuthenticationSuccess(request, response, authentication)
}
}
}
I tried adding a call to determineTargetUrl(request, response) but it always returns '/' even though I've requested a resource like /admin/foo which is protected.
Thanks.
Requesting
super.determineTargetUrl(request, response);
should work, if you use SavedRequestAwareAuthenticationSuccessHandler as super class. I am not sure if it is possible for you to switch to this class in your scenario. Maybe this can help, but I guess you are fully aware of it: http://omarello.com/2011/09/grails-custom-target-urls-after-login/

Resources