Exemple #1
0
 def get_fapi_session(self) -> FAPISession:
     """
     Restore a FAPISession from the session store
     """
     if FAPI_SESSION_KEY not in session:
         raise ValueError('no fapi session in requests session')
     f = FAPISession.from_jwt(
         session.get(FAPI_SESSION_KEY),
         secret=self.app.config[JWT_SYMMETRIC_KEY_CONFIG],
         openid_configuration=self.app.config['OPENID_CONFIG'])
     f.auth_method = self._get_code_auth()
     return f
Exemple #2
0
    def __init__(self,
                 app,
                 client_id: str,
                 client_key: str,
                 client_cert: str,
                 issuer_url: str,
                 auth_url: str,
                 scopes: List[str],
                 login_path: str = 'login',
                 login_callback_path: str = 'login/callback',
                 directory_base_uri:
                 str = 'https://matls-dirapi.directory.energydata.org.uk/'):
        """
        Configure login and callback routes on the supplied app, and expose an @fapi decorator which will use these
        to acquire a `FAPISession` and insert it into flask.g.fapi

        :param app:
            flask app to decorate
        :param client_id:
            client ID
        :param client_key:
            file location of the client private key used for MTLS
        :param client_cert:
            file location of the client certificate used for MTLS
        :param issuer_url:
            issuer URL, used for open ID discovery
        :param auth_url:
            auth URL, this is different from the auth URL in the discovery block in our case so specify it explicitly
        :param scopes:
            list of string scopes to request
        :param login_path:
            path to insert the login route, defaults to 'login'
        :param login_callback_path:
            path to insert the login callback route (GET and POST forms), defaults to 'login/callback'
        """
        app.config[JWT_SYMMETRIC_KEY_CONFIG] = code_verifier()
        self.app = app
        self.login_path = login_path
        _fapi = FAPISession(client_id=client_id,
                            private_key=client_key,
                            certificate=client_cert,
                            issuer_url=issuer_url,
                            requested_scopes='directory:software')
        _directory = RaidiamDirectory(fapi=_fapi, base_url=directory_base_uri)
        app.config['OPENID_CONFIG'] = _fapi.openid_configuration

        class SecurePyJWKClient(jwt.PyJWKClient):
            def __init__(self):
                super().__init__(uri=_fapi.openid_configuration.jwks_uri)

            def fetch_data(self) -> Any:
                LOG.debug(f'fetching JWKS from {self.uri}')
                response = _fapi.plain_session.get(self.uri)
                response.raise_for_status()
                return response.json()

        _jwk_client = SecurePyJWKClient()

        @app.route('/' + login_path)
        def login():
            """
            Create a `CodeAuthMethod`, store it in the session, then use it to build a redirect to the appropriate
            auth endpoint, returning the redirect.
            """
            code_auth = CodeAuthMethod(redirect_uri=request.root_url +
                                       login_callback_path,
                                       client_id=client_id,
                                       issuer_uri=issuer_url)
            self._store_code_auth(code_auth)
            return redirect(location=code_auth.get_auth_uri(scopes=scopes,
                                                            auth_uri=auth_url))

        @app.route('/' + login_callback_path, methods=['GET'])
        def callback_inner():
            """
            Receives the response JWT as a URL fragment, renders a template which has a very simple form and
            bit of javascript to retrieve the URL fragment (which is only accessible on the client side) and
            POST it to the callback() function below.
            """
            return """<!doctype html><html lang="en-gb"><body><form method='post' id='fragment_form'>
                      <input type='hidden' name='fragment'/></form>
                      <script>
                        form = document.getElementById('fragment_form');
                        form.fragment.value = window.location.hash;
                        history.pushState("", document.title, window.location.pathname
                                                               + window.location.search);
                        form.submit()
                      </script></body></html>"""

        @app.route('/' + login_callback_path, methods=['POST'])
        def callback():
            """
            Handle the callback from the javascript in the page from callback_inner, this posts the URL fragment
            to *this* route, passing the entire fragment in through a form. We can then attempt to pull the JWT
            out of this data and parse it to get success or failure messages along with the necessary details
            to acquire a code
            """
            code_auth = self._get_code_auth()
            # Check we have a valid session
            if code_auth is None or not isinstance(code_auth, CodeAuthMethod):
                raise ValueError(
                    'no code_auth defined in session, or invalid type!')

            def parse_urlhash(u: str) -> Dict:
                """
                Parse a fragment string (which may start with an # or not) into multiple key=value
                pairs, returning the result as a dict
                """
                if u.startswith('#'):
                    u = u[1:]
                return {
                    item[0]: item[1]
                    for item in [p.split('=') for p in u.split('&')]
                }

            # Pull the fragment out of the POSTed data
            if request.form.get('fragment'):
                urlhash = parse_urlhash(request.form.get('fragment'))
                encoded_jwt = urlhash['response']

                # https://openid.net/specs/openid-financial-api-jarm.html#processing-rules
                # 1. Decrypt (optional) - JWTs in our case are not encrypted, so no need
                # for any processing here
                # 6. Obtain key needed to verify, use it to decode the JWT with verification
                # enabled. Also checks (3) iss, (4) client ID match to aud, (5) exp within range

                # TODO - this should be set to True to comply with JARM, but can't find the certs for now
                verify = False
                if verify:
                    signing_key = _jwk_client.get_signing_key_from_jwt(
                        encoded_jwt)
                    decoded_jwt = jwt.decode(
                        jwt=encoded_jwt,
                        key=signing_key.key,
                        algorithms=['PS256'],
                        audience=client_id,
                        issuer=issuer_url,
                        options={
                            'require': ['state', 'iss', 'aud', 'exp'],
                            'verify_signature': True
                        })
                else:
                    decoded_jwt = jwt.decode(
                        jwt=encoded_jwt,
                        audience=client_id,
                        issuer=issuer_url,
                        options={
                            'require': ['state', 'iss', 'aud', 'exp'],
                            'verify_signature': False
                        })

                # (2) Check state parameter, invalidate the code auth state, and fail if mismatch
                jwt_state = decoded_jwt['state']
                auth_state = code_auth.state
                code_auth.invalidate_state()
                if auth_state != jwt_state:
                    raise ValueError('state property mismatch')

                d = {'args': request.args, 'jwt': decoded_jwt}
                LOG.info(d)
                code_auth.code = decoded_jwt['code']
            else:
                # No fragment available, complain
                raise ValueError('response not present')

            # If we get here we should have a correctly configured CodeAuthMethod, so use it to build a FAPISession
            f = FAPISession(client_id=client_id,
                            private_key=client_key,
                            certificate=client_cert,
                            issuer_url=issuer_url,
                            requested_scopes=' '.join(scopes),
                            auth_method=code_auth,
                            openid_configuration=app.config['OPENID_CONFIG'])
            # Use the code to get an access token and stash the FAPISession in the requests session
            _ = f.access_token
            self._store_fapi_session(f)

            # Redirect to wherever it was we were going originally
            return redirect(location=session[REDIRECT_AFTER_LOGIN_KEY])
