Skip to content

Commit ecaea18

Browse files
authored
Handle authentication in own OIDCAuth class (#7)
1 parent 3b4e966 commit ecaea18

File tree

2 files changed

+227
-195
lines changed

2 files changed

+227
-195
lines changed

src/oidc_auth.py

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
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+
# "email": "[email protected]",
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+
# "email": "[email protected]"
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+
# "upn": "[email protected]",
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

Comments
 (0)