Authentication and authorization in REST Services

2019-01-25 20:36发布

问题:

We are building some services that will be exposed through a RESTful API. Primary customers of this API are Liferay portlets using Angular JS, meaning there are direct calls from client-side (Angular) to our services.

So far we have designed an authentication and authorization mechanism to assure that we can identify which logged user (Liferay) is requesting our API.

PS.: note that although we are using Liferay, it could be any other Java based application instead.

What we have designed is:

  1. When the user logs in in our portal, Liferay creates an authentication token with userLogin (or ID) + client IP + timestamp. This token is saved in a cookie;
  2. Before every REST call, Angular reads this cookie and sends its contents via a HTTP header;
  3. Our service "decrypts" the cookie content sent and verifies if the timestamp is valid, the IP is the same and, according to our business rules, if the user has access to do or read whatever he wants to.

This design looks consistent to us right now and, depending on the algorithm we choose to create this token, we believe it is a secure approach.

Our doubts are:

  • Are we, somehow, reinventing the wheel not using HTTP authentication with some kind of custom provider? How to?
  • Could Spring Security help us with that? We have read some articles about it but it's not clear if it makes sense to use it with a non-Spring application;
  • Are there any security flaws we have not considered with this approach?

Thank you in advance. Any help is appreciated.

Filipe

回答1:

Spring security solves the problem description, and as a bonus you will get all the spring security features for free.

The Token approach is great and here is how you can secure your APIs with spring-security Implements AuthenticationEntryPoint and have the commence method set 401 instead of re-direction 3XX as follows

httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED,"Access Denied");
  • Have a TokenProcessingFilter extend and leverage what UsernamePasswordAuthenticationFilter has to offer, override the doFilter() method, extract the the token from the request headers, validate and Authenticate the token as follows

@Override

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
                HttpServletRequest httpRequest = this.getAsHttpRequest(request);
                String authToken = this.extractAuthTokenFromRequest(httpRequest);
                String userName = TokenUtils.getUserNameFromToken(authToken);
                if (userName != null) {
                UserDetails userDetails = userDetailsService.loadUserByUsername(userName);

                    if (TokenUtils.validateToken(authToken, userDetails)) {
                        UsernamePasswordAuthenticationToken authentication =new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                        authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpRequest));
                        SecurityContextHolder.getContext().setAuthentication(authentication);
                    }
                }
                chain.doFilter(request, response);
            }

Your Spring-security configuration will look like

    @Configuration
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {

        @Autowired
        private AuthFailure authFailure;

        @Autowired
        private AuthSuccess authSuccess;

        @Autowired
        private EntryPointUnauthorizedHandler unauthorizedHandler;

        @Autowired
        private UserDetailsService userDetailsService;

        @Autowired
        private AuthenticationTokenProcessingFilter authTokenProcessingFilter;

        @Autowired
        public void configureAuthBuilder(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
        }

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

        @Bean public PasswordEncoder passwordEncoder(){
            return new BCryptPasswordEncoder();
        }
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                    .csrf().disable()
                    .sessionManagement()
                       .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // Restful hence stateless
                     .and()
                    .exceptionHandling()
                    .authenticationEntryPoint(unauthorizedHandler) // Notice the entry point
                    .and()
                    .addFilter(authTokenProcessingFilter) // Notice the filter
                    .authorizeRequests()
                       .antMatchers("/resources/**", "/api/authenticate").permitAll()                 
                       //.antMatchers("/admin/**").hasRole("ADMIN")
                       //.antMatchers("/providers/**").hasRole("ADMIN") 
                    .antMatchers("/persons").authenticated();
        }

}

-- Last you will need another end point for Authentication and token-generation Here is a spring MVC example

@Controller
@RequestMapping(value="/api")
public class TokenGenerator{
    @Autowired
    @Lazy
    private  AuthenticationManager authenticationManager;

    @Autowired
    private  UtilityBean utilityBean;

    @Autowired
    private  UserDetailsService userDetailsService;


    @RequestMapping(value="/authenticate", method=RequestMethod.POST, consumes=MediaType.APPLICATION_JSON_VALUE)
    ResponseEntity<?> generateToken(@RequestBody EmefanaUser user){
        ResponseEntity<?> response = null;
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserId(),user.getCredential());