Exemple #3
0
 def _store_fapi_session(self, fapi: FAPISession):
     """
     Store the FAPISession metadata in the session as an encrypted JWT
     """
     session[FAPI_SESSION_KEY] = fapi.as_jwt(
         secret=self.app.config[JWT_SYMMETRIC_KEY_CONFIG])
Exemple #4
0
        def callback():
            """
            Handle the callback from the javascript in the page from callback_inner, this posts the URL fragment
            to *this* route, passing the entire fragment in through a form. We can then attempt to pull the JWT
            out of this data and parse it to get success or failure messages along with the necessary details
            to acquire a code
            """
            code_auth = self._get_code_auth()
            # Check we have a valid session
            if code_auth is None or not isinstance(code_auth, CodeAuthMethod):
                raise ValueError(
                    'no code_auth defined in session, or invalid type!')

            def parse_urlhash(u: str) -> Dict:
                """
                Parse a fragment string (which may start with an # or not) into multiple key=value
                pairs, returning the result as a dict
                """
                if u.startswith('#'):
                    u = u[1:]
                return {
                    item[0]: item[1]
                    for item in [p.split('=') for p in u.split('&')]
                }

            # Pull the fragment out of the POSTed data
            if request.form.get('fragment'):
                urlhash = parse_urlhash(request.form.get('fragment'))
                encoded_jwt = urlhash['response']

                # https://openid.net/specs/openid-financial-api-jarm.html#processing-rules
                # 1. Decrypt (optional) - JWTs in our case are not encrypted, so no need
                # for any processing here
                # 6. Obtain key needed to verify, use it to decode the JWT with verification
                # enabled. Also checks (3) iss, (4) client ID match to aud, (5) exp within range

                # TODO - this should be set to True to comply with JARM, but can't find the certs for now
                verify = False
                if verify:
                    signing_key = _jwk_client.get_signing_key_from_jwt(
                        encoded_jwt)
                    decoded_jwt = jwt.decode(
                        jwt=encoded_jwt,
                        key=signing_key.key,
                        algorithms=['PS256'],
                        audience=client_id,
                        issuer=issuer_url,
                        options={
                            'require': ['state', 'iss', 'aud', 'exp'],
                            'verify_signature': True
                        })
                else:
                    decoded_jwt = jwt.decode(
                        jwt=encoded_jwt,
                        audience=client_id,
                        issuer=issuer_url,
                        options={
                            'require': ['state', 'iss', 'aud', 'exp'],
                            'verify_signature': False
                        })

                # (2) Check state parameter, invalidate the code auth state, and fail if mismatch
                jwt_state = decoded_jwt['state']
                auth_state = code_auth.state
                code_auth.invalidate_state()
                if auth_state != jwt_state:
                    raise ValueError('state property mismatch')

                d = {'args': request.args, 'jwt': decoded_jwt}
                LOG.info(d)
                code_auth.code = decoded_jwt['code']
            else:
                # No fragment available, complain
                raise ValueError('response not present')

            # If we get here we should have a correctly configured CodeAuthMethod, so use it to build a FAPISession
            f = FAPISession(client_id=client_id,
                            private_key=client_key,
                            certificate=client_cert,
                            issuer_url=issuer_url,
                            requested_scopes=' '.join(scopes),
                            auth_method=code_auth,
                            openid_configuration=app.config['OPENID_CONFIG'])
            # Use the code to get an access token and stash the FAPISession in the requests session
            _ = f.access_token
            self._store_fapi_session(f)

            # Redirect to wherever it was we were going originally
            return redirect(location=session[REDIRECT_AFTER_LOGIN_KEY])
