def validate_params(self): # Client validation. try: self.client = Client.objects.get(client_id=self.params['client_id']) except Client.DoesNotExist: logger.debug('[Authorize] Invalid client identifier: %s', self.params['client_id']) raise ClientIdError() # Redirect URI validation. if self.is_authentication and not self.params['redirect_uri']: logger.debug('[Authorize] Missing redirect uri.') raise RedirectUriError() clean_redirect_uri = urlsplit(self.params['redirect_uri']) clean_redirect_uri = urlunsplit(clean_redirect_uri._replace(query='')) if not (clean_redirect_uri in self.client.redirect_uris): logger.debug('[Authorize] Invalid redirect uri: %s', self.params['redirect_uri']) raise RedirectUriError() # Grant type validation. if not self.grant_type: logger.debug('[Authorize] Invalid response type: %s', self.params['response_type']) raise AuthorizeError(self.params['redirect_uri'], 'unsupported_response_type', self.grant_type) # Nonce parameter validation. if self.is_authentication and self.grant_type == 'implicit' and not self.params['nonce']: raise AuthorizeError(self.params['redirect_uri'], 'invalid_request', self.grant_type) # Response type parameter validation. if self.is_authentication and self.params['response_type'] != self.client.response_type: raise AuthorizeError(self.params['redirect_uri'], 'invalid_request', self.grant_type) # PKCE validation of the transformation method. if self.params['code_challenge']: if not (self.params['code_challenge_method'] in ['plain', 'S256']): raise AuthorizeError(self.params['redirect_uri'], 'invalid_request', self.grant_type)
def test_no_params(self): """Test with a path only and no query/frag params""" redirect_uri = self.redirect_uri + 'path' error = AuthorizeError(redirect_uri, self.error, self.grant_type) created_uri = error.create_uri(redirect_uri, '') expected_uri = '{}?error={}&error_description={}'.format( redirect_uri, self.error, self.desc) compare(expected_uri, created_uri)
def test_query_params_only(self): """Test with query param in redirect uri""" redirect_uri = self.redirect_uri + "path/?action=something" error = AuthorizeError(redirect_uri, self.error, self.grant_type) created_uri = error.create_uri(redirect_uri, '') expected_uri = '{}&error={}&error_description={}'.format( redirect_uri, self.error, self.desc) compare(expected_uri, created_uri)
def test_with_deep_link(self): """Test with a non-http schema; deep link style (think slack://)""" redirect_uri = 'slack://example.com/path' state = 'my_state' error = AuthorizeError(redirect_uri, self.error, self.grant_type) created_uri = error.create_uri(redirect_uri, state) expected_uri = '{}?error={}&error_description={}&state={}' \ .format(redirect_uri, self.error, self.desc, state) compare(expected_uri, created_uri)
def test_with_state(self): """Test with state""" redirect_uri = self.redirect_uri + 'path' state = 'my_state' error = AuthorizeError(redirect_uri, self.error, self.grant_type) created_uri = error.create_uri(redirect_uri, state) expected_uri = '{}path?error={}&error_description={}&state={}' \ .format(self.redirect_uri, self.error, self.desc, state) compare(expected_uri, created_uri)
def test_query_and_frag_params(self): """Test with both qp's and fragment""" redirect_uri = self.redirect_uri + 'path?my_qp=test' frag = '#action=something' error = AuthorizeError(redirect_uri + frag, self.error, self.grant_type) created_uri = error.create_uri(redirect_uri + frag, '') expected_uri = '{}path?my_qp=test&error={}&error_description={}{}' \ .format(self.redirect_uri, self.error, self.desc, frag) compare(expected_uri, created_uri)
def test_frag_params_only(self): """Test with fragment params only""" redirect_uri = self.redirect_uri + 'path' frag = '#action=something' error = AuthorizeError(redirect_uri + frag, self.error, self.grant_type) created_uri = error.create_uri(redirect_uri + frag, '') expected_uri = '{}path?error={}&error_description={}{}'.format( self.redirect_uri, self.error, self.desc, frag) compare(expected_uri, created_uri)
def validate_params(self): # Client validation. try: self.client = self.client_class.objects.get( client_id=self.params['client_id']) except Client.DoesNotExist: logger.debug('[Authorize] Invalid client identifier: %s', self.params['client_id']) raise ClientIdError() # Redirect URI validation. if self.is_authentication and not self.params['redirect_uri']: logger.debug('[Authorize] Missing redirect uri.') raise RedirectUriError() if not redirect_uri_valid(self.params['redirect_uri'], self.client.redirect_uris): logger.debug('[Authorize] Invalid redirect uri: %s', self.params['redirect_uri']) raise RedirectUriError() # Grant type validation. if not self.grant_type: logger.debug('[Authorize] Invalid response type: %s', self.params['response_type']) raise AuthorizeError(self.params['redirect_uri'], 'unsupported_response_type', self.grant_type) if (not self.is_authentication and (self.grant_type == 'hybrid' or self.params['response_type'] in ['id_token', 'id_token token'])): logger.debug('[Authorize] Missing openid scope.') raise AuthorizeError(self.params['redirect_uri'], 'invalid_scope', self.grant_type) # Nonce parameter validation. if self.is_authentication and self.grant_type == 'implicit' and not self.params[ 'nonce']: raise AuthorizeError(self.params['redirect_uri'], 'invalid_request', self.grant_type) # Response type parameter validation. if self.is_authentication \ and self.params['response_type'] not in self.client.response_type_values(): raise AuthorizeError(self.params['redirect_uri'], 'invalid_request', self.grant_type) # PKCE validation of the transformation method. if self.params['code_challenge']: if not (self.params['code_challenge_method'] in ['plain', 'S256']): raise AuthorizeError(self.params['redirect_uri'], 'invalid_request', self.grant_type)
def post(self, request, *args, **kwargs): authorize = AuthorizeEndpoint(request) try: authorize.validate_params() if not request.POST.get('allow'): signals.user_decline_consent.send( self.__class__, user=request.user, client=authorize.client, scope=authorize.params['scope']) raise AuthorizeError(authorize.params['redirect_uri'], 'access_denied', authorize.grant_type) signals.user_accept_consent.send(self.__class__, user=request.user, client=authorize.client, scope=authorize.params['scope']) # Save the user consent given to the client. authorize.set_client_user_consent() uri = authorize.create_response_uri() return redirect(uri) except AuthorizeError as error: uri = error.create_uri(authorize.params['redirect_uri'], authorize.params['state']) return redirect(uri)
def test_invalid_params(self, mocker, api_client): mocker.patch.object(AuthorizeEndpoint, "__init__", return_value=None) mocker.patch.object( AuthorizeEndpoint, "validate_params", side_effect=AuthorizeError("err", "desc", "grant_type"), ) response = api_client.get( "/vc/connect/authorize/?pres_req_conf_id=pid&scope=some_scope vc_authn" ) assert response.status_code == status.HTTP_400_BAD_REQUEST
def check_login_required(request, authorize): """ Raises AuthorizeError or JustRedirect if login is required for this auth. """ if 'login' in authorize.params['prompt'] or request.user.is_anonymous: if 'none' in authorize.params['prompt']: raise AuthorizeError(authorize.params['redirect_uri'], 'login_required', authorize.grant_type) else: django_user_logout(request) full_path = strip_prompt_login(request.get_full_path()) raise JustRedirect( redirect_to_login(full_path, settings.get('OIDC_LOGIN_URL')))
def post(self, request, *args, **kwargs): authorize = self.authorize_endpoint_class(request) try: authorize.validate_params() check_login_required(request, authorize) if not request.POST.get('allow'): signals.user_decline_consent.send( self.__class__, user=request.user, client=authorize.client, scope=authorize.params['scope']) call_oidc_declined_userconsent_hook(request, authorize.client) raise AuthorizeError(authorize.params['redirect_uri'], 'access_denied', authorize.grant_type) signals.user_accept_consent.send(self.__class__, user=request.user, client=authorize.client, scope=authorize.params['scope']) # Save the user consent given to the client. authorize.set_client_user_consent() uri = authorize.create_response_uri() call_oidc_prior_to_redirect_hook(request, authorize.client) return redirect(uri) except AuthorizeError as error: uri = error.create_uri(authorize.params['redirect_uri'], authorize.params['state']) return redirect(uri) except JustRedirect as error: # TODO we're trying to redirect to a login page here, but can't at this point as # 1. We don't have all the query parameters # 2. The nonce is wrong. # So just display an error. context = { 'error': "Logged out", 'description': "You have logged out or your session has timed out. Please re-try the authorization.", } return render(request, OIDC_TEMPLATES['error'], context)
def post(self, request, *args, **kwargs): authorize = AuthorizeEndpoint(request) try: authorize.validate_params() if not request.POST.get('allow'): raise AuthorizeError(authorize.params['redirect_uri'], 'access_denied', authorize.grant_type) # Save the user consent given to the client. authorize.set_client_user_consent() uri = authorize.create_response_uri() return redirect(uri) except (AuthorizeError) as error: uri = error.create_uri(authorize.params['redirect_uri'], authorize.params['state']) return redirect(uri)
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 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']: 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']) 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(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 if 'login' in authorize.params['prompt']: if 'none' in authorize.params['prompt']: raise AuthorizeError(authorize.params['redirect_uri'], 'login_required', authorize.grant_type) else: django_user_logout(request) next_page = strip_prompt_login(request.get_full_path()) return redirect_to_login( next_page, settings.get('OIDC_LOGIN_URL')) 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) if not authorize.client.require_consent and ( 'consent' not in authorize.params['prompt']): return redirect(authorize.create_response_uri()) if authorize.client.reuse_consent: # Check if user previously give consent. if authorize.client_has_user_consent() and ( 'consent' not in authorize.params['prompt']): 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(), } 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)
def create_response_uri(self): uri = urlsplit(self.params.redirect_uri) query_params = parse_qs(uri.query) query_fragment = parse_qs(uri.fragment) try: if self.grant_type == 'authorization_code': 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) code.save() query_params['code'] = code.code query_params['state'] = self.params.state if self.params.state else '' elif self.grant_type == 'implicit': token = create_token( user=self.request.user, client=self.client, scope=self.params.scope) # Check if response_type is an OpenID request with value 'id_token token' # or it's an OAuth2 Implicit Flow request. if self.params.response_type in ['id_token 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 = { "user": self.request.user, "aud": self.client.client_id, "nonce": self.params.nonce, "request": self.request } # 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) query_fragment['id_token'] = encode_id_token(id_token_dic, self.client) token.id_token = id_token_dic else: id_token_dic = {} # Store the token. token.id_token = id_token_dic token.save() query_fragment['token_type'] = 'bearer' # TODO: Create setting 'OIDC_TOKEN_EXPIRE'. query_fragment['expires_in'] = 60 * 10 query_fragment['state'] = self.params.state if self.params.state else '' except Exception as error: logger.debug('[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)) uri = uri._replace(fragment=urlencode(query_fragment, doseq=True)) return urlunsplit(uri)
def create_response_uri(self): uri = urlsplit(self.params['redirect_uri']) query_params = parse_qs(uri.query) query_fragment = parse_qs(uri.fragment) try: if self.grant_type in ['authorization_code', 'hybrid']: 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']) 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 = { '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 '' except Exception as error: logger.debug('[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)) uri = uri._replace(fragment=urlencode(query_fragment, doseq=True)) return urlunsplit(uri)