Using TokenDealer

In Runnerly, the Data Service | Strava worker link (3) is a good example of a place where authentication is required. Adding runs via the Data Service needs to be restricted to authorized services:

Adding authentication for that link is done in four steps:

  1. The TokenDealer keeps a client_id and client_secret pair for the Strava worker and shares it with the Strava worker developers (1).
  2. The Strava worker uses client_id and client_secret to ask a token to the TokenDealer (2).
  3. The Strava worker adds the token in each request against to the Data Service (3).
  4. The Data Service verifies the token by calling the TokenDealer, or by performing a local JWT verification (4)

In a full implementation, the first step is semiautomated. Generating a client secret is usually done through some web admin panel in the authentication service. That secret is then provided to the Strava microservice developers.

From there, the service can get a new token every time it needs it (because it's the first time or because the token is outdated) and add that token in the Authorization header when calling Data Service.

The following is an example of such a call using the requests library--we have in that example a TokenDealer running on localhost:5000 and a Data Service running on localhost:5001.

    import requests 
 
    server = 'http://localhost:5000' 
    secret = 'f0fdeb1f1584fd5431c4250b2e859457' 
 
    data = [('client_id', 'strava'), 
            ('client_secret', secret), 
            ('audience', 'runnerly.io'), 
            ('grant_type', 'client_credentials')] 
 
    def get_token(): 
        headers = {'Content-Type': 'application/x-www-form-urlencoded'} 
        url = server + '/oauth/token' 
        resp = requests.post(url, data=data, headers=headers) 
        return resp.json()['access_token'] 

Notice that the /oauth/token is accepting form encoded data rather than a JSON payload, since this is the standard implementation.

The get_token() function retrieves a token, which can then be used in the Authorization header, when the code calls the Data Service:

 
    _TOKEN = None 
 
    def get_auth_header(new=False): 
        global _TOKEN 
        if _TOKEN is None or new: 
            _TOKEN = get_token() 
        return 'Bearer ' + _TOKEN 
 
    _dataservice = 'http://localhost:5001'

def _call_service(endpoint, token):
# not using session etc, to simplify the reading :)
return requests.get(_dataservice + '/' + endpoint,
headers={'Authorization': token})

def call_data_service(endpoint):
token = get_auth_header()
resp = _call_service(endpoint, token)
if resp.status_code == 401:
# the token might be revoked, let's try with a fresh one
token = get_auth_header(new=True)
resp = _call_service(endpoint, token)
return resp

The call_data_service() function will try to get a new token if the call to the Data Service leads to a 401 response.

This refresh-token-on-401 pattern can be used in all your microservices to automate token generation.

This covers service-to-service authentication. You can find the full implementation in the Runnerly's GitHub repository to play with this JWT-based authentication scheme and use it as a basis for building your authentication process.

The next section of this chapter looks at another important aspect of securing your web services, that is, adding a web application firewall.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset