Skip to content

Oauth2 In Practice With Spring Security

Davide Pugliese edited this page Oct 4, 2018 · 17 revisions

Oauth2 In Practice With Spring Security

1 Introduction

1.1 About This Guide

Given my experience in the pit, Oauth2 is one of those topics where people have no clue about what to do and is kind of surrounded by an allure of mystery. I won’t enter too much into the technical details because the web is full of places where you can learn the theory, however I will try also to cover some fundamentals to help those that are new to this topic. The focus of this guide is on a particular case: an enterprise application with “password grant” which needs to authenticate a user via his credentials and return a Javascript Web Token.

1.2 What is Oauth2?

Oauth2 is an industry standard protocol for authorisation, which basically means that it checks if you have access or not to a resource in the first place. Oauth2 per se does not do any authentication checks, id est it does not verify the identity of a user. What problems does Oauth2 solve? Oauth2 main purpose is to allow third party applications to login through your app. However with the raise of Micro-service Architecture and the lack of definitive standard set in the stone for example with regard to JSON responses, programmers find it useful to have some points of reference set in the stone. In fact, by using the “password grant” type of authorisation the user will have to verify his identity through username and password. In exchange, the server, if the data provided are correct, will return a Javascript Web Token that is compliant, obviously, with the Oauth2 standards. This means that it’s something set in the stone. You can add some custom information to it anyways if you like. Also, the method used to return the JWT the first time, and the way how to send the token bearer are already set, allowing to take off your shoulders a few architectural responsibilities. In this guide I will use the Spring Security implementation because it’s something tested, publicly available and sound. There’s another approach, anyways, which is using AspectJ and create a JWT filter, but it has the disadvantage that you have to create Unit Tests for it, and if more experienced developers come on board on your project, chances are that they have some degree of experience with Spring Security already. Link to my project which uses Aspect J

2 Let's create a User with roles and privileges

2.1 Let's begin with the User

In order to create a user and assign him a few roles we need to create a @ManyToMany relationship with a @JoinTable

@Data
@Entity(name = "User")
@Table(name="users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;

    @Column(unique = true)
    private String username;
    private String password;

    @OneToOne
    private UserInfo userInfo;

    @ManyToMany(fetch = FetchType.EAGER, cascade = {CascadeType.MERGE})
    @JoinTable(
            name = "users_roles",
            joinColumns = @JoinColumn(
                    name = "user_id", referencedColumnName = "id"),
            inverseJoinColumns = @JoinColumn(
                    name = "role_id", referencedColumnName = "id"))
    private Collection<Role> roles = new HashSet<>();
}

2.2 Let's create Roles and Privileges

When you configure the roles and the privileges, given some technical issues with the Hibernate and Spring JPA versions in use, you cannot initialize with eager loading two @ManyToMany collections at the same time. However, using fetch = fetchType.LAZY will not work as expected, because it looks like that this way there is no open session when Oauth2 middleware tries to fetch the data it needs (roles and privileges which are related to our user). The solution is to use @LazyCollection(LazyCollectionOption.FALSE) which is an annotation that strictly belongs to Hibernate and not JPA, and works without any flaws.

As in a plain old Spring Security Scenario we need to create also roles and privileges.

@Data
@Entity(name = "Role")
@Table(name="roles")
public class Role {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column(unique = true)
    private String name;

    @ManyToMany(mappedBy = "roles", fetch = FetchType.LAZY, cascade = {CascadeType.MERGE})
    private Collection<User> users;

    @ManyToMany
    @LazyCollection(LazyCollectionOption.FALSE)
    @JoinTable(
            name = "roles_privileges",
            joinColumns = @JoinColumn(
                    name = "role_id", referencedColumnName = "id"),
            inverseJoinColumns = @JoinColumn(
                    name = "privilege_id", referencedColumnName = "id"))
    private Collection<Privilege> privileges = new HashSet<>();
}
@Data
@Entity(name="Privilege")
@Table(name="privileges")
public class Privilege {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    @Column(unique = true)
    private String name;

