# authentic2-auth-fc - authentic2 authentication for FranceConnect # Copyright (C) 2019 Entr'ouvert # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU Affero General Public License as published # by the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . import base64 import json import hmac import hashlib import urlparse from django.db import models from django.utils.translation import ugettext_lazy as _ from django.utils.timezone import now from django.conf import settings from authentic2_auth_oidc.utils import parse_timestamp from . import app_settings def base64url_decode(input): rem = len(input) % 4 if rem > 0: input += b'=' * (4 - rem) return base64.urlsafe_b64decode(input) def parse_id_token(id_token, client_id=None, client_secret=None): try: splitted = str(id_token).split('.') except Exception: return None, 'invalid id_token' if len(splitted) != 3: return None, 'invalid id_token' header, payload, signature = splitted try: signature = base64url_decode(signature) except (ValueError, TypeError): return None, 'invalid signature' signed = '%s.%s' % (header, payload) if client_secret is not None: h = hmac.HMAC(key=client_secret, msg=signed, digestmod=hashlib.sha256) if h.digest() != signature: return None, 'hmac signature does not match' payload = base64url_decode(str(payload)) try: payload = json.loads(payload) except ValueError: return None, 'invalid payload' if client_id and ('aud' not in payload or payload['aud'] != client_id): return None, 'invalid audience' if 'exp' not in payload or parse_timestamp(payload['exp']) < now(): return None, 'id_token is expired' def check_issuer(): parsed = urlparse.urlparse(app_settings.authorize_url) if 'iss' not in payload: return False try: parsed_issuer = urlparse.urlparse(payload['iss']) except Exception: return False return parsed_issuer.scheme == parsed.scheme and parsed_issuer.netloc == parsed.netloc if not check_issuer(): return None, 'wrong issuer received, %r' % payload['iss'] return payload, None class FcAccount(models.Model): user = models.ForeignKey( to=settings.AUTH_USER_MODEL, verbose_name=_('user'), related_name='fc_accounts') sub = models.TextField( verbose_name=_('sub'), db_index=True) token = models.TextField(verbose_name=_('access token')) user_info = models.TextField(verbose_name=_('access token'), blank=True, null=True) @property def id_token(self): return parse_id_token(self.get_token()['id_token']) def get_token(self): return json.loads(self.token) def get_user_info(self): return json.loads(self.user_info) def __unicode__(self): user_info = self.get_user_info() display_name = [] if 'given_name' in user_info: display_name.append(user_info['given_name']) if 'family_name' in user_info: display_name.append(user_info['family_name']) return ' '.join(display_name)