authentication in spring boot using graphql - authentication

I’m working on a spring boot project with GraphQL. I'm using graphql-java-tools and graphql-spring-boot-starter. I managed to configure security and session management with spring security as you can see in the java config files below.
Now the “/graphql” path is secured (it can be accessed only sending the “basic http authentication” or a session token (x-auth-token) in a http header of the request). Authenticating with “basic http authentication” on any GraphQL operation will start a new session and send back the new session token in a header, and that token can be used further to continue that session.
How to give access to anonymous users to some GraphQL queries/mutations keeping the above behavior?
If I change antMatchers("/graphql").authenticated() to antMatchers("/graphql").permitAll() in order to allow anonymous access, then my custom AuthenticationProvider is not called anymore even when I try to authenticate with “basic http authentication”.
Thanks!
Here are my configs:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private AuthenticationProvider authenticationProvider;
#Override
public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) {
authenticationManagerBuilder.authenticationProvider(authenticationProvider);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/graphql").authenticated()
.and()
.requestCache()
.requestCache(new NullRequestCache())
.and()
.httpBasic()
.and()
.headers()
.frameOptions().sameOrigin() // needed for H2 web console
.and()
.sessionManagement()
.maximumSessions(1)
.maxSessionsPreventsLogin(true)
.sessionRegistry(sessionRegistry());
}
#Bean
public SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
#Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
}
#EnableRedisHttpSession(maxInactiveIntervalInSeconds = 180)
public class HttpSessionConfig {
#Bean
public HttpSessionStrategy httpSessionStrategy() {
return new HeaderHttpSessionStrategy();
}
}

Instead of .antMatchers("/graphql").authenticated() we used .antMatchers("/graphql").permitAll(), then we removed .httpBasic() and also removed the custom AuthenticationProvider. Now the security configs look like this:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/graphql").permitAll()
.and()
.requestCache()
.requestCache(new NullRequestCache())
.and()
.headers()
.frameOptions().sameOrigin() // needed for H2 web console
.and()
.sessionManagement()
.maximumSessions(1)
.maxSessionsPreventsLogin(true)
.sessionRegistry(sessionRegistry());
}
#Bean
public SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
#Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
}
Then we created a mutation for login that accepts the user's credentials and returns the session token. Here is the graphql schema:
login(credentials: CredentialsInputDto!): String
input CredentialsInputDto {
username: String!
password: String!
}
Basically the code we had in our custom AuthenticationProvider went into the service that is called by the login operation:
public String login(CredentialsInputDto credentials) {
String username = credentials.getUsername();
String password = credentials.getPassword();
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
... credential checks and third party authentication ...
Authentication authentication = new UsernamePasswordAuthenticationToken(username, password, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
httpSession.setAttribute("SPRING_SECURITY_CONTEXT", SecurityContextHolder.getContext());
return httpSession.getId();
}
The key is that we prepared the session context with the authenticated user's authentication and then we save it (in redis) as a session attribute called "SPRING_SECURITY_CONTEXT". This is all what spring needs to be able to automatically restore the context when you make a request having the "x-auth-token" header set with the value of the session token obtained from the login operation.
Now also anonymous calls are allowed because of .antMatchers("/graphql").permitAll() and in the service layer, on public methods we can use annotations like this: #Preauthorize("isAnonymous() OR hasRole("USER")").

Even though you need to use permitAll() you can still create reasonable default for your resolver methods using AOP.
You can create your custom security aspect that will require authentication by default.
Unsecured methods may be marked for example using annotation.
See my blog post for details: https://michalgebauer.github.io/spring-graphql-security

Related

Spring security - new access token

I have following issue.
I have multitenant system (with shared Database and shared Schema). Access token that is generated when user logs in contains information about tenantId. Idea is to allow logged user to change tenat
For example: My user works for 3 tenants(hospitals). When he is logged in, he should be able to change hospital.
So the main issues is how to generate new acces token for user that will contain updated tenantId.
It would be preferable that user doesnt have to provide password again (since he is already logged in), and that request to auth-server that he triggers would contain his current token (that will confirm that he is currently authenticated) and newTenandId.
Here is some custom code:
#Service
public class CustomAuthenticationProvider implements AuthenticationProvider {
#Autowired
private MessageSource validationMessageSource;
#Autowired
private UserDetailsService userDetailsService;
#Autowired
private PasswordEncoder passwordEncoder;
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String password = authentication.getCredentials().toString();
SpringSecurityUserWithAdditionalData user = (SpringSecurityUserWithAdditionalData) userDetailsService.loadUserByUsername(username);
return checkPassword(user, password);
}
#Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
private Authentication checkPassword(SpringSecurityUserWithAdditionalData user, String rawPassword) throws AuthenticationException {
try {
if (passwordEncoder.matches(rawPassword, user.getPassword())) {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword(), user.getAuthorities());
return token;
} else {
throw new GeneralException(validationMessageSource.getMessage("security.authentication.NotValid", new Object[] {}, LocaleContextHolder.getLocaleContext().getLocale()));
}
} catch (Exception e) {
throw new BadCredentialsException(e.getMessage());
}
}
}
#Configuration
#EnableWebSecurity
public class WebSecurityConfiguration {
#Autowired
private CustomAuthenticationProvider authenticationProvider;
// #formatter:off
#Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http
...
.and()
.logout()
.clearAuthentication(true)
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
.logoutSuccessHandler(new CustomLogoutSuccessHandler())
.and()
.formLogin()
.loginPage("/login")
.loginPage("/changeTenant")
.permitAll().and();
return http.build();
}
// #formatter:on
#Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
#Autowired
public void configureAuthentication(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider);
}
/**
* JWT koji je generisao authorization server sadrzi granted permissions (Spring ih naziva granted authorities) u okviru "scope" claim-a.
* Umesto njega cemo koristiti custom claim koji sam nazvao GlobalConstants.JWT_CLAIM_ROLA_LIST za specifikaciju rola koje ima authenticated korisnik.
* Spring koristi default instance JwtAuthenticationConverter koja ocekuje granted authorities u okviru "scope"/"scp" claim-a.
* Da bi koristili umesto standardno "scope" claim-a koristili claim GlobalConstants.JWT_CLAIM_ROLA_LIST override-ovan je JwtAuthenticationConverter.
*/
#Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter converter = new JwtGrantedAuthoritiesConverter();
converter.setAuthoritiesClaimName(GlobalConstants.JWT_CLAIM_ROLA_LIST); // override authorities claim-a
converter.setAuthorityPrefix(""); // eksplicitno definisemo nazive, bez podrazumevanih prefiksa (ROLE_ SCOPE_ i slicno)
JwtAuthenticationConverter jwtConverter = new JwtAuthenticationConverter();
jwtConverter.setJwtGrantedAuthoritiesConverter(converter);
return jwtConverter;
}
#Bean
InitializingBean forcePostProcessor(BeanPostProcessor meterRegistryPostProcessor, MeterRegistry registry) {
return () -> meterRegistryPostProcessor.postProcessAfterInitialization(registry, "");
}
}
If you need any additional information, please say.
I tried adding custom fields to custom login form, that will have hidden tenantId field. But i could not manage to make it work.
The authentication process should be designed to return a list of all the tenants the user has access to, typically as a list of authorities.
Separately you need a back-end call (linked to the UI) that allows the user to choose the current tenant from the list of authorities returned in authn.
The value of the current tenant must be stored in the session.
If you really want to hack this via the auth roles, you could store the real auth token and generate your own token with only the current tenant. When user changes tenant they get a new token with the new tenant (obviously after checking against the real token)