    @ManyToMany(mappedBy = "privileges", cascade = {CascadeType.MERGE})
    @LazyCollection(LazyCollectionOption.FALSE)
    private Collection<Role> roles = new HashSet<>();
}

3 Extend A User with Spring Security methods and attributes

3.1 Let’s create a custom user principal

Before you jump off your chair, a principal is a “corporation, a program thread, an individual, anything that can have an identity”. User principal implements a Spring Security Interface that is UserDetails. We basically prefer using the composition pattern rather than using inheritance. In fact, if we decided to use inheritance we would have had issues with the constructors (having to import all the required parameters so that the signatures of the methods do match) and it has been anyways been deprecated as an approach in more recent versions. I tried that approach as well and it was a no go. Therefore, we prefer to inject user into CustomUserPrincipal and on class instantiation we assign our user to the user attribute of our object. To learn more info about UserDetails, if you have IntelliJ you can just Command(Ctrl)+Click it and read all the details. The basic Idea is that when we implement an interface, the interface is like a contract and we need to honor it, which means we need to provide an implementation for its methods.

public class CustomUserPrincipal implements UserDetails {

    private static final long serialVersionUID = 1L;

    private final User user;

    public CustomUserPrincipal(User user) {
        this.user = user;
    }


    @Override
    public String getUsername() {
        return user.getUsername();
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    private List<String> getPrivileges(Collection<Role> roles) {

        List<String> privileges = new ArrayList<>();
        List<Privilege> collection = new ArrayList<>();
        for (Role role : roles) {
            collection.addAll(role.getPrivileges());
        }
        for (Privilege item : collection) {
            privileges.add(item.getName());
        }
        return privileges;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        final List<GrantedAuthority> authorities = new ArrayList<>();
        final Collection<Role> roles = user.getRoles();
        for (String privilege : getPrivileges(roles)) {
            authorities.add(new SimpleGrantedAuthority(privilege));
        }
        return authorities;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

    public User getUser() {
        return user;
    }

}

Let's have a closer look at this code chunk

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        final List<GrantedAuthority> authorities = new ArrayList<>();
        final Collection<Role> roles = user.getRoles();
        for (String privilege : getPrivileges(roles)) {
            authorities.add(new SimpleGrantedAuthority(privilege));
        }
        return authorities;
    }

What this code does is to fill the list of authorities, which are those roles of our user that will be written in the token, with the roles coming from our user object.

3.2 Inject our User in our CustomUserPrincipal

In order for our user to be passed to our User Principal, we need to inject it somehow using Spring Boot IOC. In order for this to happen we need to create a @Service which is a stereotype, meaning an alias, for @Component. This annotation allows Component Scan to find our component, create an instance of our service and make it available to the rest of our app.

@Service public class CustomUserDetailsService implements UserDetailsService {

private UserRepository userRepository;

CustomUserDetailsService(UserRepository userRepository) {
    this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(final String username) {
    User user = userRepository.findUserByUsername(username);
    if (user == null) {
        throw new UsernameNotFoundException(username);
    }
    return new CustomUserPrincipal(user);
}

}

4 Configure your application to use security expressions

4.1 Using expressions to grant access to a resource

In order to give access to a resource (endpoint for example) we need to implement some methods which will be later on used by the @Preauthorize() annotation. You can find all the info at this url url Let's say you want to give access only to users with admin role, you will add @Preauthorize(hasRole(ROLE_ADMIN)) on top of your Controller method for that specific endpoint. Just out of curiosity, also @Controller is a stereotype for @Component.

To achieve this we must the MethodSecurityExpressionOperations interface of Spring Security.

public class CustomSecurityExpressionRoot implements MethodSecurityExpressionOperations {
    protected final Authentication authentication;
    private AuthenticationTrustResolver trustResolver;
    private RoleHierarchy roleHierarchy;
    private Set<String> roles;
    private String defaultRolePrefix = "ROLE_";

    public final boolean permitAll = true;
    public final boolean denyAll = false;
    private PermissionEvaluator permissionEvaluator;
    public final String read = "read";
    public final String write = "write";
    public final String create = "create";
    public final String delete = "delete";
    public final String admin = "administration";

