openidconnect.py 8.96 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import logging
from datetime import datetime

import jwt
from oic import rndstr
from oic.extension.message import TokenIntrospectionRequest, TokenIntrospectionResponse
from oic.oic import Client, Grant, Token
from oic.oic.message import AccessTokenResponse, Message, RegistrationResponse
from oic.utils import time_util
from oic.utils.authn.client import CLIENT_AUTHN_METHOD

log = logging.getLogger(__name__)


class OIDCmanager:
    """
    Class that interfaces with PyOIDC

    It is supposed to have a unique instance which provides all operations that require
    information from the OIDC issuers.
    """

    def __init__(self):
        self.clients = {}
        self.config = None

    def setup(self, config):
        self.config = config
        self._configure_clients(config['fts3.Providers'])
30
        self._set_keys_cache_time(int(config.get('fts3.JWKCacheSeconds', 86400)))
31
32
33
34
35
36
37
38
39
40
41
42
        self._retrieve_clients_keys()

    def _configure_clients(self, providers_config):
        # log.debug('provider_info::: {}'.format(client.provider_info))
        for provider in providers_config:
            client = Client(client_authn_method=CLIENT_AUTHN_METHOD)
            # Retrieve well-known configuration
            client.provider_config(provider)
            # Register
            client_reg = RegistrationResponse(client_id=providers_config[provider]['client_id'],
                                              client_secret=providers_config[provider]['client_secret'])
            client.store_registration_info(client_reg)
43
44
            issuer = client.provider_info['issuer']
            self.clients[issuer] = client
45
46
47
48
49
50
51
52
53
54
55
56
57

    def _retrieve_clients_keys(self):
        for provider in self.clients:
            client = self.clients[provider]
            client.keyjar.get_issuer_keys(provider)

    def _set_keys_cache_time(self, cache_time):
        for provider in self.clients:
            client = self.clients[provider]
            keybundles = client.keyjar.issuer_keys[provider]
            for keybundle in keybundles:
                keybundle.cache_time = cache_time

58
    def filter_provider_keys(self, issuer, kid=None, alg=None):
59
        """
60
61
        Return Provider Keys after applying Key ID and Algorithm filter.
        If no filters match, return the full set.
62
63
        :param issuer: provider
        :param kid: Key ID
64
65
66
        :param alg: Algorithm
        :return: keys
        :raise ValueError: client could not be retrieved
67
        """
68
69
70
71
72
73
74
75
76
        client = self.clients.get(issuer)
        if client is None:
            raise ValueError('Could not retrieve client for issuer={}'.format(issuer))
        # List of Keys (from pyjwkest)
        keys = client.keyjar.get_issuer_keys(issuer)
        filtered_keys = [key for key in keys if key.kid == kid or key.alg == alg]
        if len(filtered_keys) is 0:
            return keys
        return filtered_keys
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120

    def introspect(self, issuer, access_token):
        """
        Make a Token Introspection request
        :param issuer: issuer of the token
        :param access_token: token to introspect
        :return: JSON response
        """
        client = self.clients[issuer]
        response = client.do_any(request_args={'token': access_token},
                                 request=TokenIntrospectionRequest,
                                 response=TokenIntrospectionResponse,
                                 body_type='json',
                                 method='POST',
                                 authn_method="client_secret_basic"
                                 )
        log.debug('introspect_response::: {}'.format(response))
        return response

    def generate_refresh_token(self, issuer, access_token):
        """
        Exchange an access token for a refresh token
        :param issuer: issuer of the access token
        :param access_token:
        :return: refresh token
        :raise Exception: If refresh token cannot be obtained
        """
        log.debug("enter generate_refresh_token")
        client = self.clients[issuer]
        body = {'grant_type': 'urn:ietf:params:oauth:grant-type:token-exchange',
                'subject_token_type': 'urn:ietf:params:oauth:token-type:access_token',
                'subject_token': access_token,
                'scope': 'offline_access openid profile',
                'audience': client.client_id}
        try:
            response = client.do_any(Message,
                                     request_args=body,
                                     endpoint=client.provider_info["token_endpoint"],
                                     body_type='json',
                                     method='POST',
                                     authn_method="client_secret_basic"
                                     )
            log.debug("after do any")
            response = response.json()
121
            log.debug("response: {}".format(response))
122
123
124
125
126
127
128
129
130
            refresh_token = response['refresh_token']
            log.debug("REFRESH TOKEN IS {}".format(refresh_token))
        except Exception as ex:
            log.warning("Exception raised when requesting refresh token")
            log.warning(ex)
            raise ex
        log.debug('refresh_token_response::: {}'.format(refresh_token))
        return refresh_token

131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
    def request_token_exchange(self, issuer, access_token, scope=None, audience=None):
        """
        Do a token exchange request
        :param issuer: issuer of the access token
        :param access_token: token to exchange
        :param scope: string containing scopes separated by space
        :return: provider response in json
        :raise Exception: if request fails
        """
        client = self.clients[issuer]
        body = {'grant_type': 'urn:ietf:params:oauth:grant-type:token-exchange',
                'subject_token_type': 'urn:ietf:params:oauth:token-type:access_token',
                'subject_token': access_token
                }
        if scope:
            body['scope'] = scope
        if audience:
            body['audience'] = audience

        try:
            response = client.do_any(Message,
                                     request_args=body,
                                     endpoint=client.provider_info["token_endpoint"],
                                     body_type='json',
                                     method='POST',
                                     authn_method="client_secret_basic"
                                     )
            response = response.json()
            log.debug("response: {}".format(response))
        except Exception as ex:
            log.warning("Exception raised when exchanging token")
            log.warning(ex)
            raise ex
        return response

166
    def generate_token_with_scope(self, issuer, access_token, scope, audience=None):
167
168
169
170
171
172
173
174
        """
        Exchange an access token for another access token with the specified scope
        :param issuer: issuer of the access token
        :param access_token:
        :param scope: string containing scopes separated by space
        :return: new access token and optional refresh_token
        :raise Exception: If token cannot be obtained
        """
175
        response = self.request_token_exchange(issuer, access_token, scope=scope, audience=audience)
176
177
178
179
        access_token = response['access_token']
        refresh_token = response.get('access_token', None)
        return access_token, refresh_token

180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
    def refresh_access_token(self, credential):
        """
        Request new access token
        :param credential: Credential from DB containing an access token and a refresh token
        :return: Updated credential containing new access token
        """
        access_token, refresh_token = credential.proxy.split(':')
        unverified_payload = jwt.decode(access_token, verify=False)
        issuer = unverified_payload['iss']
        client = self.clients[issuer]
        log.debug('refresh_access_token for {}'.format(issuer))

        # Prepare and make request
        refresh_session_state = rndstr(50)
        client.grant[refresh_session_state] = Grant()
        client.grant[refresh_session_state].grant_expiration_time = time_util.utc_time_sans_frac() + 60
        resp = AccessTokenResponse()
        resp["refresh_token"] = refresh_token
        client.grant[refresh_session_state].tokens.append(Token(resp))
        new_credential = client.do_access_token_refresh(authn_method="client_secret_basic",
                                                        state=refresh_session_state)
        # A new refresh token is optional
        refresh_token = new_credential.get('refresh_token', refresh_token)
        access_token = new_credential.get('access_token')
        unverified_payload = jwt.decode(access_token, verify=False)
        expiration_time = unverified_payload['exp']
        credential.proxy = new_credential['access_token'] + ':' + refresh_token
        credential.termination_time = datetime.utcfromtimestamp(expiration_time)

        return credential


# Should be the only instance, called during the middleware initialization
oidc_manager = OIDCmanager()