We are facing a problem with Spring Security OAuth2 (v 2.0.8). The app is a Spring Boot application (v 1.2.6) with a custom authentication manager and it uses JWT tokens instead of default random value tokens. We are also using Spring Cloud Angel.SR4 and will upgrade to Brixton eventually but at the moment we "cannot" move to Spring Boot 1.3.
The application uses the default Spring Security configuration parameters to configure security for actuator endpoints:
security.user.name:user
security.user.password:password
This is the relevant part of the OAuth2 configuration:
#Configuration
#EnableAuthorizationServer
protected static class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
private AuthenticationManager oauthUserAuthenticationManager() {
Collection<UserDetails> users = new LinkedList<>();
users.add(new User("john", "doe", Arrays.asList(new SimpleGrantedAuthority("USER"))));
users.add(new User("jane", "doe", Arrays.asList(new SimpleGrantedAuthority("USER"), new SimpleGrantedAuthority("ADMIN"))));
UserDetailsManager manager = new InMemoryUserDetailsManager(users);
DaoAuthenticationProvider p = new DaoAuthenticationProvider();
p.setUserDetailsService(manager);
return new ProviderManager(Arrays.asList(p));
}
#Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
Resource fsr = resourceLoader.getResource(keystore);
KeyPair keyPair =
new KeyStoreKeyFactory(fsr, keystorePassword.toCharArray())
.getKeyPair(keyPairAlias, keyPairPassword.toCharArray());
converter.setKeyPair(keyPair);
return converter;
}
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(oauthUserAuthenticationManager())
.accessTokenConverter(jwtAccessTokenConverter());
}
}
The application works fine when using password grant (grant_type=password) and tokens are successfully granted using john/doe or jane/doe. However, when trying to use the refresh token grant (grant_type=refresh_token), an exception is thrown: UsernameNotFoundException: john.
From debugging I have seen that the error originates in DefaultTokenServices.refreshAccessToken() (line 150) when the authenticationManager is used to verify that the user still exists:
user = authenticationManager.authenticate(user);
The debugger also reveals that an authentication with username user is accepted by the authenticationManager, so it looks as if the tokenServices uses the default authentication manager and the password token granter (ResourceOwnerPasswordTokenGranter) uses the custom authentication manager that was configured by the AuthorizationServerEndpointsConfigurer in our configuration.
We believe that what we need to do is to configure the same authentication manager in both places, but how?
Related
I am using Spring Security Oauth2 Client and Keycloak as Identity provider.
My application will be deployed with multiple domain and we want to use single instance of Keycloak.
I have set up 2 realms in a single instance of Keycloak treating them as different tenants.
In the application.properties I have set the properties for two tenants -
But how come the Application 1 with URL - http://demo-app-1.com will redirect to keycloak 1 and similarly for Application 2 with URL - http://demo-app-2.com will redirect to keycloak 2.
server.port=8300
spring.security.oauth2.client.registration.demo1.client-name=spring-boot-web
spring.security.oauth2.client.registration.demo1.client-id=spring-boot-web
spring.security.oauth2.client.registration.demo1.client-secret=213e66d5-206f-4948-bd9d-bfa14a70c4cf
spring.security.oauth2.client.registration.demo1.provider=keycloak
spring.security.oauth2.client.registration.demo1.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.demo1.redirect-uri=http://localhost:8300
spring.security.oauth2.client.provider.keycloak.authorization-uri=http://localhost:8081/auth/realms/spring-boot/protocol/openid-connect/auth
spring.security.oauth2.client.provider.keycloak.token-uri=http://localhost:8081/auth/realms/spring-boot/protocol/openid-connect/token
spring.security.oauth2.client.registration.demo2.client-name=spring-boot-web
spring.security.oauth2.client.registration.demo2.client-id=spring-boot-web
spring.security.oauth2.client.registration.demo2.client-secret=d69a7fd1-2297-49d0-b236-7b8039c845b2
spring.security.oauth2.client.registration.demo2.provider=keycloak2
spring.security.oauth2.client.registration.demo2.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.demo2.redirect-uri=http://localhost:8301
spring.security.oauth2.client.provider.keycloak2.authorization-uri=http://localhost:8081/auth/realms/spring-boot-2/protocol/openid-connect/auth
spring.security.oauth2.client.provider.keycloak2.token-uri=http://localhost:8081/auth/realms/spring-boot-2/protocol/openid-connect/token
Query - Is there any additional property which we can set which can auto route the requests to the respective realm in Keycloak?
I am getting a page to choose the provider when I hit the application URL which I need to bypass
Here is the Example For Opaque Token (Multitenant Configuration) maybe this is helpful - The Key for Multitenancy in Spring Security is Authentication Manger Resolver
#Component public class CustomAuthenticationManagerResolver implements AuthenticationManagerResolver {
#Override
public AuthenticationManager resolve(HttpServletRequest request) {
String tenantId = request.getHeader("tenant");
OpaqueTokenIntrospector opaqueTokenIntrospector;
if (tenantId.equals("1")) {
opaqueTokenIntrospector = new NimbusOpaqueTokenIntrospector(
"https://test/authorize/oauth2/introspect",
"clientId",
"clientSecret"
);
} else {
opaqueTokenIntrospector = new NimbusOpaqueTokenIntrospector(
"https://test/authorize/oauth2/introspect",
"clientId",
"clientSecret");
}
return new OpaqueTokenAuthenticationProvider(opaqueTokenIntrospector)::authenticate;
}
}
Web Security Configuration
#Autowired
private CustomAuthenticationManagerResolver customAuthenticationManagerResolver;
#Override
public void configure(HttpSecurity http) throws Exception {
http.anyRequest()
.authenticated().and().oauth2ResourceServer()
.authenticationEntryPoint(restEntryPoint).authenticationManagerResolver(customAuthenticationManagerResolver);
}
I tried to use Spring WebClient and Spring Security OAuth2 Client for communication between two applications. I need to use OAuth2 Client Credentials grant type and use the same credentials and the same token for all requests between these applications. In the OAuth2 terminology resource owner is an application A itself and resource server is an application B (it is Keycloak Admin API). I use Spring Boot auto configuration from application.yaml and servlet environment. So I implemented it like this:
WebClient buildWebClient(ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository, String clientName) {
final ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = new ServletOAuth2AuthorizedClientExchangeFilterFunction(
clientRegistrationRepository, authorizedClientRepository);
oauth2Client.setDefaultClientRegistrationId(clientName);
return WebClient.builder()
.apply(oauth2Client.oauth2Configuration())
.build();
}
I found that I have an issue and invalid tokens (I use Keycloak and if my Keycloak is restarted then all old tokens are invalid) are never removed (you can see it in issue #9477) so I started debuging whole process of authorization in ServletOAuth2AuthorizedClientExchangeFilterFunction. I found that tokens are saved per user of the application A. It means that every user of the application A has its own token for authorization to the application B. But this is not my intended behavior I want to use only one token in the application A for consuming a resource from the application B.
I think that I found two solutions but both of them are not optimal.
Solution 1.:
Add another default request after ServletOAuth2AuthorizedClientExchangeFilterFunction which replaces user Authentication in attributes by an application Authentication. Problem is that it dependes on an internal implemention of ServletOAuth2AuthorizedClientExchangeFilterFunction which use a private attribute AUTHENTICATION_ATTR_NAME = Authentication.class.getName();.
return WebClient.builder()
.apply(oauth2Client.oauth2Configuration())
.defaultRequest(spec -> spec.attributes(attr ->
attr.put(AUTHENTICATION_ATTR_NAME, new CustomApplicaitonAuthentication())
)
.build();
Solution 2.:
Use a custom implementation of OAuth2AuthorizedClientRepository instead of default AuthenticatedPrincipalOAuth2AuthorizedClientRepository (which I have to do anyway because of the issue with removing invalid tokens) where I ignore user's Authentication and I use a custom application Authentication.
#Override
public <T extends OAuth2AuthorizedClient> T loadAuthorizedClient(String clientRegistrationId, Authentication principal,
HttpServletRequest request) {
return this.authorizedClientService.loadAuthorizedClient(clientRegistrationId, new CustomApplicaitonAuthentication());
}
#Override
public void saveAuthorizedClient(OAuth2AuthorizedClient authorizedClient, Authentication principal,
HttpServletRequest request, HttpServletResponse response) {
this.authorizedClientService.saveAuthorizedClient(authorizedClient, new CustomApplicaitonAuthentication());
}
But I think that both my solutions are not optimal. Is there another way how to do it more straightforward?
I found solution because I needed to run it without a servlet request (messaging, scheduler etc.). Therefore I needed to update it and use the AuthorizedClientServiceOAuth2AuthorizedClientManager instead of the default DefaultOAuth2AuthorizedClientManager. Then I reallized that it does the same thing like I need.
#Bean
WebClient buildWebClient(
OAuth2AuthorizedClientManager oAuth2AuthorizedClientManager,
OAuth2AuthorizationFailureHandler oAuth2AuthorizationFailureHandler,
String clientName) {
final ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = new ServletOAuth2AuthorizedClientExchangeFilterFunction(
oAuth2AuthorizedClientManager);
oauth2Client.setDefaultClientRegistrationId(clientName);
oauth2Client.setAuthorizationFailureHandler(oAuth2AuthorizationFailureHandler);
return WebClient.builder()
.apply(oauth2Client.oauth2Configuration())
.build();
}
#Bean
public OAuth2AuthorizedClientManager authorizedClientManager
(ClientRegistrationRepository clients, OAuth2AuthorizedClientService service, OAuth2AuthorizationFailureHandler authorizationFailureHandler) {
AuthorizedClientServiceOAuth2AuthorizedClientManager manager =
new AuthorizedClientServiceOAuth2AuthorizedClientManager(clients, service);
manager.setAuthorizationFailureHandler(authorizationFailureHandler);
return manager;
}
#Bean
public OAuth2AuthorizationFailureHandler resourceServerAuthorizationFailureHandler(
OAuth2AuthorizedClientService authorizedClientService) {
return new RemoveAuthorizedClientOAuth2AuthorizationFailureHandler(
(clientRegistrationId, principal, attributes) ->
authorizedClientService.removeAuthorizedClient(clientRegistrationId, principal.getName()));
}
We had to add a custom OAuth2AuthorizationFailureHandler because if you do not use this constructor ServletOAuth2AuthorizedClientExchangeFilterFunction(ClientRegistrationRepository clientRegistrationRepository, OAuth2AuthorizedClientRepository authorizedClientRepository) and you use a custom OAuth2AuthorizedClientManager then it is not configured and you have to do it manually.
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 creating a RESTful service that authenticates all incoming requests using the OAuth2 mechanism with an external Keycloak User Authentication Server (UAA).
The service acts as a Resource Server using the #EnableResourceServer with the following configuration:
#Configuration
#EnableResourceServer
#Order(0)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final ResourceServerTokenServices resourceServerTokenServices;
#Autowired
public SecurityConfig(ResourceServerTokenServices resourceServerTokenServices) {
this.resourceServerTokenServices = resourceServerTokenServices;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/actuator/health").permitAll()
.anyRequest().authenticated().and().addFilterAfter(oAuth2AuthenticationProcessingFilter(), AbstractPreAuthenticatedProcessingFilter.class);
}
private OAuth2AuthenticationProcessingFilter oAuth2AuthenticationProcessingFilter() {
OAuth2AuthenticationProcessingFilter oAuth2AuthenticationProcessingFilter = new OAuth2AuthenticationProcessingFilter();
oAuth2AuthenticationProcessingFilter.setAuthenticationManager(oauthAuthenticationManager());
oAuth2AuthenticationProcessingFilter.setStateless(false);
return oAuth2AuthenticationProcessingFilter;
}
private AuthenticationManager oauthAuthenticationManager() {
OAuth2AuthenticationManager oAuth2AuthenticationManager = new OAuth2AuthenticationManager();
oAuth2AuthenticationManager.setResourceId("country-microservice");
oAuth2AuthenticationManager.setTokenServices(resourceServerTokenServices);
oAuth2AuthenticationManager.setClientDetailsService(null);
return oAuth2AuthenticationManager;
}
}
I'm also using the following dependencies to include the Spring Security OAuth2:
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
</dependencies>
The users authenticate themselves on the UAA to obtain a JWT token that they must use to call the service that I'm creating. The JWT token itself contains the user information:
{
...
"realm_access": {
"roles": [
"user"
]
},
"scope": "profile email",
"email_verified": true,
"name": "Test Derp",
"preferred_username": "user1",
"given_name": "Test",
"family_name": "Derp",
"email": "test#test.com"
}
To avoid making another request to the UAA, the service uses the JWK to validate the incoming token. I'm setting the security.oauth2.resource.jwk.key-set-uri property using the Keycloak's Certificate Endpoint:
security.oauth2.resource.jwk.key-set-uri=http://localhost:9080/auth/realms/dev/protocol/openid-connect/certs
The problem is that Spring is not getting the user information that is found on the JWT token and fill it in the Authentication object.
I have the following controller to return the principal information:
#RestController
#RequestMapping(path = "/user", produces = MediaType.APPLICATION_JSON_VALUE)
public class UserController {
#GetMapping
public Object getUser(Authentication authentication) {
if (authentication != null) {
return authentication.getPrincipal();
}
return null;
}
}
The Authentication object is passed with null in the getUser function (with the JWK validation).
I've tried to use the following configuration to customize the JWKTokenStore with a JWTAccessTokenConverter, but it didn't work:
#Configuration
public class JwkStoreConfig {
private final ResourceServerProperties resource;
#Autowired
public JwkStoreConfig(ResourceServerProperties resource) {
this.resource = resource;
}
#Bean
#Primary
public JwtAccessTokenConverter accessTokenConverter() {
return new JwtAccessTokenConverter();
}
#Bean
public DefaultTokenServices jwkTokenServices(TokenStore jwkTokenStore) {
DefaultTokenServices services = new DefaultTokenServices();
services.setTokenStore(jwkTokenStore);
return services;
}
#Bean
public TokenStore jwkTokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) {
JwkTokenStore jwkTokenStore = new JwkTokenStore(this.resource.getJwk().getKeySetUri(), jwtAccessTokenConverter);
return jwkTokenStore;
}
}
The only solution that worked until now is to forget the usage of JWK and change the service to use the Keycloak's UserInfo to validate the incoming token, using the security.oauth2.resource.user-info-uri property and delete JWK URI property:
security.oauth2.resource.user-info-uri=http://localhost:9080/auth/realms/dev/protocol/openid-connect/userinfo
With this property set, the Authentication object is passed to the controller with the user information, but this makes the service to request the UAA everytime it needs to validate the incoming tokens.
Any ideas?
Thank you.
Regards.
The documentation for this is here, although to me it's not entirely clear and at least with my setup it doesn't work as it expected.
My understanding is that these two properties should be used together:
security.oauth2.resource.jwt.key-uri: http://localhost:9191/auth/realms/master
security.oauth2.resource.jwk.key-set-uri: http://localhost:9191/auth/realms/master/protocol/openid-connect/certs
That fails for me though, with a somewhat uninformative log message:
Authentication request failed: error="invalid_token", error_description="Cannot convert access token to JSON"
Debugging into Spring Security code a bit, it looks like if for what ever reason it could not internally resolve/create the public key for verifying the signature, it will give the error above.
This worked for me...
If you set the property security.oauth2.resource.jwt.key-value to the actual public key then it does verify, as well as unpack the user info from the JWT, without needing to call back to(or configure) the user info endpoint. The down side to that is the public key has to be copied into code and updated everywhere if it changes.
Note the value of this property has to include -----BEGIN PUBLIC KEY-----\n at the start and \n-----END PUBLIC KEY-----.
E.g.
security.oauth2.resource.jwt.key-value: "-----BEGIN PUBLIC KEY-----\nMIIB......\n-----END PUBLIC KEY-----"
I haven't tried, but I'm pretty sure that would also work with actual line breaks in YAML with:
security.oauth2.resource.jwt.key-value: |
-----BEGIN PUBLIC KEY-----
...
-----END PUBLIC KEY-----
I think the reason why the fist set of properties don't work for my set up is because the key returned from the key-uri doesn't have these begin and end segments and Spring Security expects them to be there.
This probably doesn't answer your question directly, but hopefully still helpful. Spring security logging was quite sparse, and I found those properties to be really fiddly in how much they changed the behavior of things without useful much logging info indicating what's going on, even on TRACE level.
We want to setup a microservice which provides a REST API so it is configured as a OAuth2 resource server. This service should also act as a OAuth2 client with the client credential grant. Here is the configuration:
spring.oauth2.client.id=clientCredentialsResource
spring.oauth2.client.accessTokenUri=http://localhost:9003/oauth/token
spring.oauth2.client.userAuthorizationUri=http://localhost:9003/oauth/authorize
spring.oauth2.client.grantType=client_credentials
spring.oauth2.client.clientId=<service-id>
spring.oauth2.client.clientSecret=<service-pw>
The resource server part works fine. For the client part we want to use Feign, Ribbon and Eureka:
#FeignClient("user")
public interface UserClient
{
#RequestMapping( method = RequestMethod.GET, value = "/user/{uid}")
Map<String, String> getUser(#PathVariable("uid") String uid);
}
Based on the gist in issue https://github.com/spring-cloud/spring-cloud-security/issues/56 I created a feign request intercepter which sets the access token from the autowired OAuth2RestOperations template in the feign request header
#Autowired
private OAuth2RestOperations restTemplate;
template.header(headerName, String.format("%s %s", tokenTypeName, restTemplate.getAccessToken().toString()));
But this gives me the error on calling the user service:
error="access_denied", error_description="Unable to obtain a new access token for resource 'clientCredentialsResource'. The provider manager is not configured to support it.
As I can see the OAuth2ClientAutoConfiguration creates always an instance of AuthorizationCodeResourceDetails for an web application but not the required ClientCredentialsResourceDetails which is only used for non-web applications. In the end the no access token privider is responsible for the resource details and the call failed in
AccessTokenProviderChain.obtainNewAccessTokenInternal(AccessTokenProviderChain.java:146)
I tried to overwrite the auto configuration but failed. Can somebody please give me a hint how to do it?
To switch off this piece of autoconfiguration you can set spring.oauth2.client.clientId= (empty), (per the source code), otherwise you have to "exclude" it in the #EnableAutoConfiguration. If you do that you can just set up your own OAuth2RestTemplate and fill in the "real" client ID from your own configuration, e.g.
#Configuration
#EnableOAuth2Client
public class MyConfiguration {
#Value("myClientId")
String myClientId;
#Bean
#ConfigurationProperties("spring.oauth2.client")
#Primary
public ClientCredentialsResourceDetails oauth2RemoteResource() {
ClientCredentialsResourceDetails details = new ClientCredentialsResourceDetails();
details.setClientId(myClientId);
return details;
}
#Bean
public OAuth2ClientContext oauth2ClientContext() {
return new DefaultOAuth2ClientContext(new DefaultAccessTokenRequest());
}
#Bean
#Primary
public OAuth2RestTemplate oauth2RestTemplate(
OAuth2ClientContext oauth2ClientContext,
OAuth2ProtectedResourceDetails details) {
OAuth2RestTemplate template = new OAuth2RestTemplate(details,
oauth2ClientContext);
return template;
}
}