I have issues with using the Pre/Post Authorization Annotations from Spring Security and the Servlet API with Keycloak integration. I investigated a lot of articles, tutorials and the following questions without further luck:
Obtaining user roles in servlet application using keycloak
Spring Boot Keycloak - How to get a list of roles assigned to a user?
Using spring security annotations with keycloak
Spring Boot + Spring Security + Hierarchical Roles
How do I add method based security to a Spring Boot project?
Configure DefaultMethodSecurityExpressionHandler using Spring Security Java Config
SpringBoot + method based hierarchical roles security: ServletContext is required
All I want is removing the ROLES_ prefix, use hierarchical roles and a comfortable way to retrieve the users' roles.
As of now, I am able to retrieve a hierarchical role like this in a Controller but cannot use the annotations:
#Controller
class HomeController {
#Autowired
AccessToken token
#GetMapping('/')
def home(Authentication auth, HttpServletRequest request) {
// Role 'admin' is defined in Keycloak for this application
assert token.getResourceAccess('my-app').roles == ['admin']
// All effective roles are mapped
assert auth.authorities.collect { it.authority }.containsAll(['admin', 'author', 'user'])
// (!) But this won't work:
assert request.isUserInRole('admin')
}
// (!) Leads to a 403: Forbidden
#GetMapping('/sec')
#PreAuthorize("hasRole('admin')") {
return "Hello World"
}
}
I am guessing that the #PreAuthorize annotation does not work, because that Servlet method is not successful.
There are only three roles - admin, author, user - defined in Keycloak and Spring:
enum Role {
USER('user'),
AUTHOR('author'),
ADMIN('admin')
final String id
Role(String id) {
this.id = id
}
#Override
String toString() {
id
}
}
Keycloak Configuration
Upon removing the #EnableGlobalMethodSecurity annotation from this Web Security reveals an Error creating bean with name 'resourceHandlerMapping' caused by a No ServletContext set error - no clue, where that comes from!
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
/**
* Registers the KeycloakAuthenticationProvider with the authentication manager.
*/
#Autowired
void configureGlobal(AuthenticationManagerBuilder auth) {
auth.authenticationProvider(keycloakAuthenticationProvider().tap { provider ->
// Assigns the Roles via Keycloaks role mapping
provider.grantedAuthoritiesMapper = userAuthoritiesMapper
})
}
#Bean
RoleHierarchyImpl getRoleHierarchy() {
new RoleHierarchyImpl().tap {
hierarchy = "$Role.ADMIN > $Role.AUTHOR > $Role.USER"
}
}
#Bean
GrantedAuthoritiesMapper getUserAuthoritiesMapper() {
new RoleHierarchyAuthoritiesMapper(roleHierarchy)
}
SecurityExpressionHandler<FilterInvocation> expressionHandler() {
// Removes the prefix
new DefaultWebSecurityExpressionHandler().tap {
roleHierarchy = roleHierarchy
defaultRolePrefix = null
}
}
// ...
#Bean
#Scope(scopeName = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
AccessToken accessToken() {
def request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest()
def authToken = (KeycloakAuthenticationToken) request.userPrincipal
def securityContext = (KeycloakSecurityContext) authToken.credentials
return securityContext.token
}
#Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http)
http
.authorizeRequests()
.expressionHandler(expressionHandler())
// ...
}
}
Global Method Security Configuration
I needed to explicitly allow allow-bean-definition-overriding, because otherwise I got a bean with that name already defined error, which reveals that I completely lost control over this whole situation and don't know what's goin on.
#Configuration
#EnableGlobalMethodSecurity(prePostEnabled = true)
class GlobalMethodSecurityConfig extends GlobalMethodSecurityConfiguration {
#Autowired
RoleHierarchy roleHierarchy
#Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
((DefaultMethodSecurityExpressionHandler)super.createExpressionHandler()).tap {
roleHierarchy = roleHierarchy
defaultRolePrefix = null
}
}
}
Any further configurations that could be important? Thanks a lot for your help!
As M. Deinum pointed out, one must remove the defaultRolePrefix in multiple places with a BeanPostProcessor, which is explained in (docs.spring.io) Disable ROLE_ Prefixing.
This approach seemed not very clean to me and so I wrote a custom AuthoritiesMapper to achieve mapping hierarchical roles from Keycloak without the need to rename them to the ROLE_ Spring standard. First of all, the Roles enumeration was modified to conform that standard inside the application scope:
enum Role {
USER('ROLE_USER'),
AUTHOR('ROLE_AUTHOR'),
ADMIN('ROLE_ADMIN')
// ...
}
Secondly, I replaced the RoleHierarchyAuthoritiesMapper with a prefixing hierarchical implementation:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
// ..
// Replaces the RoleHierarchyAuthoritiesMapper
#Bean
GrantedAuthoritiesMapper getUserAuthoritiesMapper() {
new PrefixingRoleHierarchyAuthoritiesMapper(roleHierarchy)
}
}
class PrefixingRoleHierarchyAuthoritiesMapper extends RoleHierarchyAuthoritiesMapper {
String prefix = 'ROLE_'
PrefixingRoleHierarchyAuthoritiesMapper(RoleHierarchy roleHierarchy) {
super(roleHierarchy)
}
#Override
Collection<? extends GrantedAuthority> mapAuthorities(Collection<? extends GrantedAuthority> authorities) {
def prefixedAuthorities = authorities.collect { GrantedAuthority originalAuthority ->
new GrantedAuthority() {
String authority = "${prefix}${originalAuthority.authority}".toUpperCase()
}
}
super.mapAuthorities(prefixedAuthorities)
}
}
And lastly, I got rid of the GlobalMethodSecurityConfig.
Apart from suggestions provided in (docs.spring.io) Disable ROLE_ Prefixing, and suggestion provided by M. Deinum, one more modification is needed while using KeycloakWebSecurityConfigurerAdapter.
In configureGlobal method, grantedAuthoritiesMapper bean is set in the bean keycloakAuthenticationProvider. And in grantedAuthoritiesMapper, prefix can be set to anything you want, where the default value is "ROLE_".
The code goes as follows:
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
SimpleAuthorityMapper grantedAuthoritiesMapper = new SimpleAuthorityMapper();
grantedAuthoritiesMapper.setPrefix("");
keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(grantedAuthoritiesMapper);
auth.authenticationProvider(keycloakAuthenticationProvider);
}
This solution works for me.
Related
The actual Spring Security configuration is like this:
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/uri1/**").hasAuthority(Permission.AUTHORITY1.toString())
.antMatchers("/uri2/**").hasAuthority(Permission.AUTHORITY2.toString())
.anyRequest().hasAuthority(Permission.AUTHORITY3.toString())
.and().httpBasic()
.realmName("App").and().csrf().disable();
http.authorizeRequests();
http.headers().frameOptions().sameOrigin().cacheControl().disable();
}
#Bean
public Filter shallowEtagHeaderFilter() {
return new ShallowEtagHeaderFilter();
}
}
And the web MVC configuration is like this:
#Configuration
public class DefaultView extends WebMvcConfigurerAdapter{
#Override
public void addViewControllers( ViewControllerRegistry registry ) {
registry.addViewController( "/" ).setViewName( "forward:myPage.html" );
registry.setOrder( Ordered.HIGHEST_PRECEDENCE);
super.addViewControllers( registry );
}
}
I have to replace the httpBasic authentification done in Spring Security by an authentification using onelogin (so with SAML if I understood what I found on the Internet).
By doing research, I found that a possibility was to use Shibboleth on the Apache server and an other was to use a plugin in Spring Security to manage SAML.
For the first solution (shibboleth), the aim is to manage onelogin authentification directly on Apache server (if not connected, the user is redirected on onelogin authentification page, if connected, the ressource is accessible) and to have needed informations returned in SAML response (like username and other need data) in the header of the request (to be abble to have them in Spring app).
With this solution, is it possible to keep httpBasic authentification in Spring security and to have "Basic XXXX" in the header of each request set by Shibboleth? Or, have I to remove the httpBasic authentification from Spring Security?
For the second solution (plugin to manage SAML in Spring Security), is it the same result as the first solution and how it must be implemented?
Thank you in advance for your reply.
welcome to stackoverflow.
... and to have needed informations returned in SAML response (like
username and other need data) in the header of the request (to be
abble to have them in Spring app)
If I understood correctly, you are already using spring security. This means your application is already using spring security populated context for authentication and authorization in your controller/service layers. If you use said approach, where apache is populating the authenticate user information in headers, than this is NOT going to populate the spring security context all by itself UNLESS you add a preAuthFilter in your chain to extract this information and populate your spring context appropriately.
With this solution, is it possible to keep httpBasic authentification
in Spring security and to have "Basic XXXX" in the header of each
request set by Shibboleth? Or, have I to remove the httpBasic
authentification from Spring Security?
If you are able to do it then what I said above would be a bit relaxed. Having said that, to best of my knowledge, there is no option where you can deduce a Basic authentication header using shibboleth apache module. In addition, I'll also advice to be careful with this approach since, with this approach, you'll still have to authenticate the user in your app with a dummy password (since you are NOT going to get user's correct password via SAML in this header) and this opens up your application for security exploits. I'll strongly advise against this approach. Shibboleth already has some Spoof Checking covered in their documentation.
[EDIT]
Based on the additional information, following is what you can do to achieve all handling by apache and still use spring security effectively
First provide implementation of PreAuthenticatedAuthenticationToken in your application, you can use AbstractPreAuthenticatedProcessingFilter for this purpose. A skeleton for the implementation is provided below, this is excerpt from one of my past work and very much stripped down keeping only the essential elements which are relevant for your scenario. Also take a close look at AuthenticationManager and Authentication docs and make sure you fully understand what to use and for what purpose. Please read javadocs for all these 4 classes carefully to understand the contract as it can be confusing to get it right in spring security otherwise. I have added necessary details as TODO and comments in skeleton blow that you'll have to fill in yourself in your implementation.
public class ShibbolethAuthFilter extends AbstractPreAuthenticatedProcessingFilter {
private final String containsValidPrincipalHeader = "_valid_shibboleth_header_present";
private final String shibbolethHeader = "_shibboleth_header";
private Logger logger = LoggerFactory.getLogger(getClass());
/**
* This authentication manager's authenticate method MUST return a fully populated
* org.springframework.security.core.Authentication object. You may very well use
* either PreAuthenticatedAuthenticationToken OR UsernamePasswordAuthenticationToken
* with any credentials set, most important is to correctly populate the Authorities
* in the returned object so that hasAuthority checks works as expected.
*
* Another point, you can use authentication.getPrincipal() in the implementation
* of authenticate method to access the same principal object as returned by
* getPreAuthenticatedPrincipal method of this bean. So basically you pass the
* using Principal object from this bean to AuthenticationManager's authenticate
* method which in turn return a fully populated spring's Authentication object
* with fully populated Authorities.
*/
#Autowired
private ShibbolethAuthenticationManager authenticationManager;
#Override
public void afterPropertiesSet() {
setAuthenticationManager(authenticationManager);
super.afterPropertiesSet();
}
#Override
protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
String authHeader = request.getHeader(shibbolethHeader);
if (authHeader == null) {
logger.trace("No {} header found, skipping Shibboleth Authentication", shibbolethHeader);
return null;
}
// TODO - validate if all header and it's contents are what they should be
ShibbolethAuthToken authToken = /* TODO - provide your own impl to supply java.security.Principal object here */;
request.setAttribute(containsValidPrincipalHeader, Boolean.TRUE);
return authToken;
}
/**
* No password required thus Credentials will return null
*/
#Override
protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
if (Boolean.TRUE.equals(request.getAttribute(containsValidPrincipalHeader)))
return System.currentTimeMillis(); // just returning non null value to satisfy spring security contract
logger.trace("Returning null Credentials for non authenticated request");
return null;
}
}
Register this as servlet filter in your app using following registrar
#Configuration
public class ShibbolethFilterRegistrar {
/*
* We don't want to register Shibboleth Filter in spring global chain thus
* added this explicit registration bean to disable just that.
*/
#Bean
public FilterRegistrationBean shibbolethFilterRegistrar(Shibboleth shibbolethAuthFilter) {
FilterRegistrationBean registration = new FilterRegistrationBean(shibbolethAuthFilter);
registration.setEnabled(false);
return registration;
}
#Bean
public ShibbolethAuthFilter shibbolethAuthFilter() {
return new ShibbolethAuthFilter();
}
}
Followed by this, change your WebSecurityConfig to following
#Override
protected void configure(HttpSecurity http) throws Exception {
/* autowire shibbolethAuthFilter bean as well */
http
.addFilterBefore(shibbolethAuthFilter, AbstractPreAuthenticatedProcessingFilter.class);
.authorizeRequests()
.antMatchers("/uri1/**").hasAuthority(Permission.AUTHORITY1.toString())
.antMatchers("/uri2/**").hasAuthority(Permission.AUTHORITY2.toString())
.anyRequest().hasAuthority(Permission.AUTHORITY3.toString())
.and()
.realmName("App").and().csrf().disable();
http.authorizeRequests();
http.headers().frameOptions().sameOrigin().cacheControl().disable();
}
Hope these pointers helps you to integrate external auth successfully.
IMHO, following is still valid - as much as I have understood your scenario, if I had to do it, I'll personally prefer to use spring security inbuilt SAML auth for this purpose since that provides very smooth integration with spring security in every possible context within the framework. In addition, it also simplifies my deployment scenario where I'll also have to take care of provisioning apache which'll typically fall under additional workload for DevOps team. For simplicity and scalability, spring security inbuilt SAML SSO support would be my first choice unless there's a constraint which is forcing me to do otherwise (which I am not able to see in current discussion context based on the explanation provided). There are ample tutorials and examples available on net to get it done. I know this is not what you asked for but I thought to share with you what I have done myself in past for similar SSO solutions in spring distributed apps and learning that I had. Hope it helps!!
This is the entire solution I used to connect to my application using Onelogin, shibboleth (Apache) and Spring Security. I used http but you have to adapt if you want to use https.
Onelogin
Configure a "SAML Test Connector (SP Shibboleth)" with the following configuration:
Login URL : http://myserver:<port>/my-app
ACS (Consumer) URL : http://myserver:<port>/Shibboleth.sso/SAML2/POST
SAML Recipient : http://myserver:<port>/Shibboleth.sso/SAML2/POST
SAML Single Logout URL : http://myserver:<port>/Shibboleth.sso/Logout
ACS (Consumer) URL Validator : ^http://myserver:<port>/Shibboleth.sso/SAML2/POST$
Audience : http://myserver:<port>/my-app
An parameter "username" has been added and a value is defined for this parameter for each user.
Apache and shibboleth
See: https://wiki.shibboleth.net/confluence/display/SHIB2/NativeSPJavaInstall
I installed shibboleth.
I activated AJP (module mod_proxy_ajp). It is recommended to use AJP instead of HTTP request headers.
I updated my apache conf file:
<VirtualHost *:[port]>
...
ProxyIOBufferSize 65536
<location /my-app >
ProxyPass "ajp://myappserver:<portAJPApp>"
AuthType shibboleth
ShibRequestSetting requireSession 1
Require valid-user
ProxyPassReverse /
ProxyHTMLEnable On
ProxyHTMLURLMap http://myappserver:<portHttpApp>/ /my-app/
ProxyHTMLURLMap / /my-app/
</location>
<Location /Shibboleth.sso>
SetHandler shib
</Location>
...
</VirtualHost>
In shibboleth2.xml:
<SPConfig xmlns="urn:mace:shibboleth:2.0:native:sp:config"
xmlns:conf="urn:mace:shibboleth:2.0:native:sp:config"
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"
clockSkew="180">
...
<ApplicationDefaults id="default" policyId="default"
entityID="http://myserver:<port>/my-app"
REMOTE_USER="eppn persistent-id targeted-id"
signing="false" encryption="false"
attributePrefix="AJP_">
<!-- entityId in IdP metadata file -->
<SSO entityID="https://app.onelogin.com/saml/metadata/XXXX">
SAML2
</SSO>
<MetadataProvider type="XML"
uri="https://app.onelogin.com/saml/metadata/XXX"
backingFilePath="onelogin_metadata.xml" reloadInterval="7200">
</MetadataProvider>
</ApplicationDefaults>
...
</SPConfig>
In attribute-map.xml:
<Attributes xmlns="urn:mace:shibboleth:2.0:attribute-map" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
...
<!-- OneLogin attributes: "name" corresponds to the attribute name defined in Onelogin and received in SAML response. "id" is the name of the attribute in shibboleth session accissible by http://myserver:<port>/Shibboleth.sso/Session -->
<Attribute name="username" nameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic" id="username">
<AttributeDecoder xsi:type="StringAttributeDecoder"/>
</Attribute>
...
</Attributes>
Spring-boot
Tomcat configuration to add an AJP connector (attributes loaded from yml with a "server" property):
#Configuration
#ConfigurationProperties(prefix = "server")
public class TomcatConfiguration {
private int ajpPort;
private boolean ajpAllowTrace;
private boolean ajpSecure;
private String ajpScheme;
private boolean ajpEnabled;
#Bean
public EmbeddedServletContainerCustomizer customizer() {
return container -> {
if (container instanceof TomcatEmbeddedServletContainerFactory) {
TomcatEmbeddedServletContainerFactory tomcatServletFactory = ((TomcatEmbeddedServletContainerFactory) container);
...
// New connector for AJP
// Doc: http://tomcat.apache.org/tomcat-7.0-doc/config/ajp.html
if (isAjpEnabled()) {
Connector ajpConnector = new Connector("AJP/1.3");
ajpConnector.setPort(getAjpPort());
ajpConnector.setSecure(isAjpSecure());
ajpConnector.setAllowTrace(isAjpAllowTrace());
ajpConnector.setScheme(getAjpScheme());
ajpConnector.setAttribute("packetSize", 65536);
tomcatServletFactory.addAdditionalTomcatConnectors(ajpConnector);
}
}
};
}
// Getters and setters
}
Spring security configuration (the shibboleth filter can be activated through yml with a "shibboleth-filter" property defined in an "authentication" property):
#Configuration
#ConfigurationProperties(prefix = "authentication")
#EnableWebSecurity
#Import(ShibbolethFilterRegistrar.class)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private boolean shibbolethFilter;
#Autowired
private ShibbolethAuthFilter shibbolethAuthFilter;
#Override
protected void configure(HttpSecurity http) throws Exception {
if(isShibbolethFilter()) {
http.addFilterBefore(shibbolethAuthFilter, AbstractPreAuthenticatedProcessingFilter.class)
.authorizeRequests()
.antMatchers("/uri1/**").hasAuthority(Permission.AUTHORITY1.toString())
.antMatchers("/uri2/**").hasAuthority(Permission.AUTHORITY2.toString())
.anyRequest().hasAuthority(Permission.AUTHORITY3.toString())
.and().csrf().disable();
http.authorizeRequests();
http.headers().frameOptions().sameOrigin().cacheControl().disable();
}
else {
http
.authorizeRequests()
.antMatchers("/uri1/**").hasAuthority(Permission.AUTHORITY1.toString())
.antMatchers("/uri2/**").hasAuthority(Permission.AUTHORITY2.toString())
.anyRequest().hasAuthority(Permission.AUTHORITY3.toString())
.and().httpBasic()
.realmName("MyApp")
.and().csrf().disable();
http.authorizeRequests();
http.headers().frameOptions().sameOrigin().cacheControl().disable();
}
}
// Getter and setter for shibbolethFilter loaded from yml
}
ShibbolethFilterRegistrar:
#Configuration
public class ShibbolethFilterRegistrar {
#Bean
public ShibbolethAuthenticationManager shibbolethAuthenticationManager() {
return new ShibbolethAuthenticationManager();
}
#Bean
public FilterRegistrationBean shibbolethFilterRegistration(ShibbolethAuthFilter shibbolethAuthFilter) {
FilterRegistrationBean registration = new FilterRegistrationBean(shibbolethAuthFilter);
registration.setEnabled(false);
return registration;
}
#Bean
public ShibbolethAuthFilter shibbolethAuthFilter() {
return new ShibbolethAuthFilter();
}
}
ShibbolethAuthFilter:
public class ShibbolethAuthFilter extends AbstractPreAuthenticatedProcessingFilter {
private static final String USERNAME_ATTRIBUTE_NAME = "username";
private static final String VALID_SHIBBOLETH_ATTR = "_valid_shibboleth_attribute";
#Autowired
private ShibbolethAuthenticationManager shibbolethAuthenticationManager;
#Override
public void afterPropertiesSet() {
setAuthenticationManager(shibbolethAuthenticationManager);
super.afterPropertiesSet();
}
#Override
protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
// Attribute received in AJP request
Object username = request.getAttribute(USERNAME_ATTRIBUTE_NAME);
if(username == null) {
return null;
}
request.setAttribute(VALID_SHIBBOLETH_ATTR, Boolean.TRUE);
ShibbolethAuthToken authToken = new ShibbolethAuthToken(username.toString());
return authToken;
}
#Override
protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
if (Boolean.TRUE.equals(request.getAttribute(VALID_SHIBBOLETH_ATTR))) {
return System.currentTimeMillis(); // just returning non null value to satisfy spring security contract
}
logger.trace("Returning null Credentials for non authenticated request");
return null;
}
}
ShibbolethAuthenticationManager:
public class ShibbolethAuthenticationManager implements AuthenticationManager {
#Autowired
private MyAuthenticationProvider myAuthenticationProvider;
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
ShibbolethAuthToken principal = (ShibbolethAuthToken) authentication.getPrincipal();
Object credentials = authentication.getCredentials();
UserDetails userDetails = myAuthenticationProvider.loadUserByUsername(principal.getName());
if(userDetails == null || userDetails.getAuthorities() == null || userDetails.getAuthorities().isEmpty()) {
throw new BadCredentialsException("User rights cannot be retrieved for user " + principal.getName());
}
return new PreAuthenticatedAuthenticationToken(principal, credentials, userDetails.getAuthorities());
}
}
ShibbolethAuthToken implements Principal.
Thank you for your help.
I'm trying to set up Camel REST to use basic auth with a simple username/password from my application.properties and can't for the life of me seem to configure Camel Spring Security to do that. I'm trying to follow the Spring Security component documentation which seems to be missing the example of configuring the required beans. I found the missing example here under 'Controlling access to Camel routes' but this only shows the xml configuration.
How do I set up the required SpringSecurityAuthorizationPolicy bean? It needs an AuthenticationManager and an AccessDecisionManager and it also seems to require that I set its SpringSecurityAccessPolicy which I have no idea how to do.
I haven't gotten to test these yet, because I can't get my beans set up, but my rest route looks like:
rest("/ingest")
.post("/json").consumes("application/json")
.route()
.process(authProcessor)
.policy(authPolicy) // this is the bean I don't know how to configure
.to("direct:ingest")
.endRest();
and my AuthProcessor (taken from the camel component doc) looks like:
#Component
public class AuthProcessor implements Processor {
public void process(Exchange exchange) {
String userpass = new String(Base64.decodeBase64(exchange.getIn().getHeader("Authorization", String.class)));
String[] tokens = userpass.split(":");
// create an Authentication object
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(tokens[0], tokens[1]);
// wrap it in a Subject
Subject subject = new Subject();
subject.getPrincipals().add(authToken);
// place the Subject in the In message
exchange.getIn().setHeader(Exchange.AUTHENTICATION, subject);
}
}
and here's my broken bean configuration for what it's worth:
#Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Bean
public SpringSecurityAuthorizationPolicy springSecurityAuthorizationPolicy(
AuthenticationManager authenticationManager, AccessDecisionManager accessDecisionManager) {
SpringSecurityAuthorizationPolicy policy = new SpringSecurityAuthorizationPolicy();
SpringSecurityAccessPolicy springSecurityAccessPolicy = new SpringSecurityAccessPolicy();
policy.setAuthenticationManager(authenticationManager);
policy.setAccessDecisionManager(accessDecisionManager);
policy.setSpringSecurityAccessPolicy(????);
return policy;
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("user").password("pass").roles("USER");
}
#Bean(name = BeanIds.AUTHENTICATION_MANAGER)
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Bean
public AccessDecisionManager accessDecisionManager() {
AffirmativeBased affirmativeBased = new AffirmativeBased(ImmutableList.of(
new RoleVoter()
));
affirmativeBased.setAllowIfAllAbstainDecisions(true);
return affirmativeBased;
}
}
I've been banging my head against the wall trying to understand this so an example of how to do this would be amazing. It looks like the xml configuration for what I want to do (in the second link) is simple enough but I can't seem to replicate it in Java configuration.
I know it's an old topic, but I ran into similar questions. I managed to get it working. Not by overriding the accessDecisionManager() method within the WebSecurityConfigurerAdapter class, but by constructing a new instance while building my SpringSecurityAuthorizationPolicy:
#Bean
public Policy adminPolicy(AuthenticationManager authenticationManager) {
RoleVoter roleVoter = new RoleVoter();
SpringSecurityAuthorizationPolicy policy = new SpringSecurityAuthorizationPolicy();
policy.setAuthenticationManager(authenticationManager);
policy.setAccessDecisionManager(new UnanimousBased(List.of(roleVoter)));
policy.setSpringSecurityAccessPolicy(new SpringSecurityAccessPolicy(roleVoter.getRolePrefix() + "<ROLE_NAME>");
return policy;
}
I have this little OAuth server class and I am using Spring Boot 2.0.4 and the spring-security-oauth2-autoconfigure 2.0.0.RELEASE dependency :
#RestController
#SpringBootApplication
#EnableAuthorizationServer
#Order(200) // really needed ?
public class MyOAuthServerApplication extends WebSecurityConfigurerAdapter {
#RequestMapping({ "/me" })
public Map<String, String> user(Principal principal) {
Map<String, String> map = new LinkedHashMap<>();
map.put("name", principal.getName());
return map;
}
#Configuration
#EnableResourceServer
protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
#Override
public void configure(HttpSecurity http) throws Exception {
// #formatter:off
http.antMatcher("/me").authorizeRequests().anyRequest().authenticated();
// #formatter:on
}
}
#Bean
#Override
public UserDetailsService userDetailsService() {
UserDetails mary =
User.withUsername("mary")
.password("{bcrypt}$2a$10$B3NUb0x.MYnSfx7WJItrvO/ymEQwLCKQNehmCuA8keL1uTyHizI0i")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(mary);
}
public static void main(String[] args) {
SpringApplication.run(MyOAuthServerApplication.class, args);
}
}
This seems to work well with and without the #Order(200) annotation.
So is this annotation really needed ?
The Order annotation is used to define the injection precedence.
Read more her: https://www.baeldung.com/spring-order
In your case it's because of the EnableResourceServer annotation. And you must keep the annotation.
From the doc:
The #EnableResourceServer annotation creates a security filter with
#Order(SecurityProperties.ACCESS_OVERRIDE_ORDER-1) by default, so by
moving the main application security to
#Order(SecurityProperties.ACCESS_OVERRIDE_ORDER) we ensure that the
rule for "/me" takes precedence.
Please find the tutorial here: https://spring.io/guides/tutorials/spring-boot-oauth2/
You need it if you have another WebSecurityConfigurerAdapter configuration.
For example if you allow users to login via login form with a different UserDetailsService and so on. Then this should be tried before your oauth authentification and thus needs a lower order, for example #Order(199).
Another example would be different configuration for your API access.
If you don't have any other configuration, then you don't need to set the order.
Setting the order to 200 also seems to be an arbitrary value, that should simply be higher then the others and thus executed last.
I'm trying to add HTTP basic authentication into my springboot microservice.
When I use the "code" way described in Spring doc it works perfectly:
#RestController
public class Ping
{
#GetMapping("/ping")
public String ping()
{
return "pong";
}
}
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter
{
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth)
throws Exception
{
auth.inMemoryAuthentication()
.withUser("user").password("password")
.roles("USER");
}
}
But, if I want to use the properties way it seems that it's not possible or I'm doing something wrong:
security.basic.enabled = true
security.basic.path = /**
security.basic.realm = Spring
security.user.name = user
security.user.password = xxx
security.user.role = USER
security.sessions = always
Using this solution, only the resource http://localhost/env is secured, and /ping is not. It is possible to use only application.properties to configure a "simple" basic authentication to all the resources?
Is there a way I can disable the global method security using the boolean securityEnabled from my config.properties? Any other approach?
#EnableWebSecurity
#EnableGlobalMethodSecurity(securedEnabled=true)
#PropertySource("classpath:config.properties")
public class SecurityConfig
extends WebSecurityConfigurerAdapter {
#Value("${securityconfig.enabled}")
private boolean securityEnabled;
...
}
The easiest way to do this is:
Extract method security to its own class
Remove the securedEnabled attribute entirely
Override the customMethodSecurityMetadataSource method and return the result based on the configured value.
For example:
#EnableWebSecurity
#Configuration
#PropertySource("classpath:config.properties")
public class SecurityConfig extends WebSecurityConfigurerAdapter {
...
}
#EnableGlobalMethodSecurity
#Configuration
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
#Value("${securityconfig.enabled}")
private boolean securityEnabled;
protected MethodSecurityMetadataSource customMethodSecurityMetadataSource() {
return securityEnabled ? new SecuredAnnotationSecurityMetadataSource() : null;
}
}
I've managed this by defining a Spring "securityDisabled" profile and conditionally applying security config based off that. I'm using Spring Boot 2.0.2. I believe this should work if not using Spring Boot and in previous versions of Spring Boot, but I have not tested. It's possible some tweaks may be required to property and class names because I know in Spring 2.0 some of that changed.
// In application.properties
spring.profiles.include=securityDisabled
Then my security config looks like this:
#Configuration
public class SecurityConfig {
// When the securityDisabled profile is applied the following configuration gets used
#Profile("securityDisabled")
#EnableWebSecurity
public class SecurityDisabledConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
// Configure http as needed from Spring Security defaults when
// NO security is desired
}
}
// When the securityDisabled profile is NOT applied the following configuration gets used
#Profile("!securityDisabled")
#EnableGlobalMethodSecurity(prePostEnabled = true)
#EnableWebSecurity
public class SecurityEnabledConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
// Configure http as needed from Spring Security defaults when
// security is desired
}
}
}
In Springboot2, a simple solution consists in replacing the security method interceptor by a dummy one when the security is off :
#EnableGlobalMethodSecurity(prePostEnabled = true)
static class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
#Value("${disableSecurity}")
private boolean disableSecurity;
public MethodInterceptor methodSecurityInterceptor(MethodSecurityMetadataSource methodSecurityMetadataSource) {
return disableSecurity ? new SimpleTraceInterceptor()
: super.methodSecurityInterceptor(methodSecurityMetadataSource);
}
}
Thanks to Rob Winch for the solution. For folks who would like to do something similar but with prePostEnabled i have tried and tested the below similar approach and works just fine.
#EnableGlobalMethodSecurity(securedEnabled = true)
#Configuration
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
#Value("${security.prePostEnabled}")
private boolean prePostEnabled;
#Autowired
private DefaultMethodSecurityExpressionHandler defaultMethodSecurityExpressionHandler;
protected MethodSecurityMetadataSource customMethodSecurityMetadataSource() {
return prePostEnabled ? new PrePostAnnotationSecurityMetadataSource(new ExpressionBasedAnnotationAttributeFactory(defaultMethodSecurityExpressionHandler)) : null ;
}}
EDIT: In addition to above i realized it is required to add following beans to the class. The below will help using the expression based pre invocation checks along with avoiding "ROLE_" prefix that is defaulted in all the handlers
protected AccessDecisionManager accessDecisionManager() {
AffirmativeBased accessDecisionManager = (AffirmativeBased) super.accessDecisionManager();
ExpressionBasedPreInvocationAdvice expressionAdvice = new ExpressionBasedPreInvocationAdvice();
expressionAdvice.setExpressionHandler(getExpressionHandler());
//This is required in order to allow expression based Voter to allow access
accessDecisionManager.getDecisionVoters()
.add(new PreInvocationAuthorizationAdviceVoter(expressionAdvice));
//Remove the ROLE_ prefix from RoleVoter for #Secured and hasRole checks on methods
accessDecisionManager.getDecisionVoters().stream()
.filter(RoleVoter.class::isInstance)
.map(RoleVoter.class::cast)
.forEach(it -> it.setRolePrefix(""));
return accessDecisionManager;
}
/**
* Allow skip ROLE_ when check permission using #PreAuthorize, like:
* #PreAuthorize("hasAnyRole('USER', 'SYSTEM_ADMIN')")
* Added all the Beans
*/
#Bean
public DefaultMethodSecurityExpressionHandler defaultMethodSecurityExpressionHandler() {
DefaultMethodSecurityExpressionHandler defaultMethodSecurityExpressionHandler = new DefaultMethodSecurityExpressionHandler();
defaultMethodSecurityExpressionHandler.setDefaultRolePrefix("");
return defaultMethodSecurityExpressionHandler;
}