Introduction

Authorizations definition and implementation is one of the important protection measure of an application. They are defined in the creation phase of the project and, even if authorization issues are found when the application is initially released and submitted to a security audit before to go live, the most significant number of issues related to authorization came in the maintenance lifetime of the application.

This situation is often explained by the fact that features are added/modified and no review of the authorizations was performed on the application before the publishing of the new release, for cost or time issue reason.

Context

In order to try to address this situation, it’s can be interesting to automate the evaluation of the authorizations definition and implementation on the application. This, to constantly ensure that implementation of the authorizations in the application is consistent with the authorizations definition.

An authorization is often composed by 2 elements (also named dimensions): The Feature and the Logical Role that can access it (sometime a third dimension named Data is added in order to define a access that include a filtering at business data level).

The representation of the different combinations of these 2 dimensions is often named an Authorization matrix and is often formalized in an Microsoft Excel file.

During a test of an authorization, a Logical Role is also called a Point Of View.

Objective

This article describe a proposition of implementation in order to automate the tests of an authorization matrix.

This article use the assumption that 2 dimensions are used to represents an authorization for the technical proposition described and take as example a application exposing REST services.

The objective is to provide starting ideas/hints in order to create a tailored way of testing of the authorization matrix for the target application.

Proposition

In order to achieve the full automation of the evaluation of the authorization matrix, the following actions has been performed:

  1. Formalize the authorization matrix in a pivot format file allowing:

    1. The processing by a program in a easy way.
    2. To be read and updated by a human for the follow-up of the authorization combinations.
    3. Hierarchy in the information in order to easily materialize the different combinations.
    4. The maximum possible of independence from the technology and design used to implements the application exposing the features.
  2. Create a set of integration tests that fully use the authorization matrix pivot file as input source in order to evaluate the different combinations with:

    1. The minimum possible of maintenance when the authorization matrix pivot file is updated.
    2. A clear indication, in case of failed test, of the source authorization combination that do not respect the authorization matrix.

Authorization matrix pivot file

The XML format has been used to formalize the authorization matrix.

The XML structure contains 3 main sections:

  • Node roles: This node describe the possible logical roles used in the system, is used to provide a list and the explanation of the different roles (authorization level).
  • Node services: This node list and describe the available services exposed by the system and the associated logical role(s) that can call them.
  • Node services-testing: This node provide a test payload for each service if the service use input data other than coming from url or path.

This is an example of the XML used to represents the authorization:

Placeholders (values between {}) are used to mark location where test value must be placed by the integration tests if needed

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <!--
  3. This file materialize the authorization matrix for the different
  4. services exposed by the system.
  5. It will be used by the tests as a input sources for the different tests cases:
  6. 1) Evaluate legitimate access and is correct implementation
  7. 2) Identify not legitimate access (authorization definition issue
  8. on service implementation)
  9. The "name" attribute is used for identify uniquely a SERVICE or a ROLE.
  10. -->
  11. <authorization-matrix>
  12. <!-- Describe the possible logical roles used in the system, is used here to
  13. provide a list+explanation
  14. of the different roles (authorization level) -->
  15. <roles>
  16. <role name="ANONYMOUS"
  17. description="Indicate that no authorization is needed"/>
  18. <role name="BASIC"
  19. description="Role affected to a standard user (lowest access right just above anonymous)"/>
  20. <role name="ADMIN"
  21. description="Role affected to a administrator user (highest access right)"/>
  22. </roles>
  23. <!-- List and describe the available services exposed by the system and the associated
  24. logical role(s) that can call them -->
  25. <services>
  26. <service name="ReadSingleMessage" uri="/{messageId}" http-method="GET"
  27. http-response-code-for-access-allowed="200" http-response-code-for-access-denied="403">
  28. <role name="ANONYMOUS"/>
  29. <role name="BASIC"/>
  30. <role name="ADMIN"/>
  31. </service>
  32. <service name="ReadAllMessages" uri="/" http-method="GET"
  33. http-response-code-for-access-allowed="200" http-response-code-for-access-denied="403">
  34. <role name="ANONYMOUS"/>
  35. <role name="BASIC"/>
  36. <role name="ADMIN"/>
  37. </service>
  38. <service name="CreateMessage" uri="/" http-method="PUT"
  39. http-response-code-for-access-allowed="200" http-response-code-for-access-denied="403">
  40. <role name="BASIC"/>
  41. <role name="ADMIN"/>
  42. </service>
  43. <service name="DeleteMessage" uri="/{messageId}" http-method="DELETE"
  44. http-response-code-for-access-allowed="200" http-response-code-for-access-denied="403">
  45. <role name="ADMIN"/>
  46. </service>
  47. </services>
  48. <!-- Provide a test payload for each service if needed -->
  49. <services-testing>
  50. <service name="ReadSingleMessage">
  51. <payload/>
  52. </service>
  53. <service name="ReadAllMessages">
  54. <payload/>
  55. </service>
  56. <service name="CreateMessage">
  57. <payload content-type="application/json">
  58. {"content":"test"}
  59. </payload>
  60. </service>
  61. <service name="DeleteMessage">
  62. <payload/>
  63. </service>
  64. </services-testing>
  65. </authorization-matrix>

