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:
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:
Thanks
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.
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.
(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).
(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.
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;
}
}
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.
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.