|
| 1 | +import os |
| 2 | + |
| 3 | +from authlib.integrations.flask_client import OAuth |
| 4 | +from authlib.integrations.flask_oauth2 import current_token |
| 5 | +from flask import ( |
| 6 | + url_for, request, session, redirect, make_response |
| 7 | +) |
| 8 | +from flask_jwt_extended import ( |
| 9 | + create_access_token, set_access_cookies, unset_jwt_cookies |
| 10 | +) |
| 11 | + |
| 12 | +from qwc_services_core.auth import GroupNameMapper |
| 13 | +from qwc_services_core.runtime_config import RuntimeConfig |
| 14 | + |
| 15 | +class OIDCAuth: |
| 16 | + """OIDCAuth class |
| 17 | +
|
| 18 | + User login with OpenID Connect |
| 19 | + """ |
| 20 | + |
| 21 | + def __init__(self, tenant, app): |
| 22 | + """Constructor |
| 23 | +
|
| 24 | + :param str tenant: Tenant ID |
| 25 | + :param App app: Flask application |
| 26 | + """ |
| 27 | + self.tenant = tenant |
| 28 | + self.app = app |
| 29 | + self.logger = app.logger |
| 30 | + |
| 31 | + config_handler = RuntimeConfig("oidcAuth", self.logger) |
| 32 | + self._config = config_handler.tenant_config(tenant) |
| 33 | + |
| 34 | + oauth = OAuth(app) |
| 35 | + client_id = self._config.get('client_id', os.getenv('CLIENT_ID')) |
| 36 | + client_secret = self._config.get('client_secret', os.getenv('CLIENT_SECRET')) |
| 37 | + issuer_url = self._config.get('issuer_url', os.getenv('ISSUER_URL')) |
| 38 | + # e.g. https://accounts.google.com/.well-known/openid-configuration |
| 39 | + metadata_url = f"{issuer_url}/.well-known/openid-configuration" |
| 40 | + openid_scopes = self._config.get('openid_scopes', 'openid email profile') |
| 41 | + oauth.register( |
| 42 | + name=tenant, |
| 43 | + client_id=client_id, |
| 44 | + client_secret=client_secret, |
| 45 | + server_metadata_url=metadata_url, |
| 46 | + client_kwargs={ |
| 47 | + 'scope': openid_scopes |
| 48 | + } |
| 49 | + # authorize_params={'resource': 'urn:microsoft:userinfo'} |
| 50 | + ) |
| 51 | + self._oidc = oauth.create_client(tenant) |
| 52 | + |
| 53 | + def config(self): |
| 54 | + return self._config |
| 55 | + |
| 56 | + def tenant_base(self): |
| 57 | + """base path for tenant""" |
| 58 | + # Updates config['JWT_ACCESS_COOKIE_PATH'] as side effect |
| 59 | + prefix = self.app.session_interface.get_cookie_path(self.app) |
| 60 | + return prefix.rstrip('/') + '/' |
| 61 | + |
| 62 | + def callback(self): |
| 63 | + token = self._oidc.authorize_access_token() |
| 64 | + userinfo = token.get('userinfo') |
| 65 | + # { |
| 66 | + # "userinfo": { |
| 67 | + # "at_hash": "3lI-Bs8Ym0SmXLpEM6Idqw", |
| 68 | + # "aud": "cf5ec860-ced2-013a-f0b6-0a510fd395c5120854", |
| 69 | + |
| 70 | + # "exp": 1662635070, |
| 71 | + # "family_name": "Doe", |
| 72 | + # "given_name": "John", |
| 73 | + # "iat": 1662627870, |
| 74 | + # "iss": "https://qwc2-dev.onelogin.com/oidc/2", |
| 75 | + # "name": "John Doe", |
| 76 | + # "nonce": "2pqk3WdRWhMdIOhaNw1o", |
| 77 | + # "preferred_username": "[email protected]", |
| 78 | + # "sid": "9587e574-0a0b-4d2d-b5ba-ed539d5dc81c", |
| 79 | + # "sub": "37078758", |
| 80 | + # "updated_at": 1662627811 |
| 81 | + # } |
| 82 | + # } |
| 83 | + # |
| 84 | + # eduid.ch: |
| 85 | + # { |
| 86 | + # "userinfo": { |
| 87 | + # "at_hash": "bcCpXNOtQPCKIolbBKVrWg", |
| 88 | + # "sub": "AW3CJEEOCDQSNR4GLF7CGRINMFPZVTOW", |
| 89 | + # "swissEduPersonUniqueID": "[email protected]", |
| 90 | + # "email_verified": true, |
| 91 | + # "iss": "https://login.eduid.ch/", |
| 92 | + # "given_name": "John", |
| 93 | + # "nonce": "rseXKUJ3MaJDe7rmm1lL", |
| 94 | + # "aud": "<client_id>", |
| 95 | + # "acr": "password", |
| 96 | + # "auth_time": 1664372815, |
| 97 | + # "name": "John Doe", |
| 98 | + # "exp": 1664387215, |
| 99 | + # "iat": 1664372815, |
| 100 | + # "family_name": "Doe", |
| 101 | + |
| 102 | + # } |
| 103 | + # } |
| 104 | + # |
| 105 | + # ADFS: |
| 106 | + # { |
| 107 | + # "userinfo": { |
| 108 | + # "appid": "c8699d44-facf-4329-b2c2-ff1f8c385beb", |
| 109 | + # "apptype": "Confidential", |
| 110 | + # "aud": "c8699d44-facf-4329-b2c2-ff1f8c385beb", |
| 111 | + # "auth_time": 1662626992, |
| 112 | + # "authmethod": "http://schemas.microsoft.com/ws/2008/06/identity/authenticationmethod/windows", |
| 113 | + # "exp": 1662630592, |
| 114 | + # "group": ["User", Admin"], |
| 115 | + # "iat": 1662626992, |
| 116 | + # "iss": "https://example.com/adfs", |
| 117 | + # "nbf": 1662626992, |
| 118 | + # "nonce": "Ntyr78eXokrvA82BDKsV", |
| 119 | + # "pwd_exp": "1733602", |
| 120 | + # "pwd_url": "https://example.com/adfs/portal/updatepassword/", |
| 121 | + # "scp": "profile email openid" |
| 122 | + # "sid": "S-1-5-21-111884681-232138482-1136263860-54956", |
| 123 | + # "sub": "E8uMvTw4EzVtNJjAGGkn/HLxB5lsxPvUz9N8v2ONw6w=", |
| 124 | + # "unique_name": "DOMAIN\\USER", |
| 125 | + |
| 126 | + # "ver": "1.0", |
| 127 | + # } |
| 128 | + # } |
| 129 | + self.logger.info(userinfo) |
| 130 | + groupinfo = self._config.get('groupinfo', 'group') |
| 131 | + mapper = GroupNameMapper() |
| 132 | + |
| 133 | + if self._config.get('username'): |
| 134 | + username = userinfo.get(self._config.get('username')) |
| 135 | + else: |
| 136 | + username = userinfo.get('preferred_username', |
| 137 | + userinfo.get('upn', userinfo.get('email'))) |
| 138 | + groups = userinfo.get(groupinfo, []) |
| 139 | + if isinstance(groups, str): |
| 140 | + groups = [groups] |
| 141 | + # Add group for all authenticated users |
| 142 | + groups.append('verified') |
| 143 | + # Apply group name mappings |
| 144 | + groups = [ |
| 145 | + mapper.mapped_group(g) |
| 146 | + for g in groups |
| 147 | + ] |
| 148 | + identity = {'username': username, 'groups': groups} |
| 149 | + self.logger.info(identity) |
| 150 | + # Create the tokens we will be sending back to the user |
| 151 | + access_token = create_access_token(identity) |
| 152 | + # refresh_token = create_refresh_token(identity) |
| 153 | + |
| 154 | + base_url = self.tenant_base() |
| 155 | + target_url = session.pop('target_url', base_url) |
| 156 | + |
| 157 | + resp = make_response(redirect(target_url)) |
| 158 | + set_access_cookies(resp, access_token) |
| 159 | + return resp |
| 160 | + |
| 161 | + def login(self): |
| 162 | + target_url = request.args.get('url', self.tenant_base()) |
| 163 | + # We store the target url in the session. |
| 164 | + # Instead we could pass it as OAuth state |
| 165 | + # (state=target_url in authorize_redirect) |
| 166 | + # Then we should only pass the path as state for security reasons |
| 167 | + session['target_url'] = target_url |
| 168 | + self.logger.debug("Request headers:") |
| 169 | + self.logger.debug(request.headers) |
| 170 | + redirect_uri = self._config.get( |
| 171 | + 'redirect_uri', url_for('callback', _external=True)) |
| 172 | + self.logger.info(f"redirect_uri: {redirect_uri}") |
| 173 | + return self._oidc.authorize_redirect(redirect_uri) |
| 174 | + |
| 175 | + def logout(self): |
| 176 | + self.logger.debug("Logout from handler") |
| 177 | + target_url = request.args.get('url', self.tenant_base()) |
| 178 | + resp = make_response(redirect(target_url)) |
| 179 | + unset_jwt_cookies(resp) |
| 180 | + return resp |
| 181 | + |
| 182 | + def token_login(self): |
| 183 | + userinfo = current_token |
| 184 | + self.logger.info(userinfo) |
| 185 | + groupinfo = self._config.get('groupinfo', 'group') |
| 186 | + mapper = GroupNameMapper() |
| 187 | + |
| 188 | + if self._config.get('username'): |
| 189 | + username = userinfo.get(self._config.get('username')) |
| 190 | + else: |
| 191 | + username = userinfo.get('preferred_username', |
| 192 | + userinfo.get('upn', userinfo.get('email'))) |
| 193 | + groups = userinfo.get(groupinfo, []) |
| 194 | + if isinstance(groups, str): |
| 195 | + groups = [groups] |
| 196 | + # Add group for all authenticated users |
| 197 | + groups.append('verified') |
| 198 | + # Apply group name mappings |
| 199 | + groups = [ |
| 200 | + mapper.mapped_group(g) |
| 201 | + for g in groups |
| 202 | + ] |
| 203 | + identity = {'username': username, 'groups': groups} |
| 204 | + self.logger.info(identity) |
| 205 | + # Create the tokens we will be sending back to the user |
| 206 | + access_token = create_access_token(identity) |
| 207 | + |
| 208 | + base_url = self.tenant_base() |
| 209 | + target_url = session.pop('target_url', base_url) |
| 210 | + |
| 211 | + resp = make_response(redirect(target_url)) |
| 212 | + set_access_cookies(resp, access_token) |
| 213 | + return resp |
0 commit comments