Integration tests

Integration tests are implemented using a maximum of factorized code and one test case by Point Of View (POV) has been created in order to group the verifications by profile of access level (logical role) and facilitate the rendering/identification of the errors.

Parsing, object mapping and access to the authorization matrix information has been implemented using XML marshalling/unmarshalling built-in features provided by the technology used to implements the tests (JAXB here) in order to limit the code to the one in charge of performing the tests.

This the implementation of the integration tests case class:

  1. import org.owasp.pocauthztesting.enumeration.SecurityRole;
  2. import org.owasp.pocauthztesting.service.AuthService;
  3. import org.owasp.pocauthztesting.vo.AuthorizationMatrix;
  4. import org.apache.http.client.methods.CloseableHttpResponse;
  5. import org.apache.http.client.methods.HttpDelete;
  6. import org.apache.http.client.methods.HttpGet;
  7. import org.apache.http.client.methods.HttpPut;
  8. import org.apache.http.client.methods.HttpRequestBase;
  9. import org.apache.http.entity.StringEntity;
  10. import org.apache.http.impl.client.CloseableHttpClient;
  11. import org.apache.http.impl.client.HttpClients;
  12. import org.junit.Assert;
  13. import org.junit.BeforeClass;
  14. import org.junit.Test;
  15. import org.xml.sax.InputSource;
  16. import javax.xml.bind.JAXBContext;
  17. import javax.xml.parsers.SAXParserFactory;
  18. import javax.xml.transform.Source;
  19. import javax.xml.transform.sax.SAXSource;
  20. import java.io.File;
  21. import java.io.FileInputStream;
  22. import java.util.ArrayList;
  23. import java.util.List;
  24. import java.util.Optional;
  25. /**
  26. * Integration Test cases in charge of validate the correct implementation of the authorization matrix.
  27. * Create on test case by logical role that will test access on all services exposed by the system.
  28. * Implements here focus on readability
  29. */
  30. public class AuthorizationMatrixIT {
  31. /**
  32. * Object representation of the authorization matrix
  33. */
  34. private static AuthorizationMatrix AUTHZ_MATRIX;
  35. private static final String BASE_URL = "http://localhost:8080";
  36. /**
  37. * Load the authorization matrix in objects tree
  38. *
  39. * @throws Exception If any error occurs
  40. */
  41. @BeforeClass
  42. public static void globalInit() throws Exception {
  43. try (FileInputStream fis = new FileInputStream(new File("authorization-matrix.xml"))) {
  44. SAXParserFactory spf = SAXParserFactory.newInstance();
  45. spf.setFeature("http://xml.org/sax/features/external-general-entities", false);
  46. spf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
  47. spf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
  48. Source xmlSource = new SAXSource(spf.newSAXParser().getXMLReader(), new InputSource(fis));
  49. JAXBContext jc = JAXBContext.newInstance(AuthorizationMatrix.class);
  50. AUTHZ_MATRIX = (AuthorizationMatrix) jc.createUnmarshaller().unmarshal(xmlSource);
  51. }
  52. }
  53. /**
  54. * Test access to the services from a anonymous user.
  55. *
  56. * @throws Exception
  57. */
  58. @Test
  59. public void testAccessUsingAnonymousUserPointOfView() throws Exception {
  60. //Run the tests - No access token here
  61. List<String> errors = executeTestWithPointOfView(SecurityRole.ANONYMOUS, null);
  62. //Verify the test results
  63. Assert.assertEquals("Access issues detected using the ANONYMOUS USER point of view:\n" + formatErrorsList(errors), 0, errors.size());
  64. }
  65. /**
  66. * Test access to the services from a basic user.
  67. *
  68. * @throws Exception
  69. */
  70. @Test
  71. public void testAccessUsingBasicUserPointOfView() throws Exception {
  72. //Get access token representing the authorization for the associated point of view
  73. String accessToken = generateTestCaseAccessToken("basic", SecurityRole.BASIC);
  74. //Run the tests
  75. List<String> errors = executeTestWithPointOfView(SecurityRole.BASIC, accessToken);
  76. //Verify the test results
  77. Assert.assertEquals("Access issues detected using the BASIC USER point of view:\n " + formatErrorsList(errors), 0, errors.size());
  78. }
  79. /**
  80. * Test access to the services from a administrator user.
  81. *
  82. * @throws Exception
  83. */
  84. @Test
  85. public void testAccessUsingAdministratorUserPointOfView() throws Exception {
  86. //Get access token representing the authorization for the associated point of view
  87. String accessToken = generateTestCaseAccessToken("admin", SecurityRole.ADMIN);
  88. //Run the tests
  89. List<String> errors = executeTestWithPointOfView(SecurityRole.ADMIN, accessToken);
  90. //Verify the test results
  91. Assert.assertEquals("Access issues detected using the ADMIN USER point of view:\n" + formatErrorsList(errors), 0, errors.size());
  92. }
  93. /**
  94. * Evaluate the access to all service using the point of view (POV) specified.
  95. *
  96. * @param pointOfView Point of view to use
  97. * @param accessToken Access token that is linked to the point of view in terms of authorization.
  98. * @return List of errors detected
  99. * @throws Exception If any error occurs
  100. */
  101. private List<String> executeTestWithPointOfView(SecurityRole pointOfView, String accessToken) throws Exception {
  102. List<String> errors = new ArrayList<>();
  103. String errorMessageTplForUnexpectedReturnCode = "The service '%s' when called with POV '%s' return a response code %s that is not the expected one in allowed or denied case.";
  104. String errorMessageTplForIncorrectReturnCode = "The service '%s' when called with POV '%s' return a response code %s that is not the expected one (%s expected).";
  105. String fatalErrorMessageTpl = "The service '%s' when called with POV %s meet the error: %s";
  106. //Get the list of services to call
  107. List<AuthorizationMatrix.Services.Service> services = AUTHZ_MATRIX.getServices().getService();
  108. //Get the list of services test payload to use
  109. List<AuthorizationMatrix.ServicesTesting.Service> servicesTestPayload = AUTHZ_MATRIX.getServicesTesting().getService();
  110. //Call all services sequentially (no special focus on performance here)
  111. services.forEach(service -> {
  112. //Get the service test payload for the current service
  113. String payload = null;
  114. String payloadContentType = null;
  115. Optional<AuthorizationMatrix.ServicesTesting.Service> serviceTesting = servicesTestPayload.stream().filter(srvPld -> srvPld.getName().equals(service.getName())).findFirst();
  116. if (serviceTesting.isPresent()) {
  117. payload = serviceTesting.get().getPayload().getValue();
  118. payloadContentType = serviceTesting.get().getPayload().getContentType();
  119. }
  120. //Call the service and verify if the response is consistent
  121. try {
  122. //Call the service
  123. int serviceResponseCode = callService(service.getUri(), payload, payloadContentType, service.getHttpMethod(), accessToken);
  124. //Check if the role represented by the specified point of view is defined for the current service
  125. Optional<AuthorizationMatrix.Services.Service.Role> role = service.getRole().stream().filter(r -> r.getName().equals(pointOfView.name())).findFirst();
  126. boolean accessIsGrantedInAuthorizationMatrix = role.isPresent();
  127. //Verify behavior consistency according to the response code returned and the authorization configured in the matrix
  128. if (serviceResponseCode == service.getHttpResponseCodeForAccessAllowed()) {
  129. //Roles is not in the list of role allowed to access to the service so it's an error
  130. if (!accessIsGrantedInAuthorizationMatrix) {
  131. errors.add(String.format(errorMessageTplForIncorrectReturnCode, service.getName(), pointOfView.name(), serviceResponseCode,
  132. service.getHttpResponseCodeForAccessDenied()));
  133. }
  134. } else if (serviceResponseCode == service.getHttpResponseCodeForAccessDenied()) {
  135. //Roles is in the list of role allowed to access to the service so it's an error
  136. if (accessIsGrantedInAuthorizationMatrix) {
  137. errors.add(String.format(errorMessageTplForIncorrectReturnCode, service.getName(), pointOfView.name(), serviceResponseCode,
  138. service.getHttpResponseCodeForAccessAllowed()));
  139. }
  140. } else {
  141. errors.add(String.format(errorMessageTplForUnexpectedReturnCode, service.getName(), pointOfView.name(), serviceResponseCode));
  142. }
  143. } catch (Exception e) {
  144. errors.add(String.format(fatalErrorMessageTpl, service.getName(), pointOfView.name(), e.getMessage()));
  145. }
  146. });
  147. return errors;
  148. }
  149. /**
  150. * Call a service with a specific payload and return the HTTP response code received.
  151. * Delegate this step in order to made the test cases more easy to maintain.
  152. *
  153. * @param uri URI of the target service
  154. * @param payloadContentType Content type of the payload to send
  155. * @param payload Payload to send
  156. * @param httpMethod HTTP method to use
  157. * @param accessToken Access token to specify to represent the identity of the caller
  158. * @return The HTTP response code received
  159. * @throws Exception If any error occurs
  160. */
  161. private int callService(String uri, String payload, String payloadContentType, String httpMethod, String accessToken) throws Exception {
  162. int rc;
  163. //Build the request - Use Apache HTTP Client in order to be more flexible in the combination
  164. HttpRequestBase request;
  165. String url = (BASE_URL + uri).replaceAll("\\{messageId\\}", "1");
  166. switch (httpMethod) {
  167. case "GET":
  168. request = new HttpGet(url);
  169. break;
  170. case "DELETE":
  171. request = new HttpDelete(url);
  172. break;
  173. case "PUT":
  174. request = new HttpPut(url);
  175. if (payload != null) {
  176. request.setHeader("Content-Type", payloadContentType);
  177. ((HttpPut) request).setEntity(new StringEntity(payload.trim()));
  178. }
  179. break;
  180. default:
  181. throw new UnsupportedOperationException(httpMethod + " not supported !");
  182. }
  183. request.setHeader("Authorization", (accessToken != null) ? accessToken : "");
  184. //Send the request and get the HTTP response code
  185. try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
  186. try (CloseableHttpResponse httpResponse = httpClient.execute(request)) {
  187. //Don't care here about the response content...
  188. rc = httpResponse.getStatusLine().getStatusCode();
  189. }
  190. }
  191. return rc;
  192. }
  193. /**
  194. * Generate a JWT token the user and role specified.
  195. *
  196. * @param login User login
  197. * @param role Authorization logical role
  198. * @return The JWT token
  199. * @throws Exception If any error occurs during the creation
  200. */
  201. private String generateTestCaseAccessToken(String login, SecurityRole role) throws Exception {
  202. return new AuthService().issueAccessToken(login, role);
  203. }
  204. /**
  205. * Format a list of errors to a printable string
  206. *
  207. * @param errors Error list
  208. * @return Printable string
  209. */
  210. private String formatErrorsList(List<String> errors) {
  211. StringBuilder buffer = new StringBuilder();
  212. errors.forEach(e -> buffer.append(e).append("\n"));
  213. return buffer.toString();
  214. }
  215. }

