2015-05-28 10:57:24 +02:00
|
|
|
import uuid
|
2013-10-11 17:33:20 +02:00
|
|
|
import logging
|
2013-10-18 14:59:51 +02:00
|
|
|
import json
|
2017-10-09 15:15:04 +02:00
|
|
|
import urlparse
|
|
|
|
import urllib
|
2018-02-11 22:09:36 +01:00
|
|
|
import requests
|
2013-10-11 17:33:20 +02:00
|
|
|
|
|
|
|
from requests_oauthlib import OAuth2Session
|
|
|
|
|
2014-11-06 21:22:23 +01:00
|
|
|
|
2017-06-14 09:35:53 +02:00
|
|
|
from django.views.generic import View, FormView
|
2015-11-02 10:50:25 +01:00
|
|
|
from django.views.generic.detail import SingleObjectMixin
|
2017-06-14 09:35:53 +02:00
|
|
|
from django.http import HttpResponseRedirect, Http404
|
2017-09-18 15:48:48 +02:00
|
|
|
from django.contrib.auth import authenticate, REDIRECT_FIELD_NAME, get_user_model
|
2013-10-11 17:33:20 +02:00
|
|
|
from django.contrib import messages
|
2013-12-06 15:05:09 +01:00
|
|
|
from django.shortcuts import resolve_url, render
|
2013-10-11 17:33:20 +02:00
|
|
|
from django.utils.translation import ugettext as _
|
2016-02-23 11:28:12 +01:00
|
|
|
from django.utils.http import is_safe_url, urlencode
|
2013-10-11 17:33:20 +02:00
|
|
|
from django.conf import settings
|
2016-04-11 23:57:47 +02:00
|
|
|
from django.core import signing
|
2016-10-21 20:53:33 +02:00
|
|
|
from django.core.cache import InvalidCacheBackendError, caches
|
2015-10-31 00:39:58 +01:00
|
|
|
from django.core.exceptions import PermissionDenied
|
2016-04-11 23:57:47 +02:00
|
|
|
from django.core.urlresolvers import reverse
|
2016-04-13 10:03:09 +02:00
|
|
|
from django.forms import Form
|
2013-10-11 17:33:20 +02:00
|
|
|
|
2016-04-29 10:13:20 +02:00
|
|
|
from authentic2 import app_settings as a2_app_settings
|
2018-01-22 11:54:28 +01:00
|
|
|
from authentic2 import utils as a2_utils, hooks, constants
|
2017-09-18 15:48:48 +02:00
|
|
|
from authentic2.a2_rbac.utils import get_default_ou
|
2014-11-06 21:22:23 +01:00
|
|
|
|
2016-02-12 12:42:31 +01:00
|
|
|
from . import app_settings, models, utils
|
2014-01-08 01:18:45 +01:00
|
|
|
|
2014-11-06 21:22:23 +01:00
|
|
|
|
2016-04-29 10:13:20 +02:00
|
|
|
SET_PASSWORD_FORM_CLASS = a2_utils.import_module_or_class(
|
2017-06-14 09:35:53 +02:00
|
|
|
a2_app_settings.A2_REGISTRATION_SET_PASSWORD_FORM_CLASS)
|
2016-04-29 10:13:20 +02:00
|
|
|
|
|
|
|
|
2014-11-20 14:51:21 +01:00
|
|
|
class LoggerMixin(object):
|
|
|
|
def __init__(self, *args, **kwargs):
|
2016-02-22 16:05:37 +01:00
|
|
|
self.logger = logging.getLogger(__name__)
|
2014-11-20 14:51:21 +01:00
|
|
|
super(LoggerMixin, *args, **kwargs)
|
2014-11-06 21:22:23 +01:00
|
|
|
|
2013-10-18 14:59:51 +02:00
|
|
|
try:
|
2016-10-21 20:53:33 +02:00
|
|
|
cache = caches['fc']
|
2013-10-18 14:59:51 +02:00
|
|
|
except InvalidCacheBackendError:
|
2016-10-21 20:53:33 +02:00
|
|
|
cache = caches['default']
|
2013-10-18 14:59:51 +02:00
|
|
|
|
2014-11-06 21:22:23 +01:00
|
|
|
|
2013-10-18 14:59:51 +02:00
|
|
|
CACHE_TIMEOUT = 60
|
2013-10-11 17:33:20 +02:00
|
|
|
|
2014-11-06 21:22:23 +01:00
|
|
|
|
2016-03-15 12:25:23 +01:00
|
|
|
def ask_authorization(request, scopes, logger):
|
2013-10-11 17:33:20 +02:00
|
|
|
'''Compute an authorize URL for obtaining the given scope'''
|
|
|
|
if not isinstance(scopes, (list, tuple)):
|
|
|
|
scopes = [scopes]
|
|
|
|
redirect_uri = request.build_absolute_uri()
|
2017-06-14 09:35:41 +02:00
|
|
|
state = unicode(uuid.uuid4())
|
2017-06-21 11:16:51 +02:00
|
|
|
states = request.session.setdefault('fc_states', {})
|
2017-06-14 09:35:41 +02:00
|
|
|
states[state] = {
|
|
|
|
'redirect_uri': redirect_uri,
|
|
|
|
}
|
|
|
|
request.session.modified = True
|
2013-10-11 17:33:20 +02:00
|
|
|
params = {
|
2015-05-28 00:18:57 +02:00
|
|
|
'client_id': app_settings.client_id,
|
|
|
|
'scope': ' '.join(scopes),
|
|
|
|
'redirect_uri': redirect_uri,
|
|
|
|
'response_type': 'code',
|
2017-06-14 09:35:41 +02:00
|
|
|
'state': state,
|
2017-06-14 09:43:41 +02:00
|
|
|
'nonce': state,
|
2013-10-11 17:33:20 +02:00
|
|
|
}
|
2016-03-15 12:25:23 +01:00
|
|
|
logger.debug('query string %s', params)
|
2016-02-23 11:28:12 +01:00
|
|
|
url = '{0}?{1}'.format(app_settings.authorize_url, urlencode(params))
|
2016-03-15 12:25:23 +01:00
|
|
|
logger.debug('redirect to %s', url)
|
2017-10-06 13:15:15 +02:00
|
|
|
response = HttpResponseRedirect(url)
|
|
|
|
response.display_message = False
|
|
|
|
return response
|
2013-10-11 17:33:20 +02:00
|
|
|
|
2014-11-06 21:22:23 +01:00
|
|
|
|
2016-02-22 16:05:37 +01:00
|
|
|
def resolve_access_token(authorization_code, redirect_uri, logger):
|
2014-11-06 21:22:23 +01:00
|
|
|
'''Exchange an authorization_code for an access_token'''
|
2013-10-11 17:33:20 +02:00
|
|
|
data = {
|
2015-05-28 00:18:57 +02:00
|
|
|
'code': authorization_code,
|
|
|
|
'client_id': app_settings.client_id,
|
|
|
|
'client_secret': app_settings.client_secret,
|
|
|
|
'redirect_uri': redirect_uri,
|
|
|
|
'grant_type': 'authorization_code',
|
2013-10-11 17:33:20 +02:00
|
|
|
}
|
2017-06-15 15:48:45 +02:00
|
|
|
logger.debug('access token request %s', data)
|
2016-02-12 16:04:20 +01:00
|
|
|
try:
|
2018-02-11 22:09:36 +01:00
|
|
|
session = utils.requests_retry_session()
|
|
|
|
response = session.post(
|
2017-06-14 09:35:53 +02:00
|
|
|
app_settings.token_url, data=data,
|
|
|
|
verify=app_settings.verify_certificate,
|
2018-02-11 22:09:36 +01:00
|
|
|
allow_redirects=False, timeout=3)
|
2017-06-15 15:48:45 +02:00
|
|
|
if response.status_code != 200:
|
2017-03-10 10:16:37 +01:00
|
|
|
try:
|
|
|
|
data = response.json()
|
2017-06-15 15:48:45 +02:00
|
|
|
logger.warning(u'oauth2 error on access token retrieval: %r', data)
|
2017-03-10 10:16:37 +01:00
|
|
|
except ValueError:
|
|
|
|
data = {}
|
2017-06-15 15:48:45 +02:00
|
|
|
logger.warning(u'oauth2 error on access token retrieval: %r', response.content)
|
2017-03-10 10:16:37 +01:00
|
|
|
return
|
2016-02-12 16:04:20 +01:00
|
|
|
except requests.exceptions.RequestException as e:
|
2018-02-11 22:09:36 +01:00
|
|
|
logger.warning(u'unable to retrieve access token {}'.format(e))
|
2016-02-12 16:04:20 +01:00
|
|
|
else:
|
2016-02-22 15:58:57 +01:00
|
|
|
try:
|
|
|
|
response = response.json()
|
|
|
|
logger.debug('token resolved : {}'.format(response))
|
|
|
|
return response
|
|
|
|
except ValueError:
|
2017-06-14 09:35:53 +02:00
|
|
|
logger.warning(
|
|
|
|
"no JSON object can be decoded from the data received from {} : '{}'".format(
|
2016-02-22 15:58:57 +01:00
|
|
|
app_settings.token_url, response.content))
|
2013-10-11 17:33:20 +02:00
|
|
|
|
2014-11-06 21:22:23 +01:00
|
|
|
|
2016-02-22 16:05:37 +01:00
|
|
|
def access_token_from_request(request, logger):
|
2013-10-18 14:59:51 +02:00
|
|
|
'''Resolve an access token given a request returning from the authorization
|
|
|
|
endpoint.
|
|
|
|
'''
|
2017-06-14 09:35:41 +02:00
|
|
|
code = request.GET.get('code')
|
|
|
|
state = request.GET.get('state')
|
|
|
|
if not code:
|
|
|
|
return
|
|
|
|
if not state:
|
|
|
|
return
|
2017-06-21 11:16:51 +02:00
|
|
|
states = request.session.get('fc_states', {})
|
2017-06-14 09:35:41 +02:00
|
|
|
if state not in states:
|
|
|
|
return
|
|
|
|
# there should not be many FC SSO in flight
|
|
|
|
redirect_uri = states[state]['redirect_uri']
|
|
|
|
return resolve_access_token(code, redirect_uri, logger)
|
2013-10-11 17:33:20 +02:00
|
|
|
|
2014-11-06 21:22:23 +01:00
|
|
|
|
2013-10-11 17:33:20 +02:00
|
|
|
ACCESS_GRANT_CODE = 'accessgrantcode'
|
|
|
|
|
2014-11-06 21:22:23 +01:00
|
|
|
|
2015-05-27 16:07:44 +02:00
|
|
|
class FcOAuthSessionViewMixin(LoggerMixin):
|
2014-11-06 21:22:23 +01:00
|
|
|
'''Add the OAuth2 dance to a view'''
|
2016-03-31 19:21:07 +02:00
|
|
|
scopes = ['openid', 'profile', 'birth', 'email']
|
2013-10-11 17:33:20 +02:00
|
|
|
redirect_field_name = REDIRECT_FIELD_NAME
|
|
|
|
in_popup = False
|
2013-12-06 15:57:54 +01:00
|
|
|
token = None
|
2013-10-11 17:33:20 +02:00
|
|
|
|
|
|
|
def get_in_popup(self):
|
|
|
|
return self.in_popup
|
|
|
|
|
|
|
|
def redirect_to(self, request, *args, **kwargs):
|
2017-09-18 15:43:59 +02:00
|
|
|
if request.method == 'POST':
|
|
|
|
redirect_to = request.POST.get(self.redirect_field_name,
|
|
|
|
request.GET.get(self.redirect_field_name, ''))
|
|
|
|
else:
|
|
|
|
redirect_to = request.GET.get(self.redirect_field_name, '')
|
|
|
|
|
2013-10-11 17:33:20 +02:00
|
|
|
if not is_safe_url(url=redirect_to, host=request.get_host()):
|
|
|
|
redirect_to = resolve_url(settings.LOGIN_REDIRECT_URL)
|
|
|
|
return redirect_to
|
|
|
|
|
2013-12-06 15:06:34 +01:00
|
|
|
def close_popup_redirect(self, request, next_url, *args, **kwargs):
|
|
|
|
'''Show a page to close the current popup and reload the parent window
|
|
|
|
with the return url.
|
|
|
|
'''
|
2015-05-28 00:18:57 +02:00
|
|
|
return render(request, 'authentic2_auth_fc/close-popup-redirect.html',
|
|
|
|
{'redirect_to': next_url})
|
2013-12-06 15:06:34 +01:00
|
|
|
|
|
|
|
def simple_redirect(self, request, next_url, *args, **kwargs):
|
|
|
|
return HttpResponseRedirect(next_url)
|
2013-10-11 17:33:20 +02:00
|
|
|
|
|
|
|
def redirect(self, request, *args, **kwargs):
|
2013-12-06 15:06:34 +01:00
|
|
|
next_url = kwargs.pop('next_url', None)
|
|
|
|
if next_url is None:
|
|
|
|
next_url = self.redirect_to(request, *args, **kwargs)
|
2013-10-11 17:33:20 +02:00
|
|
|
if self.get_in_popup():
|
2015-05-28 00:18:57 +02:00
|
|
|
return self.close_popup_redirect(request, next_url, *args,
|
|
|
|
**kwargs)
|
2013-10-11 17:33:20 +02:00
|
|
|
else:
|
2013-12-06 15:06:34 +01:00
|
|
|
return self.simple_redirect(request, next_url, *args, **kwargs)
|
2013-10-11 17:33:20 +02:00
|
|
|
|
2013-12-06 15:07:39 +01:00
|
|
|
def redirect_and_come_back(self, request, next_url, *args, **kwargs):
|
|
|
|
old_next_url = self.redirect_to(request, *args, **kwargs)
|
2015-05-28 00:18:57 +02:00
|
|
|
here = '{0}?{1}'.format(
|
2016-02-23 11:28:12 +01:00
|
|
|
request.path, urlencode({REDIRECT_FIELD_NAME: old_next_url}))
|
2017-06-14 09:35:53 +02:00
|
|
|
there = '{0}{2}{1}'.format(
|
|
|
|
next_url, urlencode({REDIRECT_FIELD_NAME: here}),
|
2015-05-28 00:18:57 +02:00
|
|
|
'&' if '?' in next_url else '?')
|
2013-12-06 15:07:39 +01:00
|
|
|
return self.redirect(request, next_url=there, *args, **kwargs)
|
|
|
|
|
2013-10-11 17:33:20 +02:00
|
|
|
def get_scopes(self):
|
2017-07-26 19:06:40 +02:00
|
|
|
if app_settings.scopes:
|
|
|
|
return list(set(['openid'] + app_settings.scopes))
|
|
|
|
else:
|
|
|
|
return self.scopes
|
2013-10-11 17:33:20 +02:00
|
|
|
|
2016-02-17 16:57:27 +01:00
|
|
|
def get_ressource(self, url, verify):
|
|
|
|
try:
|
2017-06-14 09:35:53 +02:00
|
|
|
data = self.oauth_session().get(url, verify=verify, allow_redirects=False, timeout=3)
|
2016-02-17 16:57:27 +01:00
|
|
|
data.raise_for_status()
|
|
|
|
except requests.exceptions.RequestException as e:
|
2018-02-11 22:09:36 +01:00
|
|
|
self.logger.warning(u'unable to retrieve ressource from {} due to {}'.format(url, e))
|
2016-02-17 16:57:27 +01:00
|
|
|
else:
|
|
|
|
try:
|
|
|
|
data = data.json()
|
|
|
|
self.logger.debug('ressource resolved : {}'.format(data))
|
|
|
|
return data
|
|
|
|
except ValueError:
|
2017-06-14 09:35:53 +02:00
|
|
|
self.logger.warning(
|
|
|
|
"no JSON object can be decoded from the data received from {} : '{}'".format(
|
2016-02-17 16:57:27 +01:00
|
|
|
url, data.content))
|
|
|
|
|
2015-05-27 16:07:44 +02:00
|
|
|
def get_user_info(self):
|
2016-02-17 16:57:27 +01:00
|
|
|
return self.get_ressource(app_settings.userinfo_url + '?schema=openid',
|
2017-06-14 09:35:53 +02:00
|
|
|
app_settings.verify_certificate)
|
2013-10-11 17:33:20 +02:00
|
|
|
|
2015-05-29 11:28:57 +02:00
|
|
|
def get_data(self, scopes=[]):
|
2016-02-23 11:28:12 +01:00
|
|
|
data = dict()
|
2015-05-29 11:28:57 +02:00
|
|
|
if not app_settings.fd_list:
|
|
|
|
return data
|
|
|
|
for scope in scopes:
|
|
|
|
for fd in app_settings.fd_list[scope]:
|
|
|
|
url = fd['url']
|
|
|
|
if fd['query_dic']:
|
2016-02-23 11:28:12 +01:00
|
|
|
url += '?' + urlencode(fd['query_dic'])
|
2016-02-17 16:57:27 +01:00
|
|
|
d = self.get_ressource(url, app_settings.verify_certificate)
|
|
|
|
if d:
|
2016-02-23 11:28:12 +01:00
|
|
|
data.setdefault(scope, []).append((fd['name'], d))
|
2015-05-29 11:28:57 +02:00
|
|
|
return data
|
|
|
|
|
2013-10-11 17:33:20 +02:00
|
|
|
def authorization_error(self, request, *args, **kwargs):
|
2017-09-18 15:43:59 +02:00
|
|
|
if request.method == 'POST':
|
|
|
|
error = request.POST.get('error')
|
|
|
|
else:
|
|
|
|
error = request.GET.get('error')
|
|
|
|
if error == 'access_denied':
|
2016-02-17 15:28:47 +01:00
|
|
|
messages.warning(request, _('You refused the connection.'))
|
2014-11-20 14:51:21 +01:00
|
|
|
self.logger.debug('authorization_error %r', request.GET)
|
2013-10-11 17:33:20 +02:00
|
|
|
return self.redirect(request)
|
|
|
|
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
|
|
'''Interpret the OAuth authorization dance'''
|
|
|
|
if 'code' in request.GET:
|
2016-02-22 16:05:37 +01:00
|
|
|
self.token = access_token_from_request(request, self.logger)
|
2016-02-12 15:27:40 +01:00
|
|
|
|
2016-02-12 16:04:20 +01:00
|
|
|
# Token request may not be completly processed and result in no
|
|
|
|
# token
|
|
|
|
if not self.token:
|
2016-02-17 15:28:47 +01:00
|
|
|
messages.warning(request, _('Unable to connect to FranceConnect.'))
|
2016-02-12 16:04:20 +01:00
|
|
|
return self.redirect(request)
|
|
|
|
|
2016-02-12 15:27:40 +01:00
|
|
|
# The token request may fail, 'error' is then required.
|
|
|
|
# A bad client secret results in error equals to invalid_request
|
|
|
|
# for FC and invalid_client for oidc_provider.
|
2017-06-14 09:35:53 +02:00
|
|
|
if 'error' in self.token:
|
2016-02-12 15:27:40 +01:00
|
|
|
msg = 'token request failed : {}'.format(self.token)
|
|
|
|
self.logger.warning(msg)
|
2016-02-17 15:28:47 +01:00
|
|
|
messages.warning(request, _('Unable to connect to FranceConnect.'))
|
2016-02-12 15:27:40 +01:00
|
|
|
return self.redirect(request)
|
2017-06-15 15:45:05 +02:00
|
|
|
key = app_settings.client_secret
|
|
|
|
if isinstance(key, unicode):
|
|
|
|
key = key.encode('utf-8')
|
|
|
|
self.id_token, error = models.parse_id_token(
|
|
|
|
self.token['id_token'], client_id=app_settings.client_id, client_secret=key)
|
|
|
|
if not self.id_token:
|
|
|
|
self.logger.warning(u'validation of id_token failed: %s', error)
|
|
|
|
messages.warning(request, _('Unable to connect to FranceConnect.'))
|
|
|
|
return self.redirect(request)
|
2017-06-14 09:43:41 +02:00
|
|
|
nonce = self.id_token.get('nonce')
|
2017-06-21 11:16:51 +02:00
|
|
|
states = request.session.get('fc_states', {})
|
2017-06-14 09:43:41 +02:00
|
|
|
if not nonce or nonce not in states:
|
|
|
|
self.logger.warning(u'invalid nonce in id_token %s, known ones %s', nonce,
|
|
|
|
u', '.join(states.keys()))
|
|
|
|
messages.warning(request, _('Unable to connect to FranceConnect.'))
|
|
|
|
return self.redirect(request)
|
2015-05-28 00:31:26 +02:00
|
|
|
self.logger.debug('fc id_token %s', self.id_token)
|
2015-05-27 16:07:44 +02:00
|
|
|
for key in self.id_token:
|
2015-05-28 00:18:57 +02:00
|
|
|
setattr(self, key, self.id_token[key])
|
2018-02-11 22:09:36 +01:00
|
|
|
self.oauth_session = lambda: utils.requests_retry_session(
|
|
|
|
session=OAuth2Session(
|
|
|
|
app_settings.client_id, token=self.token))
|
2015-05-28 00:31:26 +02:00
|
|
|
self.user_info = self.get_user_info()
|
2016-02-17 16:57:27 +01:00
|
|
|
if not self.user_info:
|
|
|
|
msg = 'userinfo resolution failed : {}'.format(self.token)
|
|
|
|
self.logger.warning(msg)
|
|
|
|
messages.warning(request, _('Unable to connect to FranceConnect.'))
|
|
|
|
return self.redirect(request)
|
2015-05-28 00:31:26 +02:00
|
|
|
self.logger.debug('fc user_info %s', self.user_info)
|
2017-06-21 11:16:51 +02:00
|
|
|
self.request.session['fc_id_token'] = self.id_token
|
|
|
|
self.request.session['fc_id_token_raw'] = self.token['id_token']
|
|
|
|
self.request.session['fc_user_info'] = self.user_info
|
2015-05-29 11:28:57 +02:00
|
|
|
if 'fd_scopes' in request.GET:
|
|
|
|
scopes = request.GET.get('fd_scopes')
|
2016-02-23 11:28:12 +01:00
|
|
|
scopes = scopes.split()
|
2015-05-29 11:28:57 +02:00
|
|
|
self.data = self.get_data(scopes)
|
|
|
|
self.logger.debug('fc data %s', self.data)
|
2017-06-21 11:16:51 +02:00
|
|
|
fc_data = self.request.session.setdefault('fc_data', {})
|
2016-02-23 11:28:12 +01:00
|
|
|
for scope in self.data:
|
|
|
|
fc_data.setdefault(scope, []).extend(self.data[scope])
|
2017-06-21 11:16:51 +02:00
|
|
|
self.logger.debug('fc data in session %s', self.request.session['fc_data'])
|
2015-05-28 00:18:57 +02:00
|
|
|
return super(FcOAuthSessionViewMixin, self).dispatch(request,
|
|
|
|
*args,
|
|
|
|
**kwargs)
|
2013-10-11 17:33:20 +02:00
|
|
|
elif 'error' in request.GET:
|
|
|
|
return self.authorization_error(request, *args, **kwargs)
|
|
|
|
else:
|
2015-05-29 11:28:57 +02:00
|
|
|
if 'fd_scopes' in request.GET:
|
|
|
|
scopes = request.GET.get('fd_scopes')
|
2016-02-23 11:28:12 +01:00
|
|
|
scopes = scopes.split()
|
2015-05-29 11:28:57 +02:00
|
|
|
self.scopes.extend(scopes)
|
2016-03-15 12:25:23 +01:00
|
|
|
return ask_authorization(request, self.get_scopes(), self.logger)
|
2013-10-11 17:33:20 +02:00
|
|
|
|
2015-05-28 00:18:57 +02:00
|
|
|
|
2013-10-11 17:33:20 +02:00
|
|
|
class PopupViewMixin(object):
|
|
|
|
def get_in_popup(self):
|
2017-09-18 15:43:59 +02:00
|
|
|
return 'popup' in self.request.GET
|
2013-10-11 17:33:20 +02:00
|
|
|
|
2013-12-06 15:09:21 +01:00
|
|
|
|
2015-05-27 16:07:44 +02:00
|
|
|
class LoginOrLinkView(PopupViewMixin, FcOAuthSessionViewMixin, View):
|
|
|
|
'''Login with FC, if the FC account is already linked, connect this user,
|
2014-11-06 21:22:23 +01:00
|
|
|
if a user is logged link the user to this account, otherwise display an
|
|
|
|
error message.
|
|
|
|
'''
|
2015-05-29 11:15:07 +02:00
|
|
|
|
|
|
|
def update_user_info(self):
|
|
|
|
self.fc_account.user_info = json.dumps(self.user_info)
|
|
|
|
self.fc_account.save()
|
2017-03-07 11:29:57 +01:00
|
|
|
utils.apply_user_info_mappings(self.fc_account.user, self.user_info)
|
2015-05-29 11:15:07 +02:00
|
|
|
self.logger.debug('updating user_info %s', self.fc_account.user_info)
|
|
|
|
|
2013-12-06 15:09:21 +01:00
|
|
|
def get(self, request, *args, **kwargs):
|
2016-04-12 00:18:38 +02:00
|
|
|
registration = True if 'registration' in request.GET else False
|
2013-12-06 15:09:21 +01:00
|
|
|
'''Request an access grant code and associate it to the current user'''
|
2018-01-22 11:54:28 +01:00
|
|
|
self.service_slug = request.GET.get(constants.SERVICE_FIELD_NAME)
|
2016-02-12 16:22:26 +01:00
|
|
|
if request.user.is_authenticated():
|
2016-05-02 11:30:01 +02:00
|
|
|
# Prevent to add a link with an FC account already linked with another user.
|
|
|
|
try:
|
|
|
|
fc_account = models.FcAccount.objects.get(sub=self.sub, user__is_active=True)
|
|
|
|
if fc_account.user is not request.user:
|
|
|
|
msg = 'Attempt to link FC account {} already linked with user {}'
|
|
|
|
self.logger.info(msg.format(self.sub, fc_account.user))
|
2017-11-14 11:37:24 +01:00
|
|
|
messages.error(request,
|
|
|
|
_('The FranceConnect account {} is already'
|
|
|
|
' linked with another account.').format(fc_account))
|
2016-05-02 11:30:01 +02:00
|
|
|
return self.redirect(request)
|
|
|
|
except models.FcAccount.DoesNotExist:
|
|
|
|
pass
|
|
|
|
|
2017-11-14 11:37:10 +01:00
|
|
|
# Prevent to add a link to an user which is already linked to an FC account
|
|
|
|
if request.user.fc_accounts.exists():
|
|
|
|
self.logger.warning(u'cannot link to sub %s, account is already linked to an '
|
|
|
|
u'FC account', self.sub)
|
|
|
|
messages.error(request,
|
|
|
|
_('Your account is already linked to a FranceConnect account'))
|
|
|
|
return self.redirect(request)
|
|
|
|
|
2015-05-27 18:47:43 +02:00
|
|
|
json_token = json.dumps(self.token)
|
2015-05-29 11:15:07 +02:00
|
|
|
self.fc_account, created = models.FcAccount.objects.get_or_create(
|
2015-05-27 18:47:43 +02:00
|
|
|
defaults={'token': json_token},
|
|
|
|
sub=self.sub, user=request.user)
|
|
|
|
if created:
|
2015-05-28 00:31:26 +02:00
|
|
|
self.logger.info('fc link created sub %s', self.sub)
|
2016-02-17 15:28:47 +01:00
|
|
|
self.update_user_info()
|
2016-05-12 19:14:30 +02:00
|
|
|
data = utils.get_mapped_attributes_flat(request)
|
|
|
|
if 'email' in data:
|
2017-06-14 09:35:53 +02:00
|
|
|
messages.info(request,
|
|
|
|
_('Your FranceConnect account {} with '
|
|
|
|
'email {} has been linked.').format(self.fc_account,
|
|
|
|
data['email']))
|
2016-05-12 19:14:30 +02:00
|
|
|
else:
|
2017-06-14 09:35:53 +02:00
|
|
|
messages.info(request, _('Your FranceConnect account {} '
|
|
|
|
'has been linked.').format(self.fc_account))
|
2017-11-27 09:52:59 +01:00
|
|
|
hooks.call_hooks('event', name='fc-link', user=request.user, sub=self.sub,
|
|
|
|
request=request)
|
2015-05-27 18:47:43 +02:00
|
|
|
else:
|
2015-05-29 11:15:07 +02:00
|
|
|
self.fc_account.token = json_token
|
|
|
|
self.fc_account.save()
|
2016-02-17 15:28:47 +01:00
|
|
|
self.update_user_info()
|
|
|
|
messages.info(request, _('Your local account has been updated.'))
|
2016-02-12 16:22:26 +01:00
|
|
|
return self.redirect(request)
|
|
|
|
|
2017-09-18 15:48:48 +02:00
|
|
|
default_ou = get_default_ou()
|
|
|
|
email_is_unique = a2_app_settings.A2_EMAIL_IS_UNIQUE or default_ou.email_is_unique
|
2016-02-12 16:22:26 +01:00
|
|
|
user = authenticate(sub=self.sub, user_info=self.user_info,
|
|
|
|
token=self.token)
|
2017-09-18 15:48:48 +02:00
|
|
|
if not user and self.user_info.get('email') and email_is_unique:
|
|
|
|
email = self.user_info['email']
|
|
|
|
User = get_user_model()
|
|
|
|
qs = User.objects.filter(email=email)
|
|
|
|
if not a2_app_settings.A2_EMAIL_IS_UNIQUE and default_ou.email_is_unique:
|
|
|
|
qs = qs.filter(ou=default_ou)
|
2017-11-14 11:33:57 +01:00
|
|
|
|
2017-09-18 15:48:48 +02:00
|
|
|
if qs.exists():
|
|
|
|
# there should not be multiple accounts with the same mail
|
|
|
|
if len(qs) > 1:
|
2017-11-14 11:33:57 +01:00
|
|
|
self.logger.error(u'multiple accounts with the same mail %s, %s', email,
|
|
|
|
list(qs))
|
|
|
|
# ok we have one account
|
|
|
|
elif len(qs) == 1:
|
|
|
|
user = qs[0]
|
|
|
|
# but does he have already a link to an FC account ?
|
|
|
|
if not user.fc_accounts.exists():
|
|
|
|
fc_account, created = models.FcAccount.objects.get_or_create(
|
|
|
|
defaults={'token': json.dumps(self.token)},
|
|
|
|
sub=self.sub, user=user)
|
2017-11-22 21:16:36 +01:00
|
|
|
if created:
|
|
|
|
self.logger.info(u'fc link created sub %s user %s', self.sub, user)
|
2017-11-27 09:52:59 +01:00
|
|
|
hooks.call_hooks('event', name='fc-link', user=user, sub=self.sub,
|
|
|
|
request=request)
|
2017-11-14 11:33:57 +01:00
|
|
|
user = authenticate(sub=self.sub, user_info=self.user_info,
|
|
|
|
token=self.token)
|
|
|
|
else:
|
2018-02-11 22:42:43 +01:00
|
|
|
messages.warning(
|
|
|
|
request,
|
2018-02-15 13:18:48 +01:00
|
|
|
_('Your FranceConnect email address \'%s\' is already used by another '
|
|
|
|
'account, so we cannot create an account for you. Please create an '
|
|
|
|
'account with another email address then link it to FranceConnect '
|
|
|
|
'using your account management page.') % email)
|
2018-01-22 11:50:02 +01:00
|
|
|
return self.redirect(request)
|
2016-02-12 16:22:26 +01:00
|
|
|
if user:
|
2018-01-22 11:54:28 +01:00
|
|
|
a2_utils.login(request, user, 'france-connect', service_slug=self.service_slug)
|
2016-02-12 16:22:26 +01:00
|
|
|
self.fc_account = models.FcAccount.objects.get(sub=self.sub, user=user)
|
2017-09-18 15:48:48 +02:00
|
|
|
self.fc_account.token = json.dumps(self.token)
|
|
|
|
self.fc_account.save(update_fields=['token'])
|
2016-02-12 16:22:26 +01:00
|
|
|
self.update_user_info()
|
|
|
|
self.logger.info('logged in using fc sub %s', self.sub)
|
2013-12-06 15:09:21 +01:00
|
|
|
return self.redirect(request)
|
|
|
|
else:
|
2016-04-12 00:18:38 +02:00
|
|
|
if registration:
|
|
|
|
return self.redirect_and_come_back(request, reverse('fc-registration'))
|
2016-03-31 19:24:07 +02:00
|
|
|
else:
|
2016-04-15 15:38:14 +02:00
|
|
|
messages.info(request, _('If you already have an account, please log in, else '
|
|
|
|
'create your account.'))
|
2016-04-12 00:18:38 +02:00
|
|
|
if app_settings.show_button_quick_account_creation:
|
|
|
|
return self.redirect_and_come_back(request, settings.LOGIN_URL)
|
|
|
|
else:
|
2017-06-14 09:35:53 +02:00
|
|
|
return self.redirect_and_come_back(request,
|
|
|
|
'{0}?nofc=1'.format(settings.LOGIN_URL))
|
2013-12-06 15:09:21 +01:00
|
|
|
|
2015-12-03 17:49:21 +01:00
|
|
|
|
2016-04-11 23:57:47 +02:00
|
|
|
class RegistrationView(LoggerMixin, View):
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
|
|
data = utils.get_mapped_attributes_flat(request)
|
|
|
|
data['no_password'] = True
|
2017-06-08 17:12:48 +02:00
|
|
|
if app_settings.auto_register:
|
|
|
|
data['confirm_data'] = 'required'
|
|
|
|
else:
|
|
|
|
data['confirm_data'] = True
|
2017-09-18 15:43:59 +02:00
|
|
|
redirect_to = request.GET.get(REDIRECT_FIELD_NAME, '')
|
2016-04-11 23:57:47 +02:00
|
|
|
if not is_safe_url(url=redirect_to, host=request.get_host()):
|
|
|
|
redirect_to = resolve_url(settings.LOGIN_REDIRECT_URL)
|
2017-10-09 15:15:04 +02:00
|
|
|
|
|
|
|
# Prevent errors when redirect_to does not contain fc-login-or-link view
|
|
|
|
parsed_redirect_to = urlparse.urlparse(redirect_to)
|
|
|
|
if parsed_redirect_to.path != reverse('fc-login-or-link'):
|
|
|
|
redirect_to = '%s?%s=%s' % (
|
|
|
|
reverse('fc-login-or-link'),
|
|
|
|
REDIRECT_FIELD_NAME,
|
|
|
|
urllib.quote(redirect_to))
|
2016-04-11 23:57:47 +02:00
|
|
|
if not 'email' in data:
|
|
|
|
data[REDIRECT_FIELD_NAME] = redirect_to
|
2017-11-14 11:37:24 +01:00
|
|
|
messages.warning(request,
|
|
|
|
_("FranceConnect didn't provide your email address, please do."))
|
2016-04-11 23:57:47 +02:00
|
|
|
return HttpResponseRedirect("{}?token={}".format(reverse('registration_register'),
|
|
|
|
signing.dumps(data)))
|
|
|
|
data['valid_email'] = False
|
2017-07-11 16:45:51 +02:00
|
|
|
data['franceconnect'] = True
|
2017-10-09 15:15:04 +02:00
|
|
|
activation_url = a2_utils.build_activation_url(request,
|
|
|
|
next_url=redirect_to,
|
|
|
|
**data)
|
2016-04-11 23:57:47 +02:00
|
|
|
return HttpResponseRedirect(activation_url)
|
|
|
|
|
|
|
|
|
2017-11-07 17:33:37 +01:00
|
|
|
class UnlinkView(LoggerMixin, FormView):
|
2015-10-31 00:39:58 +01:00
|
|
|
template_name = 'authentic2_auth_fc/unlink.html'
|
|
|
|
|
2016-04-13 10:03:09 +02:00
|
|
|
def get_success_url(self):
|
2017-07-04 12:06:26 +02:00
|
|
|
url = reverse('account_management')
|
2016-04-13 10:03:09 +02:00
|
|
|
if app_settings.logout_when_unlink:
|
2018-06-22 15:39:50 +02:00
|
|
|
# logout URL can be None if not session exists with FC
|
|
|
|
url = utils.build_logout_url(self.request, next_url=url) or url
|
2017-07-04 12:06:26 +02:00
|
|
|
return url
|
2016-04-13 10:03:09 +02:00
|
|
|
|
|
|
|
def get_form_class(self):
|
|
|
|
form_class = Form
|
2017-11-07 17:33:37 +01:00
|
|
|
if not self.request.user.has_usable_password():
|
2016-04-29 10:13:20 +02:00
|
|
|
form_class = SET_PASSWORD_FORM_CLASS
|
2016-04-13 10:03:09 +02:00
|
|
|
return form_class
|
|
|
|
|
|
|
|
def get_form_kwargs(self, **kwargs):
|
|
|
|
kwargs = super(UnlinkView, self).get_form_kwargs(**kwargs)
|
2017-11-07 17:33:37 +01:00
|
|
|
if not self.request.user.has_usable_password():
|
|
|
|
kwargs['user'] = self.request.user
|
2016-04-13 10:03:09 +02:00
|
|
|
return kwargs
|
|
|
|
|
2015-10-31 00:39:58 +01:00
|
|
|
def dispatch(self, request, *args, **kwargs):
|
2018-03-09 13:30:29 +01:00
|
|
|
if not request.user.is_authenticated():
|
|
|
|
raise PermissionDenied()
|
2016-04-29 10:13:20 +02:00
|
|
|
# We prevent unlinking if the user has no usable password and can't change it
|
|
|
|
# because we assume that the password is the unique other mean of authentication
|
|
|
|
# and unlinking would make the account unreachable.
|
|
|
|
if not request.user.has_usable_password() and not \
|
|
|
|
a2_app_settings.A2_REGISTRATION_CAN_CHANGE_PASSWORD:
|
|
|
|
# Prevent access to the view.
|
|
|
|
raise Http404
|
2015-10-31 00:39:58 +01:00
|
|
|
return super(UnlinkView, self).dispatch(request, *args, **kwargs)
|
|
|
|
|
2016-04-13 10:03:09 +02:00
|
|
|
def form_valid(self, form):
|
2017-11-07 17:33:37 +01:00
|
|
|
if not self.request.user.has_usable_password():
|
2016-04-14 10:44:13 +02:00
|
|
|
form.save()
|
2017-11-07 17:33:37 +01:00
|
|
|
self.logger.info(u'user %s has set a password', self.request.user)
|
|
|
|
links = models.FcAccount.objects.filter(user=self.request.user)
|
|
|
|
for link in links:
|
|
|
|
self.logger.info(u'user %s unlinked from %s', self.request.user, link)
|
2017-11-22 21:16:36 +01:00
|
|
|
hooks.call_hooks('event', name='fc-unlink', user=self.request.user)
|
2017-11-07 17:33:37 +01:00
|
|
|
messages.info(self.request, _('The link with the FranceConnect account has been deleted.'))
|
|
|
|
links.delete()
|
2016-04-13 10:03:09 +02:00
|
|
|
return super(UnlinkView, self).form_valid(form)
|
|
|
|
|
|
|
|
def get_context_data(self, **kwargs):
|
|
|
|
context = super(UnlinkView, self).get_context_data(**kwargs)
|
2017-11-07 17:33:37 +01:00
|
|
|
if not self.request.user.has_usable_password():
|
2016-04-13 10:03:09 +02:00
|
|
|
context['no_password'] = True
|
|
|
|
return context
|
|
|
|
|
2015-10-31 00:39:58 +01:00
|
|
|
def post(self, request, *args, **kwargs):
|
2016-04-13 10:03:09 +02:00
|
|
|
if 'cancel' in request.POST:
|
|
|
|
return a2_utils.redirect(request, 'account_management')
|
|
|
|
return super(UnlinkView, self).post(request, *args, **kwargs)
|
2013-12-06 15:09:21 +01:00
|
|
|
|
2015-12-22 12:15:55 +01:00
|
|
|
|
2013-12-06 15:09:21 +01:00
|
|
|
login_or_link = LoginOrLinkView.as_view()
|
2016-04-11 23:57:47 +02:00
|
|
|
registration = RegistrationView.as_view()
|
2015-10-31 00:39:58 +01:00
|
|
|
unlink = UnlinkView.as_view()
|
2015-12-03 17:49:21 +01:00
|
|
|
|
|
|
|
|
|
|
|
class LogoutReturnView(View):
|
|
|
|
def get(self, request, *args, **kwargs):
|
2017-07-04 12:06:26 +02:00
|
|
|
state = request.GET.get('state')
|
2017-06-21 11:16:51 +02:00
|
|
|
request.session.pop('fc_id_token', None)
|
|
|
|
request.session.pop('fc_id_token_raw', None)
|
|
|
|
request.session.pop('fc_user_info', None)
|
|
|
|
request.session.pop('fc_data', None)
|
2017-07-04 12:06:26 +02:00
|
|
|
states = request.session.pop('fc_states', None)
|
|
|
|
next_url = None
|
2017-07-05 09:59:39 +02:00
|
|
|
if states and state in states:
|
2017-07-04 12:06:26 +02:00
|
|
|
next_url = states[state].get('next')
|
|
|
|
if not next_url:
|
2017-07-11 16:45:25 +02:00
|
|
|
next_url = reverse('auth_logout')
|
2017-07-04 12:06:26 +02:00
|
|
|
return HttpResponseRedirect(next_url)
|
2015-12-03 17:49:21 +01:00
|
|
|
|
|
|
|
logout = LogoutReturnView.as_view()
|