        try {
            Authentication authentication = authenticationManager.authenticate(authenticationToken);
            SecurityContextHolder.getContext().setAuthentication(authentication);

            /*
             * Reload user as password of authentication principal will be null
             * after authorization and password is needed for token generation
             */
            UserDetails userDetails = userDetailsService.loadUserByUsername(user.getUserId());
            String token = TokenUtils.createToken(userDetails);
            response = ResponseEntity.ok(new TokenResource(utilityBean.encodePropertyValue(token)));
        } catch (AuthenticationException e) {
            response = ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
        }
        return response;
    } 

}

1 Generate token, 2. subsequent API-calls should have the token Yes spring-security can do this and you don`t have to break new grounds in Authentication, Authorization.

  • Hope this helps


回答2:

Take a look at Single Sign On and Spring Security OAuth2 token authentication.

Here is example: sso-with-oauth2-angular-js-and-spring-security.

Note that Spring 4.2 might have some handy CORS support.



回答3:

I can't uprate someone's answer with my current rating but The answer above is probably the right direction. It sounds like what you need to investigate is something named CORS which provides security with cross site scripting. I'm sorry I don't quite know how it works yet (I'm in the same situation) but this is the main topic of this NSA document on REST

For Spring, try here to start maybe?



回答4:

I'm late to the party but here are my two cents.

Disclaimer: The previous answers are a possible way to tackle this. The next insight is what I've learned while implementing RESTful APIs in Liferay.

If I understand correctly the question then you have two scenarios here. The first one is you need to create a RESTful api that will be called by already Logged in users. This means that the AJAX calls will, probably, get execute within the client's renderization of the portal. The main issue here is the security, how to secure yous REST calls. First of all I think one should try to leverage on whatever framework one is using before implementing something else. Liferay DOES uses Spring in the backend but they've already implemented security. I would recommend to use the Delegate Servlet. This servlet will execute any custom class and put it inside Liferay's Authentication path, meaning that you could just use PortalUtil.getUser(request) and if it's 0 or null then the user is not authenticated. In order to use the delegate servlet you just need to configure it in your web.xml file

<servlet>
    <servlet-name>My Servlet</servlet-name>
    <servlet-class>com.liferay.portal.kernel.servlet.PortalDelegateServlet</servlet-class>
    <init-param>
        <param-name>servlet-class</param-name>
        <param-value>com.samples.MyClass</param-value>
    </init-param>
    <init-param>
        <param-name>sub-context</param-name>
        <param-value>api</param-value>
    </init-param>
    <load-on-startup>3</load-on-startup>
</servlet>

As you can see we are instantiating another servlet. This servlet is going to be defined by the PortalDelegateServlet. The Delegate Servlet will use whatever class is on the value of the sevlet-class param. Within that class you can just check if there's a valid username in the HttpServletRequest object with Liferay's Utils and if there is then the user is OK to go. Now, the way you access this is that the Delegate Servlet uses the value of the sub-context to know which class are you refering to from the URL. So, in this example you'll be access com.samples.MyClass by going to https://my.portal/delegate/api The 'delegate' part will always be there, the second part of the URL is what we define in the init-param. Notice that you can only define one level of the URI for sub-context, i.e. you can't set /api/v2.0/ as sub-context. From then on you can do whatever you want on your servlet class and handle the parsing of the REST URI as you want.

You can also use spring's Dispatcher class as the class that the Delegate Servlet will call and just setup a spring servlet, hence having url annotation mappins.

It is important to know that this is only good for RESTful or Resource serving, since the Delegate Servlet will not know how to handle renderization of views.

The second scenario you have is to be able to call this RESTful API from any external application (doesn't matter what implementation they have). This is an entire different beast and I would have to reference the answer by iamiddy and using Spring's Authentication Token could be a nice way to do this.

Another way to do this, would be to handle unauthorized users in your servlet class by sending them to the login page or something of the sort. Once they succesfully login Liferay's Utils should recognize the authenticated user with the request. If you want to do this within an external application then you would need to mock a form-based login and just use the same cookie jar for the entire time. Although I haven't tried this, in theory it should work. Then again, in theory, communism works.

Hope this help some other poor soul out there.