OAuth 2.0 using Spring Security + WSO2 Identity Server

YAM picture YAM · Dec 18, 2014 · Viewed 8.2k times · Source

I'm developing a web application to expose a number of RESTful services secured by OAuth 2.0. Here is the planned architecture:

1- OAuth Authorization Provider: WSO2 Identity Server (IS)

2- OAuth Resource Server: Java web application using the following technologies:

  • Jersey (to implement and expose the web services)
  • Spring Security (to implement the OAuth Resource Server part)

I've seen several examples (ex1, ex2, ex3, etc...) on how to secure RESTful services using WSO2 IS as an authorization server + WSO2 ESB as a resource server. This is NOT what I need in my case.

Unfortunately, the interaction between the authorization server and the resource server is beyond the scope of the OAuth2 RFC. So, I couldn't find much about how should it look like.

Here are my questions:

  • How to configure spring security to act as a resource server to validate an access token issued by an external OAuth provider (e.g. WSO2 IS)?
  • How should the resource server identify the scope of a given access token?
  • How to identify the resource owner given an access token from WSO2 IS?

Thanks

Answer

YAM picture YAM · Mar 23, 2015

After doing some research, I figured out how to do it. The solution is divided into 2 main parts: WSO2 IS configuration & Resources server configuration.

The basic scenario goes as follows:

1- A client (e.g. mobile app) consume a secured resource (e.g. web service) by sending a request to the resources sever (Java web application in my case).

2- The resources server validates the "Authorization" header in the request and extracts the access token.

3- The resources server validates the access token by sending it to the authorization server (WSO2 IS).

4- The authorization server responds with validation response.

5- The resources server validates the response and decides whether to grant or deny access to the requested resource.

In my demo, I used WSO2 IS 5.0.0 and Spring security 3.1.0.


1- WSO2 IS Configuration

WSO2 IS will act as the authorization server. So, it should be configured to support OAuth 2.0. To do so, a new service provider should be added and configured as follows:

(a) Login to WSO2 IS management console.

(b) Add a new service provider and give it a name and description.

Service provider configuration: screenshot 1

(c) Under Inbound Authentication Configuration >> OAuth/OpenID Connect Configuration >> Click Configure.

(d) Configure OAuth 2.0 provider as shown in the below screenshot and click Add. We'll need Password grant type which maps to Resource Owner Password Credentials grant type. It is best suited for my case (securing web services).

Service provider configuration: screenshot 2

(e) Under OAuth/OpenID Connect Configuration, you'll find OAuth Client Key and OAuth Client Secret generated. They are used along with username, password, and scope to generate access tokens.


2- Resources Server Configuration

As mentioned earlier, the demo Java web application will act as Resources server and client at the same time. To act as resources server, Spring security needs to know how to validate access tokens. So, a token services implementation should be provided.

(a) Configure spring to act as resources server. Here is a sample configuration:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xmlns:security="http://www.springframework.org/schema/security"
   xmlns:oauth2="http://www.springframework.org/schema/security/oauth2"
   xsi:schemaLocation="
    http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
    http://www.springframework.org/schema/security
    http://www.springframework.org/schema/security/spring-security-3.1.xsd
    http://www.springframework.org/schema/security/oauth2
    http://www.springframework.org/schema/security/spring-security-oauth2.xsd">

    <bean id="tokenServices" class="com.example.security.oauth2.wso2.TokenServiceWSO2" />

    <bean id="authenticationEntryPoint" class="org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint" />

    <security:authentication-manager alias="authenticationManager" />

    <oauth2:resource-server id="resourcesServerFilter" token-services-ref="tokenServices" />

    <security:http pattern="/services/**" create-session="stateless" entry-point-ref="authenticationEntryPoint" >
        <security:anonymous enabled="false" />
        <security:custom-filter ref="resourcesServerFilter" before="PRE_AUTH_FILTER" />
        <security:intercept-url pattern="/services/**" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
    </security:http>
</beans>

Here, a resource-server that uses a token services implementation TokenServiceWSO2 is configured. The resource-server tag is actually transformed to a security filter. An interception pattern is added to "/services/**" and the resources sever filter is added to the chain.

(b) Implement OAuth 2.0 token services ResourceServerTokenServices. The implementation will take an access token as an input, pass it to OAuth2TokenValidationService service exposed by WSO2 IS, validate the response and return a processed object containing the basic data about the token's issuer, validity, scope, corresponding JWT token, ...

public class TokenServiceWSO2 implements ResourceServerTokenServices {

    @Autowired
    TokenValidatorWSO2 tokenValidatorWSO2;

    public OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException {

        try {
            TokenValidationResponse validationResponse = tokenValidatorWSO2.validateAccessToken(accessToken);
            OAuth2Request oAuth2Request = new OAuth2Request(null, null, null, true, validationResponse.getScope(), null, null, null,null);
            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(validationResponse.getAuthorizedUserIdentifier(), null, null);
            OAuth2Authentication oAuth2Authentication = new OAuth2Authentication(oAuth2Request, authentication);
            return oAuth2Authentication;
        } catch (ApplicationException ex) {
            // Handle exception
        }
    }

