__init__.py 6.42 KB
Newer Older
Lukas Burgey's avatar
Lukas Burgey committed
1

Lukas Burgey's avatar
Lukas Burgey committed
2
3
4
import logging
import json
from urllib.request import Request, urlopen
Lukas Burgey's avatar
Lukas Burgey committed
5

Lukas Burgey's avatar
Lukas Burgey committed
6
from django.db import models as db_models
Lukas Burgey's avatar
Lukas Burgey committed
7
from django.core.exceptions import ImproperlyConfigured
Lukas Burgey's avatar
Lukas Burgey committed
8
from django_mysql.models import JSONField
Lukas Burgey's avatar
Lukas Burgey committed
9
10
from oic.oic import Client
from oic.oic.message import RegistrationResponse
11
from oic.utils.authn.client import CLIENT_AUTHN_METHOD
Lukas Burgey's avatar
Lukas Burgey committed
12

Lukas Burgey's avatar
Lukas Burgey committed
13
from .. import utils
Lukas Burgey's avatar
Lukas Burgey committed
14

Lukas Burgey's avatar
Lukas Burgey committed
15
LOGGER = logging.getLogger(__name__)
16

Lukas Burgey's avatar
Lukas Burgey committed
17
OIDC_CLIENT = {}
Lukas Burgey's avatar
Lukas Burgey committed
18

19

Lukas Burgey's avatar
Lukas Burgey committed
20
def scopes_default():
21
22
23
24
25
26
27
    return [
        'openid',
        'profile',
        'email',
        'credentials',
        'eduperson_entitlement',
    ]
Lukas Burgey's avatar
Lukas Burgey committed
28

Lukas Burgey's avatar
Lukas Burgey committed
29
30
31
32
33
34

class OIDCConfig(db_models.Model):
    client_id = db_models.CharField(max_length=200)
    client_secret = db_models.CharField(max_length=200)
    redirect_uri = db_models.CharField(max_length=200)
    issuer_uri = db_models.CharField(max_length=200)
35
36
    enabled = db_models.BooleanField(default=False)
    name = db_models.CharField(max_length=200)
Lukas Burgey's avatar
Lukas Burgey committed
37

Lukas Burgey's avatar
Lukas Burgey committed
38
39
40
41
    # scopes as a list of strings
    scopes = JSONField(
        default=scopes_default,
        editable=True,
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
        help_text='The scopes we request when requesting user infos',
    )

    # ENTITLEMENT CHANGES

    # path in the group tree to the VO Groups
    # can be empty if we use the root
    vo_subtree_path = db_models.CharField(
        max_length=200,
        blank=True,
        null=True,
        help_text='If not emtpy: Operate with groups of the described subtree of group (or entitlements). For example: Let\'s say the groups [/,/foo,/bar] exist and you set vo_subtree_path to "/". In that case the VO-Groups would be /foo and /bar',
    )

    # If True we shall ignore subgroups of the VO-Groups
    # (VO-Group are the group on the path described by subtree_path)
    ignore_subgroups = db_models.BooleanField(
        default=False,
        help_text='Ignore subgroups of VO describing groups. E.g. ignores the group :foo:bar if :foo exists.',
    )

    # The field in the userinfo (served by this IdP) that describes groups of the user
    userinfo_field_groups = db_models.CharField(
        max_length=200,
        help_text="The field in the userinfo (served by this IdP) that contains groups of the user. Leave blank if you don't want to use groups of this IdP",
        default=None,
        blank=True,
        null=True,
    )

    # The field in the userinfo (served by this IdP) that describes entitlements of the user
    userinfo_field_entitlements = db_models.CharField(
        max_length=200,
        help_text="The field in the userinfo (served by this IdP) that contains entitlements of the user. Leave blank if you don't want to use entitlements of this IdP",
        default=None,
        blank=True,
        null=True,
Lukas Burgey's avatar
Lukas Burgey committed
79
80
    )

Lukas Burgey's avatar
Lukas Burgey committed
81
82
    @property
    def registration_response(self):
Lukas Burgey's avatar
Lukas Burgey committed
83
84
85
86
        info = {
            'client_id': self.client_id,
            'client_secret': self.client_secret
        }
Lukas Burgey's avatar
Lukas Burgey committed
87
88
89
90
        return RegistrationResponse(**info)

    @property
    def oidc_client(self):
Lukas Burgey's avatar
Lukas Burgey committed
91
        # create the client object if does no yet exist
Lukas Burgey's avatar
Lukas Burgey committed
92
        if self.id not in OIDC_CLIENT:
93
94
95
            new_oidc_client = Client(client_authn_method=CLIENT_AUTHN_METHOD)
            new_oidc_client.provider_config(self.issuer_uri)
            new_oidc_client.store_registration_info(self.registration_response)
Lukas Burgey's avatar
Lukas Burgey committed
96
            OIDC_CLIENT[self.id] = new_oidc_client
Lukas Burgey's avatar
Lukas Burgey committed
97

Lukas Burgey's avatar
Lukas Burgey committed
98
        return OIDC_CLIENT[self.id]
Lukas Burgey's avatar
Lukas Burgey committed
99
100
101
102
103

    @property
    def provider_info(self):
        return self.oidc_client.provider_info

