0002-add-franceconnect-connector-53879.patch
passerelle/apps/franceconnect/fc.py | ||
---|---|---|
1 |
# passerelle - uniform access to multiple data sources and services |
|
2 |
# Copyright (C) 2021 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
17 |
import base64 |
|
18 |
import json |
|
19 |
import urllib.parse |
|
20 |
import uuid |
|
21 | ||
22 |
import requests |
|
23 |
from django.utils.translation import ugettext_lazy as _ |
|
24 | ||
25 | ||
26 |
class FranceConnectError(Exception): |
|
27 |
def __init__(self, message, **kwargs): |
|
28 |
self.data = tuple(kwargs.items()) |
|
29 |
super().__init__(message) |
|
30 | ||
31 | ||
32 |
class Test: |
|
33 |
slug = 'test' |
|
34 |
name = _('Testing') |
|
35 |
authorize_url = 'https://fcp.integ01.dev-franceconnect.fr/api/v1/authorize' |
|
36 |
token_endpoint_url = 'https://fcp.integ01.dev-franceconnect.fr/api/v1/token' |
|
37 |
user_info_endpoint_url = 'https://fcp.integ01.dev-franceconnect.fr/api/v1/userinfo' |
|
38 |
logout_url = 'https://fcp.integ01.dev-franceconnect.fr/api/v1/logout' |
|
39 | ||
40 | ||
41 |
class Prod: |
|
42 |
slug = 'prod' |
|
43 |
name = _('Production') |
|
44 |
authorize_url = 'https://app.franceconnect.gouv.fr/api/v1/authorize' |
|
45 |
token_endpoint_url = 'https://app.franceconnect.gouv.fr/api/v1/token' |
|
46 |
user_info_endpoint_url = 'https://app.franceconnect.gouv.fr/api/v1/userinfo' |
|
47 |
logout_url = 'https://app.franceconnect.gouv.fr/api/v1/logout' |
|
48 | ||
49 | ||
50 |
PLATFORMS = [Test, Prod] |
|
51 |
PLATFORMS_BY_SLUG = {platform.slug: platform for platform in PLATFORMS} |
|
52 | ||
53 | ||
54 |
def base64url_decode(input): |
|
55 |
rem = len(input) % 4 |
|
56 |
if rem > 0: |
|
57 |
input += b'=' * (4 - rem) |
|
58 |
return base64.urlsafe_b64decode(input) |
|
59 | ||
60 | ||
61 |
class FranceConnect: |
|
62 |
def __init__(self, session, logger): |
|
63 |
self.session = session |
|
64 |
self.logger = logger |
|
65 |
self.items = [] |
|
66 |
self.correlation_id = str(uuid.uuid4()) |
|
67 | ||
68 |
def authorization_request(self, platform, client_id, scopes, redirect_uri, acr_values='eidas1'): |
|
69 |
'''Launch an authorization request to FranceConnect''' |
|
70 |
qs = urllib.parse.urlencode( |
|
71 |
{ |
|
72 |
'response_type': 'code', |
|
73 |
'client_id': client_id, |
|
74 |
'redirect_uri': redirect_uri, |
|
75 |
'scope': 'openid ' + scopes, |
|
76 |
'state': str(uuid.uuid4()), |
|
77 |
'nonce': str(uuid.uuid4()), |
|
78 |
'acr_values': acr_values, |
|
79 |
} |
|
80 |
) |
|
81 |
return '%s?%s' % (platform.authorize_url, qs) |
|
82 | ||
83 |
def handle_authorization_response( |
|
84 |
self, platform, client_id, client_secret, redirect_uri, code, error, error_description |
|
85 |
): |
|
86 |
if error: |
|
87 |
raise FranceConnectError( |
|
88 |
'No authorization code', error=error, error_description=error_description |
|
89 |
) |
|
90 | ||
91 |
data = { |
|
92 |
'grant_type': 'authorization_code', |
|
93 |
'redirect_uri': redirect_uri, |
|
94 |
'client_id': client_id, |
|
95 |
'client_secret': client_secret, |
|
96 |
'code': code, |
|
97 |
} |
|
98 | ||
99 |
response_content = self.request('token endpoint', 'POST', platform.token_endpoint_url, data=data) |
|
100 | ||
101 |
try: |
|
102 |
self.add('fc_token_endpoint_response', response_content) |
|
103 |
self.add('fc_access_token', response_content['access_token']) |
|
104 |
self.add('fc_id_token', response_content['id_token']) |
|
105 |
header, payload, signature = self.fc_id_token.split('.') |
|
106 |
self.add('fc_id_token_payload', json.loads(base64url_decode(payload.encode()))) |
|
107 |
except Exception as e: |
|
108 |
raise FranceConnectError('Error in token endpoint response', sub_exception=e) |
|
109 | ||
110 |
fc_user_info = self.request( |
|
111 |
'user_info endpoint', |
|
112 |
'GET', |
|
113 |
platform.user_info_endpoint_url, |
|
114 |
headers={'Authorization': 'Bearer %s' % self.fc_access_token}, |
|
115 |
) |
|
116 |
self.add('fc_user_info', fc_user_info) |
|
117 | ||
118 |
def request_dgfip_access_token(self, dgfip_username, dgfip_password, scope=None): |
|
119 |
data = { |
|
120 |
'grant_type': 'client_credentials', |
|
121 |
} |
|
122 |
if scope: |
|
123 |
data['scope'] = scope |
|
124 |
dgfip_response = self.request( |
|
125 |
'dgfip token endpoint', |
|
126 |
'POST', |
|
127 |
'https://gwfc.impots.gouv.fr/token', |
|
128 |
data=data, |
|
129 |
auth=(dgfip_username, dgfip_password), |
|
130 |
) |
|
131 | ||
132 |
self.add('dgfip_token_endpoint_response', dgfip_response) |
|
133 | ||
134 |
try: |
|
135 |
dgfip_access_token = dgfip_response['access_token'] |
|
136 |
except (TypeError, KeyError) as e: |
|
137 |
raise FranceConnectError('dgfip token endpoint error %s' % e, response=dgfip_response) |
|
138 |
self.add('dgfip_access_token', dgfip_access_token) |
|
139 | ||
140 |
def request_dgfip_ir(self, annrev, id_teleservice=None): |
|
141 |
headers = { |
|
142 |
'Authorization': 'Bearer %s' % self.dgfip_access_token, |
|
143 |
'X-FranceConnect-OAuth': self.fc_access_token, |
|
144 |
'X-Correlation-ID': str(uuid.uuid4()), |
|
145 |
'Accept': 'application/prs.dgfip.part.situations.ir.assiettes.v1+json', |
|
146 |
} |
|
147 |
if id_teleservice: |
|
148 |
headers['ID_Teleservice'] = id_teleservice |
|
149 | ||
150 |
try: |
|
151 |
dgfip_ressource_ir_response = self.request( |
|
152 |
'ressource IR endpoint', |
|
153 |
'GET', |
|
154 |
'https://gwfc.impots.gouv.fr/impotparticulier/1.0/situations/ir/assiettes/annrev/%s' % annrev, |
|
155 |
headers=headers, |
|
156 |
) |
|
157 |
except FranceConnectError as e: |
|
158 |
dgfip_ressource_ir_response = {'error_desc': str(e), 'error': e.data} |
|
159 | ||
160 |
# accumulate data |
|
161 |
try: |
|
162 |
data = self.dgfip_ressource_ir_response |
|
163 |
except AttributeError: |
|
164 |
data = {} |
|
165 |
data[annrev] = dgfip_ressource_ir_response |
|
166 |
self.add('dgfip_ressource_ir_response', data) |
|
167 | ||
168 |
def __getattr__(self, name): |
|
169 |
try: |
|
170 |
return dict(self.items)[name] |
|
171 |
except KeyError: |
|
172 |
raise AttributeError(name) |
|
173 | ||
174 |
def add(self, key, value): |
|
175 |
self.items.append((key, value)) |
|
176 | ||
177 |
def request(self, label, method, url, *args, **kwargs): |
|
178 |
self.logger.debug('request %s %s args:%s kwargs:%s', label, method, args, kwargs) |
|
179 |
self.add(label.replace(' ', '_') + '_request', [method, url, args, kwargs]) |
|
180 |
try: |
|
181 |
response = getattr(self.session, method.lower())(url, *args, **kwargs) |
|
182 |
try: |
|
183 |
response_content = response.json() |
|
184 |
except ValueError: |
|
185 |
response_content = response.text[:1024] |
|
186 |
response.raise_for_status() |
|
187 |
raise |
|
188 |
else: |
|
189 |
response.raise_for_status() |
|
190 |
except requests.HTTPError as e: |
|
191 |
raise FranceConnectError('%s error %s' % (label, e), response=response_content) |
|
192 |
except requests.RequestException as e: |
|
193 |
raise FranceConnectError('%s error %s' % (label, e)) |
|
194 |
except ValueError as e: |
|
195 |
raise FranceConnectError('%s error %s' % (label, e), response=response_content) |
|
196 |
return response_content |
passerelle/apps/franceconnect/migrations/0001_initial.py | ||
---|---|---|
1 |
# Generated by Django 2.2.19 on 2021-05-17 11:43 |
|
2 | ||
3 |
from django.db import migrations, models |
|
4 | ||
5 | ||
6 |
class Migration(migrations.Migration): |
|
7 | ||
8 |
initial = True |
|
9 | ||
10 |
dependencies = [ |
|
11 |
('base', '0029_auto_20210202_1627'), |
|
12 |
] |
|
13 | ||
14 |
operations = [ |
|
15 |
migrations.CreateModel( |
|
16 |
name='Resource', |
|
17 |
fields=[ |
|
18 |
( |
|
19 |
'id', |
|
20 |
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), |
|
21 |
), |
|
22 |
('title', models.CharField(max_length=50, verbose_name='Title')), |
|
23 |
('slug', models.SlugField(unique=True, verbose_name='Identifier')), |
|
24 |
('description', models.TextField(verbose_name='Description')), |
|
25 |
( |
|
26 |
'fc_platform_slug', |
|
27 |
models.CharField( |
|
28 |
choices=[('test', 'Testing'), ('prod', 'Production')], |
|
29 |
max_length=4, |
|
30 |
verbose_name='FranceConnect platform', |
|
31 |
), |
|
32 |
), |
|
33 |
('fc_client_id', models.CharField(max_length=64, verbose_name='FranceConnect client_id')), |
|
34 |
( |
|
35 |
'fc_client_secret', |
|
36 |
models.CharField(max_length=64, verbose_name='FranceConnect client_secret'), |
|
37 |
), |
|
38 |
( |
|
39 |
'fc_scopes', |
|
40 |
models.TextField(default='identite_pivot', verbose_name='FranceConnect scopes'), |
|
41 |
), |
|
42 |
( |
|
43 |
'fc_text_template', |
|
44 |
models.TextField( |
|
45 |
default="{{ given_name }} {{ family_name }} {% if gender == 'male' %}né{% else %}née{% endif %} le {{ birthdate }} à {{ birthplace }}", |
|
46 |
verbose_name='FranceConnect text template', |
|
47 |
), |
|
48 |
), |
|
49 |
( |
|
50 |
'dgfip_username', |
|
51 |
models.CharField( |
|
52 |
blank=True, max_length=64, null=True, verbose_name='api.impots.gouv.fr username' |
|
53 |
), |
|
54 |
), |
|
55 |
( |
|
56 |
'dgfip_password', |
|
57 |
models.CharField( |
|
58 |
blank=True, max_length=64, null=True, verbose_name='api.impots.gouv.fr password' |
|
59 |
), |
|
60 |
), |
|
61 |
( |
|
62 |
'dgfip_scopes', |
|
63 |
models.TextField(blank=True, null=True, verbose_name='api.impots.gouv.fr scopes'), |
|
64 |
), |
|
65 |
( |
|
66 |
'dgfip_id_teleservice', |
|
67 |
models.TextField(blank=True, null=True, verbose_name='api.impots.gouv.fr ID_Teleservice'), |
|
68 |
), |
|
69 |
( |
|
70 |
'users', |
|
71 |
models.ManyToManyField( |
|
72 |
blank=True, |
|
73 |
related_name='_resource_users_+', |
|
74 |
related_query_name='+', |
|
75 |
to='base.ApiUser', |
|
76 |
), |
|
77 |
), |
|
78 |
], |
|
79 |
options={ |
|
80 |
'verbose_name': 'FranceConnect', |
|
81 |
}, |
|
82 |
), |
|
83 |
] |
passerelle/apps/franceconnect/models.py | ||
---|---|---|
1 |
# passerelle - uniform access to multiple data sources and services |
|
2 |
# Copyright (C) 2021 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
17 |
import uuid |
|
18 | ||
19 |
from django.core.cache import cache |
|
20 |
from django.core.exceptions import PermissionDenied |
|
21 |
from django.db import models |
|
22 |
from django.http import HttpResponseBadRequest, HttpResponseRedirect |
|
23 |
from django.template import Context, Template |
|
24 |
from django.template.response import TemplateResponse |
|
25 |
from django.urls import reverse |
|
26 |
from django.utils.http import urlencode |
|
27 |
from django.utils.timezone import now |
|
28 |
from django.utils.translation import ugettext_lazy as _ |
|
29 | ||
30 |
from passerelle.base.models import BaseResource |
|
31 |
from passerelle.utils import get_trusted_services |
|
32 |
from passerelle.utils.api import endpoint |
|
33 |
from passerelle.utils.origin import is_same_origin |
|
34 | ||
35 |
from . import fc |
|
36 | ||
37 |
# from passerelle.utils.jsonresponse import APIError |
|
38 | ||
39 | ||
40 |
class Resource(BaseResource): |
|
41 |
category = _('Business Process Connectors') |
|
42 | ||
43 |
fc_platform_slug = models.CharField( |
|
44 |
_('FranceConnect platform'), |
|
45 |
max_length=4, |
|
46 |
choices=[(platform.slug, platform.name) for platform in fc.PLATFORMS], |
|
47 |
) |
|
48 | ||
49 |
fc_client_id = models.CharField(_('FranceConnect client_id'), max_length=64) |
|
50 | ||
51 |
fc_client_secret = models.CharField(_('FranceConnect client_secret'), max_length=64) |
|
52 | ||
53 |
fc_scopes = models.TextField(_('FranceConnect scopes'), default='identite_pivot') |
|
54 | ||
55 |
fc_text_template = models.TextField( |
|
56 |
_('FranceConnect text template'), |
|
57 |
default=( |
|
58 |
'''{{ given_name }} {{ family_name }} ''' |
|
59 |
'''{% if gender == 'male' %}né{% else %}née{% endif %} le {{ birthdate }} ''' |
|
60 |
'''à {{ birthplace }}''' |
|
61 |
), |
|
62 |
) |
|
63 | ||
64 |
dgfip_username = models.CharField(_('api.impots.gouv.fr username'), max_length=64, blank=True, null=True) |
|
65 | ||
66 |
dgfip_password = models.CharField(_('api.impots.gouv.fr password'), max_length=64, blank=True, null=True) |
|
67 | ||
68 |
dgfip_scopes = models.TextField(_('api.impots.gouv.fr scopes'), blank=True, null=True) |
|
69 | ||
70 |
dgfip_id_teleservice = models.TextField(_('api.impots.gouv.fr ID_Teleservice'), blank=True, null=True) |
|
71 | ||
72 |
log_requests_errors = False |
|
73 | ||
74 |
class Meta: |
|
75 |
verbose_name = _('FranceConnect') |
|
76 | ||
77 |
@property |
|
78 |
def fc_platform(self): |
|
79 |
return fc.PLATFORMS_BY_SLUG[self.fc_platform_slug] |
|
80 | ||
81 |
def build_callback_url(self, request, **kwargs): |
|
82 |
redirect_uri = request.build_absolute_uri( |
|
83 |
reverse( |
|
84 |
'generic-endpoint', |
|
85 |
kwargs={'slug': self.slug, 'connector': self.get_connector_slug(), 'endpoint': 'callback'}, |
|
86 |
) |
|
87 |
) |
|
88 |
if kwargs: |
|
89 |
redirect_uri += '?' + urlencode( |
|
90 |
{key: value for key, value in kwargs.items() if value is not None} |
|
91 |
) |
|
92 |
return redirect_uri |
|
93 | ||
94 |
def is_trusted_origin(self, request, origin): |
|
95 |
for service in get_trusted_services(): |
|
96 |
if is_same_origin(origin, service['url']): |
|
97 |
return True |
|
98 | ||
99 |
if is_same_origin(request.build_absolute_uri(), origin): |
|
100 |
return True |
|
101 | ||
102 |
return False |
|
103 | ||
104 |
@endpoint( |
|
105 |
description=_('Init request'), |
|
106 |
parameters={ |
|
107 |
'mode': { |
|
108 |
'description': _('What to retrieve, default to FranceConnect identity, can be "dgfip"'), |
|
109 |
}, |
|
110 |
'origin': { |
|
111 |
'description': _('Origin for returning results through window.postMessage'), |
|
112 |
}, |
|
113 |
'test': { |
|
114 |
'description': _('If set to one, activate the test callback view.'), |
|
115 |
}, |
|
116 |
}, |
|
117 |
) |
|
118 |
def init_request(self, request, origin, mode=None, test=None): |
|
119 |
if not request.user.is_superuser and not self.is_trusted_origin(request, origin): |
|
120 |
return HttpResponseBadRequest('Missing or invalid origin') |
|
121 | ||
122 |
redirect_uri = self.build_callback_url(request, origin=origin, mode=mode, test=test) |
|
123 |
franceconnect = fc.FranceConnect(session=self.requests, logger=self.logger) |
|
124 |
return HttpResponseRedirect( |
|
125 |
franceconnect.authorization_request( |
|
126 |
platform=self.fc_platform, |
|
127 |
client_id=self.fc_client_id, |
|
128 |
scopes=self.fc_scopes, |
|
129 |
redirect_uri=redirect_uri, |
|
130 |
) |
|
131 |
) |
|
132 | ||
133 |
@endpoint( |
|
134 |
description=_('FranceConnect callback (internal use)'), |
|
135 |
parameters={ |
|
136 |
'origin': { |
|
137 |
'description': _('HTTP Origin, needed to secure window.postMessage'), |
|
138 |
}, |
|
139 |
'mode': { |
|
140 |
'description': _('Mode'), |
|
141 |
}, |
|
142 |
'test': { |
|
143 |
'description': _('Use test mode (to see exchanges)'), |
|
144 |
}, |
|
145 |
}, |
|
146 |
) |
|
147 |
def callback(self, request, origin, mode=None, test=None, **kwargs): |
|
148 |
if not request.user.is_superuser and not self.is_trusted_origin(request, origin): |
|
149 |
return HttpResponseBadRequest('Missing or invalid origin.') |
|
150 | ||
151 |
if test and not request.user.is_superuser: |
|
152 |
return HttpResponseBadRequest('Only admin can use test mode.') |
|
153 | ||
154 |
franceconnect = fc.FranceConnect(session=self.requests, logger=self.logger) |
|
155 |
redirect_uri = self.build_callback_url(request, origin=origin, mode=mode, test=test) |
|
156 |
context = { |
|
157 |
'origin': origin, |
|
158 |
'franceconnect': franceconnect, |
|
159 |
'redirect_uri': redirect_uri, |
|
160 |
'test': test, |
|
161 |
} |
|
162 |
try: |
|
163 |
franceconnect.handle_authorization_response( |
|
164 |
platform=self.fc_platform, |
|
165 |
client_id=self.fc_client_id, |
|
166 |
client_secret=self.fc_client_secret, |
|
167 |
redirect_uri=redirect_uri, |
|
168 |
code=request.GET.get('code'), |
|
169 |
error=request.GET.get('error'), |
|
170 |
error_description=request.GET.get('error_description'), |
|
171 |
) |
|
172 |
token = {'franceconnect': franceconnect.fc_user_info} |
|
173 |
if mode == 'dgfip': |
|
174 |
franceconnect.request_dgfip_access_token( |
|
175 |
self.dgfip_username, self.dgfip_password, scope=self.dgfip_scopes |
|
176 |
) |
|
177 |
current_year = now().year |
|
178 |
for year in range(current_year - 3, current_year): |
|
179 |
franceconnect.request_dgfip_ir(str(year), id_teleservice=self.dgfip_id_teleservice) |
|
180 |
token['dgfip_ir'] = franceconnect.dgfip_ressource_ir_response |
|
181 |
try: |
|
182 |
template = Template(self.fc_text_template) |
|
183 |
token['text'] = template.render(Context(franceconnect.fc_user_info)) |
|
184 |
except Exception: |
|
185 |
token['text'] = '<failed to render>' |
|
186 |
context['data'] = {'id': self.store(token), 'text': token['text']} |
|
187 |
except fc.FranceConnectError as e: |
|
188 |
context['error'] = e |
|
189 |
return TemplateResponse(request, 'franceconnect/callback.html', context=context) |
|
190 | ||
191 |
@endpoint( |
|
192 |
description=_('Demo page (to check your configuration)'), |
|
193 |
) |
|
194 |
def demo(self, request, **kwargs): |
|
195 |
if not request.user.is_superuser: |
|
196 |
return PermissionDenied |
|
197 |
return TemplateResponse( |
|
198 |
request, |
|
199 |
'franceconnect/demo.html', |
|
200 |
context={'origin': request.build_absolute_uri('/'), 'resource': self}, |
|
201 |
) |
|
202 | ||
203 |
@endpoint( |
|
204 |
description=_('Data source'), |
|
205 |
) |
|
206 |
def data_source(self, request, id=None, **kwargs): |
|
207 |
if id: |
|
208 |
return { |
|
209 |
'data': [ |
|
210 |
dict(self.retrieve(id), id=id), |
|
211 |
] |
|
212 |
} |
|
213 |
return { |
|
214 |
'data': [ |
|
215 |
{ |
|
216 |
'id': '', |
|
217 |
'text': '', |
|
218 |
'init_request_url': request.build_absolute_uri( |
|
219 |
reverse( |
|
220 |
'generic-endpoint', |
|
221 |
kwargs={ |
|
222 |
'slug': self.slug, |
|
223 |
'connector': self.get_connector_slug(), |
|
224 |
'endpoint': 'init_request', |
|
225 |
}, |
|
226 |
) |
|
227 |
), |
|
228 |
} |
|
229 |
] |
|
230 |
} |
|
231 | ||
232 |
def store(self, data): |
|
233 |
ref = str(uuid.uuid4().hex) |
|
234 |
cache.set(ref, data) |
|
235 |
return ref |
|
236 | ||
237 |
def retrieve(self, ref): |
|
238 |
return cache.get(ref) |
passerelle/apps/franceconnect/templates/franceconnect/callback.html | ||
---|---|---|
1 |
<html> |
|
2 |
<head> |
|
3 |
</head> |
|
4 |
<body> |
|
5 |
{% if test %}<button id="continue">Continue</button> |
|
6 |
{{ data|json_script:"data" }} |
|
7 |
<p>redirect_uri: <pre>{{ redirect_uri|pprint }}</pre></p> |
|
8 |
<p>correlation_id: <pre>{{ franceconnect.correlation_id }}</pre></p> |
|
9 |
{% if error %} |
|
10 |
<p>{{ error }}<p> |
|
11 |
{% if error.data %} |
|
12 |
<dl> |
|
13 |
{% for key, value in error.data %} |
|
14 |
<dt>{{ key }}</td> |
|
15 |
<dd><pre>{{ value|pprint }}</pre></dd> |
|
16 |
{% endfor %} |
|
17 |
</dl> |
|
18 |
{% endif %} |
|
19 |
{% endif %} |
|
20 |
<ul> |
|
21 |
{% for key, value in franceconnect.items reversed %} |
|
22 |
<li>{{ key }} : <pre>{{ value|pprint }}</pre></li> |
|
23 |
{% endfor %} |
|
24 |
</ul> |
|
25 |
{% endif %} |
|
26 |
<script> |
|
27 |
(function () { |
|
28 |
const data = JSON.parse(document.getElementById('data').textContent); |
|
29 |
const continue_button = document.getElementById('continue'); |
|
30 |
const post_message = function () { |
|
31 |
window.opener.postMessage(data, "{{ origin }}"); |
|
32 |
} |
|
33 |
{% if test %}continue_button.addEventListener('click', function () { post_message(); }); |
|
34 |
{% else %}post_message(){% endif %} |
|
35 |
})(); |
|
36 |
</script> |
|
37 |
</body> |
|
38 |
</html> |
|
39 |
passerelle/apps/franceconnect/templates/franceconnect/demo.html | ||
---|---|---|
1 |
{% extends "passerelle/manage.html" %} |
|
2 |
{% load i18n %} |
|
3 | ||
4 |
{% block breadcrumb %} |
|
5 |
{{ block.super }} |
|
6 |
<a href="{{ resource.get_absolute_url }}">{{ resource.title }}</a> |
|
7 |
<a href="#">{% trans "Demo view" %}</a> |
|
8 |
{% endblock %} |
|
9 | ||
10 |
{% block appbar %} |
|
11 |
{% endblock %} |
|
12 | ||
13 |
{% block content %} |
|
14 |
<h1>{% trans "Demo view" %}</h1> |
|
15 |
<p> |
|
16 |
<button id="start">{% trans "Get FranceConnect data" %}</button> |
|
17 |
</p> |
|
18 |
<p> |
|
19 |
<label for="dgfip">{% trans "Get DGFIP data" %}</label> |
|
20 |
<input type="checkbox" id="dgfip"/> |
|
21 |
</p> |
|
22 |
<script> |
|
23 |
var popup = null; |
|
24 |
const popupCenter = function(url, title, w, h) { |
|
25 |
// Fixes dual-screen position Most browsers Firefox |
|
26 |
const dualScreenLeft = window.screenLeft !== undefined ? window.screenLeft : window.screenX; |
|
27 |
const dualScreenTop = window.screenTop !== undefined ? window.screenTop : window.screenY; |
|
28 | ||
29 |
const width = window.innerWidth ? window.innerWidth : document.documentElement.clientWidth ? document.documentElement.clientWidth : screen.width; |
|
30 |
const height = window.innerHeight ? window.innerHeight : document.documentElement.clientHeight ? document.documentElement.clientHeight : screen.height; |
|
31 | ||
32 |
const systemZoom = width / window.screen.availWidth; |
|
33 |
const left = (width - w) / 2 / systemZoom + dualScreenLeft |
|
34 |
const top = (height - h) / 2 / systemZoom + dualScreenTop |
|
35 |
popup = window.open(url, title, |
|
36 |
` |
|
37 |
scrollbars=yes, |
|
38 |
width=${w / systemZoom}, |
|
39 |
height=${h / systemZoom}, |
|
40 |
top=${top}, |
|
41 |
left=${left} |
|
42 |
` |
|
43 |
) |
|
44 | ||
45 |
if (window.focus) popup.focus(); |
|
46 |
}; |
|
47 | ||
48 |
$('#start').on('click', function () { |
|
49 |
$('#user-info').hide(); |
|
50 |
if (popup) { popup.close(); popup = null; }; |
|
51 |
var url = 'init_request?test=1&origin={{ origin }}'; |
|
52 |
var checkbox = document.getElementById('dgfip'); |
|
53 |
if (checkbox && checkbox.checked) { |
|
54 |
url += '&mode=dgfip'; |
|
55 |
}; |
|
56 |
popupCenter(url, 'FranceConnect', 1000, 670); |
|
57 |
}); |
|
58 |
$(window).on('message', function(event) { |
|
59 |
var data = event.originalEvent.data; |
|
60 |
var origin = event.originalEvent.origin; |
|
61 |
if (origin != window.location.origin) { |
|
62 |
return; |
|
63 |
} |
|
64 |
$.getJSON("data_source?id=" + data.id, function(result) { |
|
65 |
$('#user-info').show(); |
|
66 |
$('#user-info-preview').text(JSON.stringify(result, null, 2)); |
|
67 |
}); |
|
68 |
popup.close(); |
|
69 |
}); |
|
70 |
</script> |
|
71 |
<div id="user-info" style="display: none; position: relative;"> |
|
72 |
<h3>{% trans "Data-source data" %}</h3> |
|
73 |
<pre id="user-info-preview" style="background: white; border: 0.2ex solid black; color: black; padding: 1em; width: calc(100% - 5em);"></pre> |
|
74 |
</div> |
|
75 |
{% endblock %} |
passerelle/apps/franceconnect/templates/franceconnect/resource_detail.html | ||
---|---|---|
1 |
{% extends "passerelle/manage/service_view.html" %} |
|
2 |
{% load i18n passerelle %} |
|
3 | ||
4 |
{% block description %} |
|
5 |
{{ block.super }} |
|
6 |
{% url "generic-endpoint" connector="franceconnect" slug=object.slug endpoint="callback" as callback_url %} |
|
7 |
<p>URL de callback pour FranceConnect: <a href="{{ request.scheme }}://{{ request.get_host }}{{ callback_url }}">{{ request.scheme }}://{{ request.get_host }}{{ callback_url }}</a></p> |
|
8 |
{% endblock %} |
passerelle/settings.py | ||
---|---|---|
141 | 141 |
'passerelle.apps.esirius', |
142 | 142 |
'passerelle.apps.family', |
143 | 143 |
'passerelle.apps.feeds', |
144 |
'passerelle.apps.franceconnect', |
|
144 | 145 |
'passerelle.apps.gdc', |
145 | 146 |
'passerelle.apps.gesbac', |
146 | 147 |
'passerelle.apps.jsondatastore', |
147 |
- |