    public OAuth2AccessToken readAccessToken(String accessToken) {
        // TODO Add implementation
    }

}

TokenValidatorWSO2 class implements the logic to call WSO2 IS's web service OAuth2TokenValidationService

@Component
public class TokenValidatorWSO2 implements OAuth2TokenValidator{

    private static final Logger logger = Logger.getLogger(TokenValidatorWSO2.class);

    @Value("${server_url}")
    private String serverUrl;

    @Value("${validation_service_name}")
    private String validationServiceName;

    @Value("${comsumer_key}")
    private String consumerKey;

    @Value("${admin_username}")
    private String adminUsername;

    @Value("${admin_password}")
    private String adminPassword;

    private OAuth2TokenValidationServiceStub stub;

    private static final int TIMEOUT_IN_MILLIS = 15 * 60 * 1000;

    public TokenValidationResponse validateAccessToken(String accessToken) throws ApplicationException {
        logger.debug("validateAccessToken(String) - start");

        if(stub == null) {
            initializeValidationService();
        }

        OAuth2TokenValidationRequestDTO  oauthRequest;
        TokenValidationResponse validationResponse;
        OAuth2TokenValidationRequestDTO_OAuth2AccessToken oAuth2AccessToken;

        try {
            oauthRequest = new OAuth2TokenValidationRequestDTO();
            oAuth2AccessToken = new OAuth2TokenValidationRequestDTO_OAuth2AccessToken();
            oAuth2AccessToken.setIdentifier(accessToken);
            oAuth2AccessToken.setTokenType("bearer");
            oauthRequest.setAccessToken(oAuth2AccessToken);
            OAuth2TokenValidationResponseDTO response = stub.validate(oauthRequest);

            if(!response.getValid()) {
                throw new ApplicationException("Invalid access token");
            }

            validationResponse = new TokenValidationResponse();
            validationResponse.setAuthorizedUserIdentifier(response.getAuthorizedUser());
            validationResponse.setJwtToken(response.getAuthorizationContextToken().getTokenString());
            validationResponse.setScope(new LinkedHashSet<String>(Arrays.asList(response.getScope())));
            validationResponse.setValid(response.getValid());

        } catch(Exception ex) {
            logger.error("validateAccessToken() - Error when validating WSO2 token, Exception: {}", ex);
        }

        logger.debug("validateAccessToken(String) - end");
        return validationResponse;
    }

    private void initializeValidationService() throws ApplicationException {
        try {
            String serviceURL = serverUrl + validationServiceName;
            stub = new OAuth2TokenValidationServiceStub(null, serviceURL);
            CarbonUtils.setBasicAccessSecurityHeaders(adminUsername, adminPassword, true, stub._getServiceClient());
            ServiceClient client = stub._getServiceClient();
            Options options = client.getOptions();
            options.setTimeOutInMilliSeconds(TIMEOUT_IN_MILLIS);
            options.setProperty(HTTPConstants.SO_TIMEOUT, TIMEOUT_IN_MILLIS);
            options.setProperty(HTTPConstants.CONNECTION_TIMEOUT, TIMEOUT_IN_MILLIS);
            options.setCallTransportCleanup(true);
            options.setManageSession(true);
        } catch(AxisFault ex) {
            // Handle exception
        }
    }
}

TokenValidationResponse class holds the basic data returned in token validation response.

public class TokenValidationResponse {

    private String jwtToken;
    private boolean valid;
    private Set<String> scope;
    private String authorizedUserIdentifier;

    public String getJwtToken() {
        return jwtToken;
    }

    public void setJwtToken(String jwtToken) {
        this.jwtToken = jwtToken;
    }

    public boolean isValid() {
        return valid;
    }

    public void setValid(boolean valid) {
        this.valid = valid;
    }

    public Set<String> getScope() {
        return scope;
    }

    public void setScope(Set<String> scope) {
        this.scope = scope;
    }

    public String getAuthorizedUserIdentifier() {
        return authorizedUserIdentifier;
    }

    public void setAuthorizedUserIdentifier(String authorizedUserIdentifier) {
        this.authorizedUserIdentifier = authorizedUserIdentifier;
    }
}

3- Client Application Configuration

The last step is to configure the resources to be protected by OAuth 2.0. Basically, configure the web services to be secured with a root URL path "/services/**". In my demo, I used Jersey.


4- Test The Client Application

The last step is to consume the secured web services. This is done by adding Authorization header to the request with value " ", for example "bearer 7fbd71c5b28fdf0bdb922b07915c4d5".


P.S. The described sample is just for clarification purposes. It may be missing some implementations, exception handling, ... Kindly comment for further inquiries.