Providing OE Shared Data¶
To participate in the OE3 ecosystem as a provider of shared data, a data provider (DP) must expose an HTTP API secured by the Financial Grade API (FAPI) standard. This standard is complex, building as it does on top of OAuth2 and then OpenID Connect, but within the context of Open Energy a provider must:
Require MTLS, requesting a client certificate as part of any API call
Require a bearer token
Validate the supplied token before any other processing is performed
In our case, the third of the above is done by calling the token introspection endpoint of our authorization server and parsing and processing the response to ensure that the supplied token is valid, live, and belongs to the presenting client. Only once these checks have passed should your API process the request (and it may then make use of other information from the token introspection response to determine access control if appropriate).
To simplify this process, the library provides a class ib1.openenergy.support.AccessTokenValidator
which encapsulates
the process of checking that a request has presented a valid access token. When correctly instantiated, the
AccessTokenValidator.introspects
can be used as a decorator on regular Flask routes to automatically perform these
checks before your route handler is called.
Example data provider¶
The example below shows the simplest possible secure application. It configures an instance of
AccessTokenValidator
with:
issuer_url
: The URL of the authorization serverclient_id
: OAuth2 client ID used when making requests to the introspection endpointprivate_key
: Location on disk of the private key used when making requests to the introspection endpointcertificate
: Location on disk of the certificate used when making requests to the introspection endpoint
Note
Although we’re configuring a server here, we need to set up the validator with the necessary credentials to access
the authorization server as a client, hence the client_id
, private_key
and certificate
arguments.
If you’re a participant in Open Energy you should already have this information, if you do not then please ask us!
Client certificate extraction¶
Part of the validation process is to check that the token was originally issued to the same client as is now trying to use it. To do this we compare the thumbprint of the presented client certificate with the corresponding claim in the token introspection response, but there is no standard way for an application server to access the client certificate.
In this example, we are using the default mechanism designed to work with the built-in local SSL runner (see Local Data Provider Development Mode) but in any production context you will not be using this and will need to configure some kind of certificate pass-through. This is typically done by terminating the MTLS connection elsewhere and configuring that termination point to push the certificate information into a header which is then accessible to your application.
To support this, the AccessTokenValidator
takes an additional, optional, initialisation argument:
client_cert_parser
: a Zero argument function which returns an instance ofcryptography.x509.Certificate
containing the presented client certificate.
Using the validator¶
The configured AccessTokenValidator
is used to decorate a simple flask route within the app. The decorator takes an
optional scope
argument, if provided then only tokens containing the specified scope will be regarded as valid.
1import logging
2
3import flask
4
5from ib1.openenergy.support import AccessTokenValidator
6from ib1.openenergy.support.flask_ssl_dev import get_command_line_ssl_args, run_app
7
8logging.basicConfig(level=logging.INFO)
9
10LOG = logging.getLogger('ib1.oe.testapp')
11
12options = get_command_line_ssl_args(default_client_private_key='a.key',
13 default_client_certificate='a.pem',
14 default_server_private_key='127.0.0.1/key.pem',
15 default_server_certificate='127.0.0.1/cert.pem',
16 default_client_id='kZuAsn7UYZ98WWh29hDPf')
17
18validator = AccessTokenValidator(client_id=options.client_id, certificate=options.client_certificate,
19 private_key=options.client_private_key,
20 issuer_url='https://matls-auth.directory.energydata.org.uk/')
21app = flask.Flask(__name__)
22
23
24@app.route('/')
25@validator.introspects(scope='')
26def homepage():
27 """
28 This is a very simple route that doesn't do much, but as it's decorated with the validator
29 token introspection endpoint it will trigger inspection of the supplied bearer token via the
30 directory introspection point, and the resultant object will be passed as flask.g.introspection_response.
31 """
32 LOG.info(f'home: received MTLS HTTPS request from {flask.request.remote_addr}')
33 LOG.info(f'home: token introspection response is {flask.g.introspection_response}')
34 return flask.send_from_directory(directory='/home/tom/Desktop/data-provider',
35 filename='Postcode_level_all_meters_electricity_2019.csv')
36
37
38run_app(app=app,
39 server_private_key=options.server_private_key,
40 server_certificate=options.server_certificate)
The simple route defined on line 26 has been decorated with @validator.introspects(scope='directory:software')
,
within the @app.route('/')
decorator. When this route is accessed, the library will examine the request, checking
for presence of a client certificate, presence of a bearer token, then passing that token to the introspection endpoint
and checking the response for validity. The response is cached for 60 seconds by the library for performance, so
repeated calls from the same client will only be validated once (although token liveness is calculated per call based
on the expiry time specified in the introspection response).
Within your decorated route, you can access the actual introspection response through flask.g
as
flask.g.introspection_response
, you may need to use this to determine whether to grant the client access to a
given piece of data it’s requesting within that route.
Note
This example also includes the necessary extra logic to run locally in dev mode, see Local Data Provider Development Mode for more details. Normally you wouldn’t need the logic on line 12, or the command to run the app on line 37 as you’d be running the Flask app in a server such as gunicorn and behind a front-end system like NGINX.