def test_hmac_sha1_signature(self): self.prepare_data() handle = self.create_route() url = '/user' params = [ ('oauth_consumer_key', 'client'), ('oauth_token', 'valid-token'), ('oauth_signature_method', 'HMAC-SHA1'), ('oauth_timestamp', str(int(time.time()))), ('oauth_nonce', 'hmac-sha1-nonce'), ] base_string = signature.construct_base_string( 'GET', 'http://testserver/user', params ) sig = signature.hmac_sha1_signature( base_string, 'secret', 'valid-token-secret') params.append(('oauth_signature', sig)) auth_param = ','.join(['{}="{}"'.format(k, v) for k, v in params]) auth_header = 'OAuth ' + auth_param # case 1: success request = self.factory.get(url, HTTP_AUTHORIZATION=auth_header) resp = handle(request) data = json.loads(to_unicode(resp.content)) self.assertIn('username', data) # case 2: exists nonce request = self.factory.get(url, HTTP_AUTHORIZATION=auth_header) resp = handle(request) data = json.loads(to_unicode(resp.content)) self.assertEqual(data['error'], 'invalid_nonce')
def test_rsa_sha1_signature(self): self.prepare_data() handle = self.create_route() url = '/user' params = [ ('oauth_consumer_key', 'client'), ('oauth_token', 'valid-token'), ('oauth_signature_method', 'RSA-SHA1'), ('oauth_timestamp', str(int(time.time()))), ('oauth_nonce', 'rsa-sha1-nonce'), ] base_string = signature.construct_base_string( 'GET', 'http://testserver/user', params ) sig = signature.rsa_sha1_signature( base_string, read_file_path('rsa_private.pem')) params.append(('oauth_signature', sig)) auth_param = ','.join(['{}="{}"'.format(k, v) for k, v in params]) auth_header = 'OAuth ' + auth_param request = self.factory.get(url, HTTP_AUTHORIZATION=auth_header) resp = handle(request) data = json.loads(to_unicode(resp.content)) self.assertIn('username', data) # case: invalid signature auth_param = auth_param.replace('rsa-sha1-nonce', 'alt-sha1-nonce') auth_header = 'OAuth ' + auth_param request = self.factory.get(url, HTTP_AUTHORIZATION=auth_header) resp = handle(request) data = json.loads(to_unicode(resp.content)) self.assertEqual(data['error'], 'invalid_signature')
def list_to_scope(scope): """Convert a list of scopes to a space separated string.""" if isinstance(scope, (set, tuple, list)): return " ".join([to_unicode(s) for s in scope]) if scope is None: return scope return to_unicode(scope)
def _sign(h): protected, _, signature = self._sign_signature( h['protected'], payload, key, payload_segment) rv = { 'protected': to_unicode(protected), 'signature': to_unicode(signature) } if 'header' in header: rv['header'] = h['header'] return rv
def serialize_json(self, header_obj, payload, key): """Generate a JWS JSON Serialization. The JWS JSON Serialization represents digitally signed or MACed content as a JSON object, per `Section 7.2`_. :param header_obj: A dict/list of header :param payload: A string/dict of payload :param key: Private key used to generate signature :return: JWSObject Example ``header_obj`` of JWS JSON Serialization:: { "protected: {"alg": "HS256"}, "header": {"kid": "jose"} } Pass a dict to generate flattened JSON Serialization, pass a list of header dict to generate standard JSON Serialization. """ payload_segment = json_b64encode(payload) def _sign(jws_header): self._validate_header(jws_header) _alg, _key = prepare_algorithm_key(self._algorithms, jws_header, payload, key, private=True) protected_segment = json_b64encode(jws_header.protected) signing_input = b'.'.join([protected_segment, payload_segment]) signature = urlsafe_b64encode(_alg.sign(signing_input, _key)) rv = { 'protected': to_unicode(protected_segment), 'signature': to_unicode(signature) } if jws_header.header is not None: rv['header'] = jws_header.header return rv if isinstance(header_obj, dict): data = _sign(JWSHeader.from_dict(header_obj)) data['payload'] = to_unicode(payload_segment) return data signatures = [_sign(JWSHeader.from_dict(h)) for h in header_obj] return { 'payload': to_unicode(payload_segment), 'signatures': signatures }
def test_invalid_request_parameters(self): self.prepare_data() handle = self.create_route() url = '/user' # case 1 request = self.factory.get(url) resp = handle(request) data = json.loads(to_unicode(resp.content)) self.assertEqual(data['error'], 'missing_required_parameter') self.assertIn('oauth_consumer_key', data['error_description']) # case 2 request = self.factory.get( add_params_to_uri(url, {'oauth_consumer_key': 'a'})) resp = handle(request) data = json.loads(to_unicode(resp.content)) self.assertEqual(data['error'], 'invalid_client') # case 3 request = self.factory.get( add_params_to_uri(url, {'oauth_consumer_key': 'client'})) resp = handle(request) data = json.loads(to_unicode(resp.content)) self.assertEqual(data['error'], 'missing_required_parameter') self.assertIn('oauth_token', data['error_description']) # case 4 request = self.factory.get( add_params_to_uri(url, { 'oauth_consumer_key': 'client', 'oauth_token': 'a' }) ) resp = handle(request) data = json.loads(to_unicode(resp.content)) self.assertEqual(data['error'], 'invalid_token') # case 5 request = self.factory.get( add_params_to_uri(url, { 'oauth_consumer_key': 'client', 'oauth_token': 'valid-token' }) ) resp = handle(request) data = json.loads(to_unicode(resp.content)) self.assertEqual(data['error'], 'missing_required_parameter') self.assertIn('oauth_timestamp', data['error_description'])
def _sign(jws_header): self._validate_private_headers(jws_header) _alg, _key = self._prepare_algorithm_key(jws_header, payload, key) protected_segment = json_b64encode(jws_header.protected) signing_input = b'.'.join([protected_segment, payload_segment]) signature = urlsafe_b64encode(_alg.sign(signing_input, _key)) rv = { 'protected': to_unicode(protected_segment), 'signature': to_unicode(signature) } if jws_header.header is not None: rv['header'] = jws_header.header return rv
def generate_api_key(kid, private_key, user_id, expires_in, scopes, client_id): """ Generate a JWT refresh token and output a UTF-8 string of the encoded JWT signed with the private key. Args: kid (str): key id of the keypair used to generate token private_key (str): RSA private key to sign and encode the JWT with user_id (user id): User id to generate token for expires_in (int): seconds until expiration scopes (List[str]): oauth scopes for user_id Return: str: encoded JWT refresh token signed with ``private_key`` """ headers = {"kid": kid} iat, exp = issued_and_expiration_times(expires_in) jti = str(uuid.uuid4()) sub = str(user_id) claims = { "pur": "api_key", "aud": scopes, "sub": sub, "iss": config.get("BASE_URL"), "iat": iat, "exp": exp, "jti": jti, "azp": client_id or "", } logger.info("issuing JWT API key with id [{}] to [{}]".format(jti, sub)) logger.debug("issuing JWT API key\n" + json.dumps(claims, indent=4)) token = jwt.encode(claims, private_key, headers=headers, algorithm="RS256") logger.debug(str(token)) token = to_unicode(token, "UTF-8") return JWTResult(token=token, kid=kid, claims=claims)
def dumps_private_key(self): obj = self.dumps_public_key(self.private_key.public_key()) d_bytes = self.private_key.private_bytes(Encoding.Raw, PrivateFormat.Raw, NoEncryption()) obj['d'] = to_unicode(urlsafe_b64encode(d_bytes)) return obj
def init_jwt_config(self, app): """Initialize JWT related configuration.""" jwt_iss = app.config.get('OAUTH2_JWT_ISS') if not jwt_iss: raise RuntimeError('Missing "OAUTH2_JWT_ISS" configuration.') jwt_key_path = app.config.get('OAUTH2_JWT_KEY_PATH') if jwt_key_path: with open(jwt_key_path, 'r') as f: if jwt_key_path.endswith('.json'): jwt_key = json.load(f) else: jwt_key = to_unicode(f.read()) else: jwt_key = app.config.get('OAUTH2_JWT_KEY') if not jwt_key: raise RuntimeError('Missing "OAUTH2_JWT_KEY" configuration.') jwt_alg = app.config.get('OAUTH2_JWT_ALG') if not jwt_alg: raise RuntimeError('Missing "OAUTH2_JWT_ALG" configuration.') jwt_exp = app.config.get('OAUTH2_JWT_EXP', 3600) self.config.setdefault('jwt_iss', jwt_iss) self.config.setdefault('jwt_key', jwt_key) self.config.setdefault('jwt_alg', jwt_alg) self.config.setdefault('jwt_exp', jwt_exp)
def scope_to_list(scope): """Convert a space separated string to a list of scopes.""" if isinstance(scope, (tuple, list, set)): return [to_unicode(s) for s in scope] elif scope is None: return None return scope.strip().split()
def decode(self, s, key, claims_cls=None, claims_options=None, claims_params=None): """Decode the JWS with the given key. This is similar with :meth:`verify`, except that it will raise BadSignatureError when signature doesn't match. :param s: text of JWT :param key: key used to verify the signature :param claims_cls: class to be used for JWT claims :param claims_options: `options` parameters for claims_cls :param claims_params: `params` parameters for claims_cls :return: claims_cls instance :raise: BadSignatureError """ if claims_cls is None: claims_cls = JWTClaims header, bytes_payload = super(JWT, self).decode(s, key) payload = json.loads(to_unicode(bytes_payload)) return claims_cls( payload, header, options=claims_options, params=claims_params, )
def create_oauth2_request_from_json(self, request): """Build the OAuth2Request object from the JSON request body. Args: request (obj): The request object to parse for the necessary elements to build the OAuth2Request. Returns: (obj): The OAuth2Request object. """ request_cls = OAuth2Request if isinstance(request, request_cls): return request if not request: request = flask_req # in case we cannot determine if the header is json, we hand the workload off to the base method. try: if request.headers['Content-Type'] != 'application/json': return self.create_authorization_request(request) except Exception: return self.create_oauth2_request(request) if request.method == 'POST': body = request.json else: body = None url = request.base_url if request.query_string: url = url + '?' + to_unicode(request.query_string) return request_cls(request.method, url, body, request.headers)
def generate_id_token(self, token, request, nonce=None, auth_time=None, code=None): scopes = scope_to_list(token['scope']) if not scopes or scopes[0] != 'openid': return None # TODO: merge scopes and claims user_info = self.generate_user_info(request.user, scopes) now = int(time.time()) if auth_time is None: auth_time = now config = self.server.config payload = { 'iss': config['jwt_iss'], 'aud': [request.client.client_id], 'iat': now, 'exp': now + token['expires_in'], 'auth_time': auth_time, } if nonce: payload['nonce'] = nonce # calculate at_hash alg = config.get('jwt_alg', 'HS256') access_token = token.get('access_token') if access_token: at_hash = to_unicode(create_half_hash(access_token, alg)) payload['at_hash'] = at_hash # calculate c_hash if code: payload['c_hash'] = to_unicode(create_half_hash(code, alg)) payload.update(user_info) jwt = JWT(algorithms=alg) header = {'alg': alg} key = config['jwt_key'] id_token = jwt.encode(header, payload, key) return to_unicode(id_token)
def dumps_public_key(self, public_key=None): if public_key is None: public_key = self.public_key x_bytes = public_key.public_bytes(Encoding.Raw, PublicFormat.Raw) return { 'crv': self.get_key_curve(public_key), 'x': to_unicode(urlsafe_b64encode(x_bytes)), }
def decode_payload(bytes_payload): try: payload = json.loads(to_unicode(bytes_payload)) except ValueError: raise DecodeError('Invalid payload value') if not isinstance(payload, dict): raise DecodeError('Invalid payload type') return payload
def extract_basic_authorization(token): """Extract token from Basic Authorization.""" try: query = to_unicode(base64.b64decode(token)) except TypeError: return None, None if ':' in query: return query.split(':', 1) return query, None
def config_app(self): jwt_key_path = get_file_path('rsa_private.pem') with open(jwt_key_path, 'r') as f: jwt_key = to_unicode(f.read()) DUMMY_JWT_CONFIG.update({ 'iss': 'Authlib', 'key': jwt_key, 'alg': 'RS256', })
def _create_oauth1_request(): if _req.method == 'POST': body = _req.form.to_dict(flat=True) else: body = None url = _req.base_url if _req.query_string: url = url + '?' + to_unicode(_req.query_string) return OAuth1Request(_req.method, url, body, _req.headers)
def serialize_json(self, header, payload, key): """Generate a JWS JSON Serialization. The JWS JSON Serialization represents digitally signed or MACed content as a JSON object, per `Section 7.2`_. :param header: A dict/list of header :param payload: A string/dict of payload :param key: Private key used to generate signature :return: dict Example header of JWS JSON Serialization:: { "protected: {"alg": "HS256"}, "header": {"kid": "jose"} } Pass a dict to generate flattened JSON Serialization, pass a list of header dict to generate standard JSON Serialization. """ payload_segment = _b64encode_json(payload) def _sign(h): protected, _, signature = self._sign_signature( h['protected'], payload, key, payload_segment) rv = { 'protected': to_unicode(protected), 'signature': to_unicode(signature) } if 'header' in header: rv['header'] = h['header'] return rv if isinstance(header, dict): data = _sign(header) data['payload'] = to_unicode(payload_segment) return data signatures = [_sign(h) for h in header] return { 'payload': to_unicode(payload_segment), 'signatures': signatures }
def create_basic_header(username, password): """ Create an authorization header from the username and password according to RFC 2617 (https://tools.ietf.org/html/rfc2617). Use this to send client credentials in the authorization header. """ text = "{}:{}".format(username, password) auth = to_unicode(base64.b64encode(to_bytes(text))) return {"Authorization": "Basic " + auth}
def extract_payload(self, payload_segment): """Extract payload into JSON dict format.""" bytes_payload = super(JWT, self).extract_payload(payload_segment) try: payload = json.loads(to_unicode(bytes_payload)) except ValueError: raise DecodeError('Invalid payload value') if not isinstance(payload, dict): raise DecodeError('Invalid payload type') return payload
def _ensure_dict(s): if not isinstance(s, dict): try: s = json.loads(to_unicode(s)) except (ValueError, TypeError): raise DecodeError('Invalid JWS') if not isinstance(s, dict): raise DecodeError('Invalid JWS') return s
def generate_signed_refresh_token(kid, private_key, user, expires_in, scopes, iss=None, client_id=None): """ Generate a JWT refresh token and output a UTF-8 string of the encoded JWT signed with the private key. Args: kid (str): key id of the keypair used to generate token private_key (str): RSA private key to sign and encode the JWT with user (fence.models.User): User to generate token for expires_in (int): seconds until expiration scopes (List[str]): oauth scopes for user Return: str: encoded JWT refresh token signed with ``private_key`` """ headers = {"kid": kid} iat, exp = issued_and_expiration_times(expires_in) jti = str(uuid.uuid4()) sub = str(user.id) if not iss: try: iss = config.get("BASE_URL") except RuntimeError: raise ValueError("must provide value for `iss` (issuer) field if" " running outside of flask application") claims = { "pur": "refresh", "sub": sub, "iss": iss, "aud": [iss], "iat": iat, "exp": exp, "jti": jti, "azp": client_id or "", "scope": scopes, } if client_id: claims["aud"].append(client_id) logger.info("issuing JWT refresh token with id [{}] to [{}]".format( jti, sub)) logger.debug(f"issuing JWT refresh token: {claims}") token = jwt.encode(claims, private_key, headers=headers, algorithm="RS256") token = to_unicode(token, "UTF-8") return JWTResult(token=token, kid=kid, claims=claims)
def ensure_dict(s, structure_name): if not isinstance(s, dict): try: s = json_loads(to_unicode(s)) except (ValueError, TypeError): raise DecodeError('Invalid {}'.format(structure_name)) if not isinstance(s, dict): raise DecodeError('Invalid {}'.format(structure_name)) return s
def test_plaintext_signature(self): self.prepare_data() handle = self.create_route() url = '/user' # case 1: success auth_header = ('OAuth oauth_consumer_key="client",' 'oauth_signature_method="PLAINTEXT",' 'oauth_token="valid-token",' 'oauth_signature="secret&valid-token-secret"') request = self.factory.get(url, HTTP_AUTHORIZATION=auth_header) resp = handle(request) data = json.loads(to_unicode(resp.content)) self.assertIn('username', data) # case 2: invalid signature auth_header = auth_header.replace('valid-token-secret', 'invalid') request = self.factory.get(url, HTTP_AUTHORIZATION=auth_header) resp = handle(request) data = json.loads(to_unicode(resp.content)) self.assertEqual(data['error'], 'invalid_signature')
def prepare_grant_uri(uri, client_id, response_type, redirect_uri=None, scope=None, state=None, **kwargs): """Prepare the authorization grant request URI. The client constructs the request URI by adding the following parameters to the query component of the authorization endpoint URI using the ``application/x-www-form-urlencoded`` format: :param uri: The authorize endpoint to fetch "code" or "token". :param client_id: The client identifier as described in `Section 2.2`_. :param response_type: To indicate which OAuth 2 grant/flow is required, "code" and "token". :param redirect_uri: The client provided URI to redirect back to after authorization as described in `Section 3.1.2`_. :param scope: The scope of the access request as described by `Section 3.3`_. :param state: 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`_. :param kwargs: Extra arguments to embed in the grant/authorization URL. An example of an authorization code grant authorization URL:: /authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb .. _`Section 2.2`: http://tools.ietf.org/html/rfc6749#section-2.2 .. _`Section 3.1.2`: http://tools.ietf.org/html/rfc6749#section-3.1.2 .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3 .. _`section 10.12`: http://tools.ietf.org/html/rfc6749#section-10.12 """ params = [('response_type', response_type), ('client_id', client_id)] if redirect_uri: params.append(('redirect_uri', redirect_uri)) if scope: params.append(('scope', list_to_scope(scope))) if state: params.append(('state', state)) for k in kwargs: if kwargs[k]: params.append((to_unicode(k), kwargs[k])) return add_params_to_uri(uri, params)
def thumbprint(self): """Implementation of RFC7638 JSON Web Key (JWK) Thumbprint.""" fields = list(self.REQUIRED_JSON_FIELDS) fields.append('kty') fields.sort() data = OrderedDict() for k in fields: data[k] = self.tokens[k] json_data = json_dumps(data) digest_data = hashlib.sha256(to_bytes(json_data)).digest() return to_unicode(urlsafe_b64encode(digest_data))
def generate_signed_access_token( kid, private_key, user, expires_in, scopes, forced_exp_time=None): """ Generate a JWT access token and output a UTF-8 string of the encoded JWT signed with the private key. Args: kid (str): key id of the keypair used to generate token private_key (str): RSA private key to sign and encode the JWT with user (fence.models.User): User to generate ID token for expires_in (int): seconds until expiration scopes (List[str]): oauth scopes for user Return: str: encoded JWT access token signed with ``private_key`` """ headers = {'kid': kid} iat, exp = issued_and_expiration_times(expires_in) # force exp time if provided exp = forced_exp_time or exp sub = str(user.id) jti = str(uuid.uuid4()) claims = { 'pur': 'access', 'aud': scopes, 'sub': sub, 'iss': flask.current_app.config.get('BASE_URL'), 'iat': iat, 'exp': exp, 'jti': jti, 'context': { 'user': { 'name': user.username, 'is_admin': user.is_admin, 'projects': dict(user.project_access), }, }, } flask.current_app.logger.info( 'issuing JWT access token with id [{}] to [{}]'.format(jti, sub) ) flask.current_app.logger.debug( 'issuing JWT access token\n' + json.dumps(claims, indent=4) ) token = jwt.encode(claims, private_key, headers=headers, algorithm='RS256') flask.current_app.logger.debug(str(token)) token = to_unicode(token, 'UTF-8') return token
def setQueryArguments(self, **kwargs): """Set query arguments""" for k in kwargs: # Quote value before add it to request query value = ( "+".join([quote(str(v)) for v in kwargs[k]]) if isinstance(kwargs[k], list) else quote(str(kwargs[k])) ) # Remove argument from uri query = re.sub(r"&{argument}(=[^&]*)?|^{argument}(=[^&]*)?&?".format(argument=k), "", self.query) # Add new one if query: query += "&" query += "%s=%s" % (k, value) # Re-init class self.__init__(self.method, to_unicode(self.path + "?" + query))