    //

    private Object filterObject;
    private Object returnObject;

    public CustomSecurityExpressionRoot(Authentication authentication) {
        if (authentication == null) {
            throw new IllegalArgumentException("Authentication object cannot be null");
        }
        this.authentication = authentication;
    }

    @Override
    public final boolean hasAuthority(String authority) {
        throw new RuntimeException("method hasAuthority() not allowed");
    }


    @Override
    public final boolean hasAnyAuthority(String... authorities) {
        return hasAnyAuthorityName(null, authorities);
    }

    @Override
    public final boolean hasRole(String role) {
        return hasAnyRole(role);
    }

    @Override
    public final boolean hasAnyRole(String... roles) {
        return hasAnyAuthorityName(defaultRolePrefix, roles);
    }

    private boolean hasAnyAuthorityName(String prefix, String... roles) {
        final Set<String> roleSet = getAuthoritySet();

        for (final String role : roles) {
            final String defaultedRole = getRoleWithDefaultPrefix(prefix, role);
            if (roleSet.contains(defaultedRole)) {
                return true;
            }
        }

        return false;
    }

    @Override
    public final Authentication getAuthentication() {
        return authentication;
    }

    @Override
    public final boolean permitAll() {
        return true;
    }

    @Override
    public final boolean denyAll() {
        return false;
    }

    @Override
    public final boolean isAnonymous() {
        return trustResolver.isAnonymous(authentication);
    }

    @Override
    public final boolean isAuthenticated() {
        return !isAnonymous();
    }

    @Override
    public final boolean isRememberMe() {
        return trustResolver.isRememberMe(authentication);
    }

    @Override
    public final boolean isFullyAuthenticated() {
        return !trustResolver.isAnonymous(authentication) && !trustResolver.isRememberMe(authentication);
    }

    public Object getPrincipal() {
        return authentication.getPrincipal();
    }

    public void setTrustResolver(AuthenticationTrustResolver trustResolver) {
        this.trustResolver = trustResolver;
    }

    public void setRoleHierarchy(RoleHierarchy roleHierarchy) {
        this.roleHierarchy = roleHierarchy;
    }

    public void setDefaultRolePrefix(String defaultRolePrefix) {
        this.defaultRolePrefix = defaultRolePrefix;
    }

    private Set<String> getAuthoritySet() {
        if (roles == null) {
            roles = new HashSet<String>();
            Collection<? extends GrantedAuthority> userAuthorities = authentication.getAuthorities();

            if (roleHierarchy != null) {
                userAuthorities = roleHierarchy.getReachableGrantedAuthorities(userAuthorities);
            }

            roles = AuthorityUtils.authorityListToSet(userAuthorities);
        }

        return roles;
    }

    @Override
    public boolean hasPermission(Object target, Object permission) {
        return permissionEvaluator.hasPermission(authentication, target, permission);
    }

    @Override
    public boolean hasPermission(Object targetId, String targetType, Object permission) {
        return permissionEvaluator.hasPermission(authentication, (Serializable) targetId, targetType, permission);
    }

    public void setPermissionEvaluator(PermissionEvaluator permissionEvaluator) {
        this.permissionEvaluator = permissionEvaluator;
    }

    private static String getRoleWithDefaultPrefix(String defaultRolePrefix, String role) {
        if (role == null) {
            return role;
        }
        if ((defaultRolePrefix == null) || (defaultRolePrefix.length() == 0)) {
            return role;
        }
        if (role.startsWith(defaultRolePrefix)) {
            return role;
        }
        return defaultRolePrefix + role;
    }

    @Override
    public Object getFilterObject() {
        return this.filterObject;
    }

    @Override
    public Object getReturnObject() {
        return this.returnObject;
    }

    @Override
    public Object getThis() {
        return this;
    }

    @Override
    public void setFilterObject(Object obj) {
        this.filterObject = obj;
    }

    @Override
    public void setReturnObject(Object obj) {
        this.returnObject = obj;
    }
}

4.2 Obtain more control on your resources

We can control the access on a parameter in one of our controller methods like this:

