I'm working on modernizing monolithic application to be microservice based application supporting multi tenancy using Spring boot, Keycloak 17, the configuration is Keycloak configuration file depending on the path referring to this example
For me it it working, and can load the deployments from json, login, below is the url for the application and I'm parsing branch1 after "tenant" without issues
http://localhost:8100/tenant/branch1/
The main issue is rendering css and JS files which is containing tenant name knwoing that I'm using sing WAR
with multiple realms
http://localhost:8100/tenant/branch1/resources/bootstrap/js/bootstrap.min.js --> return 404 which is not exist
Actual code for including static contents
in The jsp files I'm reading css/js files as before <link rel="stylesheet" href="resources/bootstrap/css/bootstrap.min.css">
keycloal json file example
{"realm": "branch1",
"auth-server-url": "http://localhost:8181/",
"ssl-required": "external",
"resource": "app",
"public-client": true,
"confidential-port": 0,
"principal-attribute": "preferred_username"}
Please advise
rendering static content
is there any guidance after authentication to return one URL without tenant/branch1 specially I'm using CurrentTenantIdentifierResolver inside my application
#ConditionalOnProperty(prefix = "keycloak.config", name = "resolver", havingValue = "path")
public class PathBasedConfigResolver implements KeycloakConfigResolver {
private final ConcurrentHashMap<String, KeycloakDeployment> cache = new ConcurrentHashMap<>();
#SuppressWarnings("unused")
private static AdapterConfig adapterConfig;
#Override
public KeycloakDeployment resolve(OIDCHttpFacade.Request request) {
System.out.println("inside resolve :: ");
String realm = SubdomainUtils.obtainTenantFromSubdomain(request.getURI());
if (realm.contains("?")) {
realm = realm.split("\\?")[0];
}
if (!cache.containsKey(realm)) {
InputStream is = this.getClass().getResourceAsStream("/" + realm + "-keycloak.json");
cache.put(realm, KeycloakDeploymentBuilder.build(is));
}
return cache.get(realm);
}
static void setAdapterConfig(AdapterConfig adapterConfig) {
PathBasedConfigResolver.adapterConfig = adapterConfig;
}
}
public class SpringKeycloakSecurityConfiguration {
#DependsOn("keycloakConfigResolver")
#KeycloakConfiguration
#ConditionalOnProperty(name = "keycloak.enabled", havingValue = "true", matchIfMissing = true)
public static class KeycloakConfigurationAdapter extends KeycloakWebSecurityConfigurerAdapter {
/**
* Registers the KeycloakAuthenticationProvider with the authentication manager.
*/
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
SimpleAuthorityMapper soa = new SimpleAuthorityMapper();
keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(soa);
auth.authenticationProvider(keycloakAuthenticationProvider);
}
/**
* Defines the session authentication strategy.
*/
#Bean
#Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
// required for bearer-only applications.
// return new NullAuthenticatedSessionStrategy();
return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
}
#Override
protected AuthenticationEntryPoint authenticationEntryPoint() throws Exception {
return new MultitenantKeycloakAuthenticationEntryPoint(adapterDeploymentContext());
}
#Override
protected KeycloakAuthenticationProcessingFilter keycloakAuthenticationProcessingFilter() throws Exception {
KeycloakAuthenticationProcessingFilter filter = new KeycloakAuthenticationProcessingFilter(
authenticationManager(), new AntPathRequestMatcher("/tenant/*/sso/login"));
filter.setSessionAuthenticationStrategy(sessionAuthenticationStrategy());
return filter;
}
#SuppressWarnings({ "rawtypes", "unchecked" })
#Bean
public FilterRegistrationBean keycloakAuthenticationProcessingFilterRegistrationBean(
KeycloakAuthenticationProcessingFilter filter) {
FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
registrationBean.setEnabled(false);
return registrationBean;
}
#SuppressWarnings({ "rawtypes", "unchecked" })
#Bean
public FilterRegistrationBean keycloakPreAuthActionsFilterRegistrationBean(
KeycloakPreAuthActionsFilter filter) {
FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
registrationBean.setEnabled(false);
return registrationBean;
}
#SuppressWarnings({ "rawtypes", "unchecked" })
#Bean
public FilterRegistrationBean keycloakAuthenticatedActionsFilterBean(
KeycloakAuthenticatedActionsFilter filter) {
FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
registrationBean.setEnabled(false);
return registrationBean;
}
#SuppressWarnings({ "rawtypes", "unchecked" })
#Bean
public FilterRegistrationBean keycloakSecurityContextRequestFilterBean(
KeycloakSecurityContextRequestFilter filter) {
FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
registrationBean.setEnabled(false);
return registrationBean;
}
#Bean
#Override
#ConditionalOnMissingBean(HttpSessionManager.class)
protected HttpSessionManager httpSessionManager() {
return new HttpSessionManager();
}
/**
* Configuration spécifique à keycloak (ajouts de filtres, etc)
*
* #param http
* #throws Exception
*/
#Override
protected void configure(HttpSecurity http) throws Exception {
http.sessionManagement()
// use previously declared bean
.sessionAuthenticationStrategy(sessionAuthenticationStrategy())
// keycloak filters for securisation
.and().addFilterBefore(keycloakPreAuthActionsFilter(), LogoutFilter.class)
.addFilterBefore(keycloakAuthenticationProcessingFilter(), X509AuthenticationFilter.class)
.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint())
.and().logout().addLogoutHandler(keycloakLogoutHandler()).logoutUrl("/tenant/*/logout")
.logoutSuccessHandler(
// logout handler for API
(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) -> response.setStatus(HttpServletResponse.SC_OK))
.and().authorizeRequests().antMatchers("mobileservlet/**").permitAll().antMatchers("**/favicon.ico")
.permitAll().antMatchers("/error").permitAll().antMatchers("/login.go").permitAll()
.antMatchers("/resources/*").permitAll().anyRequest().authenticated();
}
#Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList(HttpMethod.OPTIONS.name(), "GET", "POST"));
configuration.setAllowedHeaders(
Arrays.asList("Access-Control-Allow-Headers", "Access-Control-Allow-Origin", "Authorization"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
}
public class MultitenantKeycloakAuthenticationEntryPoint extends KeycloakAuthenticationEntryPoint {
public MultitenantKeycloakAuthenticationEntryPoint(AdapterDeploymentContext adapterDeploymentContext) {
super(adapterDeploymentContext);
}
public MultitenantKeycloakAuthenticationEntryPoint(AdapterDeploymentContext adapterDeploymentContext, RequestMatcher apiRequestMatcher) {
super(adapterDeploymentContext, apiRequestMatcher);
}
#Override
protected void commenceLoginRedirect(HttpServletRequest request, HttpServletResponse response) throws IOException {
System.out.println("inside commenceLoginRedirect :: ");
String path = request.getRequestURI();
int multitenantIndex = path.indexOf("tenant/");
if (multitenantIndex == -1) {
throw new IllegalStateException("Not able to resolve the realm from the request path!");
}
String realm = path.substring(path.indexOf("tenant/")).split("/")[1];
if (realm.contains("?")) {
realm = realm.split("\\?")[0];
}
String contextAwareLoginUri = request.getContextPath() + "/tenant/" + realm + DEFAULT_LOGIN_URI;
response.sendRedirect(contextAwareLoginUri);
}
}
Bad news, the Keycloak adapters for spring you are using are very deprecated. Don't use it.
Better news, I host spring-boot starters for resource-servers which support multi-tenancy: accept identities issued by more than just one issuer (as many realms as you need in your case) and retrieve "roles" from realms and clients with the mapping you want (control case and prefix). It also enables you to configure "public" routes and CORS configuration from preperties file (plus a few more things).
Configuration for realm1 and other-realm both used by two clients (some client and other-client) is as simple as:
<dependency>
<groupId>com.c4-soft.springaddons</groupId>
<!-- replace "webflux" with "webmvc" if your app is a servlet -->
<!-- replace "jwt" with "introspecting" to use token introspection instead of JWT decoding -->
<artifactId>spring-addons-webflux-jwt-resource-server</artifactId>
<!-- this version is to be used with spring-boot 3.0.0-RC2, use 5.x for spring-boot 2.6.x or before -->
<version>6.0.5</version>
</dependency>
#EnableMethodSecurity
public static class WebSecurityConfig { }
com.c4-soft.springaddons.security.issuers[0].location=https://localhost:8443/realms/realm1
com.c4-soft.springaddons.security.issuers[0].authorities.claims=realm_access.roles,ressource_access.some-client.roles,ressource_access.other-client.roles
com.c4-soft.springaddons.security.issuers[1].location=https://localhost:8443/realms/other-realm
com.c4-soft.springaddons.security.issuers[1].authorities.claims=realm_access.roles,ressource_access.some-client.roles,ressource_access.other-client.roles
com.c4-soft.springaddons.security.cors[0].path=/some-api
Related
I've been trying to set up a Keycloak locally with docker to be able to login to our application with SAML 2.0.
Versions used:
Keyloak 19.0.3
Spring Boot 2.7.3
When I call an REST endpoint of the application, I am correctly redirected to Keycloak (redirect to http://localhost:8085/realms/PocRealm/protocol/saml). But I don't get a login form but the message: "We are sorry ... Invalid Request" .
I then see the following entry in the Docker console logs.
2022-10-07 12:22:41,972 WARN [org.keycloak.events] (executor-thread-104) type=LOGIN_ERROR, realmId=e026f301-c74b-4247-bb0a-58cdb651ae00, clientId=null, userId=null, ipAddress=172.17.0.1, error=client_not_found, reason=Cannot_match_source_hash
These are my configurations:
application.properties
# Spring Server Settings
server.port=8081
#Keycloak Settings
keycloak.auth-server-url=http://localhost:8085
keycloak.realm=PocRealm
keycloak.resource=pocApp
keycloak.principal-attribute=preferred_username
#SAML Settings
spring.security.saml2.relyingparty.registration.keycloak.signing.credentials[0].private-key-location=classpath:credentials/myKey.key
spring.security.saml2.relyingparty.registration.keycloak.signing.credentials[0].certificate-location=classpath:credentials/myCert.crt
spring.security.saml2.relyingparty.registration.keycloak.assertingparty.metadata-uri=http://localhost:8085/realms/PocRealm/protocol/saml/descriptor
KeycloakConfig.java
#Configuration
public class KeycloakConfig {
#Bean
public KeycloakSpringBootConfigResolver keycloakConfigResolver() {
return new KeycloakSpringBootConfigResolver();
}
}
SecurityConfig.java
#KeycloakConfiguration
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter
{
#Autowired
public void configureGlobal(AuthenticationManagerBuilder authenticationManagerBuilder)
{
SimpleAuthorityMapper simpleAuthorityMapper = new SimpleAuthorityMapper();
simpleAuthorityMapper.setPrefix("ROLE_");
KeycloakAuthenticationProvider keycloakAuthenticationProvider =
keycloakAuthenticationProvider();
keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(simpleAuthorityMapper);
authenticationManagerBuilder.authenticationProvider(keycloakAuthenticationProvider);
}
#Bean
#Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy ()
{
return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
}
#Bean
#Override
#ConditionalOnMissingBean(HttpSessionManager.class)
protected HttpSessionManager httpSessionManager()
{
return new HttpSessionManager();
}
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception
{
OpenSaml4AuthenticationProvider authenticationProvider = new OpenSaml4AuthenticationProvider();
authenticationProvider.setResponseAuthenticationConverter(groupsConverter());
// #formatter:off
httpSecurity
.authorizeHttpRequests(authorize -> authorize
.mvcMatchers("/favicon.ico").permitAll()
.anyRequest().authenticated()
)
.saml2Login(saml2 -> saml2
.authenticationManager(new ProviderManager(authenticationProvider))
)
.saml2Logout(withDefaults());
// #formatter:on
}
private Converter<OpenSaml4AuthenticationProvider.ResponseToken, Saml2Authentication> groupsConverter() {
Converter<ResponseToken, Saml2Authentication> delegate =
OpenSaml4AuthenticationProvider.createDefaultResponseAuthenticationConverter();
return (responseToken) -> {
Saml2Authentication authentication = delegate.convert(responseToken);
Saml2AuthenticatedPrincipal principal = (Saml2AuthenticatedPrincipal) authentication.getPrincipal();
List<String> groups = principal.getAttribute("groups");
Set<GrantedAuthority> authorities = new HashSet<>();
if (groups != null) {
groups.stream().map(SimpleGrantedAuthority::new).forEach(authorities::add);
} else {
authorities.addAll(authentication.getAuthorities());
}
return new Saml2Authentication(principal, authentication.getSaml2Response(), authorities);
};
}
}
I don't see the problem and even after hours of research I can't get anywhere at this point and maybe someone here can help me? Maybe there is a better approach in general? (OpenID-Connect instead of SAML is unfortunately not an option)
Thank you
So I am using the code below to connect to our LDAP server in my spring boot app, I can authorized successfully using the userPrincipalName -> (LastName.FirstName#enterprise.com), but I want to use the assigned sAMAccountName instead (ID00001).
I messed around with the setSearchFilter by doing: provider.setSearchFilter("(sAMAccountName ={0})")
but it's not working. (I'm getting bad credentials)
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/myapplication")
.authorizeRequests()
.anyRequest().fullyAuthenticated()
.and()
.httpBasic();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(activeDirectoryLdapAuthenticationProvider());
}
#Bean
public AuthenticationManager authenticationManager() {
return new ProviderManager(Arrays.asList(activeDirectoryLdapAuthenticationProvider()));
}
#Bean
public AuthenticationProvider activeDirectoryLdapAuthenticationProvider() {
ActiveDirectoryLdapAuthenticationProvider provider = new ActiveDirectoryLdapAuthenticationProvider("", "ldap://test.enterprise.com","dc=ORG1, dc=ORG2");
provider.setConvertSubErrorCodesToExceptions(true);
provider.setUseAuthenticationRequestCredentials(true);
provider.setSearchFilter("(sAMAccountName={0})"); // THIS DOES NOT WORK
provider.setSearchFilter("(userPrincipalName={0})"); // THIS WORKS
return provider;
}
}
EDIT: OK turns out I have been using the wrong field, there is another field: sAMAccountName that has the same value that I should be using, updated the title and question contents.
EDIT:
Ok I tried:
provider.setSearchFilter("(&(objectClass=user)(cn={0}))");
provider.setSearchFilter("(&(objectClass=user)(sAMAccountName={0}))");
provider.setSearchFilter("(&(objectClass=user)(cn={1}))");
provider.setSearchFilter("(&(objectClass=user)(sAMAccountName={1}))");
and it's still the same error,
bad credentials, AcceptSecurityContextError, 52e v2580
I managed to solve my issue but instead of using spring ldap dependencies, I created a custom authentication provider that implements AuthenticationProvider and and used the following code to connect to ldap and validate the credentials:
autowire it to WebSecurityConfig class.
#Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
#Autowired
private UserDetails userDetails;
#Override
public Authentication authenticate(Authentication auth) throws
AuthenticationException {
String user = authentication.getName();
String pass = authentication.getCredentials.ToString();
try {
if (isRegistrered(user,pass) {
return new UsernamePasswordAuthenticationToken(user,pass, new
ArrayList<>());
} else {
return null;
}
} catch (Exception e) {
e.printStackTrace();
}
}
#Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
private isRegisterd(String user, String pass) {
boolean result = false;
try {
// Set up the environment for creating the initial context
Hashtable<String, String> env = new Hashtable<String, String>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, "ldap://ldap_server:389");
//
env.put(Context.SECURITY_AUTHENTICATION, "simple");
env.put(Context.SECURITY_PRINCIPAL, "DOMAIN\\USER");
env.put(Context.SECURITY_CREDENTIALS, "PASSWORD");
// Create the initial context
DirContext ctx = new InitialDirContext(env);
if(ctx != null) {
ctx.close();
}
return result;
} catch (Exception e) {
return result;
}
}
private void retrieveUserDetails(DirContext ctx, String username) throws NamingException {
String userSearchBase = "dc=TEST,dc=SAMPLE";
String userSearchFilter = String.format("sAMAccountName=%s", username);
//any attributes that you want
String[] attributes = { "sAMAccountName", "department" };
SearchControls controls = new SearchControls();
controls.setSearchScope(SearchControls.SUBTREE_SCOPE);
controls.setReturningAttributes(attributes);
SearchResult result = null;
NamingEnumeration<?> users = ctx.search(userSearchBase, userSearchFilter, controls);
while(users.hasmore()) {
result = (SearchResult) users.next();
Attributes attr = result.getAttribtutes();
String sAMAccountName = attr.get("sAMAccountName").get(0).toString();
String department = attr.get("department").get(0).toString();
//assign to autowired object to be accessed anywhere
this.userDetails.setAccountName(sAMAccountName);
this.userDetails.setDepartment(department);
}
}
}
There's another approach by using LdapAuthenticationProvider. I use LdapAuthenticationProvider with a single parameter constructor.
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
#Profile("integration")
public class SecurityConfigurationT extends WebSecurityConfigurerAdapter {
private final AuthenticationManagerBuilder authenticationManagerBuilder;
private final TokenProvider tokenProvider;
public SecurityConfigurationT(
AuthenticationManagerBuilder authenticationManagerBuilder,
TokenProvider tokenProvider) {
this.authenticationManagerBuilder = authenticationManagerBuilder;
this.tokenProvider = tokenProvider;
}
#PostConstruct
public void initIntegration() {
try {
authenticationManagerBuilder
.authenticationProvider(ldapAuthenticationProvider());
} catch (Exception e) {
throw new BeanInitializationException("Security configuration failed", e);
}
}
#Override
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Bean
public LdapAuthenticationProvider ldapAuthenticationProvider() throws Exception {
return new LdapAuthenticationProvider(ldapAuthenticator());
}
#Bean
public LdapContextSource ldapContextSource() throws Exception {
PasswordPolicyAwareContextSource contextSource = new PasswordPolicyAwareContextSource("ldaps://int.root.company.ag:636");
contextSource.setUserDn("CN=system_user,OU=companygroup svc accs,DC=int,DC=root,DC=company,DC=ag");
contextSource.setPassword("XXXXXX");
return contextSource;
}
// Use this for other filter such as "sAMAccountName".
#Bean
public LdapAuthenticator ldapAuthenticator() {
BindAuthenticator authenticator = new BindAuthenticator(ldapContextSource());
authenticator.setUserSearch(new FilterBasedLdapUserSearch("OU=company,OU=companygroup users,DC=int,DC=root,DC=company,DC=ag", "(sAMAccountName={0})", ldapContextSource()));
return authenticator;
}
}
Reference:
https://www.stevenschwenke.de/LDAPWithSpringSecurity
i am trying to send session id in response header in rest controller but excludepathpattern() seems not working
** the configuration class is not triggering **
i have tried changing the sevlet version but it didnt work
ContextListener
#Override
public void contextInitialized(ServletContextEvent sce) {
ServletContext context = sce.getServletContext();
Map<String, HttpSession> map = new HashMap<>();
context.setAttribute("activeUsers", map);
HttpSessionListener
ServletContext context = session.getServletContext();
Map<String, HttpSession> activeUsers = (Map<String, HttpSession>) context.getAttribute("activeUsers");
activeUsers.put(session.getId(), session);
HandlerInterceptor
ServletContext context = request.getServletContext();
Map<String, HttpSession> activeUsers = (Map<String, HttpSession>) context.getAttribute("activeUsers");
String sessionId = request.getHeader("sessionId");
String requestUrl = request.getRequestURL().toString();
if (requestUrl.contains("/getOtp") || requestUrl.contains("/validateOtp")) {
return true;
} else {
if (activeUsers.containsKey(sessionId)) {
return true;
} else {
response.setStatus(401);
return false;
}
}
interceptorconfigurartion by extendig websecurityconfigure
#Configuration
#EnableAutoConfiguration
public class SessionInterceptorConfig implements WebMvcConfigurer {
#Autowired
private SessionHanlderInterceptor sessionHandlerIntercepto;
#Override
public void addInterceptors(InterceptorRegistry registry) {
// List<String> paths = new ArrayList<String>();
// paths.add("/auth/*");
registry.addInterceptor(sessionHandlerIntercepto).excludePathPatterns("/auth/**");
}
#Bean
public ServletListenerRegistrationBean<CustomSessionListener> filterRegistrationBean() {
ServletListenerRegistrationBean<CustomSessionListener> registrationBean = new ServletListenerRegistrationBean<CustomSessionListener>();
CustomSessionListener customURLFilter = new CustomSessionListener();
registrationBean.setListener(customURLFilter);
registrationBean.setOrder(1); // set precedence
return registrationBean;
}
#Bean
public ServletListenerRegistrationBean<CustomServletContextListener> filterContextRregistration() {
ServletListenerRegistrationBean<CustomServletContextListener> registrationBean = new ServletListenerRegistrationBean<CustomServletContextListener>();
CustomServletContextListener customURLFilter = new CustomServletContextListener();
registrationBean.setListener(customURLFilter);
registrationBean.setOrder(1); // set precedence
return registrationBean;
}
Sprinboot main class
#SpringBootApplication
public class CustomerApplication extends SpringBootServletInitializer {
i expect to add the session id to header in response and to check for the sessionid in request
You can use spring web component "OncePerRequestFilter". You need to inject a bean which extends OncePerRequestFilter. Example:
public class CustomHeaderFilter extends OncePerRequestFilter {
#Override
public void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response,
final FilterChain chain) throws IOException, ServletException {
response.setHeader(customHeaderName, customHeaderValue);
chain.doFilter(request, response);
}
}
I have a problem with the following scenario: multiple back-end servers process SAML requests forwarded by a reverse-proxy.
I tried to configure the contextProvider as follow: https://docs.spring.io/spring-security-saml/docs/2.0.x/reference/htmlsingle/#configuration-load-balancing
This is my SAML configuration
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private static final Logger log = LoggerFactory.getLogger(OsiApplication.class);
private static final String PROTOCOL = "https";
private static final String BINDING = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST";
#Value("${saml.metadata-file}")
private String metadataFile;
#Value("${saml.idp-selection-path}")
private String idpSelectionPath;
#Value("${saml.openmind-app-id}")
private String openmindAppId;
#Value("${saml.success-redirect}")
private String successRedirect;
#Value("${saml.failure-redirect}")
private String failureRedirect;
#Value("${saml.lb-server-name}")
private String lbServerName;
#Value("${saml.lb-scheme}")
private String lbScheme;
#Value("${saml.lb-context-path}")
private String lbContextPath;
#Value("${saml.lb-server-port}")
private int lbServerPort;
#Value("${saml.lb-include-port}")
private boolean lbIncludePort;
#Value("${saml.store-path}")
private String storePath;
#Value("${saml.store-pass}")
private String storePass;
#Value("${saml.store-default-key}")
private String storeDefaultKey;
#Value("${saml.secured}")
private boolean secured;
#Value("${application.admin-code}")
private String adminCode;
#Value("${application.user-code}")
private String userCode;
#Autowired
private SAMLUserDetailsServiceImpl samlUserDetailsServiceImpl;
private Timer backgroundTaskTimer;
private MultiThreadedHttpConnectionManager multiThreadedHttpConnectionManager;
#PostConstruct
public void init() {
this.backgroundTaskTimer = new Timer(true);
this.multiThreadedHttpConnectionManager = new MultiThreadedHttpConnectionManager();
}
#PreDestroy
public void destroy() {
this.backgroundTaskTimer.purge();
this.backgroundTaskTimer.cancel();
this.multiThreadedHttpConnectionManager.shutdown();
}
// Initialization of the velocity engine
// XML parser pool needed for OpenSAML parsing
#Bean(initMethod = "initialize")
public StaticBasicParserPool parserPool() {
return new StaticBasicParserPool();
}
#Bean(name = "parserPoolHolder")
public ParserPoolHolder parserPoolHolder() {
return new ParserPoolHolder();
}
// Bindings, encoders and decoders used for creating and parsing messages
#Bean
public HttpClient httpClient() {
return new HttpClient(this.multiThreadedHttpConnectionManager);
}
// SAML Authentication Provider responsible for validating of received SAML
// messages
#Bean
public SAMLAuthenticationProvider samlAuthenticationProvider() {
SAMLAuthenticationProvider samlAuthenticationProvider = new SAMLAuthenticationProvider();
samlAuthenticationProvider.setUserDetails(samlUserDetailsServiceImpl);
samlAuthenticationProvider.setForcePrincipalAsString(false);
return samlAuthenticationProvider;
}
// Provider of default SAML Context
#Bean
public SAMLContextProviderLB contextProvider() {
// This error may occur during SP-initiated SSO. A SAML authn request is sent to the IdP and a
// SAML response is returned. We check that the InResponseTo field in the SAML response matches
// the ID field of the authn request.
// If they don't match then we throw the error you see
// FIX This can be done by setting DisableInResponseToCheck to true in the <PartnerIdentityProvider> entry in your saml.config.
SAMLContextProviderLB sAMLContextProviderImpl = new SAMLContextProviderLB();
// configuration of reverse proxy of saml
sAMLContextProviderImpl.setServerName(lbServerName);
sAMLContextProviderImpl.setScheme(lbScheme);
sAMLContextProviderImpl.setContextPath(lbContextPath);
sAMLContextProviderImpl.setServerPort(lbServerPort);
sAMLContextProviderImpl.setIncludeServerPortInRequestURL(false);
/*EmptyStorageFactory emptyStorageFactory = new EmptyStorageFactory();
sAMLContextProviderImpl.setStorageFactory(emptyStorageFactory);*/
return sAMLContextProviderImpl;
}
// Initialization of OpenSAML library
#Bean
public static SAMLBootstrap sAMLBootstrap() {
return new SAMLBootstrap();
}
// Logger for SAML messages and events
#Bean
public SAMLDefaultLogger samlLogger() {
return new SAMLDefaultLogger();
}
// SAML 2.0 WebSSO Assertion Consumer
#Bean
public WebSSOProfileConsumer webSSOprofileConsumer() {
WebSSOProfileConsumerImpl webSSOProfileConsumer = new WebSSOProfileConsumerImpl();
webSSOProfileConsumer.setMaxAuthenticationAge(28800);
return webSSOProfileConsumer;
}
// SAML 2.0 Holder-of-Key WebSSO Assertion Consumer
#Bean
public WebSSOProfileConsumerHoKImpl hokWebSSOprofileConsumer() {
return new WebSSOProfileConsumerHoKImpl();
}
// SAML 2.0 Web SSO profile
#Bean
public WebSSOProfile webSSOprofile() {
return new WebSSOProfileImpl();
}
// SAML 2.0 Holder-of-Key Web SSO profile
#Bean
public WebSSOProfileConsumerHoKImpl hokWebSSOProfile() {
return new WebSSOProfileConsumerHoKImpl();
}
// SAML 2.0 ECP profile
#Bean
public WebSSOProfileECPImpl ecpprofile() {
return new WebSSOProfileECPImpl();
}
#Bean
public SingleLogoutProfile logoutprofile() {
return new SingleLogoutProfileImpl();
}
// Central storage of cryptographic keys
#Bean
public KeyManager keyManager() {
DefaultResourceLoader loader = new DefaultResourceLoader();
Resource storeFile = loader
.getResource(storePath);
String defaultKey = storeDefaultKey;
Map<String, String> passwords = new HashMap<>();
passwords.put(defaultKey, storePass);
return new JKSKeyManager(storeFile, storePass, passwords, defaultKey);
}
// Setup TLS Socket Factory
#Bean
public TLSProtocolConfigurer tlsProtocolConfigurer() {
return new TLSProtocolConfigurer();
}
#Bean
public ProtocolSocketFactory socketFactory() {
return new TLSProtocolSocketFactory(keyManager(), null, "default");
}
#Bean
public Protocol socketFactoryProtocol() {
return new Protocol(PROTOCOL, socketFactory(), 443);
}
#Bean
public MethodInvokingFactoryBean socketFactoryInitialization() {
MethodInvokingFactoryBean methodInvokingFactoryBean = new MethodInvokingFactoryBean();
methodInvokingFactoryBean.setTargetClass(Protocol.class);
methodInvokingFactoryBean.setTargetMethod("registerProtocol");
Object[] args = { PROTOCOL, socketFactoryProtocol() };
methodInvokingFactoryBean.setArguments(args);
return methodInvokingFactoryBean;
}
#Bean
public WebSSOProfileOptions defaultWebSSOProfileOptions() {
WebSSOProfileOptions webSSOProfileOptions = new WebSSOProfileOptions();
webSSOProfileOptions.setIncludeScoping(false);
webSSOProfileOptions.setBinding(BINDING);
return webSSOProfileOptions;
}
// Entry point to initialize authentication, default values taken from
// properties file
#Bean
public SAMLEntryPoint samlEntryPoint() {
SAMLEntryPoint samlEntryPoint = new SAMLEntryPoint();
samlEntryPoint.setDefaultProfileOptions(defaultWebSSOProfileOptions());
return samlEntryPoint;
}
// Setup advanced info about metadata
#Bean
public ExtendedMetadata extendedMetadata() {
ExtendedMetadata extendedMetadata = new ExtendedMetadata();
extendedMetadata.setIdpDiscoveryEnabled(false);
extendedMetadata.setSignMetadata(true);
extendedMetadata.setEcpEnabled(false);
return extendedMetadata;
}
// IDP Discovery Service
#Bean
public SAMLDiscovery samlIDPDiscovery() {
SAMLDiscovery idpDiscovery = new SAMLDiscovery();
idpDiscovery.setIdpSelectionPath(idpSelectionPath);
return idpDiscovery;
}
#Bean
#Qualifier("idp-ssocircle")
public ExtendedMetadataDelegate ssoCircleExtendedMetadataProvider()
throws MetadataProviderException {
File file = null;
String metadata = metadataFile;
DefaultResourceLoader loader = new DefaultResourceLoader();
try {
file = loader.getResource(metadata).getFile();
} catch (IOException e) {
log.error("IOException => {}", e);
}
FilesystemMetadataProvider filesystemMetadataProvider = new FilesystemMetadataProvider(file);
filesystemMetadataProvider.setRequireValidMetadata(true);
filesystemMetadataProvider.setParserPool(new BasicParserPool());
filesystemMetadataProvider.initialize();
//
ExtendedMetadata extendedMetadata = new ExtendedMetadata();
extendedMetadata.setIdpDiscoveryEnabled(false);
extendedMetadata.setSignMetadata(true);
ExtendedMetadataDelegate extendedMetadataDelegate = new ExtendedMetadataDelegate(filesystemMetadataProvider, extendedMetadata());
extendedMetadataDelegate.setMetadataTrustCheck(false);
extendedMetadataDelegate.setMetadataRequireSignature(false);
return extendedMetadataDelegate;
}
// IDP Metadata configuration - paths to metadata of IDPs in circle of trust
// is here
// Do no forget to call initalize method on providers
#Bean
#Qualifier("metadata")
public CachingMetadataManager metadata() throws MetadataProviderException {
List<MetadataProvider> providers = new ArrayList<>();
providers.add(ssoCircleExtendedMetadataProvider());
return new CachingMetadataManager(providers);
}
// Filter automatically generates default SP metadata
#Bean
public MetadataGenerator metadataGenerator() {
MetadataGenerator metadataGenerator = new MetadataGenerator();
metadataGenerator.setEntityId(openmindAppId);
metadataGenerator.setExtendedMetadata(extendedMetadata());
metadataGenerator.setIncludeDiscoveryExtension(false);
metadataGenerator.setKeyManager(keyManager());
metadataGenerator.setEntityBaseURL(successRedirect);
return metadataGenerator;
}
// The filter is waiting for connections on URL suffixed with filterSuffix
// and presents SP metadata there
#Bean
public MetadataDisplayFilter metadataDisplayFilter() {
return new MetadataDisplayFilter();
}
// Handler deciding where to redirect user after successful login
#Bean
public SavedRequestAwareAuthenticationSuccessHandler successRedirectHandler() {
SavedRequestAwareAuthenticationSuccessHandler successRedirectHandler = new SavedRequestAwareAuthenticationSuccessHandler();
successRedirectHandler.setDefaultTargetUrl(successRedirect);
return successRedirectHandler;
}
// Handler deciding where to redirect user after failed login
#Bean
public SimpleUrlAuthenticationFailureHandler authenticationFailureHandler() {
SimpleUrlAuthenticationFailureHandler simpleUrlAuthenticationFailureHandler = new SimpleUrlAuthenticationFailureHandler();
simpleUrlAuthenticationFailureHandler.setDefaultFailureUrl(failureRedirect);
return simpleUrlAuthenticationFailureHandler;
}
#Bean
public SAMLWebSSOHoKProcessingFilter samlWebSSOHoKProcessingFilter() throws Exception {
SAMLWebSSOHoKProcessingFilter samlWebSSOHoKProcessingFilter = new SAMLWebSSOHoKProcessingFilter();
samlWebSSOHoKProcessingFilter.setAuthenticationSuccessHandler(successRedirectHandler());
samlWebSSOHoKProcessingFilter.setAuthenticationManager(authenticationManager());
samlWebSSOHoKProcessingFilter.setAuthenticationFailureHandler(authenticationFailureHandler());
return samlWebSSOHoKProcessingFilter;
}
// Processing filter for WebSSO profile messages
#Bean
public SAMLProcessingFilter samlWebSSOProcessingFilter() throws Exception {
SAMLProcessingFilter samlWebSSOProcessingFilter = new SAMLProcessingFilter();
samlWebSSOProcessingFilter.setAuthenticationManager(authenticationManager());
samlWebSSOProcessingFilter.setAuthenticationSuccessHandler(successRedirectHandler());
samlWebSSOProcessingFilter.setAuthenticationFailureHandler(authenticationFailureHandler());
return samlWebSSOProcessingFilter;
}
#Bean
public MetadataGeneratorFilter metadataGeneratorFilter() {
return new MetadataGeneratorFilter(metadataGenerator());
}
// Handler for successful logout
#Bean
public SimpleUrlLogoutSuccessHandler successLogoutHandler() {
SimpleUrlLogoutSuccessHandler successLogoutHandler = new SimpleUrlLogoutSuccessHandler();
successLogoutHandler.setDefaultTargetUrl(lbContextPath);
return successLogoutHandler;
}
// Logout handler terminating local session
#Bean
public SecurityContextLogoutHandler logoutHandler() {
SecurityContextLogoutHandler logoutHandler = new SecurityContextLogoutHandler();
logoutHandler.setInvalidateHttpSession(true);
logoutHandler.setClearAuthentication(true);
return logoutHandler;
}
// Filter processing incoming logout messages
// First argument determines URL user will be redirected to after successful
// global logout
#Bean
public SAMLLogoutProcessingFilter samlLogoutProcessingFilter() {
return new SAMLLogoutProcessingFilter(successLogoutHandler(),
logoutHandler());
}
// Overrides default logout processing filter with the one processing SAML
// messages
#Bean
public SAMLLogoutFilter samlLogoutFilter() {
return new SAMLLogoutFilter(successLogoutHandler(),
new LogoutHandler[] { logoutHandler() },
new LogoutHandler[] { logoutHandler() });
}
// Bindings
private ArtifactResolutionProfile artifactResolutionProfile() {
final ArtifactResolutionProfileImpl artifactResolutionProfile = new ArtifactResolutionProfileImpl(httpClient());
artifactResolutionProfile.setProcessor(new SAMLProcessorImpl(soapBinding()));
return artifactResolutionProfile;
}
#Bean
public HTTPArtifactBinding artifactBinding(ParserPool parserPool, VelocityEngine velocityEngine) {
return new HTTPArtifactBinding(parserPool, velocityEngine, artifactResolutionProfile());
}
#Bean
public HTTPSOAP11Binding soapBinding() {
return new HTTPSOAP11Binding(parserPool());
}
#Bean
public HTTPPostBinding httpPostBinding() {
return new HTTPPostBinding(parserPool(), velocityEngine());
}
#Bean
public HTTPRedirectDeflateBinding httpRedirectDeflateBinding() {
return new HTTPRedirectDeflateBinding(parserPool());
}
#Bean
public HTTPSOAP11Binding httpSOAP11Binding() {
return new HTTPSOAP11Binding(parserPool());
}
#Bean
public HTTPPAOS11Binding httpPAOS11Binding() {
return new HTTPPAOS11Binding(parserPool());
}
// Processor
#Bean
public SAMLProcessorImpl processor() {
Collection<SAMLBinding> bindings = new ArrayList<>();
bindings.add(httpRedirectDeflateBinding());
bindings.add(httpPostBinding());
bindings.add(artifactBinding(parserPool(), velocityEngine()));
bindings.add(httpSOAP11Binding());
bindings.add(httpPAOS11Binding());
return new SAMLProcessorImpl(bindings);
}
#Bean
public VelocityEngine velocityEngine() {
return VelocityFactory.getEngine();
}
/**
* Define the security filter chain in order to support SSO Auth by using SAML 2.0
*
* #return Filter chain proxy
* #throws Exception
*/
#Bean
public FilterChainProxy samlFilter() throws Exception {
List<SecurityFilterChain> chains = new ArrayList<>();
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/login/**"),
samlEntryPoint()));
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/logout/**"),
samlLogoutFilter()));
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/metadata/**"),
metadataDisplayFilter()));
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/SSO/**"),
samlWebSSOProcessingFilter()));
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/SSOHoK/**"),
samlWebSSOHoKProcessingFilter()));
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/SingleLogout/**"),
samlLogoutProcessingFilter()));
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/discovery/**"),
samlIDPDiscovery()));
return new FilterChainProxy(chains);
}
/**
* Returns the authentication manager currently used by Spring. It represents a bean definition with the aim allow wiring from other classes performing the Inversion of Control (IoC).
*
* #throws Exception
*/
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* Defines the web based security configuration.
*
* #param http
* It allows configuring web based security for specific http requests.
* #throws Exception
*/
#Override
protected void configure(HttpSecurity http) throws Exception {
/*http.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.NEVER);*/
http
.httpBasic()
.authenticationEntryPoint(samlEntryPoint());
http
.csrf()
.disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http
.addFilterBefore(metadataGeneratorFilter(), ChannelProcessingFilter.class)
.addFilterAfter(samlFilter(), BasicAuthenticationFilter.class);
http
.authorizeRequests()
.antMatchers("/error").permitAll()
.antMatchers("/saml/**").permitAll()
.anyRequest()
//.permitAll();
.authenticated();
http
.exceptionHandling().accessDeniedPage("/403");
http
.logout()
.logoutSuccessUrl("/");
}
/**
* Sets a custom authentication provider.
*
* #param auth
* SecurityBuilder used to create an AuthenticationManager.
* #throws Exception
*/
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.authenticationProvider(samlAuthenticationProvider());
}
and this is my configuration file
saml.metadata-file= metadata.xml
saml.idp-selection-path= /osi/saml/login?idp=IDP
saml.openmind-app-id= com:na:app:local:dev
saml.success-redirect= https://localhost:4200/context
saml.success= https://localhost:4200/#/files
saml.failure-redirect= https://localhost:4200/#/error
saml.lb-server-name= localhost:4200
saml.lb-scheme= https
saml.lb-context-path= /context
saml.lb-server-port= 4200
saml.lb-include-port= true
saml.store-path= classpath:localhost.jks
saml.store-pass= pass
saml.store-default-key= 1
saml.secured= true
While trying to login the authentification failes:
INFO o.s.s.saml.log.SAMLDefaultLogger - AuthNResponse;FAILURE;100.83.63.69;com:na:app:local:dev;IDP;;;org.opensaml.common.SAMLException: InResponseToField of the Response doesn't correspond to sent message a87hdee46ffejbd317h793f719g64h
adding a web.xml file to the project with the tag solved the issues
<?xml version="1.0"?>
<web-app xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
version="2.4">
<distributable />
<!-- ... -->
</web-app>
Preface
I am working on an OAuth application for security between two servers. I have an OAuth Server and a Resource Server. The Resource Server has a single .war deployed that contains 4 APIs.
Single Responsibility
The OAuth server has to validate a the access token that was passed by an API (1 of the 4) from that same .war.
The OAuth server has to keep a hit count for a particular accessToken for a particular API. If the hit count exceeds the configured hits the OAuth server would throw a 403: Forbidden.
Every API in the .war must first validate the accessToken from the OAuth server and if it's validated, then proceed to provide the response.
What I've done:
If a .war has a single API then I can simply make the two servers communicate using a webHook, below is the code that does it.
On the Resource Server Side:
My urls for different APIs are:
localhost:8080/API/API1
localhost:8080/API/API2
Below code routes any request if they have /API/anything towards the spring security filters
<http pattern="/API/**" create-session="never" authentication-manager-ref="authenticationManager" entry-point-ref="oauthAuthenticationEntryPoint" xmlns="http://www.springframework.org/schema/security">
<anonymous enabled="false" />
<intercept-url pattern="/places/**" method="GET" access="IS_AUTHENTICATED_FULLY" />
<custom-filter ref="resourceServerFilter" before="PRE_AUTH_FILTER" />
<access-denied-handler ref="oauthAccessDeniedHandler" />
</http>
I have used remote token services and defined the webHook to route the request to the OAuth server
<bean id="tokenServices" class="org.springframework.security.oauth2.provider.token.RemoteTokenServices">
<property name="checkTokenEndpointUrl" value="http://localhost:8181/OUTPOST/oauth/check_token"/>
<property name="clientId" value="atlas"/>
<property name="clientSecret" value="atlas"/>
</bean>
Configuration for Auth server
#EnableAuthorizationServer
#Configuration
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
private static String REALM="OUTPOST_API";
#Autowired
private ClientDetailsService clientService;
#Autowired
public AuthorizationServerConfig(AuthenticationManager authenticationManager,RedisConnectionFactory redisConnectionFactory) {
this.authenticationManager = authenticationManager;
this.redisTokenStore = new RedisTokenStore(redisConnectionFactory);
}
// #Autowired
// #Qualifier("authenticationManagerBean")
private AuthenticationManager authenticationManager;
private TokenStore redisTokenStore;
#Autowired
private UserApprovalHandler userApprovalHandler;
#Autowired
private RedisConnectionFactory redisConnectionFactory;
#Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.tokenKeyAccess("isAuthenticated()")
.checkTokenAccess("isAuthenticated()").
realm(REALM+"/client");
}
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients
.inMemory()
.withClient("cl1")
.secret("pwd")
.authorizedGrantTypes("password", "client_credentials", "refresh_token")
.authorities("ROLE_CLIENT", "ROLE_ADMIN")
.scopes("read", "write", "trust")/*
.resourceIds("sample-oauth")*/
.accessTokenValiditySeconds(1000)
.refreshTokenValiditySeconds(5000)
.and()
.withClient("atlas")
.secret("atlas");
}
#Bean
#Autowired
public TokenStore tokenStore(RedisConnectionFactory redisConnectionFactory) {
this.redisTokenStore = new RedisTokenStore(redisConnectionFactory);
return this.redisTokenStore;
}
#Bean
public WebResponseExceptionTranslator loggingExceptionTranslator() {
return new DefaultWebResponseExceptionTranslator() {
#Override
public ResponseEntity<OAuth2Exception> translate(Exception e) throws Exception {
// This is the line that prints the stack trace to the log. You can customise this to format the trace etc if you like
e.printStackTrace();
// Carry on handling the exception
ResponseEntity<OAuth2Exception> responseEntity = super.translate(e);
HttpHeaders headers = new HttpHeaders();
headers.setAll(responseEntity.getHeaders().toSingleValueMap());
OAuth2Exception excBody = responseEntity.getBody();
return new ResponseEntity<>(excBody, headers, responseEntity.getStatusCode());
}
};
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(redisTokenStore).userApprovalHandler(userApprovalHandler)
.authenticationManager(authenticationManager)
.exceptionTranslator(loggingExceptionTranslator());
}
public void setRedisConnectionFactory(RedisConnectionFactory redisConnectionFactory) {
this.redisConnectionFactory = redisConnectionFactory;
}
#Bean
public TokenStoreUserApprovalHandler userApprovalHandler(){
TokenStoreUserApprovalHandler handler = new TokenStoreUserApprovalHandler();
handler.setTokenStore(redisTokenStore);
handler.setRequestFactory(new DefaultOAuth2RequestFactory(clientService));
handler.setClientDetailsService(clientService);
return handler;
}
#Bean
#Autowired
public ApprovalStore approvalStore() throws Exception {
TokenApprovalStore store = new TokenApprovalStore();
store.setTokenStore(redisTokenStore);
return store;
}
#Bean
#Primary
#Autowired
public DefaultTokenServices tokenServices() {
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setSupportRefreshToken(true);
tokenServices.setTokenStore(redisTokenStore);
return tokenServices;
}
}
#Component
class MyOAuth2AuthenticationEntryPoint extends OAuth2AuthenticationEntryPoint{}
What I need help with:
The issue is with the support for single .war and multiple API. The issue is the spring config is created at a package level because of which all the APIs in the .war have the same clientID and clientSecret.
How would my OAuth server know, which specific API is being accessed and of which API the hitCount needs to be deducted.
Possible Solution?
I was thinks of customizing RemoteTokenService and adding a request parameter at the webHoot URL and then using a filter at OAuth server to get the passed tag (if I may call it that)
Is this even possible? Is there any better approch than this, that doesn't involve all these work arounds?
Eureka !! I finally found a way out to resolve this problem.
All you have to do is :
Configuration at Resource server
Instead of using RemoteTokenService make a custom remote token service which appends some data (query parameter) in the generated request.
public class CustomRemoteTokenService implements ResourceServerTokenServices {
protected final Log logger = LogFactory.getLog(getClass());
private RestOperations restTemplate;
private String checkTokenEndpointUrl;
private String clientId;
private String clientSecret;
private String tokenName = "token";
private AccessTokenConverter tokenConverter = new DefaultAccessTokenConverter();
#Autowired
public CustomRemoteTokenService() {
restTemplate = new RestTemplate();
((RestTemplate) restTemplate).setErrorHandler(new DefaultResponseErrorHandler() {
#Override
// Ignore 400
public void handleError(ClientHttpResponse response) throws IOException {
if (response.getRawStatusCode() != 400) {
super.handleError(response);
}
}
});
}
public void setRestTemplate(RestOperations restTemplate) {
this.restTemplate = restTemplate;
}
public void setCheckTokenEndpointUrl(String checkTokenEndpointUrl) {
this.checkTokenEndpointUrl = checkTokenEndpointUrl;
}
public void setClientId(String clientId) {
this.clientId = clientId;
}
public void setClientSecret(String clientSecret) {
this.clientSecret = clientSecret;
}
public void setAccessTokenConverter(AccessTokenConverter accessTokenConverter) {
this.tokenConverter = accessTokenConverter;
}
public void setTokenName(String tokenName) {
this.tokenName = tokenName;
}
#Override
public OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException {
/*
* This code needs to be more dynamic. Every time an API is added we have to add its entry in the if check for now.
* Should be changed later.
*/
HttpServletRequest request = Context.getCurrentInstance().getRequest();
MultiValueMap<String, String> formData = new LinkedMultiValueMap<String, String>();
String uri = request.getRequestURI();
formData.add(tokenName, accessToken);
if(request != null) {
if(uri.contains("API1")) {
formData.add("api", "1");
}else if(uri.contains("API2")) {
formData.add("api", "2");
}
}
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", getAuthorizationHeader(clientId, clientSecret));
Map<String, Object> map = postForMap(checkTokenEndpointUrl, formData, headers);
if (map.containsKey("error")) {
logger.debug("check_token returned error: " + map.get("error"));
throw new InvalidTokenException(accessToken);
}
Assert.state(map.containsKey("client_id"), "Client id must be present in response from auth server");
return tokenConverter.extractAuthentication(map);
}
#Override
public OAuth2AccessToken readAccessToken(String accessToken) {
throw new UnsupportedOperationException("Not supported: read access token");
}
private String getAuthorizationHeader(String clientId, String clientSecret) {
String creds = String.format("%s:%s", clientId, clientSecret);
try {
return "Basic " + new String(Base64.encode(creds.getBytes("UTF-8")));
}
catch (UnsupportedEncodingException e) {
throw new IllegalStateException("Could not convert String");
}
}
private Map<String, Object> postForMap(String path, MultiValueMap<String, String> formData, HttpHeaders headers) {
if (headers.getContentType() == null) {
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
}
#SuppressWarnings("rawtypes")
Map map = restTemplate.exchange(path, HttpMethod.POST,
new HttpEntity<MultiValueMap<String, String>>(formData, headers), Map.class).getBody();
#SuppressWarnings("unchecked")
Map<String, Object> result = map;
return result;
}
}
By implementing ResourceServerTokenServices you can modify the request that is sent by the resource server to the auth server for authentication and authorization.
configuration at Auth Server
Override the spring security controller. What i mean by overring is make a custom controller so that the request for oauth/check_token is handled by your custom controller and not the spring defined controller.
#RestController
public class CustomCheckTokenEndpoint {
private ResourceServerTokenServices resourceServerTokenServices;
private AccessTokenConverter accessTokenConverter = new DefaultAccessTokenConverter();
protected final Log logger = LogFactory.getLog(getClass());
private WebResponseExceptionTranslator exceptionTranslator = new DefaultWebResponseExceptionTranslator();
#Autowired
KeyHitManager keyHitManager;
public CustomCheckTokenEndpoint(ResourceServerTokenServices resourceServerTokenServices) {
this.resourceServerTokenServices = resourceServerTokenServices;
}
/**
* #param exceptionTranslator
* the exception translator to set
*/
public void setExceptionTranslator(WebResponseExceptionTranslator exceptionTranslator) {
this.exceptionTranslator = exceptionTranslator;
}
/**
* #param accessTokenConverter
* the accessTokenConverter to set
*/
public void setAccessTokenConverter(AccessTokenConverter accessTokenConverter) {
this.accessTokenConverter = accessTokenConverter;
}
#RequestMapping(value = "/oauth/check_token")
#ResponseBody
public Map<String, ?> customCheckToken(#RequestParam("token") String value, #RequestParam("api") int api) {
OAuth2AccessToken token = resourceServerTokenServices.readAccessToken(value);
if (token == null) {
throw new InvalidTokenException("Token was not recognised");
}
if (token.isExpired()) {
throw new InvalidTokenException("Token has expired");
}
OAuth2Authentication authentication = resourceServerTokenServices.loadAuthentication(token.getValue());
Map<String, ?> response = accessTokenConverter.convertAccessToken(token, authentication);
String clientId = (String) response.get("client_id");
if (!keyHitManager.isHitAvailble(api,clientId)) {
throw new InvalidTokenException(
"Services for this key has been suspended due to daily/hourly transactions limit");
}
return response;
}
#ExceptionHandler(InvalidTokenException.class)
public ResponseEntity<OAuth2Exception> handleException(Exception e) throws Exception {
logger.info("Handling error: " + e.getClass().getSimpleName() + ", " + e.getMessage());
// This isn't an oauth resource, so we don't want to send an
// unauthorized code here. The client has already authenticated
// successfully with basic auth and should just
// get back the invalid token error.
#SuppressWarnings("serial")
InvalidTokenException e400 = new InvalidTokenException(e.getMessage()) {
#Override
public int getHttpErrorCode() {
return 400;
}
};
return exceptionTranslator.translate(e400);
}
}