How can I provide my custom OAuth2LoginAuthenticationProvider in spring boot

I am trying to setup OAuth2 login in spring boot, every thing is working good in my dev environment, however when I deploy it on production behind the Apache Http server using proxy configuration, authentication fails on Invalid Redirect URI check.
As suggested in some other post I already tried
1. use-forward-headers: true
2. Rewriting the request redirect URI
with no luck.
So I decided to hook my own custom OAuth2LoginAuthenticationProvider so that I can override this check.
public class SecurityConfig extends WebSecurityConfigurerAdapter{
#Value("${baseUrl}")
private String domainPath;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilterBefore(new MyCustomFilter(domainPath), OAuth2LoginAuthenticationFilter.class)
.authorizeRequests()
.antMatchers("/").permitAll()
.anyRequest().authenticated().and()
.logout().logoutSuccessUrl("/")
.and()
.oauth2Login().userInfoEndpoint().customUserType(CmmOAuth2User.class, "custom");
}
#Bean
public MyOAuth2AuthenticationProvider authenticationProvider() {
MyOAuth2AuthenticationProvider authProvider
= new MyOAuth2AuthenticationProvider();
return authProvider;
}
#Autowired
public void configureProviderManager(ProviderManager providerManager) {
providerManager.getProviders().add(0,authenticationProvider());
}
#Bean
public ProviderManager authenticationManager() {
List<AuthenticationProvider> authProviderList = new ArrayList<AuthenticationProvider>();
authProviderList.add(0,authenticationProvider());
ProviderManager providerManager = new ProviderManager(authProviderList);
return providerManager;
}
}
My custom MyOAuth2AuthenticationProvider is not being called.

Oauth2 server example with all 4 grant types

