def create_id_token(user, aud, nonce): """ Receives a user object and aud (audience). Then creates the id_token dictionary. See: http://openid.net/specs/openid-connect-core-1_0.html#IDToken Return a dic. """ sub = settings.get('OIDC_IDTOKEN_SUB_GENERATOR')(user=user) expires_in = settings.get('OIDC_IDTOKEN_EXPIRE') # Convert datetimes into timestamps. now = timezone.now() iat_time = int(time.mktime(now.timetuple())) exp_time = int(time.mktime((now + timedelta(seconds=expires_in)).timetuple())) user_auth_time = user.last_login or user.date_joined auth_time = int(time.mktime(user_auth_time.timetuple())) dic = { 'iss': get_issuer(), 'sub': sub, 'aud': str(aud), 'exp': exp_time, 'iat': iat_time, 'auth_time': auth_time, } if nonce: dic['nonce'] = str(nonce) return dic
def userinfo(request, *args, **kwargs): """ Create a diccionary with all the requested claims about the End-User. See: http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse Return a diccionary. """ token = kwargs['token'] dic = { 'sub': token.id_token.get('sub'), } standard_claims = StandardScopeClaims(token.user, token.scope) dic.update(standard_claims.create_response_dic()) if settings.get('OIDC_EXTRA_SCOPE_CLAIMS'): extra_claims = settings.get('OIDC_EXTRA_SCOPE_CLAIMS', import_str=True)(token.user, token.scope) dic.update(extra_claims.create_response_dic()) response = JsonResponse(dic, status=200) response['Cache-Control'] = 'no-store' response['Pragma'] = 'no-cache' return response
def create_response_dic(self): sub = settings.get('OIDC_IDTOKEN_SUB_GENERATOR')( user=self.code.user) id_token_dic = create_id_token( iss=get_issuer(), sub=sub, aud=self.client.client_id, auth_time=self.code.user.last_login) token = create_token( user=self.code.user, client=self.code.client, id_token_dic=id_token_dic, scope=self.code.scope) # Store the token. token.save() # We don't need to store the code anymore. self.code.delete() id_token = encode_id_token(id_token_dic, self.client.client_secret) dic = { 'access_token': token.access_token, 'token_type': 'bearer', 'expires_in': settings.get('OIDC_TOKEN_EXPIRE'), 'id_token': id_token, } return dic
def userinfo(request, *args, **kwargs): """ Create a dictionary with all the requested claims about the End-User. See: http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse Return a dictionary. """ def set_headers(response): response['Cache-Control'] = 'no-store' response['Pragma'] = 'no-cache' cors_allow_any(request, response) return response if request.method == 'OPTIONS': return set_headers(HttpResponse()) token = kwargs['token'] dic = { 'sub': token.id_token.get('sub'), } standard_claims = StandardScopeClaims(token) dic.update(standard_claims.create_response_dic()) if settings.get('OIDC_EXTRA_SCOPE_CLAIMS'): extra_claims = settings.get('OIDC_EXTRA_SCOPE_CLAIMS', import_str=True)(token) dic.update(extra_claims.create_response_dic()) success_response = JsonResponse(dic, status=200) set_headers(success_response) return success_response
def test_prompt_login_parameter(self, logout_function): """ Specifies whether the Authorization Server prompts the End-User for reauthentication and consent. See: http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest """ data = { 'client_id': self.client.client_id, 'response_type': next(self.client.response_type_values()), 'redirect_uri': self.client.default_redirect_uri, 'scope': 'openid email', 'state': self.state, 'prompt': 'login' } response = self._auth_request('get', data) self.assertIn(settings.get('OIDC_LOGIN_URL'), response['Location']) self.assertNotIn( quote('prompt=login'), response['Location'], "Found prompt=login, this leads to infinite login loop. See " "https://github.com/juanifioren/django-oidc-provider/issues/197." ) response = self._auth_request('get', data, is_user_authenticated=True) self.assertIn(settings.get('OIDC_LOGIN_URL'), response['Location']) self.assertTrue(logout_function.called_once()) self.assertNotIn( quote('prompt=login'), response['Location'], "Found prompt=login, this leads to infinite login loop. See " "https://github.com/juanifioren/django-oidc-provider/issues/197." )
def validate_params(self): # Make sure appropriate parameters are there # See: https://openid.net/specs/openid-connect-registration-1_0.html#ClientMetadata # We want client name (optional), and redirect URIs (required). # "Response type" will default to code (Authorization code flow) # Is this endpoint enabled in the configuration? if not settings.get('REGISTRATION_ENDPOINT_ENABLED'): raise RegisterError('invalid_request') # If authorization is required, has user provided valid access token? if settings.get('REGISTRATION_ENDPOINT_REQ_TOKEN'): if self.params.access_token is None: raise RegisterError('invalid_request') # Check whether token is valid try: self.token = Token.objects.get(access_token=self.params.access_token) if self.token.has_expired(): logger.error('[Register] Token has expired: %s', self.params.access_token) raise RegisterError('invalid_token') if not ('openid' in self.token.scope): logger.error('[Register] Missing openid scope.') raise RegisterError('insufficient_scope') except Token.DoesNotExist: #logger.error('[UserInfo] Token does not exist: %s', self.params.access_token) raise RegisterError('invalid_token') # Has the user provided redirect URIs in JSON? if self.params.redirecturis is None: raise RegisterError('invalid_request')
def get(self, request, *args, **kwargs): authorize = AuthorizeEndpoint(request) try: authorize.validate_params() if request.user.is_authenticated(): # Check if there's a hook setted. hook_resp = settings.get('OIDC_AFTER_USERLOGIN_HOOK', import_str=True)( request=request, user=request.user, client=authorize.client) if hook_resp: return hook_resp if settings.get('OIDC_SKIP_CONSENT_ALWAYS') and not (authorize.client.client_type == 'public'): return redirect(authorize.create_response_uri()) if settings.get('OIDC_SKIP_CONSENT_ENABLE'): # Check if user previously give consent. if authorize.client_has_user_consent() and not (authorize.client.client_type == 'public'): return redirect(authorize.create_response_uri()) # Generate hidden inputs for the form. context = { 'params': authorize.params, } hidden_inputs = render_to_string( 'oidc_provider/hidden_inputs.html', context) # Remove `openid` from scope list # since we don't need to print it. if 'openid' in authorize.params.scope: authorize.params.scope.remove('openid') context = { 'client': authorize.client, 'hidden_inputs': hidden_inputs, 'params': authorize.params, } return render(request, 'oidc_provider/authorize.html', context) else: path = request.get_full_path() return redirect_to_login(path) except (ClientIdError, RedirectUriError) as error: context = { 'error': error.error, 'description': error.description, } return render(request, 'oidc_provider/error.html', context) except (AuthorizeError) as error: uri = error.create_uri( authorize.params.redirect_uri, authorize.params.state) return redirect(uri)
def get_scopes_information(self): """ Return a list with the description of all the scopes requested. """ scopes = StandardScopeClaims.get_scopes_info(self.params.scope) if settings.get('OIDC_EXTRA_SCOPE_CLAIMS'): scopes_extra = settings.get('OIDC_EXTRA_SCOPE_CLAIMS', import_str=True).get_scopes_info(self.params.scope) for index_extra, scope_extra in enumerate(scopes_extra): for index, scope in enumerate(scopes[:]): if scope_extra['scope'] == scope['scope']: del scopes[index] else: scopes_extra = [] return scopes + scopes_extra
def create_id_token(token, user, aud, nonce='', at_hash='', request=None, scope=None): """ Creates the id_token dictionary. See: http://openid.net/specs/openid-connect-core-1_0.html#IDToken Return a dic. """ if scope is None: scope = [] sub = settings.get('OIDC_IDTOKEN_SUB_GENERATOR', import_str=True)(user=user) expires_in = settings.get('OIDC_IDTOKEN_EXPIRE') # Convert datetimes into timestamps. now = int(time.time()) iat_time = now exp_time = int(now + expires_in) user_auth_time = user.last_login or user.date_joined auth_time = int(dateformat.format(user_auth_time, 'U')) dic = { 'iss': get_issuer(request=request), 'sub': sub, 'aud': str(aud), 'exp': exp_time, 'iat': iat_time, 'auth_time': auth_time, } if nonce: dic['nonce'] = str(nonce) if at_hash: dic['at_hash'] = at_hash # Inlude (or not) user standard claims in the id_token. if settings.get('OIDC_IDTOKEN_INCLUDE_CLAIMS'): if settings.get('OIDC_EXTRA_SCOPE_CLAIMS'): custom_claims = settings.get('OIDC_EXTRA_SCOPE_CLAIMS', import_str=True)(token) claims = custom_claims.create_response_dic() else: claims = StandardScopeClaims(token).create_response_dic() dic.update(claims) dic = run_processing_hook( dic, 'OIDC_IDTOKEN_PROCESSING_HOOK', user=user, token=token, request=request) return dic
def create_code_response_dic(self): token = create_token( user=self.code.user, client=self.code.client, scope=self.code.scope) if self.code.is_authentication: id_token_dic = create_id_token( user=self.code.user, aud=self.client.client_id, nonce=self.code.nonce, at_hash=token.at_hash, request=self.request, scope=self.params['scope'], ) else: id_token_dic = {} token.id_token = id_token_dic # Store the token. token.save() # We don't need to store the code anymore. self.code.delete() dic = { 'access_token': token.access_token, 'refresh_token': token.refresh_token, 'token_type': 'bearer', 'expires_in': settings.get('OIDC_TOKEN_EXPIRE'), 'id_token': encode_id_token(id_token_dic, token.client), } return dic
def set_client_user_consent(self): """ Save the user consent given to a specific client. Return None. """ date_given = timezone.now() expires_at = date_given + timedelta( days=settings.get('OIDC_SKIP_CONSENT_EXPIRE')) uc, created = UserConsent.objects.get_or_create( user=self.request.user, client=self.client, defaults={ 'expires_at': expires_at, 'date_given': date_given, } ) uc.scope = self.params.scope # Rewrite expires_at and date_given if object already exists. if not created: uc.expires_at = expires_at uc.date_given = date_given uc.save()
def create_refresh_response_dic(self): id_token_dic = create_id_token( user=self.token.user, aud=self.client.client_id, nonce=None, ) token = create_token( user=self.token.user, client=self.token.client, id_token_dic=id_token_dic, scope=self.token.scope) # Store the token. token.save() # Forget the old token. self.token.delete() dic = { 'access_token': token.access_token, 'refresh_token': token.refresh_token, 'token_type': 'bearer', 'expires_in': settings.get('OIDC_TOKEN_EXPIRE'), 'id_token': encode_id_token(id_token_dic), } return dic
def create_response_dic(self): id_token_dic = create_id_token( user=self.code.user, aud=self.client.client_id) token = create_token( user=self.code.user, client=self.code.client, id_token_dic=id_token_dic, scope=self.code.scope) # Store the token. token.save() # We don't need to store the code anymore. self.code.delete() id_token = encode_id_token(id_token_dic, self.client.client_secret) dic = { 'access_token': token.access_token, 'token_type': 'bearer', 'expires_in': settings.get('OIDC_TOKEN_EXPIRE'), 'id_token': id_token, } logger.debug('Response dictionary --> : %s', dic) return dic
def test_user_not_logged(self): """ The Authorization Server attempts to Authenticate the End-User by redirecting to the login view. See: http://openid.net/specs/openid-connect-core-1_0.html#Authenticates """ query_str = urlencode({ 'client_id': self.client.client_id, 'response_type': 'code', 'redirect_uri': self.client.default_redirect_uri, 'scope': 'openid email', 'state': self.state, }).replace('+', '%20') url = reverse('oidc_provider:authorize') + '?' + query_str request = self.factory.get(url) request.user = AnonymousUser() response = AuthorizeView.as_view()(request) # Check if user was redirected to the login view. login_url_exists = settings.get('LOGIN_URL') in response['Location'] self.assertEqual(login_url_exists, True) # Check if the login will redirect to a valid url. try: next_value = response['Location'].split(REDIRECT_FIELD_NAME + '=')[1] next_url = unquote(next_value) is_next_ok = next_url == url except: is_next_ok = False self.assertEqual(is_next_ok, True)
def create_access_token_response_dic(self): # See https://tools.ietf.org/html/rfc6749#section-4.3 token = create_token( self.user, self.client, self.params['scope'].split(' ')) id_token_dic = create_id_token( token=token, user=self.user, aud=self.client.client_id, nonce='self.code.nonce', at_hash=token.at_hash, request=self.request, scope=token.scope, ) token.id_token = id_token_dic token.save() return { 'access_token': token.access_token, 'refresh_token': token.refresh_token, 'expires_in': settings.get('OIDC_TOKEN_EXPIRE'), 'token_type': 'bearer', 'id_token': encode_id_token(id_token_dic, token.client), }
def create_refresh_response_dic(self): token = create_token( user=self.token.user, client=self.token.client, scope=self.token.scope) # If the Token has an id_token it's an Authentication request. if self.token.id_token: id_token_dic = create_id_token( user=self.token.user, aud=self.client.client_id, nonce=None, at_hash=token.at_hash, request=self.request, scope=self.params['scope'], ) else: id_token_dic = {} token.id_token = id_token_dic # Store the token. token.save() # Forget the old token. self.token.delete() dic = { 'access_token': token.access_token, 'refresh_token': token.refresh_token, 'token_type': 'bearer', 'expires_in': settings.get('OIDC_TOKEN_EXPIRE'), 'id_token': encode_id_token(id_token_dic, self.token.client), } return dic
def get(self, request, *args, **kwargs): dic = dict() site_url = get_site_url(request=request) dic['issuer'] = get_issuer(site_url=site_url, request=request) dic['authorization_endpoint'] = site_url + reverse('oidc_provider:authorize') dic['token_endpoint'] = site_url + reverse('oidc_provider:token') dic['userinfo_endpoint'] = site_url + reverse('oidc_provider:userinfo') dic['end_session_endpoint'] = site_url + reverse('oidc_provider:end-session') types_supported = [x[0] for x in RESPONSE_TYPE_CHOICES] dic['response_types_supported'] = types_supported dic['jwks_uri'] = site_url + reverse('oidc_provider:jwks') dic['id_token_signing_alg_values_supported'] = ['HS256', 'RS256'] # See: http://openid.net/specs/openid-connect-core-1_0.html#SubjectIDTypes dic['subject_types_supported'] = ['public'] dic['token_endpoint_auth_methods_supported'] = ['client_secret_post', 'client_secret_basic'] if settings.get('OIDC_SESSION_MANAGEMENT_ENABLE'): dic['check_session_iframe'] = site_url + reverse('oidc_provider:check-session-iframe') response = JsonResponse(dic) response['Access-Control-Allow-Origin'] = '*' return response
def create_response_dic(self): id_token_dic = create_id_token( user=self.code.user, aud=self.client.client_id, nonce=self.code.nonce, ) token = create_token( user=self.code.user, client=self.code.client, id_token_dic=id_token_dic, scope=self.code.scope) # Store the token. token.save() # We don't need to store the code anymore. self.code.delete() dic = { 'access_token': token.access_token, 'token_type': 'bearer', 'expires_in': settings.get('OIDC_TOKEN_EXPIRE'), 'id_token': encode_id_token(id_token_dic), } return dic
def get_browser_state_or_default(request): """ Determine value to use as session state. """ key = (request.session.session_key or settings.get('OIDC_UNAUTHENTICATED_SESSION_MANAGEMENT_KEY')) return sha224(key.encode('utf-8')).hexdigest()
def get(self, request, *args, **kwargs): dic = dict() dic['issuer'] = get_issuer() SITE_URL = settings.get('SITE_URL') dic['authorization_endpoint'] = SITE_URL + reverse('oidc_provider:authorize') dic['token_endpoint'] = SITE_URL + reverse('oidc_provider:token') dic['userinfo_endpoint'] = SITE_URL + reverse('oidc_provider:userinfo') dic['end_session_endpoint'] = SITE_URL + reverse('oidc_provider:logout') types_supported = [x[0] for x in RESPONSE_TYPE_CHOICES] dic['response_types_supported'] = types_supported dic['jwks_uri'] = SITE_URL + reverse('oidc_provider:jwks') dic['id_token_signing_alg_values_supported'] = ['HS256', 'RS256'] # See: http://openid.net/specs/openid-connect-core-1_0.html#SubjectIDTypes dic['subject_types_supported'] = ['public'] dic['token_endpoint_auth_methods_supported'] = [ 'client_secret_post', 'client_secret_basic' ] return JsonResponse(dic)
def create_id_token(iss, sub, aud, auth_time): """ Receives a user object, iss (issuer) and aud (audience). Then creates the id_token dic. See: http://openid.net/specs/openid-connect-core-1_0.html#IDToken Return a dic. """ expires_in = settings.get('OIDC_IDTOKEN_EXPIRE') now = timezone.now() # Convert datetimes into timestamps. iat_time = time.mktime(now.timetuple()) exp_time = time.mktime((now + timedelta(seconds=expires_in)).timetuple()) user_auth_time = time.mktime(auth_time.timetuple()) dic = { 'iss': iss, 'sub': sub, 'aud': aud, 'exp': exp_time, 'iat': iat_time, 'auth_time': user_auth_time, } return dic
def create_id_token(user, aud, nonce='', at_hash='', request=None, scope=[]): """ Creates the id_token dictionary. See: http://openid.net/specs/openid-connect-core-1_0.html#IDToken Return a dic. """ sub = settings.get('OIDC_IDTOKEN_SUB_GENERATOR', import_str=True)(user=user) expires_in = settings.get('OIDC_IDTOKEN_EXPIRE') # Convert datetimes into timestamps. now = timezone.now() iat_time = int(time.mktime(now.timetuple())) exp_time = int(time.mktime((now + timedelta(seconds=expires_in)).timetuple())) user_auth_time = user.last_login or user.date_joined auth_time = int(time.mktime(user_auth_time.timetuple())) dic = { 'iss': get_issuer(request=request), 'sub': sub, 'aud': str(aud), 'exp': exp_time, 'iat': iat_time, 'auth_time': auth_time, } if nonce: dic['nonce'] = str(nonce) if at_hash: dic['at_hash'] = at_hash if ('email' in scope) and getattr(user, 'email', None): dic['email'] = user.email processing_hook = settings.get('OIDC_IDTOKEN_PROCESSING_HOOK') if isinstance(processing_hook, (list, tuple)): for hook in processing_hook: dic = settings.import_from_str(hook)(dic, user=user) else: dic = settings.import_from_str(processing_hook)(dic, user=user) return dic
def run_processing_hook(subject, hook_settings_name, **kwargs): processing_hooks = settings.get(hook_settings_name) if not isinstance(processing_hooks, (list, tuple)): processing_hooks = [processing_hooks] for hook_string in processing_hooks: hook = settings.import_from_str(hook_string) subject = hook(subject, **kwargs) return subject
def get_issuer(): """ Construct the issuer full url. Basically is the site url with some path appended. """ site_url = settings.get('SITE_URL') path = reverse('oidc_provider:provider_info') \ .split('/.well-known/openid-configuration/')[0] issuer = site_url + path return issuer
def get_rsa_key(): """ Load the rsa key previously created with `creatersakey` command. """ file_path = settings.get('OIDC_RSA_KEY_FOLDER') + '/OIDC_RSA_KEY.pem' try: with open(file_path, 'r') as f: key = f.read() except IOError: raise IOError('We could not find your key file on: ' + file_path) return key
def test_prompt_consent_parameter(self, render_patched): """ Specifies whether the Authorization Server prompts the End-User for reauthentication and consent. See: http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest """ data = { 'client_id': self.client.client_id, 'response_type': next(self.client.response_type_values()), 'redirect_uri': self.client.default_redirect_uri, 'scope': 'openid email', 'state': self.state, 'prompt': 'consent' } response = self._auth_request('get', data) self.assertIn(settings.get('OIDC_LOGIN_URL'), response['Location']) response = self._auth_request('get', data, is_user_authenticated=True) render_patched.assert_called_once() self.assertTrue( render_patched.call_args[0][1], settings.get('OIDC_TEMPLATES')['authorize'])
def dispatch(self, request, *args, **kwargs): id_token_hint = request.GET.get('id_token_hint', '') post_logout_redirect_uri = request.GET.get('post_logout_redirect_uri', '') state = request.GET.get('state', '') client = None next_page = settings.get('OIDC_LOGIN_URL') after_end_session_hook = settings.get('OIDC_AFTER_END_SESSION_HOOK', import_str=True) if id_token_hint: client_id = client_id_from_id_token(id_token_hint) try: client = Client.objects.get(client_id=client_id) if post_logout_redirect_uri in client.post_logout_redirect_uris: if state: uri = urlsplit(post_logout_redirect_uri) query_params = parse_qs(uri.query) query_params['state'] = state uri = uri._replace(query=urlencode(query_params, doseq=True)) next_page = urlunsplit(uri) else: next_page = post_logout_redirect_uri except Client.DoesNotExist: pass after_end_session_hook( request=request, id_token=id_token_hint, post_logout_redirect_uri=post_logout_redirect_uri, state=state, client=client, next_page=next_page ) self.next_page = next_page return super(EndSessionView, self).dispatch(request, *args, **kwargs)
def test_redirects(self): query_params = { 'post_logout_redirect_uri': self.LOGOUT_URL, } response = self.client.get(self.url, query_params) # With no id_token the OP MUST NOT redirect to the requested redirect_uri. self.assertRedirects(response, settings.get('LOGIN_URL'), fetch_redirect_response=False) id_token_dic = create_id_token(user=self.user, aud=self.oidc_client.client_id) id_token = encode_id_token(id_token_dic, self.oidc_client) query_params['id_token_hint'] = id_token response = self.client.get(self.url, query_params) self.assertRedirects(response, self.LOGOUT_URL, fetch_redirect_response=False)
def create_code(user, client, scope): """ Create and populate a Code object. Return a Code object. """ code = Code() code.user = user code.client = client code.code = uuid.uuid4().hex code.expires_at = timezone.now() + timedelta( seconds=settings.get('OIDC_CODE_EXPIRE')) code.scope = scope return code
def create_client_credentials_response_dic(self): # See https://tools.ietf.org/html/rfc6749#section-4.4.3 token = create_token( user=None, client=self.client, scope=self.client.scope) token.save() return { 'access_token': token.access_token, 'expires_in': settings.get('OIDC_TOKEN_EXPIRE'), 'token_type': 'bearer', 'scope': self.client._scope, }
def set_headers(response): response['Cache-Control'] = 'no-store' response['Pragma'] = 'no-cache' if settings.get('OIDC_CORS_MANAGEMENT_ENABLE'): cors_allow_any(request, response) return response
def get(self, request, *args, **kwargs): authorize = AuthorizeEndpoint(request) try: authorize.validate_params() if request.user.is_authenticated(): # Check if there's a hook setted. hook_resp = settings.get('OIDC_AFTER_USERLOGIN_HOOK', import_str=True)( request=request, user=request.user, client=authorize.client) if hook_resp: return hook_resp if settings.get('OIDC_SKIP_CONSENT_ALWAYS') and not (authorize.client.client_type == 'public') \ and not (authorize.params['prompt'] == 'consent'): return redirect(authorize.create_response_uri()) if settings.get('OIDC_SKIP_CONSENT_ENABLE'): # Check if user previously give consent. if authorize.client_has_user_consent() and not (authorize.client.client_type == 'public') \ and not (authorize.params['prompt'] == 'consent'): return redirect(authorize.create_response_uri()) if authorize.params['prompt'] == 'none': raise AuthorizeError(authorize.params['redirect_uri'], 'interaction_required', authorize.grant_type) if authorize.params['prompt'] == 'login': return redirect_to_login(request.get_full_path()) if authorize.params['prompt'] == 'select_account': # TODO: see how we can support multiple accounts for the end-user. raise AuthorizeError(authorize.params['redirect_uri'], 'account_selection_required', authorize.grant_type) # Generate hidden inputs for the form. context = { 'params': authorize.params, } hidden_inputs = render_to_string( 'oidc_provider/hidden_inputs.html', context) # Remove `openid` from scope list # since we don't need to print it. if 'openid' in authorize.params['scope']: authorize.params['scope'].remove('openid') context = { 'client': authorize.client, 'hidden_inputs': hidden_inputs, 'params': authorize.params, 'scopes': authorize.get_scopes_information(), } return render(request, 'oidc_provider/authorize.html', context) else: if authorize.params['prompt'] == 'none': raise AuthorizeError(authorize.params['redirect_uri'], 'login_required', authorize.grant_type) return redirect_to_login(request.get_full_path()) except (ClientIdError, RedirectUriError) as error: context = { 'error': error.error, 'description': error.description, } return render(request, 'oidc_provider/error.html', context) except (AuthorizeError) as error: uri = error.create_uri(authorize.params['redirect_uri'], authorize.params['state']) return redirect(uri)
def validate_params(self): try: self.client = Client.objects.get( client_id=self.params['client_id']) except Client.DoesNotExist: logger.debug('[Token] Client does not exist: %s', self.params['client_id']) raise TokenError('invalid_client') if self.client.client_type == 'confidential': if not (self.client.client_secret == self.params['client_secret']): logger.debug( '[Token] Invalid client secret: client %s do not have secret %s', self.client.client_id, self.client.client_secret) raise TokenError('invalid_client') if self.params['grant_type'] == 'authorization_code': if not redirect_uri_valid(self.params['redirect_uri'], self.client.redirect_uris): logger.debug('[Token] Invalid redirect uri: %s', self.params['redirect_uri']) raise TokenError('invalid_client') try: self.code = Code.objects.get(code=self.params['code']) except Code.DoesNotExist: logger.debug('[Token] Code does not exist: %s', self.params['code']) raise TokenError('invalid_grant') if not (self.code.client == self.client) \ or self.code.has_expired(): logger.debug( '[Token] Invalid code: invalid client or code has expired') raise TokenError('invalid_grant') # Validate PKCE parameters. if self.params['code_verifier']: if self.code.code_challenge_method == 'S256': new_code_challenge = urlsafe_b64encode( hashlib.sha256(self.params['code_verifier'].encode( 'ascii')).digest()).decode('utf-8').replace( '=', '') else: new_code_challenge = self.params['code_verifier'] # TODO: We should explain the error. if not (new_code_challenge == self.code.code_challenge): raise TokenError('invalid_grant') elif self.params['grant_type'] == 'password': if not settings.get('OIDC_GRANT_TYPE_PASSWORD_ENABLE'): raise TokenError('unsupported_grant_type') auth_args = (self.request, ) try: inspect.getcallargs(authenticate, *auth_args) except TypeError: auth_args = () user = authenticate(*auth_args, username=self.params['username'], password=self.params['password']) if not user: raise UserAuthError() self.user = user elif self.params['grant_type'] == 'refresh_token': if not self.params['refresh_token']: logger.debug('[Token] Missing refresh token') raise TokenError('invalid_grant') try: self.token = Token.objects.get( refresh_token=self.params['refresh_token'], client=self.client) except Token.DoesNotExist: logger.debug('[Token] Refresh token does not exist: %s', self.params['refresh_token']) raise TokenError('invalid_grant') elif self.params['grant_type'] == 'client_credentials': if not self.client._scope: logger.debug( '[Token] Client using client credentials with empty scope') raise TokenError('invalid_scope') else: logger.debug('[Token] Invalid grant type: %s', self.params['grant_type']) raise TokenError('unsupported_grant_type')
from oidc_provider import ( settings, views, ) app_name = 'oidc_provider' urlpatterns = [ url(r'^authorize/?$', views.AuthorizeView.as_view(), name='authorize'), url(r'^token/?$', csrf_exempt(views.TokenView.as_view()), name='token'), url(r'^refresh/?$', csrf_exempt(views.TokenRefreshClientView.as_view()), name='token-refresh'), url(r'^userinfo/?$', csrf_exempt(views.userinfo), name='userinfo'), url(r'^end-session/?$', views.EndSessionView.as_view(), name='end-session'), url(r'^\.well-known/openid-configuration/?$', views.ProviderInfoView.as_view(), name='provider-info'), url(r'^introspect/?$', views.TokenIntrospectionView.as_view(), name='token-introspection'), url(r'^jwks/?$', views.JwksView.as_view(), name='jwks'), ] if settings.get('OIDC_SESSION_MANAGEMENT_ENABLE'): urlpatterns += [ url(r'^check-session-iframe/?$', views.CheckSessionIframeView.as_view(), name='check-session-iframe'), ]
get_site_url, get_issuer, ) from oidc_provider.lib.utils.oauth2 import protected_resource_view from oidc_provider.lib.utils.token import client_id_from_id_token from oidc_provider.models import ( Client, RESPONSE_TYPE_CHOICES, RSAKey, ) from oidc_provider import settings from oidc_provider import signals logger = logging.getLogger(__name__) OIDC_TEMPLATES = settings.get('OIDC_TEMPLATES') class AuthorizeView(View): def get(self, request, *args, **kwargs): authorize = AuthorizeEndpoint(request) try: authorize.validate_params() if request.user.is_authenticated(): # Check if there's a hook setted. hook_resp = settings.get('OIDC_AFTER_USERLOGIN_HOOK', import_str=True)( request=request,
def __init__(self, user, scopes): self.user = user self.userinfo = settings.get('OIDC_USERINFO', import_str=True).get_by_user(self.user) self.scopes = scopes
def create_response_uri(self): uri = urlsplit(self.params['redirect_uri']) query_params = parse_qs(uri.query) query_fragment = {} try: if self.grant_type in ['authorization_code', 'hybrid']: try: session = self.request.session except AttributeError: session = {} code = create_code( user=self.request.user, client=self.client, scope=self.params['scope'], nonce=self.params['nonce'], is_authentication=self.is_authentication, code_challenge=self.params['code_challenge'], code_challenge_method=self.params['code_challenge_method'], acr=session['acr'] if 'acr' in session else '', amr=session['amr'] if 'amr' in session else '', ae=session['ae'] if 'ae' in session else None, rid=session['rid'] if 'rid' in session else None) code.save() if self.grant_type == 'authorization_code': query_params['code'] = code.code query_params['state'] = self.params['state'] if self.params['state'] else '' elif self.grant_type in ['implicit', 'hybrid']: token = create_token( user=self.request.user, client=self.client, scope=self.params['scope']) # Check if response_type must include access_token in the response. if (self.params['response_type'] in ['id_token token', 'token', 'code token', 'code id_token token']): query_fragment['access_token'] = token.access_token # We don't need id_token if it's an OAuth2 request. if self.is_authentication: kwargs = { 'token': token, 'user': self.request.user, 'aud': self.client.client_id, 'nonce': self.params['nonce'], 'request': self.request, 'scope': self.params['scope'], } # Include at_hash when access_token is being returned. if 'access_token' in query_fragment: kwargs['at_hash'] = token.at_hash id_token_dic = create_id_token(**kwargs) # Check if response_type must include id_token in the response. if self.params['response_type'] in [ 'id_token', 'id_token token', 'code id_token', 'code id_token token']: query_fragment['id_token'] = encode_id_token(id_token_dic, self.client) else: id_token_dic = {} # Store the token. token.id_token = id_token_dic token.save() # Code parameter must be present if it's Hybrid Flow. if self.grant_type == 'hybrid': query_fragment['code'] = code.code query_fragment['token_type'] = 'bearer' query_fragment['expires_in'] = settings.get('OIDC_TOKEN_EXPIRE') query_fragment['state'] = self.params['state'] if self.params['state'] else '' if settings.get('OIDC_SESSION_MANAGEMENT_ENABLE'): # Generate client origin URI from the redirect_uri param. redirect_uri_parsed = urlsplit(self.params['redirect_uri']) client_origin = '{0}://{1}'.format( redirect_uri_parsed.scheme, redirect_uri_parsed.netloc) # Create random salt. salt = md5(uuid4().hex.encode()).hexdigest() # The generation of suitable Session State values is based # on a salted cryptographic hash of Client ID, origin URL, # and OP browser state. session_state = '{client_id} {origin} {browser_state} {salt}'.format( client_id=self.client.client_id, origin=client_origin, browser_state=get_browser_state_or_default(self.request), salt=salt) session_state = sha256(session_state.encode('utf-8')).hexdigest() session_state += '.' + salt if self.grant_type == 'authorization_code': query_params['session_state'] = session_state elif self.grant_type in ['implicit', 'hybrid']: query_fragment['session_state'] = session_state except Exception as error: logger.exception('[Authorize] Error when trying to create response uri: %s', error) raise AuthorizeError(self.params['redirect_uri'], 'server_error', self.grant_type) uri = uri._replace( query=urlencode(query_params, doseq=True), fragment=uri.fragment + urlencode(query_fragment, doseq=True)) return urlunsplit(uri)
def get_browser_state_or_default(request): """ Determine value to use as session state. """ key = request.session.session_key or settings.get('OIDC_UNAUTHENTICATED_SESSION_MANAGEMENT_KEY') return sha224(key.encode('utf-8')).hexdigest()
from oidc_provider.lib.utils.authorize import strip_prompt_login from oidc_provider.lib.utils.common import ( redirect, get_site_url, get_issuer, cors_allow_any, ) from oidc_provider.lib.utils.oauth2 import protected_resource_view from oidc_provider.lib.utils.token import client_id_from_id_token from oidc_provider.models import (Client, RSAKey, ResponseType) from oidc_provider import settings from oidc_provider import signals logger = logging.getLogger(__name__) OIDC_TEMPLATES = settings.get('OIDC_TEMPLATES') AUTHORIZE_CONTEXT_TRANSFORMER = settings.get('OIDC_CONTEXT_TRANSFORMERS', import_str=True, key='authorize') OIDC_PRIOR_TO_REDIRECT_HOOK = settings.get('OIDC_PRIOR_TO_REDIRECT_HOOK', import_str=True) OIDC_DECLINED_USERCONSENT_HOOK = settings.get('OIDC_DECLINED_USERCONSENT_HOOK', import_str=True) def call_oidc_prior_to_redirect_hook(request, client): if OIDC_PRIOR_TO_REDIRECT_HOOK: OIDC_PRIOR_TO_REDIRECT_HOOK(request=request, user=request.user, client=client)
def get(self, request, *args, **kwargs): authorize = self.authorize_endpoint_class(request) try: authorize.validate_params() if get_attr_or_callable(request.user, 'is_authenticated'): # Check if there's a hook setted. hook_resp = settings.get('OIDC_AFTER_USERLOGIN_HOOK', import_str=True)( request=request, user=request.user, client=authorize.client) if hook_resp: return hook_resp check_login_required(request, authorize) if 'select_account' in authorize.params['prompt']: # TODO: see how we can support multiple accounts for the end-user. if 'none' in authorize.params['prompt']: raise AuthorizeError(authorize.params['redirect_uri'], 'account_selection_required', authorize.grant_type) else: django_user_logout(request) return redirect_to_login( request.get_full_path(), settings.get('OIDC_LOGIN_URL')) if {'none', 'consent'}.issubset(authorize.params['prompt']): raise AuthorizeError(authorize.params['redirect_uri'], 'consent_required', authorize.grant_type) implicit_flow_resp_types = {'id_token', 'id_token token'} allow_skipping_consent = ( authorize.client.client_type != 'public' or authorize.params['response_type'] in implicit_flow_resp_types) if not authorize.client.require_consent and ( allow_skipping_consent and 'consent' not in authorize.params['prompt']): call_oidc_prior_to_redirect_hook(request, authorize.client) return redirect(authorize.create_response_uri()) if authorize.client.reuse_consent: # Check if user previously give consent. if authorize.client_has_user_consent() and ( allow_skipping_consent and 'consent' not in authorize.params['prompt']): call_oidc_prior_to_redirect_hook( request, authorize.client) return redirect(authorize.create_response_uri()) if 'none' in authorize.params['prompt']: raise AuthorizeError(authorize.params['redirect_uri'], 'consent_required', authorize.grant_type) # Generate hidden inputs for the form. context = { 'params': authorize.params, } hidden_inputs = render_to_string( 'oidc_provider/hidden_inputs.html', context) # Remove `openid` from scope list # since we don't need to print it. if 'openid' in authorize.params['scope']: authorize.params['scope'].remove('openid') context = { 'client': authorize.client, 'hidden_inputs': hidden_inputs, 'params': authorize.params, 'scopes': authorize.get_scopes_information(), } # Extra OIDC context, if required, from a callable. if AUTHORIZE_CONTEXT_TRANSFORMER: context = AUTHORIZE_CONTEXT_TRANSFORMER(request, context) return render(request, OIDC_TEMPLATES['authorize'], context) else: if 'none' in authorize.params['prompt']: raise AuthorizeError(authorize.params['redirect_uri'], 'login_required', authorize.grant_type) if 'login' in authorize.params['prompt']: next_page = strip_prompt_login(request.get_full_path()) return redirect_to_login(next_page, settings.get('OIDC_LOGIN_URL')) return redirect_to_login(request.get_full_path(), settings.get('OIDC_LOGIN_URL')) except (ClientIdError, RedirectUriError) as error: context = { 'error': error.error, 'description': error.description, } return render(request, OIDC_TEMPLATES['error'], context) except AuthorizeError as error: uri = error.create_uri(authorize.params['redirect_uri'], authorize.params['state']) return redirect(uri) except JustRedirect as error: return error.redirection
def process_response(self, request, response): if settings.get('OIDC_SESSION_MANAGEMENT_ENABLE'): response.set_cookie('op_browser_state', get_browser_state_or_default(request)) return response
class EndSessionTestCase(TestCase): """ See: http://openid.net/specs/openid-connect-session-1_0.html#RPLogout """ def setUp(self): call_command('creatersakey') self.user = create_fake_user() self.oidc_client = create_fake_client('id_token') self.LOGOUT_URL = 'http://example.com/logged-out/' self.oidc_client.post_logout_redirect_uris = [self.LOGOUT_URL] self.oidc_client.save() self.url = reverse('oidc_provider:end-session') @override_settings(OIDC_LOGOUT_URL='/post-logout/') def test_redirects_when_aud_is_str(self): query_params = { 'post_logout_redirect_uri': self.LOGOUT_URL, } response = self.client.get(self.url, query_params) # With no id_token the OP MUST NOT redirect to the requested # redirect_uri. self.assertRedirects(response, '/post-logout/', fetch_redirect_response=False) token = create_token(self.user, self.oidc_client, []) id_token_dic = create_id_token(token=token, user=self.user, aud=self.oidc_client.client_id) id_token = encode_id_token(id_token_dic, self.oidc_client) query_params['id_token_hint'] = id_token response = self.client.get(self.url, query_params) self.assertRedirects(response, self.LOGOUT_URL, fetch_redirect_response=False) def test_redirects_when_aud_is_list(self): """Check with 'aud' containing a list of str.""" query_params = { 'post_logout_redirect_uri': self.LOGOUT_URL, } token = create_token(self.user, self.oidc_client, []) id_token_dic = create_id_token(token=token, user=self.user, aud=self.oidc_client.client_id) id_token_dic['aud'] = [id_token_dic['aud']] id_token = encode_id_token(id_token_dic, self.oidc_client) query_params['id_token_hint'] = id_token response = self.client.get(self.url, query_params) self.assertRedirects(response, self.LOGOUT_URL, fetch_redirect_response=False) @mock.patch(settings.get('OIDC_AFTER_END_SESSION_HOOK')) def test_call_post_end_session_hook(self, hook_function): self.client.get(self.url) self.assertTrue(hook_function.called, 'OIDC_AFTER_END_SESSION_HOOK should be called') self.assertTrue(hook_function.call_count == 1, 'OIDC_AFTER_END_SESSION_HOOK should be called once')