Best Practices: Authentication
Authentication can generally be defined as the act of confirming the identity of a resource - in this case the consumer of an API. Once a user has been authenticated - they are usually authorized to get access to desired resources/APIs, therefore we can say that.
- Authentication is used to determine who the user of an API is.
- Authorization is used to determine what resources the identified user has access to.
There are multiple standards and technologies available for authenticating users, for example;
- Form-based authentication - Web/HTML based authentication that commonly uses HTTP cookies.
- Basic/Digest/NTLM authentication - Uses HTTP headers to identify users.
- WS-Security SAML and Username Tokens - SOAP/XML based authentication, passes credentials and assertions in SOAP message headers, optionally signed and encrypted
- API Key based authentication - each request to an API contains a key uniquely identifying the client.
- OAuth 1.x/2 - HTTP-based interactions and flows that authorize usage of HTTP resources (API, Web, etc). OAuth indirectly includes a step for authentication but makes no claims on how that authentication should be done.
When relaying authentication credentials and keys over HTTP it should be mandatory to use HTTPS/SSL instead of unencrypted HTTP to avoid “eavesdropping” and stealing of credentials by potential attackers.
When it comes to testing APIs that require authentication there are number of general best practices to consider - both in regard to general test management but also in regard to testing of the authentication mechanism at hand.
Store Credentials Centrally and Safely
If your test scripts include user credentials or access tokens these should be stored centrally so they can be easily changed and safely (preferably encrypted) so no one that gets access to your test scripts can actually read them. Make sure that they don’t show up in log files or test results; for example if you have a test that validates a login - have error messages that conceal the actual username or password.
Authenticate as Real Users Would
Do not create some kind of “super-user” login or access key to be used for testing and demonstration purposes. It is a dubious practice; not using the same kind of authentication mechanism as your users are (as cumbersome as it may be) puts you in the risk of not identifying corresponding issues, for example expiring sessions, unauthorized access errors or errors in the authentication process itself. Therefore, if practically possible please make sure to authenticate with your system in the same way as your users would; don’t create backdoors that hide other issues from your tests.
Create Negative Tests
Make sure you have negative tests in regard to authentication and authorization. Some examples:
- pass invalid usernames and passwords
- attempt to access protected resources without credentials
- attempt to use invalid credentials/session tokens
- provoke lockout of an account and validate the locking logic/timeframe is enforced
- attempt to send (invalid) credentials over non-secured channels, i.e. HTTP instead of HTTPS, un-encrypted XML or JSON, et c.
- ensure that an API client does not get access to resources that he/she does not have access to.
Any of these negative tests should validate that the response message contains relevant error codes and messages, but no information to the client that could be used to compromise the system. For example, a failed login attempt should conceal if the provided username was actually registered in the system. (See the Best Practices section article Negative Testing.)
Design Tests with Authentication in Mind
When designing your tests there are couple of techniques that can make authentication-related flow easier:
- Modularize your tests - if possible move flows used for authentication to a shared script or test and call that from your other tests. This makes it easy to modify authentication if the underlying technology or requirements change.
- Parameterize for environments - if your tests are supposed to run against different environments, (dev, test, staging, production, etc) - make sure that it easy to switch between these.
- Use data-driven techniques for running same tests for a large number of users/credentials - to ensure that these are all handled correctly in regard to access control.
Run As Load/Stress Tests
Any of the above examples should be run as a load-test to make sure that the authentication mechanism of an API isn’t compromisable under extreme load (which is a common strategy for attackers). This could be related to incorrect thread management, database connection sharing, etc. If possible combine different types of authentication tests, for example run both negative authentication and authorization tests simultaneously with tests for bad error messages.