def test_absolutify(self): req = RequestFactory().get('/something/else') url = absolutify(req, '/foo/bar') self.assertEqual(url, 'http://testserver/foo/bar') req = RequestFactory().get('/something/else', SERVER_PORT=8888) url = absolutify(req, '/foo/bar') self.assertEqual(url, 'http://testserver:8888/foo/bar')
def process_request(self, request): if not self.is_refreshable_url(request): LOGGER.debug('request is not refreshable') return expiration = request.session.get('oidc_id_token_expiration', 0) now = time.time() if expiration > now: # The id_token is still valid, so we don't have to do anything. LOGGER.debug('id token is still valid (%s > %s)', expiration, now) return LOGGER.debug('id token has expired') # The id_token has expired, so we have to re-authenticate silently. auth_url = self.get_settings('OIDC_OP_AUTHORIZATION_ENDPOINT') client_id = self.get_settings('OIDC_RP_CLIENT_ID') state = get_random_string(self.get_settings('OIDC_STATE_SIZE', 32)) # Build the parameters as if we were doing a real auth handoff, except # we also include prompt=none. params = { 'response_type': 'code', 'client_id': client_id, 'redirect_uri': absolutify(request, reverse('oidc_authentication_callback')), # 'redirect_uri': 'https://acc.omslagroute.amsterdam.nl%s' % reverse('oidc_authentication_callback'), 'state': state, 'scope': self.get_settings('OIDC_RP_SCOPES', 'openid email'), 'prompt': 'none', } if self.get_settings('OIDC_USE_NONCE', True): nonce = get_random_string(self.get_settings('OIDC_NONCE_SIZE', 32)) params.update({'nonce': nonce}) request.session['oidc_nonce'] = nonce request.session['oidc_state'] = state request.session['oidc_login_next'] = request.get_full_path() query = urlencode(params) redirect_url = '{url}?{query}'.format(url=auth_url, query=query) if request.is_ajax(): # Almost all XHR request handling in client-side code struggles # with redirects since redirecting to a page where the user # is supposed to do something is extremely unlikely to work # in an XHR request. Make a special response for these kinds # of requests. # The use of 403 Forbidden is to match the fact that this # middleware doesn't really want the user in if they don't # refresh their session. response = JsonResponse({'refresh_url': redirect_url}, status=403) response['refresh_url'] = redirect_url return response return HttpResponseRedirect(redirect_url)
def get(self, request): """OIDC client authentication initialization HTTP endpoint. This is based on the mozilla-django-oidc library """ state = get_random_string(import_from_settings('OIDC_STATE_SIZE', 32)) redirect_field_name = import_from_settings('OIDC_REDIRECT_FIELD_NAME', 'next') params = { 'response_type': 'code', 'scope': import_from_settings('OIDC_RP_SCOPES', 'openid email'), 'client_id': self.OIDC_RP_VERIFICATION_CLIENT_ID, 'redirect_uri': absolutify( request, nonprefixed_url('phonebook:verify_identity_callback') ), 'state': state, } if import_from_settings('OIDC_USE_NONCE', True): nonce = get_random_string(import_from_settings('OIDC_NONCE_SIZE', 32)) params.update({ 'nonce': nonce }) request.session['oidc_verify_nonce'] = nonce # Add parameter to disable silent authentication params['tried_silent_auth'] = settings.OIDC_TRIED_SILENT_AUTH request.session['oidc_verify_state'] = state request.session['oidc_login_next'] = get_next_url(request, redirect_field_name) query = urlencode(params) redirect_url = '{url}?{query}'.format(url=self.OIDC_OP_AUTH_ENDPOINT, query=query) return HttpResponseRedirect(redirect_url)
def get(self, request): """OIDC client authentication initialization HTTP endpoint""" state = get_random_string(import_from_settings('OIDC_STATE_SIZE', 32)) redirect_field_name = import_from_settings('OIDC_REDIRECT_FIELD_NAME', 'next') params = { 'response_type': 'code', 'scope': 'openid', 'client_id': self.OIDC_RP_CLIENT_ID, 'redirect_uri': absolutify(reverse('oidc_authentication_callback')), 'state': state, } if import_from_settings('OIDC_USE_NONCE', True): nonce = get_random_string( import_from_settings('OIDC_NONCE_SIZE', 32)) params.update({'nonce': nonce}) request.session['oidc_nonce'] = nonce request.session['oidc_state'] = state request.session['oidc_login_next'] = request.GET.get( redirect_field_name) query = urlencode(params) redirect_url = '{url}?{query}'.format(url=self.OIDC_OP_AUTH_ENDPOINT, query=query) return HttpResponseRedirect(redirect_url)
def get(self, request): """OIDC client authentication initialization HTTP endpoint""" state = get_random_string(self.get_settings('OIDC_STATE_SIZE', 32)) redirect_field_name = self.get_settings('OIDC_REDIRECT_FIELD_NAME', 'next') reverse_url = self.get_settings('OIDC_AUTHENTICATION_CALLBACK_URL', 'oidc_authentication_callback') params = { 'response_type': 'code', 'scope': self.get_settings('OIDC_RP_SCOPES', 'openid email'), 'client_id': self.OIDC_RP_CLIENT_ID, 'redirect_uri': absolutify(request, reverse(reverse_url)), # 'redirect_uri': 'https://acc.omslagroute.amsterdam.nl%s' % reverse(reverse_url), 'state': state, } params.update(self.get_extra_params(request)) if self.get_settings('OIDC_USE_NONCE', True): nonce = get_random_string(self.get_settings('OIDC_NONCE_SIZE', 32)) params.update({'nonce': nonce}) request.session['oidc_nonce'] = nonce request.session['oidc_state'] = state request.session['oidc_login_next'] = get_next_url( request, redirect_field_name) query = urlencode(params) redirect_url = '{url}?{query}'.format(url=self.OIDC_OP_AUTH_ENDPOINT, query=query) return HttpResponseRedirect(redirect_url)
def get(self, request): """OIDC client authentication initialization HTTP endpoint""" state = get_random_string(import_from_settings('OIDC_STATE_SIZE', 32)) redirect_field_name = import_from_settings('OIDC_REDIRECT_FIELD_NAME', 'next') reverse_url = import_from_settings('OIDC_AUTHENTICATION_CALLBACK_URL', 'oidc_authentication_callback') params = { 'response_type': 'code', 'scope': import_from_settings('OIDC_RP_SCOPES', 'openid email'), 'client_id': self.OIDC_RP_CLIENT_ID, 'redirect_uri': absolutify( request, reverse(reverse_url) ), 'state': state, } params.update(self.get_extra_params(request)) if import_from_settings('OIDC_USE_NONCE', True): nonce = get_random_string(import_from_settings('OIDC_NONCE_SIZE', 32)) params.update({ 'nonce': nonce }) request.session['oidc_nonce'] = nonce request.session['oidc_state'] = state request.session['oidc_login_next'] = get_next_url(request, redirect_field_name) query = urlencode(params) redirect_url = '{url}?{query}'.format(url=self.OIDC_OP_AUTH_ENDPOINT, query=query) return HttpResponseRedirect(redirect_url)
def process_request(self, request): if not self.is_refreshable_url(request): LOGGER.debug('request is not refreshable') return expiration = request.session.get('oidc_id_token_expiration', 0) now = time.time() if expiration > now: # The id_token is still valid, so we don't have to do anything. LOGGER.debug('id token is still valid (%s > %s)', expiration, now) return LOGGER.debug('id token has expired') # The id_token has expired, so we have to re-authenticate silently. auth_url = import_from_settings('OIDC_OP_AUTHORIZATION_ENDPOINT') client_id = import_from_settings('OIDC_RP_CLIENT_ID') state = get_random_string(import_from_settings('OIDC_STATE_SIZE', 32)) # Build the parameters as if we were doing a real auth handoff, except # we also include prompt=none. params = { 'response_type': 'code', 'client_id': client_id, 'redirect_uri': absolutify( request, reverse('oidc_authentication_callback') ), 'state': state, 'scope': import_from_settings('OIDC_RP_SCOPES', 'openid email'), 'prompt': 'none', } if import_from_settings('OIDC_USE_NONCE', True): nonce = get_random_string(import_from_settings('OIDC_NONCE_SIZE', 32)) params.update({ 'nonce': nonce }) request.session['oidc_nonce'] = nonce request.session['oidc_state'] = state request.session['oidc_login_next'] = request.get_full_path() query = urlencode(params) redirect_url = '{url}?{query}'.format(url=auth_url, query=query) if request.is_ajax(): # Almost all XHR request handling in client-side code struggles # with redirects since redirecting to a page where the user # is supposed to do something is extremely unlikely to work # in an XHR request. Make a special response for these kinds # of requests. # The use of 403 Forbidden is to match the fact that this # middleware doesn't really want the user in if they don't # refresh their session. response = JsonResponse({'refresh_url': redirect_url}, status=403) response['refresh_url'] = redirect_url return response return HttpResponseRedirect(redirect_url)
def authenticate(self, **kwargs): """Authenticates a user based on the OIDC code flow.""" code = kwargs.pop('code', None) state = kwargs.pop('state', None) nonce = kwargs.pop('nonce', None) if not code or not state: return None token_payload = { 'client_id': self.OIDC_RP_CLIENT_ID, 'client_secret': self.OIDC_RP_CLIENT_SECRET, 'grant_type': 'authorization_code', 'code': code, 'redirect_uri': absolutify(reverse('oidc_authentication_callback')) } # Get the token response = requests.post(self.OIDC_OP_TOKEN_ENDPOINT, data=token_payload, verify=import_from_settings( 'OIDC_VERIFY_SSL', True)) response.raise_for_status() # Validate the token token_response = response.json() if self.verify_token(token_response.get('id_token'), nonce=nonce): access_token = token_response.get('access_token') user_response = requests.get( self.OIDC_OP_USER_ENDPOINT, headers={'Authorization': 'Bearer {0}'.format(access_token)}) user_response.raise_for_status() user_info = user_response.json() email = user_info.get('email') # email based filtering users = self.filter_users_by_claims(user_info) if len(users) == 1: return users[0] elif len(users) > 1: # In the rare case that two user accounts have the same email address, # log and bail. Randomly selecting one seems really wrong. LOGGER.warn('Multiple users with email address %s.', email) return None elif import_from_settings('OIDC_CREATE_USER', True): user = self.create_user(user_info) return user else: LOGGER.debug( 'Login failed: No user with email %s found, and ' 'OIDC_CREATE_USER is False', email) return None return None
def process_request(self, request): if not self.is_expired(request): return LOGGER.debug('id token has expired') # The id_token has expired, so we have to re-authenticate silently. auth_url = self.OIDC_OP_AUTHORIZATION_ENDPOINT client_id = self.OIDC_RP_CLIENT_ID state = get_random_string(self.OIDC_STATE_SIZE) # Build the parameters as if we were doing a real auth handoff, except # we also include prompt=none. params = { 'response_type': 'code', 'client_id': client_id, 'redirect_uri': absolutify(request, reverse(self.OIDC_AUTHENTICATION_CALLBACK_URL)), 'state': state, 'scope': self.OIDC_RP_SCOPES, 'prompt': 'none', } if self.OIDC_USE_NONCE: nonce = get_random_string(self.OIDC_NONCE_SIZE) params.update({'nonce': nonce}) add_state_and_nonce_to_session(request, state, params) request.session['oidc_login_next'] = request.get_full_path() query = urlencode(params) redirect_url = '{url}?{query}'.format(url=auth_url, query=query) if request.is_ajax(): # Almost all XHR request handling in client-side code struggles # with redirects since redirecting to a page where the user # is supposed to do something is extremely unlikely to work # in an XHR request. Make a special response for these kinds # of requests. # The use of 403 Forbidden is to match the fact that this # middleware doesn't really want the user in if they don't # refresh their session. response = JsonResponse({'refresh_url': redirect_url}, status=403) response['refresh_url'] = redirect_url return response return HttpResponseRedirect(redirect_url)
def authenticate(self, *args, **kwargs): if sys.version_info[0] > 2: self.request = args[0] else: self.request = None if not self.request: return None state = self.request.GET.get('state') code = self.request.GET.get('code') nonce = kwargs.pop('nonce', None) if not code or not state: return None reverse_url = import_from_settings('OIDC_AUTHENTICATION_CALLBACK_URL', 'oidc_authentication_callback') token_payload = { 'client_id': self.OIDC_RP_CLIENT_ID, 'client_secret': self.OIDC_RP_CLIENT_SECRET, 'grant_type': 'authorization_code', 'code': code, 'redirect_uri': absolutify( self.request, reverse(reverse_url) ), } # Get the token token_info = self.get_token(token_payload) id_token = token_info.get('id_token') access_token = token_info.get('access_token') refresh_token = token_info.get('refresh_token') # Validate the token verified_id = self.verify_token(id_token, nonce=nonce) if verified_id: self.save_refresh_tokens(refresh_token) user = self.get_or_create_user(access_token, id_token, verified_id) user = rewrite_user(user) ensure_has_a_group(user) return user return None
def process_request(self, request): if not self.is_refreshable_url(request): LOGGER.debug('request is not refreshable') return expiration = request.session.get('oidc_id_token_expiration', 0) now = time.time() if expiration > now: # The id_token is still valid, so we don't have to do anything. LOGGER.debug('id token is still valid (%s > %s)', expiration, now) return LOGGER.debug('id token has expired') # The id_token has expired, so we have to re-authenticate silently. auth_url = import_from_settings('OIDC_OP_AUTHORIZATION_ENDPOINT') client_id = import_from_settings('OIDC_RP_CLIENT_ID') state = get_random_string(import_from_settings('OIDC_STATE_SIZE', 32)) # Build the parameters as if we were doing a real auth handoff, except # we also include prompt=none. params = { 'response_type': 'code', 'client_id': client_id, 'redirect_uri': absolutify(request, reverse('oidc_authentication_callback')), 'state': state, 'scope': 'openid', 'prompt': 'none', } if import_from_settings('OIDC_USE_NONCE', True): nonce = get_random_string( import_from_settings('OIDC_NONCE_SIZE', 32)) params.update({'nonce': nonce}) request.session['oidc_nonce'] = nonce request.session['oidc_state'] = state request.session['oidc_login_next'] = request.get_full_path() query = urlencode(params) redirect_url = '{url}?{query}'.format(url=auth_url, query=query) return HttpResponseRedirect(redirect_url)
def authenticate(self, request, **kwargs): """Authenticates a user based on the OIDC code flow.""" self.request = request if not self.request: return None state = self.request.GET.get('state') code = self.request.GET.get('code') nonce = kwargs.pop('nonce', None) if not code or not state: return None reverse_url = self.get_settings('OIDC_AUTHENTICATION_CALLBACK_URL', 'oidc_authentication_callback') token_payload = { 'client_id': self.OIDC_RP_CLIENT_ID, 'client_secret': self.OIDC_RP_CLIENT_SECRET, 'grant_type': 'authorization_code', 'code': code, 'redirect_uri': absolutify( self.request, reverse(reverse_url) ), # 'redirect_uri': 'https://acc.omslagroute.amsterdam.nl%s' % reverse(reverse_url), } # Get the token token_info = self.get_token(token_payload) id_token = token_info.get('id_token') access_token = token_info.get('access_token') # Validate the token payload = self.verify_token(id_token, nonce=nonce) if payload: self.store_tokens(access_token, id_token) try: return self.get_or_create_user(access_token, id_token, payload) except SuspiciousOperation as exc: LOGGER.warning('failed to get or create user: %s', exc) return None return None
def get(self, request): """OIDC client authentication initialization HTTP endpoint. This is based on the mozilla-django-oidc library """ state = get_random_string(import_from_settings('OIDC_STATE_SIZE', 32)) redirect_field_name = import_from_settings('OIDC_REDIRECT_FIELD_NAME', 'next') params = { 'response_type': 'code', 'scope': import_from_settings('OIDC_RP_SCOPES', 'openid email profile'), 'client_id': self.OIDC_RP_VERIFICATION_CLIENT_ID, 'redirect_uri': absolutify(request, nonprefixed_url('phonebook:verify_identity_callback')), 'state': state, 'prompt': settings.OIDC_PROMPT } if import_from_settings('OIDC_USE_NONCE', True): nonce = get_random_string( import_from_settings('OIDC_NONCE_SIZE', 32)) params.update({'nonce': nonce}) request.session['oidc_verify_nonce'] = nonce # Add parameter to disable silent authentication and the LDAP check for AUTO_VOUCH_DOMAINS # This will allow users to verify AUTO_VOUCH_DOMAINS as contact identities params['account_linking'] = settings.OIDC_ACCOUNT_LINKING request.session['oidc_verify_state'] = state request.session['oidc_login_next'] = get_next_url( request, redirect_field_name) query = urlencode(params) redirect_url = '{url}?{query}'.format(url=self.OIDC_OP_AUTH_ENDPOINT, query=query) return HttpResponseRedirect(redirect_url)
def authenticate(self, **kwargs): self.request = kwargs.pop('request', None) if not self.request: return None state = self.request.GET.get('state') code = self.request.GET.get('code') nonce = kwargs.pop('nonce', None) if not code or not state: return None reverse_url = import_from_settings('OIDC_AUTHENTICATION_CALLBACK_URL', 'oidc_authentication_callback') token_payload = { 'client_id': self.OIDC_RP_CLIENT_ID, 'client_secret': self.OIDC_RP_CLIENT_SECRET, 'grant_type': 'authorization_code', 'code': code, 'redirect_uri': absolutify( self.request, reverse(reverse_url) ), } # Get the token token_info = self.get_token(token_payload) id_token = token_info.get('id_token') access_token = token_info.get('access_token') refresh_token = token_info.get('refresh_token') # Validate the token verified_id = self.verify_token(id_token, nonce=nonce) if verified_id: self.save_refresh_tokens(refresh_token) user = self.get_or_create_user(access_token, id_token, verified_id) user = rewrite_user(user) return user return None
def get(self, request): """OIDC client authentication initialization HTTP endpoint""" state = get_random_string(self.get_settings('OIDC_STATE_SIZE', 32)) redirect_field_name = self.get_settings('OIDC_REDIRECT_FIELD_NAME', 'next') reverse_url = self.get_settings('OIDC_AUTHENTICATION_CALLBACK_URL', 'oidc_authentication_callback') code_verifier = base64.urlsafe_b64encode( os.urandom(40)).decode('utf-8') code_verifier = re.sub('[^a-zA-Z0-9]+', '', code_verifier) code_challenge = hashlib.sha256(code_verifier.encode('utf-8')).digest() code_challenge = base64.urlsafe_b64encode(code_challenge).decode( 'utf-8') code_challenge = code_challenge.replace('=', '') params = { 'response_type': 'code', 'scope': self.get_settings('OIDC_RP_SCOPES', 'openid email'), 'client_id': self.OIDC_RP_CLIENT_ID, 'redirect_uri': absolutify(request, reverse(reverse_url)), 'state': state, 'code_challenge': code_challenge, 'code_challenge_method': 'S256', } params.update(self.get_extra_params(request)) if self.get_settings('OIDC_USE_NONCE', True): nonce = get_random_string(self.get_settings('OIDC_NONCE_SIZE', 32)) params.update({'nonce': nonce}) add_state_and_nonce_to_session(request, state, params, code_verifier) request.session['oidc_login_next'] = get_next_url( request, redirect_field_name) query = urlencode(params) redirect_url = '{url}?{query}'.format(url=self.OIDC_OP_AUTH_ENDPOINT, query=query) return HttpResponseRedirect(redirect_url)
def authenticate(self, request, **kwargs): """Authenticates a user based on the OIDC code flow.""" self.request = request if not self.request: return None state = self.request.GET.get('state') code = self.request.GET.get('code') nonce = kwargs.pop('nonce', None) if not code or not state: return None reverse_url = import_from_settings('OIDC_AUTHENTICATION_CALLBACK_URL', 'oidc_authentication_callback') token_payload = { 'client_id': self.OIDC_RP_CLIENT_ID, 'client_secret': self.OIDC_RP_CLIENT_SECRET, 'grant_type': 'authorization_code', 'code': code, 'redirect_uri': absolutify( self.request, reverse(reverse_url) ), } # Get the token token_info = self.get_token(token_payload) id_token = token_info.get('id_token') access_token = token_info.get('access_token') # Validate the token verified_id = self.verify_token(id_token, nonce=nonce) if verified_id: return self.get_or_create_user(access_token, id_token, verified_id) return None
def get(self, request): """OIDC client authentication initialization HTTP endpoint. This is based on the mozilla-django-oidc library """ state = get_random_string(import_from_settings('OIDC_STATE_SIZE', 32)) redirect_field_name = import_from_settings('OIDC_REDIRECT_FIELD_NAME', 'next') params = { 'response_type': 'code', 'scope': import_from_settings('OIDC_RP_SCOPES', 'openid email'), 'client_id': self.OIDC_RP_VERIFICATION_CLIENT_ID, 'redirect_uri': absolutify( request, nonprefixed_url('phonebook:verify_identity_callback') ), 'state': state, 'prompt': settings.OIDC_PROMPT } if import_from_settings('OIDC_USE_NONCE', True): nonce = get_random_string(import_from_settings('OIDC_NONCE_SIZE', 32)) params.update({ 'nonce': nonce }) request.session['oidc_verify_nonce'] = nonce # Add parameter to disable silent authentication and the LDAP check for AUTO_VOUCH_DOMAINS # This will allow users to verify AUTO_VOUCH_DOMAINS as contact identities params['account_linking'] = settings.OIDC_ACCOUNT_LINKING request.session['oidc_verify_state'] = state request.session['oidc_login_next'] = get_next_url(request, redirect_field_name) query = urlencode(params) redirect_url = '{url}?{query}'.format(url=self.OIDC_OP_AUTH_ENDPOINT, query=query) return HttpResponseRedirect(redirect_url)
def keycloak_logout(request): request.session.flush() redirect_uri = absolutify(request, settings.FORCE_SCRIPT_NAME[:-1] + '/logout') return f'{settings.KEYCLOAK_LOGOUT}?redirect_uri={redirect_uri}'
def test_absolutify(self): url = absolutify('/foo/bar') self.assertEqual(url, 'http://site-url.com/foo/bar')
def get(self, request): """Callback handler for OIDC authorization code flow. This is based on the mozilla-django-oidc library. This callback is used to verify the identity added by the user. Users are already logged in and we do not care about authentication. The JWT token is used to prove the identity of the user. """ profile = request.user.userprofile # This is a difference nonce than the one used to login! nonce = request.session.get('oidc_verify_nonce') if nonce: # Make sure that nonce is not used twice del request.session['oidc_verify_nonce'] # Check for all possible errors and display a message to the user. errors = [ 'code' not in request.GET, 'state' not in request.GET, 'oidc_verify_state' not in request.session, request.GET['state'] != request.session['oidc_verify_state'] ] if any(errors): msg = 'Something went wrong, account verification failed.' messages.error(request, msg) return redirect('phonebook:profile_edit') token_payload = { 'client_id': self.OIDC_RP_VERIFICATION_CLIENT_ID, 'client_secret': self.OIDC_RP_VERIFICATION_CLIENT_SECRET, 'grant_type': 'authorization_code', 'code': request.GET['code'], 'redirect_uri': absolutify(self.request, nonprefixed_url('phonebook:verify_identity_callback')), } response = requests.post(self.OIDC_OP_TOKEN_ENDPOINT, data=token_payload, verify=import_from_settings( 'OIDC_VERIFY_SSL', True)) response.raise_for_status() token_response = response.json() id_token = token_response.get('id_token') # Verify JWT jws = JWS.from_compact(force_bytes(id_token)) jwk = JWK.load(smart_bytes(self.OIDC_RP_VERIFICATION_CLIENT_SECRET)) verified_token = None if jws.verify(jwk): verified_token = jws.payload # Create the new Identity Profile. if verified_token: user_info = json.loads(verified_token) email = user_info['email'] if not user_info.get('email_verified'): msg = 'Account verification failed: Email is not verified.' messages.error(request, msg) return redirect('phonebook:profile_edit') user_q = {'auth0_user_id': user_info['sub'], 'email': email} # If we are linking GitHub we need to save # the username too. if 'github|' in user_info['sub']: user_q['username'] = user_info['nickname'] # Check that the identity doesn't exist in another Identity profile # or in another mozillians profile error_msg = '' if IdpProfile.objects.filter(**user_q).exists(): error_msg = 'Account verification failed: Identity already exists.' elif User.objects.filter(email__iexact=email).exclude( pk=profile.user.pk).exists(): error_msg = 'The email in this identity is used by another user.' if error_msg: messages.error(request, error_msg) next_url = self.request.session.get('oidc_login_next', None) return HttpResponseRedirect( next_url or reverse('phonebook:profile_edit')) # Save the new identity to the IdpProfile user_q['profile'] = profile idp, created = IdpProfile.objects.get_or_create(**user_q) current_idp = get_object_or_none(IdpProfile, profile=profile, primary=True) # The new identity is stronger than the one currently used. Let's swap append_msg = '' # We need to check for equality too in the case a user updates the primary email in # the same identity (matching auth0_user_id). If there is an addition of the same type # we are not swapping login identities if ((current_idp and current_idp.type < idp.type) or (current_idp and current_idp.auth0_user_id == idp.auth0_user_id) or (not current_idp and created and idp.type >= IdpProfile.PROVIDER_GITHUB)): IdpProfile.objects.filter(profile=profile).exclude( pk=idp.pk).update(primary=False) idp.primary = True idp.save() # Also update the primary email of the user update_email_in_basket(profile.user.email, idp.email) User.objects.filter(pk=profile.user.id).update(email=idp.email) append_msg = ' You need to use this identity the next time you will login.' send_userprofile_to_cis.delay(profile.pk) if created: msg = 'Account successfully verified.' if append_msg: msg += append_msg messages.success(request, msg) else: msg = 'Account verification failed: Identity already exists.' messages.error(request, msg) next_url = self.request.session.get('oidc_login_next', None) return HttpResponseRedirect(next_url or reverse('phonebook:profile_edit'))
def get(self, request): """Callback handler for OIDC authorization code flow. This is based on the mozilla-django-oidc library. This callback is used to verify the identity added by the user. Users are already logged in and we do not care about authentication. The JWT token is used to prove the identity of the user. """ profile = request.user.userprofile # This is a difference nonce than the one used to login! nonce = request.session.get('oidc_verify_nonce') if nonce: # Make sure that nonce is not used twice del request.session['oidc_verify_nonce'] # Check for all possible errors and display a message to the user. errors = [ 'code' not in request.GET, 'state' not in request.GET, 'oidc_verify_state' not in request.session, request.GET['state'] != request.session['oidc_verify_state'] ] if any(errors): msg = 'Something went wrong, account verification failed.' messages.error(request, msg) return redirect('phonebook:profile_edit') token_payload = { 'client_id': self.OIDC_RP_VERIFICATION_CLIENT_ID, 'client_secret': self.OIDC_RP_VERIFICATION_CLIENT_SECRET, 'grant_type': 'authorization_code', 'code': request.GET['code'], 'redirect_uri': absolutify( self.request, nonprefixed_url('phonebook:verify_identity_callback') ), } response = requests.post(self.OIDC_OP_TOKEN_ENDPOINT, data=token_payload, verify=import_from_settings('OIDC_VERIFY_SSL', True)) response.raise_for_status() token_response = response.json() id_token = token_response.get('id_token') # Verify JWT jws = JWS.from_compact(force_bytes(id_token)) jwk = JWK.load(smart_bytes(self.OIDC_RP_VERIFICATION_CLIENT_SECRET)) verified_token = None if jws.verify(jwk): verified_token = jws.payload # Create the new Identity Profile. if verified_token: user_info = json.loads(verified_token) email = user_info['email'] if not user_info.get('email_verified'): msg = 'Account verification failed: Email is not verified.' messages.error(request, msg) return redirect('phonebook:profile_edit') user_q = { 'auth0_user_id': user_info['sub'], 'email': email } # If we are linking GitHub we need to save # the username too. if 'github|' in user_info['sub']: user_q['username'] = user_info['nickname'] # Check that the identity doesn't exist in another Identity profile # or in another mozillians profile error_msg = '' if IdpProfile.objects.filter(**user_q).exists(): error_msg = 'Account verification failed: Identity already exists.' elif User.objects.filter(email__iexact=email).exclude(pk=profile.user.pk).exists(): error_msg = 'The email in this identity is used by another user.' if error_msg: messages.error(request, error_msg) next_url = self.request.session.get('oidc_login_next', None) return HttpResponseRedirect(next_url or reverse('phonebook:profile_edit')) # Save the new identity to the IdpProfile user_q['profile'] = profile idp, created = IdpProfile.objects.get_or_create(**user_q) current_idp = get_object_or_none(IdpProfile, profile=profile, primary=True) # The new identity is stronger than the one currently used. Let's swap append_msg = '' # We need to check for equality too in the case a user updates the primary email in # the same identity (matching auth0_user_id). If there is an addition of the same type # we are not swapping login identities if ((current_idp and current_idp.type < idp.type) or (current_idp and current_idp.auth0_user_id == idp.auth0_user_id) or (not current_idp and created and idp.type >= IdpProfile.PROVIDER_GITHUB)): IdpProfile.objects.filter(profile=profile).exclude(pk=idp.pk).update(primary=False) idp.primary = True idp.save() # Also update the primary email of the user update_email_in_basket(profile.user.email, idp.email) User.objects.filter(pk=profile.user.id).update(email=idp.email) append_msg = ' You need to use this identity the next time you will login.' send_userprofile_to_cis.delay(profile.pk) if created: msg = 'Account successfully verified.' if append_msg: msg += append_msg messages.success(request, msg) else: msg = 'Account verification failed: Identity already exists.' messages.error(request, msg) next_url = self.request.session.get('oidc_login_next', None) return HttpResponseRedirect(next_url or reverse('phonebook:profile_edit'))
def process_request(self, request): if not self.is_refreshable_url(request): LOGGER.debug('request is not refreshable') return expiration = request.session.get('oidc_id_token_expiration', 0) now = time.time() if expiration > now: # The id_token is still valid, so we don't have to do anything. LOGGER.debug('id token is still valid (%s > %s)', expiration, now) return LOGGER.debug('id token has expired') # The id_token has expired, so we have to re-authenticate silently. auth_url = self.OIDC_OP_AUTHORIZATION_ENDPOINT client_id = self.OIDC_RP_CLIENT_ID state = get_random_string(self.OIDC_STATE_SIZE) # This is untested # code_verifier = base64.urlsafe_b64encode( os.urandom(40)).decode('utf-8') code_verifier = re.sub('[^a-zA-Z0-9]+', '', code_verifier) code_challenge = hashlib.sha256(code_verifier.encode('utf-8')).digest() code_challenge = base64.urlsafe_b64encode(code_challenge).decode( 'utf-8') code_challenge = code_challenge.replace('=', '') ## # Build the parameters as if we were doing a real auth handoff, except # we also include prompt=none. params = { 'response_type': 'code', 'client_id': client_id, 'redirect_uri': absolutify(request, reverse(self.OIDC_AUTHENTICATION_CALLBACK_URL)), 'state': state, 'scope': self.OIDC_RP_SCOPES, 'prompt': 'none', 'code_challenge': code_challenge, 'code_challenge_method': 'S256', } if self.OIDC_USE_NONCE: nonce = get_random_string(self.OIDC_NONCE_SIZE) params.update({'nonce': nonce}) add_state_and_nonce_to_session(request, state, params, code_verifier) request.session['oidc_login_next'] = request.get_full_path() query = urlencode(params) redirect_url = '{url}?{query}'.format(url=auth_url, query=query) if request.is_ajax(): # Almost all XHR request handling in client-side code struggles # with redirects since redirecting to a page where the user # is supposed to do something is extremely unlikely to work # in an XHR request. Make a special response for these kinds # of requests. # The use of 403 Forbidden is to match the fact that this # middleware doesn't really want the user in if they don't # refresh their session. response = JsonResponse({'refresh_url': redirect_url}, status=403) response['refresh_url'] = redirect_url return response return HttpResponseRedirect(redirect_url)
def process_request(self, request): site = request.site if not hasattr(site, "oidcsettings"): raise RuntimeError( "Site {} has no settings configured.".format(site)) if not self.is_refreshable_url(request): return expiration = request.session.get('oidc_id_token_expiration', 0) now = time.time() if expiration > now: # The id_token is still valid, so we don't have to do anything. return # The id_token has expired, so we have to re-authenticate silently. auth_url = import_from_settings('OIDC_OP_AUTHORIZATION_ENDPOINT') client_id = site.oidcsettings.oidc_rp_client_id state = get_random_string(import_from_settings('OIDC_STATE_SIZE', 32)) # Build the parameters as if we were doing a real auth handoff, except # we also include prompt=none. params = { 'response_type': 'code', 'client_id': client_id, 'redirect_uri': absolutify(request, reverse('oidc_authentication_callback')), 'state': state, 'scope': site.oidcsettings.oidc_rp_scopes, 'prompt': 'none', } if import_from_settings('OIDC_USE_NONCE', True): nonce = get_random_string( import_from_settings('OIDC_NONCE_SIZE', 32)) params.update({'nonce': nonce}) request.session['oidc_nonce'] = nonce request.session['oidc_state'] = state request.session['oidc_login_next'] = request.get_full_path() query = urlencode(params) redirect_url = '{url}?{query}'.format(url=auth_url, query=query) if request.is_ajax(): # Almost all XHR request handling in client-side code struggles # with redirects since redirecting to a page where the user # is supposed to do something is extremely unlikely to work # in an XHR request. Make a special response for these kinds # of requests. # The use of 403 Forbidden is to match the fact that this # middleware doesn't really want the user in if they don't # refresh their session. response = JsonResponse({'refresh_url': redirect_url}, status=403) response['refresh_url'] = redirect_url return response return HttpResponseRedirect(redirect_url)
def test_absolutify_path_host_injection(self): req = RequestFactory(HTTP_X_FORWARDED_PROTO='https').get( '/', SERVER_PORT=443) url = absolutify(req, 'evil.com/foo/bar') self.assertEqual(url, 'https://testserver/evil.com/foo/bar')
def test_absolutify_https(self): req = RequestFactory(HTTP_X_FORWARDED_PROTO='https').get( '/', SERVER_PORT=443) url = absolutify(req, '/foo/bar') self.assertEqual(url, 'https://testserver/foo/bar')