Lukas Burgey's avatar
Lukas Burgey committed
104
105
    def __str__(self):
        return self.name
106

Lukas Burgey's avatar
Lukas Burgey committed
107
108
109
110
    def get_auth_request(self, client, state):
        args = {
            'client_id': self.client_id,
            'response_type': 'code',
Lukas Burgey's avatar
Lukas Burgey committed
111
            'scope': self.scopes,
Lukas Burgey's avatar
Lukas Burgey committed
112
113
114
115
116
            'redirect_uri': self.redirect_uri,
            'state': state,
        }

        auth_req = client.construct_AuthorizationRequest(
Lukas Burgey's avatar
Lukas Burgey committed
117
            request_args=args,
Lukas Burgey's avatar
Lukas Burgey committed
118
        )
Lukas Burgey's avatar
Lukas Burgey committed
119
120
121
        return auth_req.request(client.authorization_endpoint)


122
def default_idp():
Lukas Burgey's avatar
Lukas Burgey committed
123
124
125
126
    available_idps = OIDCConfig.objects.filter(enabled=True)
    if not available_idps.exists():
        raise ImproperlyConfigured('No OIDCConfig available')
    return available_idps.first()
Lukas Burgey's avatar
Lukas Burgey committed
127
128
129


class OIDCTokenAuthBackend(object):
Lukas Burgey's avatar
Lukas Burgey committed
130

131
    AuthException = Exception('Unable to authenticate user')
Lukas Burgey's avatar
Lukas Burgey committed
132

Lukas Burgey's avatar
Lukas Burgey committed
133
    def get_userinfo(self, oidc_client, access_token=None):
134
135
136
137
        user_info = None

        if access_token is not None:
            req = Request(
Lukas Burgey's avatar
Lukas Burgey committed
138
                oidc_client.provider_info['userinfo_endpoint'],
139
140
141
142
143
144
            )
            auth = ('Bearer ' + access_token)
            req.add_header('Authorization', auth)

            userinfo_bytes = urlopen(req).read()
            user_info = json.loads(userinfo_bytes.decode('UTF-8'))
145
            user_info['iss'] = oidc_client.provider_info['issuer']
146
147

        else:
148
            LOGGER.error('Invalid parameters for get_userinfo')
149

150
        # LOGGER.debug('Got user info:\n%s\n', user_info)
151
        return user_info
Lukas Burgey's avatar
Lukas Burgey committed
152

Lukas Burgey's avatar
Lukas Burgey committed
153
    def authenticate(self, request, token=None, issuer_uri=None):
Lukas Burgey's avatar
Lukas Burgey committed
154
155
        from ....models.users import User

Lukas Burgey's avatar
Lukas Burgey committed
156
157
158
        if token is None:
            return None

Lukas Burgey's avatar
Lukas Burgey committed
159
        idp_id = utils.get_session(request, 'idp_id', None)
Lukas Burgey's avatar
Lukas Burgey committed
160
161
162
163
164
        oidc_client = None

        try:

            if issuer_uri is not None:
165
                LOGGER.debug('Attempting to find IdP %s', issuer_uri)
Lukas Burgey's avatar
Lukas Burgey committed
166
167
168
169
170
171
                oidc_client = OIDCConfig.objects.get(issuer_uri=issuer_uri)

            elif idp_id is not None:
                oidc_client = OIDCConfig.objects.get(id=idp_id)

            if oidc_client is None:
172
                LOGGER.error('Unable to determine IdP for authentication')
Lukas Burgey's avatar
Lukas Burgey committed
173
174
175
                return None

        except OIDCConfig.DoesNotExist:
176
            LOGGER.error('Unable to determine IdP for authentication')
Lukas Burgey's avatar
Lukas Burgey committed
177
            return None
178
179
180
181
182
183

        # get the user info from the idp
        userinfo = self.get_userinfo(
            oidc_client,
            access_token=token,
        )
Lukas Burgey's avatar
Lukas Burgey committed
184

185
        return User.get_user(
Lukas Burgey's avatar
Lukas Burgey committed
186
187
188
            userinfo,
            oidc_client,
        )
Lukas Burgey's avatar
Lukas Burgey committed
189
190

    def get_user(self, user_id):
Lukas Burgey's avatar
Lukas Burgey committed
191
        from ....models.users import User
Lukas Burgey's avatar
Lukas Burgey committed
192
        query = User.objects.filter(
Lukas Burgey's avatar
Lukas Burgey committed
193
194
195
196
197
198
199
200
201
202
203
            user_type='oidcuser',
            pk=user_id
        )
        if query.exists():
            if len(query) == 1:
                return query.first()

            LOGGER.error(
                'OIDCTokenAuthBackend: query for user id %s: %s results',
                user_id,
                len(query),
Lukas Burgey's avatar
Lukas Burgey committed
204
            )
Lukas Burgey's avatar
Lukas Burgey committed
205
            return None
Lukas Burgey's avatar
Lukas Burgey committed
206
207
208
209
210
211

        LOGGER.error(
            'OIDCTokenAuthBackend: query for user id %s: no results',
            user_id,
        )
        return None