def authenticate(self, request): """ Checks for id/secret pair in Authorization header, or else POSTed request parameters. @type request: webob.Request @param request: HTTP request object @rtype: str @return: id of authenticated client/resource Raise OauthException if authentication fails. """ cid = secret = None if 'Authorization' in request.headers and request.headers[ 'Authorization'].startswith('Basic'): cid, secret = b64decode( request.headers['Authorization'][6:]).split(':', 1) elif 'client_id' in request.POST and 'client_secret' in request.POST: cid = request.POST['client_id'] secret = request.POST['client_secret'] if not cid or not secret: raise OauthException( 'invalid_%s' % self.typ, 'No %s password authentication supplied' % self.typ) for authorization in self._register.register.itervalues(): if authorization.id == cid and authorization.secret == secret: return authorization.id raise OauthException('invalid_%s' % self.typ, ('%s access denied: %s' % (cid, self.typ)))
def authenticate(self, request): """ Checks for an SSL certificate distinguished name in the environ and if found, returns it. @type request: webob.Request @param request: HTTP request object @rtype: str @return: ID of authenticated client/resource, or None if authentication is not to be performed. Raise OauthException if authentication fails. """ dn = request.environ.get(self.CERT_DN_ENVIRON_KEY) if not dn: raise OauthException('invalid_%s' % self.typ, 'No certificate DN found.') for authorization in self._register.register.itervalues(): if authorization.authentication_data == dn: return authorization.id raise OauthException( 'invalid_%s' % self.typ, ('Certificate DN does not match that for any registered %s: %s' % (self.typ, dn)))
def generate_authorization_grant(self, auth_request, request): """Generates an authorization grant. @type auth_request: ndg.oauth.server.lib.oauth.authorize.AuthorizeRequest @param auth_request: authorization request @type request: webob.Request @param request: HTTP request object @rtype: tuple ( ndg.oauth.server.lib.register.authorization_grant.AuthorizationGrant str ) @return: tuple ( authorization grant authorization code ) """ user_identifier = request.environ.get(self.user_identifier_env_key) if not user_identifier: log.error('Could not find user identifier key "%s" in environ', self.user_identifier_env_key) raise OauthException('server_error', 'Authorization grant could not be created') code = uuid.uuid4().hex additional_data = { self.user_identifier_grant_data_key: user_identifier } grant = AuthorizationGrant(code, auth_request, self.lifetime, additional_data=additional_data) return grant, code
def _redirect_after_authorize(self, authz_request, authz_response=None, error=None, error_description=None): """Redirects to the redirect URI after the authorization process as completed. @type resp: ndg.oauth.server.lib.oauth.authorize.AuthorizeRequest @param resp: OAuth authorize request @type resp: ndg.oauth.server.lib.oauth.authorize.AuthorizeResponse @param resp: OAuth authorize response @type error: str @param error: OAuth error @type error_description: str @param error_description: error description """ # Check for inconsistencies that should be reported directly to the user. if not authz_response and not error: error = 'server_error' error_description = 'Internal server error' # Get the redirect URI. client = self.client_register.register[authz_request.client_id] redirect_uri = ( authz_request.redirect_uri if authz_request.redirect_uri else \ client.redirect_uris[0] ) if not redirect_uri: return ( None, httplib.BAD_REQUEST, 'An authorization request has been made without a return URI.') # Redirect back to client with authorization code or error. if error: url_parameters = [('error', error), ('error_description', error_description)] elif isinstance(authz_response, AuthorizeResponse): url_parameters = [('code', authz_response.code)] elif isinstance(authz_response, ImplicitGrantAccessTokenResponse): url_parameters = authz_response.get_as_dict().items() else: raise OauthException( 'Expecting authorisation response or implicit ' 'grant response, got %r' % authz_response) full_redirect_uri = self._make_combined_url(redirect_uri, url_parameters, authz_request.state) log.debug("Redirecting to URI: %s", full_redirect_uri) return full_redirect_uri, None, None
def check_request(self, request, params, post_only=False): """ Checks that the request is valid in the following respects: o Must be over HTTPS. o Optionally, must use the POST method. o Parameters must not be repeated. If the request is directly from the client, the user must be authenticated - it is assumed that the caller has checked this. Raises OauthException if any check fails. @type request: webob.Request @param request: HTTP request object @type params: dict @param params: request parameters @type post_only: bool @param post_only: True if the HTTP method must be POST, otherwise False """ if request.scheme != 'https': raise OauthException( 'invalid_request', 'Transport layer security must be used for ' 'this request.') if post_only and (request.method != 'POST'): raise OauthException( 'invalid_request', 'HTTP POST method must be used for this ' 'request.') # Check for duplicate parameters. param_counts = {} for key in params.iterkeys(): count = param_counts.get(key, 0) param_counts[key] = count + 1 for key, count in param_counts.iteritems(): if count > 1: raise OauthException('invalid_request', 'Parameter "%s" is repeated.' % key) return
def make_access_token(token_request, client_id, access_token_register, access_token_generator, authorization_grant_register, request): """ Makes an access token based on a token request and using a given access token generator. Returns access token response or None if an error occurs that is not one of those that can be reported in an error response, i.e., internal server error """ # Check token_request valid and matches a registered authorization grant. if token_request.grant_type != AUTHORIZATION_CODE_GRANT_TYPE: raise OauthException('invalid_request', 'Invalid grant_type') try: grant = authorization_grant_register.get_value(token_request.code) except KeyError: raise OauthException('invalid_grant', 'Invalid authorization code') if (grant.redirect_uri is not None) and (token_request.redirect_uri != grant.redirect_uri): raise OauthException('invalid_grant', 'Invalid redirect URI') if grant.granted: # Invalidate the associated token. if grant.token: grant.token.valid = False raise OauthException('invalid_grant', 'Token already granted for authorization grant') # Check whether expired. if grant.expires <= datetime.utcnow(): raise OauthException('invalid_grant', 'Authorization grant expired') # Check that the grant is issued to the requesting client. # This requires that the client has authenticated itself so that the # client identity is known. # client_id is None if client authentication is not configured - this # signals that authentication is disabled for testing. if client_id and (grant.client_id != client_id): raise OauthException('invalid_grant', 'Token granted for different client') token = access_token_generator.get_access_token(token_request, grant, request) if not token: return None grant.granted = True grant.token = token response = AccessTokenResponse(token.token_id, token.token_type, token.lifetime, refresh_token=None) if access_token_register.add_token(token): return response else: return None
def authorize(self, request, client_authorized): """Handle an authorization request. It is assumed that the caller has checked whether the user is authenticated and that the user has authorised the client and scope. Request query parameters (from http://tools.ietf.org/html/draft-ietf-oauth-v2-22): response_type REQUIRED. Value MUST be set to "code". client_id REQUIRED. The client identifier as described in Section 2.2. redirect_uri OPTIONAL, as described in Section 3.1.2. scope OPTIONAL. The scope of the access request as described by Section 3.3. state RECOMMENDED. An opaque value used by the client to maintain state between the request and callback. The authorization server includes this value when redirecting the user-agent back to the client. The parameter SHOULD be used for preventing cross-site request forgery as described in Section 10.12. Response: application/x-www-form-urlencoded format: code REQUIRED. The authorization code generated by the authorization server. The authorization code MUST expire shortly after it is issued to mitigate the risk of leaks. A maximum authorization code lifetime of 10 minutes is RECOMMENDED. The client MUST NOT use the authorization code more than once. If an authorization code is used more than once, the authorization server MUST deny the request and SHOULD attempt to revoke all tokens previously issued based on that authorization code. The authorization code is bound to the client identifier and redirection URI. state REQUIRED if the "state" parameter was present in the client authorization request. The exact value received from the client. @type request: webob.Request @param request: HTTP request object @type client_authorized: bool @param client_authorized: True if resource owner has authorized client @rtype: tuple: (str, int, str) @return: tuple ( redirect_uri HTTP status if error error description ) """ log.debug("Starting authorization request") # Parameters should only be taken from the query string. params = request.GET auth_request = AuthorizeRequest(params.get('response_type', None), params.get('client_id', None), params.get('redirect_uri', None), params.get('scope', None), params.get('state', None)) try: self.check_request(request, params, post_only=False) # Check for required parameters. required_parameters = ['response_type', 'client_id'] for param in required_parameters: if param not in params: log.error("Missing request parameter %s from params: %s", param, params) raise OauthException( 'invalid_request', "Missing request parameter: %s" % param) if not client_authorized: raise OauthException('access_denied', 'User has declined authorization') response_type = params.get('response_type', None) if response_type != 'code': raise OauthException( 'unsupported_response_type', "Response type %s not supported" % response_type) client_error = self.client_register.is_valid_client( auth_request.client_id, auth_request.redirect_uri) if client_error: log.error("Invalid client: %s", client_error) return (None, httplib.BAD_REQUEST, client_error) # redirect_uri must be included in the request if the client has # more than one registered. client = self.client_register.register[auth_request.client_id] if len(client.redirect_uris ) != 1 and not auth_request.redirect_uri: log.error("An authorization request has been made without a " "return URI") return (None, httplib.BAD_REQUEST, ('An authorization request has been made without a ' 'return URI.')) # Preconditions satisfied - generate grant. (grant, code) = self.authorizer.generate_authorization_grant( auth_request, request) auth_response = AuthorizeResponse(code, auth_request.state) if not self.authorization_grant_register.add_grant(grant): log.error('Registering grant failed') raise OauthException( 'server_error', 'Authorization grant could not be created') except OauthException, exc: log.error("Redirecting back after error: %s - %s", exc.error, exc.error_description) return self._redirect_after_authorize(auth_request, None, exc.error, exc.error_description)
def access_token(self, request): """ Handles a request for an access token. Request parameters in post data (from http://tools.ietf.org/html/draft-ietf-oauth-v2-22): The client makes a request to the token endpoint by adding the following parameters using the "application/x-www-form-urlencoded" format in the HTTP request entity-body: grant_type REQUIRED. Value MUST be set to "authorization_code". code REQUIRED. The authorization code received from the authorization server. redirect_uri REQUIRED, if the "redirect_uri" parameter was included in the authorization request as described in Section 4.1.1, and their values MUST be identical. Response: application/json format: access_token access token token_type token type expires_in lifetime of token in seconds refresh_token @type request: webob.Request @param request: HTTP request object @rtype: tuple: (str, int, str) @return: tuple ( OAuth JSON response HTTP status if error error description ) """ log.debug("Starting access token request") try: # Parameters should only be taken from the body, not the URL query # string. params = request.POST self.check_request(request, params, post_only=True) # Check that the client is authenticated as a registered client. client_id = self.client_authenticator.authenticate(request) if client_id is None: log.warn('Client authentication not performed') else: log.debug("Client id: %s", client_id) # redirect_uri is only required if it was included in the # authorization request. required_parameters = ['grant_type', 'code'] for param in required_parameters: if param not in params: log.error("Missing request parameter %s from inputs: %s", param, params) raise OauthException( 'invalid_request', "Missing request parameter: %s" % param) except OauthException, exc: return (self._error_access_token_response(exc.error, exc.error_description), None, None)