In case of detection of a authorization issue(s) the output is the following:

  1. testAccessUsingAnonymousUserPointOfView(org.owasp.pocauthztesting.AuthorizationMatrixIT)
  2. Time elapsed: 1.009 s ### FAILURE
  3. java.lang.AssertionError:
  4. Access issues detected using the ANONYMOUS USER point of view:
  5. The service 'DeleteMessage' when called with POV 'ANONYMOUS' return
  6. a response code 200 that is not the expected one (403 expected).
  7. The service 'CreateMessage' when called with POV 'ANONYMOUS' return
  8. a response code 200 that is not the expected one (403 expected).
  9. testAccessUsingBasicUserPointOfView(org.owasp.pocauthztesting.AuthorizationMatrixIT)
  10. Time elapsed: 0.05 s ### FAILURE!
  11. java.lang.AssertionError:
  12. Access issues detected using the BASIC USER point of view:
  13. The service 'DeleteMessage' when called with POV 'BASIC' return
  14. a response code 200 that is not the expected one (403 expected).

Rendering of the authorization matrix for audit / review

Even if the authorization matrix is stored in a human readable format (XML), it can be interesting to provide an on-the-fly rendering representation of the XML file in order to facilitate the review, audit and discussion about the authorization matrix in order to spot potential inconsistencies.