  @PreAuthorize("hasPermission(#contact, 'admin')")
  public void deletePermission(Contact contact, Sid recipient, Permission permission);

So before to eventually delete contact we see if the user has the admin role.

To achieve this you need to enable

In order to obtain this you need to implement one more interface named PermissionEvaluator and provide your implementations of hasPermission

public class CustomPermissionEvaluator implements PermissionEvaluator {

    @Override
    public boolean hasPermission(Authentication auth, Object targetDomainObject, Object permission) {
        if ((auth == null) || (targetDomainObject == null) || !(permission instanceof String)) {
            return false;
        }
        final String targetType = targetDomainObject.getClass().getSimpleName().toUpperCase();
        return hasPrivilege(auth, targetType, permission.toString().toUpperCase());
    }

    @Override
    public boolean hasPermission(Authentication auth, Serializable targetId, String targetType, Object permission) {
        if ((auth == null) || (targetType == null) || !(permission instanceof String)) {
            return false;
        }
        return hasPrivilege(auth, targetType.toUpperCase(), permission.toString().toUpperCase());
    }

    private boolean hasPrivilege(Authentication auth, String targetType, String permission) {
        for (final GrantedAuthority grantedAuth : auth.getAuthorities()) {
            System.out.println("here " + grantedAuth);
            if (grantedAuth.getAuthority().startsWith(targetType)) {
                if (grantedAuth.getAuthority().contains(permission)) {
                    return true;
                }
            }
        }
        return false;
    }
}

4.3 Use privileges along with roles for a more granular control

In order to obtain more control on the access to our resources we can use privileges instead of roles. For example, a user who has not completed the registration process yet might not be given the read permission on a certain resource from start, with the purpose to push him to take action. To do this we can use the PreAuthorize annotation like this: @PreAuthorize("hasPrivilege('read')"). To enable this nice piece of syntactic sugar, you need obviously, as you can see in the code chunk above, to provide an implementation of hasPrivilege, which extends PermissionEvaluator, which by default has just hasPermission in its contract.

4.4 Activate filtering in Security Expressions

In order to activate filtering in Security Expressions, this must be enabled. In order to do so some more configuration is needed.

public class CustomMethodSecurityExpressionHandler extends DefaultMethodSecurityExpressionHandler {
    private final AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();

    @Override
    protected MethodSecurityExpressionOperations createSecurityExpressionRoot(Authentication authentication, MethodInvocation invocation) {
        // final CustomMethodSecurityExpressionRoot root = new CustomMethodSecurityExpressionRoot(authentication);
        final CustomSecurityExpressionRoot root = new CustomSecurityExpressionRoot(authentication);
        root.setPermissionEvaluator(getPermissionEvaluator());
        root.setTrustResolver(this.trustResolver);
        root.setRoleHierarchy(getRoleHierarchy());
        return root;
    }
}

4.5 Let's activate Spring Security ACL annotations

Also @Preauthorize, @PostAuthorize annotations need to be activated

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
    @Override
    protected MethodSecurityExpressionHandler createExpressionHandler() {
        // final DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
        final CustomMethodSecurityExpressionHandler expressionHandler = new CustomMethodSecurityExpressionHandler();
        expressionHandler.setPermissionEvaluator(new CustomPermissionEvaluator());
        return expressionHandler;
    }
}

4.6 Adding custom fields to the JWT Token

We can add some custom fields to our tokens.

public class CustomTokenEnhancer implements TokenEnhancer {

@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
    final Map<String, Object> additionalInfo = new HashMap<>();
    additionalInfo.put("organization", authentication.getName() + randomAlphabetic(4));
    ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
    return accessToken;
}

}

5 Configure Oauth2 Authorization Service

In order to configure different aspects of Oauth2 we need to extend AuthorizationServerConfigurerAdapter. By doing so we can also register within Spring IOC other providers (beans). @Configuration is an annotation that uses CGLIB under the hood. This allows to use singleton instances of these providers giving us the possibility to use them inside the same class. When you don't have this necessity, you can use @Component.