Is it possible to have an auth server defining all four types of grant types?
And if yes then how can I test them? A sample auth client for grant type password would be helpful.
I am presuming that you want to implement it using Spring OAuth server implementation. One example by Dave Syer is hosted on GitHub, and it uses JDBC as the token store.
Now for the grant types, you have the option to configure that per client that you register with your OAuth server. You will notice that it has been setup using "authorizedGrantTypes" in the example below.
For testing - password grant type is very easy to test using a REST client like Postman and making a call to the OAuth server.
Feel free to comment on this if you need more help. All the best!
Resource Server
#Configuration
#EnableResourceServer
protected static class ResourceServer extends ResourceServerConfigurerAdapter {
#Autowired
private TokenStore tokenStore;
#Override
public void configure(ResourceServerSecurityConfigurer resources)
throws Exception {
resources.tokenStore(tokenStore);
}
#Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated();
}
}
Authorization Server
#Configuration
#EnableAuthorizationServer
protected static class OAuth2Config extends AuthorizationServerConfigurerAdapter {
#Autowired
private AuthenticationManager auth;
#Autowired
private DataSource dataSource;
private BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
#Bean
public JdbcTokenStore tokenStore() {
return new JdbcTokenStore(dataSource);
}
#Bean
protected AuthorizationCodeServices authorizationCodeServices() {
return new JdbcAuthorizationCodeServices(dataSource);
}
#Override
public void configure(AuthorizationServerSecurityConfigurer security)
throws Exception {
security.passwordEncoder(passwordEncoder);
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints)
throws Exception {
endpoints.authorizationCodeServices(authorizationCodeServices())
.authenticationManager(auth).tokenStore(tokenStore())
.approvalStoreDisabled();
}
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// #formatter:off
clients.jdbc(dataSource)
.passwordEncoder(passwordEncoder)
.withClient("my-trusted-client")
.authorizedGrantTypes("password", "authorization_code",
"refresh_token", "implicit")
.authorities("ROLE_CLIENT", "ROLE_TRUSTED_CLIENT")
.scopes("read", "write", "trust")
.resourceIds("oauth2-resource")
.accessTokenValiditySeconds(60).and()
.withClient("my-client-with-registered-redirect")
.authorizedGrantTypes("authorization_code")
.authorities("ROLE_CLIENT").scopes("read", "trust")
.resourceIds("oauth2-resource")
.redirectUris("http://anywhere?key=value").and()
.withClient("my-client-with-secret")
.authorizedGrantTypes("client_credentials", "password")
.authorities("ROLE_CLIENT").scopes("read")
.resourceIds("oauth2-resource").secret("secret");
// #formatter:on
}
}

spring security binding to LDAP with bindDN - bindCredential

We have an ldap that is configure to bind with a bindDN different from the user logging in and its own bindCredential as well.
bindDN cn=appUser,dc=testOrgManagement,dc=com
bindCredential: somePassword
I am trying to configure Spring security LDAP module to use it but it seems that by default it tries to bind as the authenticated user which will not be able to read the roles so it returns no roles.
Tried the following but to no avail.
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(securedEnabled = true)
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.ldapAuthentication()
.contextSource()
.url("ldaps://ldap.dev.testorg.com:636/")
.managerDn("cn=appUser,dc=testOrgManagement,dc=com")
.managerPassword("somePassword")
.and()
.userSearchFilter("(uid={0})")
.userSearchBase("ou=division1,dc=testorg,dc=com")
.groupSearchBase("ou=division1,dc=testorg,dc=com")
.groupSearchFilter("(uniqueMember={1})")
.groupRoleAttribute("cn" )
.authoritiesMapper(grantedAuthoritiesMapper());
}
public GrantedAuthoritiesMapper grantedAuthoritiesMapper() {
return new GrantedAuthoritiesMapper() {
Set roles = new HashSet<LdapAuthority>();
#Override
public Collection<? extends GrantedAuthority> mapAuthorities(Collection<? extends GrantedAuthority> authorities) {
for (GrantedAuthority authority : authorities) {
if (SecurityRoles.getRoleMap().containsKey(authority.getAuthority())) {
roles.add(new LdapAuthority((String) SecurityRoles.getRoleMap().get(authority.getAuthority())));
}
}
return roles;
}
};
}
}
It seems that spring uses BindAuthenticator.java which tries to bind as the user and that does not return any roles when searching. I thought that managerDn and managerPassword were actually the bindDN and its credential but it does not seem to be the case. It seems to be just a user rather.
Any idea how to achieve this? Thank you!

HTTP basic Auth and OAuth in combination

I have a client application that needs to authenticate specific request pattern via Basic Auth and rest else via OAuth. I am using #EnableOAuth2Sso to enable the Oauth.
I have autowired a secutiryConfig bean in my springBootApp like this
public class ClientApp {
#Autowired
private SecurityConfig config;
}
and SecurityConfig is defined as
#Component
#EnableOAuth2Sso
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/js/**").permitAll()
.antMatchers("/css/**").permitAll()
.anyRequest().authenticated();
}
I need to have basic auth on pattern '/admin/** '
and OAuth on '/users/** '
Basic auth has to be using the jdbcAuthentication.
I can make Oauth or Basic Auth separately working for all patterns. But how to make them work simultaneously for this case.