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);
}
Related
I have existing Spring Boot application with Spring Security 5 and OAuth2 client, I've successfully configured authentication with external OAuth2 provider (GitLab, in my case).
Now I have problem with configuring authorization. I'd like to have some method that would let me write code to resolve roles for given user (either by making a call to the database or just checking hard-coded username).
I've found out that it could be achieved by using PrincipalExtractor and AuthoritiesExtractor, described in a nice article. However, those classes are no longer present in recent Spring Security. What is an alternative way to achieve that, compatible with Spring Security 5?
What you are looking for is called GrantedAuthoritiesMapper
it is documented here in the official spring security documentation
And here is a code example:
#EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.oauth2Login(oauth2 -> oauth2
.userInfoEndpoint(userInfo -> userInfo
.userAuthoritiesMapper(this.userAuthoritiesMapper())
...
)
);
}
private GrantedAuthoritiesMapper userAuthoritiesMapper() {
return (authorities) -> {
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
authorities.forEach(authority -> {
if (OidcUserAuthority.class.isInstance(authority)) {
OidcUserAuthority oidcUserAuthority = (OidcUserAuthority)authority;
OidcIdToken idToken = oidcUserAuthority.getIdToken();
OidcUserInfo userInfo = oidcUserAuthority.getUserInfo();
// Map the claims found in idToken and/or userInfo
// to one or more GrantedAuthority's and add it to mappedAuthorities
} else if (OAuth2UserAuthority.class.isInstance(authority)) {
OAuth2UserAuthority oauth2UserAuthority = (OAuth2UserAuthority)authority;
Map<String, Object> userAttributes = oauth2UserAuthority.getAttributes();
// Map the attributes found in userAttributes
// to one or more GrantedAuthority's and add it to mappedAuthorities
}
});
return mappedAuthorities;
};
}
}
There are several more examples and explanations in the spring security documentation.
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 just created a basic layer to secure my Spring API.
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Bean
#Override
public UserDetailsService userDetailsService() {
List<UserDetails> users = new ArrayList<>();
users.add(User.withDefaultPasswordEncoder().username("Admin").password("xxx!!").roles("ADMIN").build());
users.add(User.withDefaultPasswordEncoder().username("Pharmacist").password("xxx!!").roles("PHARMACIST").build());
users.add(User.withDefaultPasswordEncoder().username("Office").password("xxx!!").roles("OFFICE").build());
return new InMemoryUserDetailsManager(users);
}
}
I am using Postman to test my endpoints and it's working via the Authorization function of Postman.
What if I want to use this parameters (username,password,role) in my frontend?
I mean, that I am trying somehow to see how spring security passes the values to the url: http://localhost:8080 but i cant find out how. I need this to be able to handle in my Frontend with these vaules.
I am really new in this Spring Security Chapter.
Introduction:
I have just started using spring boot. For understanding how it works I have tried to convert my existing project (spring MVC, JSP in frontend) to spring boot approach with REST-controller and AngularJS in frontend.
Facing problem:
During migration I have faced to big problem with security. As I understood the best way for having good security layer now is working with JWT tokens and supporting oauth2, on which there are a lot of posts/tutorials which give different information even about basics of the security layer architecture.
So the question is:
Could someone point out full list of security-layer parts/classes which are needed for having basic (but not hello world) security features for spring boot app with REST controllers. Please don't suggest to use stormpath: I want to implement it myself to get better understanding.
Reasoning of asking this big question here:
I have done my own investigation on this topic, but I thought that most of the links which I have checked contain a lot of bad practices, so possible incorrect architecture of security layer. so I really would like to know some kind of good practice of designing architecture of security layer.
Details on needed features:
I have standard list of features which I want to support.
oauth2 support (but also to have possibility to authenticate without it)
register request (creation of jwt token and returning to client)
login request (acquiring jwt token if user was registered)
logout request (releasing jwt token)
token timeout
multiple roles
business rest controllers which checks for authentication and authorization (could you please give an example portion of code)
business rest controllers which doesn't require security
basic filtering http urls (like excluding "statics" from allowed url addresses)
Current layers of the project:
Below are some additional information about my current project structure:
Currently I have implemented the following modules:
controller: Currently MVC controllers, but I am going to convert them to REST
dto: Possibly will be changed a little bit, because of REST approach
model: Will stay unchanged after conversation
exception: For business logic
repository: Will stay unchanged after conversation
service: Possibly will be changed a little bit, because of micro-services
validator: Will stay unchanged after conversation
other business logic modules
If I understood correctly I will need to add two additional layers here:
configuration: I have already converted some xml configurators to java-configs, but haven't touched security configurators
security: I guest here will be placed authentication/authorization managers/tools. One of the goals of this question is to understand what exactly to place here.
app class with main method in the root package (relative root)
You can start by creating 3 projects.
Auth Server: This will take care of authenticating clients and users, issuing token, revoking token etc.
Rest API: All rest controllers, business logic, persistence layer etc.
Front-end: Angular JS, HTML, CSS etc.
Read about OAuth2 grant types.
We use password authorization grant type when authorization server and client is developed by same organization, or when there is a high degree of trust between the resource owner and the client.
Following are the essential classes you'd need for OAuth2 implementation:
A class which extends AuthorizationServerConfigurerAdapter to configure authorization server.
Here you can configure endpoints like userDetailsService (custom class to load user data by username from database), tokenStore (to store tokens in database and perform operations on it), clientDetailsService (load client details from database; your Rest API project could be client).
#Override
public void configure(final AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager);
endpoints.userDetailsService(userDetailsService);
endpoints.tokenStore(tokenStore);
endpoints.setClientDetailsService(clientDetailsService);
endpoints.accessTokenConverter(accessTokenConverter);
}
#Override
public void configure(final AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
//The expression below easies access to /oauth/check_token endpoint from the default denyAll to isAuthenticated.
oauthServer.checkTokenAccess("isAuthenticated()");
oauthServer.allowFormAuthenticationForClients();
oauthServer.passwordEncoder(passwordEncoder);
}
#Override
public void configure(final ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(clientDetailsService);
}
A class which extends ResourceServerConfigurerAdapter. Here you can configure security configuration for the resource server. Resources would be Rest controllers defined in Auth Servers (like controllers for performing CRUD operation on a user object, endpoint to revoke token; controllers which need to be in Auth Server).
#Override
public void configure(final HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().fullyAuthenticated(); //To restrict all http requests.
/*http.authorizeRequests().antMatchers("/users/**").permitAll(); //Notice ant matcher here, this tells endpoints which do not require authentication. Lots of http configuration options (like applying filters, cors, csrf etc.) are available here. Please explore*/
}
Check out TokenStore default implementation classes (like JdbcTokenStore, JwtTokenStore). If you'd like to use NoSQL db like Cassandra then provide custom TokenStore implementation.
Following is the sample code snippet for custom Token Store used for Cassandra:
#Override
public void storeAccessToken(final OAuth2AccessToken token, final OAuth2Authentication authentication) {
String refreshToken = null;
if (token.getRefreshToken() != null) {
refreshToken = token.getRefreshToken().getValue();
}
if (readAccessToken(token.getValue()) != null) {
removeAccessToken(token.getValue());
}
final AccessTokenBuilder accessTokenBuilder = new AccessTokenBuilder();
accessTokenRepository.save(accessTokenBuilder
.withAuthenticationId(authenticationKeyGenerator.extractKey(authentication))
.withTokenId(extractTokenKey(token.getValue()))
.withTokenBody(ByteBuffer.wrap(serializeAccessToken(token)))
.withUsername(authentication.getName())
.withClientId(authentication.getOAuth2Request().getClientId())
.withAuthentication(ByteBuffer.wrap(serializeAuthentication(authentication)))
.withRefreshTokenId(extractTokenKey(refreshToken))
.build());
}
#Override
public void storeRefreshToken(final OAuth2RefreshToken refreshToken, final OAuth2Authentication authentication) {
final RefreshTokenBuilder refreshTokenBuilder = new RefreshTokenBuilder();
refreshTokenRepository.save(refreshTokenBuilder
.withTokenId(extractTokenKey(refreshToken.getValue()))
.withTokenBody(ByteBuffer.wrap(serializeRefreshToken(refreshToken)))
.withAuthentication(ByteBuffer.wrap(serializeAuthentication(authentication)))
.build());
}
#Override
public OAuth2Authentication readAuthentication(final OAuth2AccessToken token) {
return readAuthentication(token.getValue());
}
#Override
public OAuth2Authentication readAuthentication(final String token) {
OAuth2Authentication authentication = null;
try {
final AccessToken authAccessToken = accessTokenRepository.findByTokenId(extractTokenKey(token));
authentication = deserializeAuthentication(authAccessToken.getAuthentication().array());
} catch (final IllegalArgumentException e) {
removeAccessToken(token);
}
return authentication;
}
#Override
public OAuth2AccessToken readAccessToken(final String tokenValue) {
final AccessToken accessToken = accessTokenRepository.findByTokenId(extractTokenKey(tokenValue));
return accessToken != null ? deserializeAccessToken(accessToken.getTokenBody().array()) : null;
}
#Override
public OAuth2RefreshToken readRefreshToken(final String tokenValue) {
final RefreshToken refreshToken = refreshTokenRepository.findOne(extractTokenKey(tokenValue));
return refreshToken != null ? deserializeRefreshToken(refreshToken.getTokenBody().array()) : null;
}
#Override
public OAuth2Authentication readAuthenticationForRefreshToken(final OAuth2RefreshToken token) {
return readAuthenticationForRefreshToken(token.getValue());
}
OAuth2Authentication readAuthenticationForRefreshToken(final String tokenValue) {
final RefreshToken refreshToken = refreshTokenRepository.findOne(extractTokenKey(tokenValue));
return refreshToken != null ? deserializeAuthentication(refreshToken.getAuthentication().array()) : null;
}
#Override
public OAuth2AccessToken getAccessToken(final OAuth2Authentication authentication) {
OAuth2AccessToken oAuth2AccessToken = null;
final String key = authenticationKeyGenerator.extractKey(authentication);
final AccessToken accessToken = accessTokenRepository.findOne(key);
if (accessToken != null) {
oAuth2AccessToken = deserializeAccessToken(accessToken.getTokenBody().array());
if (oAuth2AccessToken != null && !key.equals(authenticationKeyGenerator.extractKey(readAuthentication(oAuth2AccessToken.getValue())))) {
removeAccessToken(oAuth2AccessToken.getValue());
storeAccessToken(oAuth2AccessToken, authentication);
}
}
return oAuth2AccessToken;
}
You'd need to declare repository interfaces for db operations. Interfaces which extends CrudRepository. For most of the DB operations we don't need to provide implementation, it is handled by Spring. For Cassandra implementation is in SimpleCassandraRepository class. Sample code for Access Token:
public interface AccessTokenRepository extends CrudRepository<AccessToken, String> {
#Query("SELECT * FROM auth_service.oauth_access_token WHERE token_id = :tokenId ALLOW FILTERING")
AccessToken findByTokenId(#Param("tokenId") String tokenId);
}
Sample code for ClientDetails
public interface ClientDetailsRepository extends CrudRepository<ClientDetails, String> {
}
Please note, we don't need to provide implementation for these interfaces. Regular CRUD queries are already implemented and taken care by Spring.
public interface RefreshTokenRepository extends CrudRepository<RefreshToken, String> {
}
Rest API project
Controllers declared here would get called when request received from frontend (AJAX request from javascript). All business logic and persistence layer would go here.
Here you can think about creating a module, a gateway, which talks to Auth Server. This gateway would be between your Rest API and Auth Server.
You can use RestTemplate to call remote Rest service.
If you need that not any Rest API project can make remote calls to Auth Server, then user client_credentials as well along with password grant type. And, use OAuth2RestTemplate instead of RestTemplate. Sample code:
<bean id="oAuth2RestTemplate" class="org.springframework.security.oauth2.client.OAuth2RestTemplate">
<constructor-arg ref="clientCredentialsResourceDetails"/>
<constructor-arg ref="defaultOAuth2ClientContext"/>
<property name="requestFactory" ref="httpComponentsClientHttpRequestFactory"/>
</bean>
<bean id="httpComponentsClientHttpRequestFactory" class="org.springframework.http.client.HttpComponentsClientHttpRequestFactory">
<constructor-arg ref="selfSignedHttpsClientFactory"/>
</bean>
<bean id="clientCredentialsResourceDetails" class="org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails">
<property name="accessTokenUri" value="${authentication.service.client.token.url:https://localhost:8443/oauth/token}"/>
<property name="clientId" value="${authentication.service.client.id:testClient}"/>
<property name="clientSecret" value="${authentication.service.client.secret:password}"/>
</bean>
<bean id="defaultOAuth2ClientContext" class="org.springframework.security.oauth2.client.DefaultOAuth2ClientContext"/>
I hope this was helpful.
Not sure if you have seen this, but here is a nice article:
https://www.toptal.com/java/rest-security-with-jwt-spring-security-and-java
. And a project on github, more or less based on that article:
https://github.com/szerhusenBC/jwt-spring-security-demo
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?