@Configuration
@EnableAuthorizationServer
@DependsOn("authenticationManagerBean")
public class OAuth2AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    @Autowired
    @Qualifier("authenticationManagerBean")
    private AuthenticationManager authenticationManager;

    @Autowired
    @Qualifier("encoder")
    private PasswordEncoder passwordEncoder;


    @Autowired
    private CustomUserDetailsService userDetailsService;


    /*
        tokenKeyAccess, checkTokenAccess take as a parameter one
        of the security expressions defined in CustomSecurityExpressionRoot
     */

    @Override
    public void configure(final AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
        oauthServer.tokenKeyAccess("permitAll()").checkTokenAccess("isAuthenticated()");
    }

    @Override
    public void configure(final ClientDetailsServiceConfigurer clients) throws Exception {

        // A few examples with implementations of different grant types

        clients.inMemory().withClient("sampleClientId").authorizedGrantTypes("implicit").scopes("read", "write", "foo", "bar").autoApprove(false).accessTokenValiditySeconds(3600).redirectUris("http://localhost:8083/")

                .and().withClient("fooClientIdPassword").secret(passwordEncoder.encode("secret")).authorizedGrantTypes("password", "authorization_code", "refresh_token").scopes("foo", "read", "write").accessTokenValiditySeconds(3600)
                // 1 hour
                .refreshTokenValiditySeconds(2592000)
                // 30 days
                .redirectUris("xxx","http://localhost:8089/")

                .and().withClient("barClientIdPassword").secret(passwordEncoder.encode("secret")).authorizedGrantTypes("password", "authorization_code", "refresh_token").scopes("bar", "read", "write").accessTokenValiditySeconds(3600)
                // 1 hour
                .refreshTokenValiditySeconds(2592000) // 30 days

                .and().withClient("testImplicitClientId").authorizedGrantTypes("implicit").scopes("read", "write", "foo", "bar").autoApprove(true).redirectUris("xxx");

    }

    @Bean
    @Primary
    public DefaultTokenServices tokenServices() {
        final DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
        defaultTokenServices.setTokenStore(tokenStore());
        defaultTokenServices.setSupportRefreshToken(true);
        return defaultTokenServices;
    }

    @Override
    public void configure(final AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        final TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        tokenEnhancerChain.setTokenEnhancers(Arrays.asList(tokenEnhancer(), accessTokenConverter()));
        endpoints
        .tokenStore(tokenStore())
        .tokenEnhancer(tokenEnhancerChain)
        .authenticationManager(authenticationManager)
        .userDetailsService(userDetailsService);
    }

    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(accessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        final JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey("123");
        // final KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("mytest.jks"), "mypass".toCharArray());
        // converter.setKeyPair(keyStoreKeyFactory.getKeyPair("mytest"));
        return converter;
    }

    @Bean
    public TokenEnhancer tokenEnhancer() {
        return new CustomTokenEnhancer();
    }
}

6 Customize WebSecurityConfigurerAdapter

@Configuration
@Order(SecurityProperties.DEFAULT_FILTER_ORDER-1)

//@ComponentScan("com.example.springdemo.security")

public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private CustomUserDetailsService userDetailsService;

    @Override
    protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(authenticationProvider());
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/resources/**");
    }

    @Override
    protected void configure(final HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/login/**").permitAll()
                .antMatchers("/api/**").permitAll()
                .antMatchers("/admin").hasRole("ADMIN")
                .and().formLogin()
                .and().csrf().and().httpBasic().disable();
    }

    @Bean
    public DaoAuthenticationProvider authenticationProvider() {
        final DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(userDetailsService);
        authProvider.setPasswordEncoder(encoder());
        return authProvider;
    }

    @Bean
    public PasswordEncoder encoder() {
        return new BCryptPasswordEncoder(11);
    }

    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

In this file we want to configure Spring Security so that WebSecurityConfigurerAdapter does not interfere with AuthorizationServerConfigurerAdapter. Here we have added also a PasswordEncoder which is used when a new user wants to create a new account. Here you can find the link to this project.

Finally here is an image that shows you how to use postman to obtain a token and test if everything is working. I will update this repository as I find out new things.

Postman Example