__init__.py 5.13 KB
Newer Older
Lukas Burgey's avatar
Lukas Burgey committed
1
2
3

import logging
import json
4
import jwt
Lukas Burgey's avatar
Lukas Burgey committed
5
6
7

from urllib.error import HTTPError
from urllib.request import Request, urlopen
8
from rest_framework.authentication import BaseAuthentication
Lukas Burgey's avatar
Lukas Burgey committed
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

from . import utils
from .models import OIDCConfig


LOGGER = logging.getLogger(__name__)


class OIDCTokenAuthBackend:

    def get_userinfo(self, oidc_client, access_token):
        req = Request(
            oidc_client.provider_info['userinfo_endpoint'],
        )
        req.add_header('Authorization', access_token)

        # execute request
        userinfo_bytes = urlopen(req).read()

        user_info = json.loads(userinfo_bytes.decode('UTF-8'))
        user_info['iss'] = oidc_client.provider_info['issuer']

        return user_info

Lukas Burgey's avatar
Lukas Burgey committed
33
34
    # no issuer -> try all idps :/
    def get_userinfo_bruteforce(self, access_token):
Lukas Burgey's avatar
Lukas Burgey committed
35
        for oidc_client in OIDCConfig.objects.filter(enabled=True):
Lukas Burgey's avatar
Lukas Burgey committed
36
37
38
39
40
41
42
43
44
45
46
47
            try:
                return oidc_client, self.get_userinfo(
                    oidc_client,
                    access_token,
                )
            except HTTPError as exception:
                pass

        raise OIDCConfig.DoesNotExist('Unable to determine IdP')



Lukas Burgey's avatar
Lukas Burgey committed
48
49
50
51
    # raises OIDCConfig.DoesNotExist if no idp can be determined
    def get_idp(self, request):
        # OPTION 1: issuer set in the 'X-Issuer' header
        if 'HTTP_X_ISSUER' in request.META:
52
53
54
55
            return OIDCConfig.objects.get(
                issuer_uri=request.META['HTTP_X_ISSUER'],
                enabled=True,
            )
Lukas Burgey's avatar
Lukas Burgey committed
56
57
58
59

        # OPTION 2: issuer set in users session (before redirecting to IdP)
        idp_id = utils.get_session(request, 'idp_id', None)
        if idp_id is not None:
60
61
62
63
64
            return OIDCConfig.objects.get(
                id=idp_id,
                enabled=True,
            )

65
66
67
68
69
70
71
72
73
74
75
76
77
        # OPTION 3: read 'iss' JWT
        access_token = request.META['HTTP_AUTHORIZATION']
        try:
            data = jwt.decode(access_token)
            if 'iss' in data:
                return OIDCConfig.objects.get(
                    issuer_uri=data['iss'],
                    enabled=True,
                )
            LOGGER.debug("JWT access token does not contain iss field")

        except jwt.exceptions.InvalidTokenError as exception: # base exception for jwt.decode
            pass
Lukas Burgey's avatar
Lukas Burgey committed
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

        raise OIDCConfig.DoesNotExist('Unable to determine IdP')

    def get_user(self, user_id):
        from feudal.backend.models.users import User
        query = User.objects.filter(
            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),
            )
            return None

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

    def authenticate(self, request):
        if 'HTTP_AUTHORIZATION' not in request.META:
Lukas Burgey's avatar
Lukas Burgey committed
106
            request.session['auth_error'] = 'Not authenticated'
Lukas Burgey's avatar
Lukas Burgey committed
107
            return None
Lukas Burgey's avatar
Lukas Burgey committed
108
109
110
111
112

        access_token = request.META['HTTP_AUTHORIZATION']

        from feudal.backend.models.users import User

Lukas Burgey's avatar
Lukas Burgey committed
113
114
115
116
        idp = None
        userinfo = None

        # DETERMINE idp AND userinfo
Lukas Burgey's avatar
Lukas Burgey committed
117
118
119
        try:
            idp = self.get_idp(request)

Lukas Burgey's avatar
Lukas Burgey committed
120
            # get the user info from the idp
Lukas Burgey's avatar
Lukas Burgey committed
121
122
123
124
            userinfo = self.get_userinfo(
                idp,
                access_token,
            )
Lukas Burgey's avatar
Lukas Burgey committed
125
126
127
128
129
130
131
132
133
134

        except OIDCConfig.DoesNotExist: # from get_idp
            # Idp was not provided in param / session / JWT -> just try all of them
            try:
                idp, userinfo = self.get_userinfo_bruteforce(access_token)
            except OIDCConfig.DoesNotExist:
                request.session['auth_error'] = 'Unable to determine IdP'
                return None

        except HTTPError as exception: # from get_userinfo
Lukas Burgey's avatar
Lukas Burgey committed
135
136
            request.session['auth_error'] = 'HTTP when retrieving user info: {}'.format(exception)
            return None
Lukas Burgey's avatar
Lukas Burgey committed
137

Lukas Burgey's avatar
Lukas Burgey committed
138
139
        # idp and userinfo are set correctly below this point

Lukas Burgey's avatar
Lukas Burgey committed
140
141
142
143
144
        try:
            user = User.get_user(
                userinfo,
                idp,
            )
Lukas Burgey's avatar
Lukas Burgey committed
145
146
147
148
149
150
151

            if not user.is_active:
                LOGGER.info('Login attempt: %s', user)
                utils.set_session(request, 'msg', 'Account deactivated')
                utils.set_session(request, 'deactivated', True)
                return None

Lukas Burgey's avatar
Lukas Burgey committed
152
            LOGGER.info('Authenticated: %s', user)
Lukas Burgey's avatar
Lukas Burgey committed
153
154
            # reset all values we possibly set in the session
            utils.del_session(request, ['deactivated', 'auth_error'])
Lukas Burgey's avatar
Lukas Burgey committed
155
156
157
            return user

        except User.DoesNotExist:
Lukas Burgey's avatar
Lukas Burgey committed
158
159
            return None
            # raise AuthenticationFailed(detail='No such user')
Lukas Burgey's avatar
Lukas Burgey committed
160
161
162

        except Exception as exception:
            LOGGER.error(exception)
Lukas Burgey's avatar
Lukas Burgey committed
163
            return None
Lukas Burgey's avatar
Lukas Burgey committed
164
165
166
167
168
169
170
171
172


class OIDCTokenAuthHTTPBackend(BaseAuthentication):

    def authenticate_header(self, request):
        return 'Bearer realm="openidconnect"'

    def authenticate(self, request):
        return (OIDCTokenAuthBackend().authenticate(request), None)