def authenticate(self, context, auth_payload, auth_context): """Turn a signed request with an access key into a keystone token.""" if not self.oauth2_api: raise exception.Unauthorized(_('%s not supported') % self.method) access_token_id = auth_payload['access_token_id'] if not access_token_id: raise exception.ValidationError(attribute='oauth2_token', target='request') headers = context['headers'] uri = controller.V3Controller.base_url(context, context['path']) http_method = 'POST' required_scopes = ['all_info'] request_validator = validator.OAuth2Validator() server = oauth2_core.Server(request_validator) body = {'access_token': access_token_id} valid, oauthlib_request = server.verify_request( uri, http_method, body, headers, required_scopes) # oauthlib_request has a few convenient attributes set such as # oauthlib_request.client = the client associated with the token # oauthlib_request.user = the user associated with the token # oauthlib_request.scopes = the scopes bound to this token if valid: auth_context['user_id'] = oauthlib_request.user #auth_context['access_token_id'] = access_token_id #auth_context['project_id'] = project_id return None else: msg = _('Could not validate the access token') raise exception.Unauthorized(msg)
def create_authorization_code(self, context, user_auth): request_validator = validator.OAuth2Validator() server = core.Server(request_validator) # Validate request headers = context['headers'] body = user_auth uri = self.base_url(context, context['path']) http_method = 'POST' # Fetch authorized scopes from the request scopes = body.get('scopes') if not scopes: raise exception.ValidationError(attribute='scopes', target='request') # Fetch the credentials saved in the pre authorization phase client_id = body.get('client_id') if not client_id: raise exception.ValidationError(attribute='client_id', target='request') user_id = body.get('user_id') if not user_id: # Try to extract the user_id from the token user_id = self._extract_user_id_from_token(context['token_id']) credentials = self.oauth2_api.get_consumer_credentials( client_id, user_id) try: headers, body, status = server.create_authorization_response( uri, http_method, body, headers, scopes, credentials) # headers = {'Location': 'https://foo.com/welcome_back?code=somera # ndomstring&state=xyz '}, this might change to include suggested # headers related to cache best practices etc. # body = '', this might be set in future custom grant types # status = 302, suggested HTTP status code response = wsgi.render_response(body, status=(302, 'Found'), headers=headers.items()) LOG.info( 'OAUTH2: Created Authorization Code to consumer %(consumer)s \ for user %(user)s with scope %(scope)s. Redirecting to %(uri)s', { 'consumer': client_id, 'user': user_id, 'scope': scopes, 'uri': headers['Location'] }) return response except FatalClientError as e: # NOTE(garcianavalon) form the OAuthLib documentation and comments: # Errors during authorization where user should not be redirected back. # If the request fails due to a missing, invalid, or mismatching # redirection URI, or if the client identifier is missing or invalid, # the authorization server SHOULD inform the resource owner of the # error and MUST NOT automatically redirect the user-agent to the # invalid redirection URI. # Instead the user should be informed of the error by the provider itself. # Fatal errors occur when the client_id or redirect_uri is invalid or # missing. These must be caught by the provider and handled, how this # is done is outside of the scope of OAuthLib but showing an error # page describing the issue is a good ideaself. msg = e.json LOG.warning('OAUTH2: FatalClientError %s' % msg) raise exception.ValidationError(message=msg)
def create_access_token(self, context, token_request): request_validator = validator.OAuth2Validator() server = core.Server(request_validator) # Validate request headers = context['headers'] # NOTE(garcianavalon) Work around the keystone limitation with content types # Keystone only accepts JSON bodies while OAuth2.0 (RFC 6749) requires # x-www-form-urlencoded # We leave it like this to support future versions where the use of # x-www-form-urlencoded is accepted if headers['Content-Type'] == 'application/x-www-form-urlencoded': body = context['query_string'] elif headers['Content-Type'] == 'application/json': # TODO(garcianavalon) are these checks really necessary or # can we delegate them to oauthlib? grant_type = token_request.get('grant_type', None) if not grant_type: msg = _('grant_type missing in request body: {0}').format( token_request) raise exception.ValidationError(message=msg) if (grant_type == 'authorization_code' and not 'code' in token_request): msg = _('code missing in request body: %s') % token_request raise exception.ValidationError(message=msg) body = urllib.urlencode(token_request) else: msg = _( 'Content-Type: %s is not supported') % headers['Content-Type'] raise exception.ValidationError(message=msg) # check headers for authentication authmethod, auth = headers['Authorization'].split(' ', 1) if authmethod.lower() != 'basic': msg = _('Authorization error: %s. Only HTTP Basic is supported' ) % headers['Authorization'] raise exception.ValidationError(message=msg) uri = self.base_url(context, context['path']) http_method = 'POST' # Extra credentials you wish to include credentials = None # TODO(garcianavalon) headers, body, status = server.create_token_response( uri, http_method, body, headers, credentials) # headers will contain some suggested headers to add to your response # { # 'Content-Type': 'application/json', # 'Cache-Control': 'no-store', # 'Pragma': 'no-cache', # } # body will contain the token in json format and expiration from now # in seconds. # { # 'access_token': 'sldafh309sdf', # 'refresh_token': 'alsounguessablerandomstring', # 'expires_in': 3600, # 'scope': 'https://example.com/userProfile https://example.com/pictures', # 'token_type': 'Bearer' # } # body will contain an error code and possibly an error description if # the request failed, also in json format. # { # 'error': 'invalid_grant_type', # 'description': 'athorizatoin_coed is not a valid grant type' # } # status will be a suggested status code, 200 on ok, 400 on bad request # and 401 if client is trying to use an invalid authorization code, # fail to authenticate etc. # NOTE(garcianavalon) oauthlib returns the body as a JSON string already, # and the Keystone base controlers expect a dictionary body = json.loads(body) # TODO(garcianavalon) body contains scope instead of scopes and is only a # space separated string instead of a list. We can wait for a change in # Oauthlib or implement our own TokenProvider if status == 200: response = wsgi.render_response(body, status=(status, 'OK'), headers=headers.items()) LOG.info('OAUTH2: Created Access Token %s' % body['access_token']) return response # Build the error message and raise the corresponding error msg = _(body['error']) if hasattr(body, 'description'): msg = msg + ': ' + _(body['description']) LOG.warning('OAUTH2: Error creating Access Token %s' % msg) if status == 400: raise exception.ValidationError(message=msg) elif status == 401: # TODO(garcianavalon) custom exception class raise exception.Unauthorized(message=msg)
def request_authorization_code(self, context): request_validator = validator.OAuth2Validator() server = core.Server(request_validator) # Validate request headers = context['headers'] body = context['query_string'] uri = self.base_url(context, context['path']) http_method = 'GET' response = {} try: scopes, credentials = server.validate_authorization_request( uri, http_method, body, headers) # scopes will hold default scopes for client, i.e. #['https://example.com/userProfile', 'https://example.com/pictures'] # credentials is a dictionary of # { # 'client_id': 'foo', # 'redirect_uri': 'https://foo.com/welcome_back', # 'response_type': 'code', # 'state': 'randomstring', # 'request' : The request object created internally. # } # these credentials will be needed in the post authorization view and # should be persisted between. None of them are secret but take care # to ensure their integrity if embedding them in the form or cookies. # NOTE(garcianavalon) We are not storing this for now, # but might do it in the future request = credentials.pop('request') # get the user id to identify the credentials in later stages credentials['user_id'] = self._extract_user_id_from_token( context['token_id']) credentials_ref = self._assign_unique_id( self._normalize_dict(credentials)) self.oauth2_api.store_consumer_credentials(credentials_ref) # Present user with a nice form where client (id foo) request access to # his default scopes (omitted from request), after which you will # redirect to his default redirect uri (omitted from request). # This JSON is to be used by the next layer (ie a Django server) to # populate the view response['data'] = { 'consumer': { 'id': credentials['client_id'] # TODO(garcianavalon) add consumer description }, 'redirect_uri': credentials['redirect_uri'], 'requested_scopes': request.scopes } LOG.info( 'OAUTH2: Requested Authorization Code by consumer %(consumer)s \ to user %(user)s, with scope %(scope)s and redirect uri %(uri)s', { 'consumer': credentials['client_id'], 'user': credentials['user_id'], 'scope': request.scopes, 'uri': credentials['redirect_uri'] }) except FatalClientError as e: # NOTE(garcianavalon) form the OAuthLib documentation and comments: # Errors during authorization where user should not be redirected back. # If the request fails due to a missing, invalid, or mismatching # redirection URI, or if the client identifier is missing or invalid, # the authorization server SHOULD inform the resource owner of the # error and MUST NOT automatically redirect the user-agent to the # invalid redirection URI. # Instead the user should be informed of the error by the provider itself. # Fatal errors occur when the client_id or redirect_uri is invalid or # missing. These must be caught by the provider and handled, how this # is done is outside of the scope of OAuthLib but showing an error # page describing the issue is a good idea. msg = e.json LOG.warning('OAUTH2: FatalClientError %s' % msg) raise exception.ValidationError(message=msg) except OAuth2Error as e: # NOTE(garcianavalon) form the OAuthLib documentation and comments: # A normal error could be a missing response_type parameter or the client # attempting to access scope it is not allowed to ask authorization for. # Normal errors can safely be included in the redirection URI and # sent back to the client. # We send back the errors in the response body response['error'] = json.loads(e.json) LOG.warning('OAUTH2: OAuth2Error %s' % response['error']) return response