The Following XSL stylesheet can be used:

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
  3. <xsl:template match="/">
  4. <html>
  5. <head>
  6. <title>Authorization Matrix</title>
  7. <link rel="stylesheet"
  8. href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/css/bootstrap.min.css"
  9. integrity="sha384-rwoIResjU2yc3z8GV/NPeZWAv56rSmLldC3R/AZzGRnGxQQKnKkoFVhFQhNUwEyJ"
  10. crossorigin="anonymous" />
  11. </head>
  12. <body>
  13. <h3>Roles</h3>
  14. <ul>
  15. <xsl:for-each select="authorization-matrix/roles/role">
  16. <xsl:choose>
  17. <xsl:when test="@name = 'ADMIN'">
  18. <div class="alert alert-warning" role="alert">
  19. <strong>
  20. <xsl:value-of select="@name" />
  21. </strong>
  22. :
  23. <xsl:value-of select="@description" />
  24. </div>
  25. </xsl:when>
  26. <xsl:when test="@name = 'BASIC'">
  27. <div class="alert alert-info" role="alert">
  28. <strong>
  29. <xsl:value-of select="@name" />
  30. </strong>
  31. :
  32. <xsl:value-of select="@description" />
  33. </div>
  34. </xsl:when>
  35. <xsl:otherwise>
  36. <div class="alert alert-danger" role="alert">
  37. <strong>
  38. <xsl:value-of select="@name" />
  39. </strong>
  40. :
  41. <xsl:value-of select="@description" />
  42. </div>
  43. </xsl:otherwise>
  44. </xsl:choose>
  45. </xsl:for-each>
  46. </ul>
  47. <h3>Authorizations</h3>
  48. <table class="table table-hover table-sm">
  49. <thead class="thead-inverse">
  50. <tr>
  51. <th>Service</th>
  52. <th>URI</th>
  53. <th>Method</th>
  54. <th>Role</th>
  55. </tr>
  56. </thead>
  57. <tbody>
  58. <xsl:for-each select="authorization-matrix/services/service">
  59. <xsl:variable name="service-name" select="@name" />
  60. <xsl:variable name="service-uri" select="@uri" />
  61. <xsl:variable name="service-method" select="@http-method" />
  62. <xsl:for-each select="role">
  63. <tr>
  64. <td scope="row">
  65. <xsl:value-of select="$service-name" />
  66. </td>
  67. <td>
  68. <xsl:value-of select="$service-uri" />
  69. </td>
  70. <td>
  71. <xsl:value-of select="$service-method" />
  72. </td>
  73. <td>
  74. <xsl:variable name="service-role-name" select="@name" />
  75. <xsl:choose>
  76. <xsl:when test="@name = 'ADMIN'">
  77. <div class="alert alert-warning" role="alert">
  78. <xsl:value-of select="@name" />
  79. </div>
  80. </xsl:when>
  81. <xsl:when test="@name = 'BASIC'">
  82. <div class="alert alert-info" role="alert">
  83. <xsl:value-of select="@name" />
  84. </div>
  85. </xsl:when>
  86. <xsl:otherwise>
  87. <div class="alert alert-danger" role="alert">
  88. <xsl:value-of select="@name" />
  89. </div>
  90. </xsl:otherwise>
  91. </xsl:choose>
  92. </td>
  93. </tr>
  94. </xsl:for-each>
  95. </xsl:for-each>
  96. </tbody>
  97. </table>
  98. </body>
  99. </html>
  100. </xsl:template>
  101. </xsl:stylesheet>

Example of the rendering:

RenderingExample

Sources of the prototype

Github repository