from ib1.openenergy.support.metadata import Metadata, load_metadata
from ib1.openenergy.support.raidiam import Organisation, AuthorisationServer

LOG = logging.getLogger('ib1.sample_metadata_harvester')

logging.basicConfig(format='%(asctime)s %(levelname)-8s %(message)s',
                    level=logging.INFO,
                    datefmt='%Y-%m-%d %H:%M:%S')

httpclient_logging_patch(level=logging.DEBUG)

# Set up a session, this will get a token from the directory when needed

f = FAPISession(client_id='kZuAsn7UYZ98WWh29hDPf',
                issuer_url='https://matls-auth.directory.energydata.org.uk',
                requested_scopes='directory:software foo',
                private_key='/home/tom/Desktop/certs/a.key',
                certificate='/home/tom/Desktop/certs/a.pem')

# Create a client to the directory
directory = RaidiamDirectory(
    fapi=f, base_url='https://matls-dirapi.directory.energydata.org.uk/')

# Get all organisations from the directory
organisations: List[Organisation] = directory.organisations()

# Fetch all authorisation servers for organisations within the directory, building a
# map of org ID to list of authorisation server objects
orgid_to_auth: Dict[str, List[AuthorisationServer]] = {
    org.organisation_id:
    directory.authorisation_servers(org_id=org.organisation_id)
Exemple #6
0
def get_directory_client(parser=None) -> RaidiamDirectory:
    """
    Parse arguments and get a directory client

    :param parser:
        Existing parser to use, None by default will create a new one
    :return:
        A `RaidiamDirectory` client
    """
    def check_file(f, name):
        """
        Check that a file exists, logging complaints and returning false if it doesn't. If the argument
        is None then interpret as optional.
        """
        if f:
            path = abspath(f)
            file_found = isfile(path)
            if not file_found:
                LOG.error(f'SSL - {name} = {path} not found!')
            else:
                LOG.info(f'SSL - {name} = {path}')
            return file_found
        else:
            return True

    parser = parser or ArgumentParser()
    parser.add_argument('-k',
                        '--client_private_key',
                        type=str,
                        help='Client private key file location',
                        default='/home/tom/Desktop/certs/a.key')
    parser.add_argument('-c',
                        '--client_certificate',
                        type=str,
                        help='Client certificate file location',
                        default='/home/tom/Desktop/certs/a.pem')
    parser.add_argument('-id',
                        '--client_id',
                        type=str,
                        help='OAuth2 client ID for calls made from this app',
                        default='kZuAsn7UYZ98WWh29hDPf')
    parser.add_argument(
        '-iu',
        '--issuer_url',
        type=str,
        help='Issuer URL for auth service',
        default='https://matls-auth.directory.energydata.org.uk')
    parser.add_argument(
        '-u',
        '--directory_url',
        type=str,
        help='Root directory URL',
        default='https://matls-dirapi.directory.energydata.org.uk/')
    options = parser.parse_args()
    check_file(options.client_certificate, name='client cert')
    check_file(options.client_private_key, name='client key')
    f = FAPISession(client_id=options.client_id,
                    issuer_url=options.issuer_url,
                    requested_scopes='directory:software',
                    private_key=options.client_private_key,
                    certificate=options.client_certificate)
    return RaidiamDirectory(fapi=f, base_url=options.directory_url)