async def validate_userinfo_params(request: Request) -> Dict[str, Any]: provider = get_provider(request) if request.method == "POST": req_dict = request.form else: req_dict = request.args access_token = None if "authorization" in request.headers: hdr = request.headers["authorization"] if "Bearer" in hdr: access_token = hdr.split()[-1] else: raise NotImplementedError(hdr) elif "access_token" in req_dict: access_token = req_dict.get("access_token") if not access_token: raise TokenError("invalid_grant") token = await provider.tokens.get_token_by_access_token(access_token) if not token: raise TokenError("invalid_grant") result = { "token": token, } return result
async def well_known_finger_handler( request: sanic.request.Request) -> sanic.response.BaseHTTPResponse: provider = get_provider(request) scheme = get_scheme(request) resource = request.args.get("resource") rel = request.args.get("rel") finger_url = request.app.url_for("well_known_finger_handler", _scheme=scheme, _external=True, _server=request.host) issuer = "{0}://{1}".format(scheme, request.host) logger.info("finger for resource: {0} rel: {1}".format(resource, rel)) try: resp = provider.handle_finger(resource, rel, issuer, finger_url) return sanic.response.json( resp, content_type="application/jrd+json", headers={"Access-Control-Allow-Origin": "*"}) except Exception as err: logger.exception("Caught error whilst handling finger url", exc_info=err) return sanic.response.HTTPResponse(status=500)
async def create_refresh_response_dic( request: sanic.request.Request, params: Dict[str, Any]) -> Dict[str, Any]: provider = get_provider(request) # See https://tools.ietf.org/html/rfc6749#section-6 scope_param = params["scope"] scope = scope_param if scope_param else params["token_obj"]["scope"] unauthorized_scopes = set(scope) - set(params["token_obj"]["scope"]) if unauthorized_scopes: raise TokenError("invalid_scope") user = await provider.users.get_user_by_username( params["token_obj"]["user"]) client = params["client"] token = provider.tokens.create_token( user=user, client=client, scope=scope, auth_time=params["token_obj"]["auth_time"], specific_claims=params["specific_claims"], expire_delta=provider.token_expire_time, ) scheme = get_scheme(request) issuer = "{0}://{1}".format(scheme, request.host) # If the Token has an id_token it's an Authentication request. if params["token_obj"]["id_token"]: id_token_dic = provider.tokens.create_id_token( user=user, auth_time=token["auth_time"], issuer=issuer, client=client, nonce=None, expire_delta=provider.token_expire_time, at_hash=token["at_hash"], scope=token["scope"], specific_claims=token["specific_claims"], ) else: id_token_dic = {} token["id_token"] = id_token_dic await provider.tokens.save_token(token) await provider.tokens.delete_token_by_access_token( params["token_obj"]["access_token"]) id_token = await client.sign(id_token_dic, jwk_set=provider.jwk_set) dic = { "access_token": token["access_token"], "refresh_token": token["refresh_token"], "token_type": "bearer", "expires_in": provider.token_expire_time, "id_token": id_token, } return dic
async def create_code_response_dic(request: sanic.request.Request, params: Dict[str, Any]) -> Dict[str, Any]: provider = get_provider(request) # See https://tools.ietf.org/html/rfc6749#section-4.1 user = await provider.users.get_user_by_username(params["code_obj"]["user"] ) client = params["client"] token = provider.tokens.create_token( user=user, client=client, auth_time=params["code_obj"]["auth_time"], scope=params["code_obj"]["scope"], expire_delta=provider.token_expire_time, specific_claims=params["code_obj"]["specific_claims"], code=params["code"], ) scheme = get_scheme(request) issuer = "{0}://{1}".format(scheme, request.host) id_token_dic = provider.tokens.create_id_token( user=user, auth_time=token["auth_time"], client=client, issuer=issuer, expire_delta=provider.token_expire_time, nonce=params["code_obj"]["nonce"], at_hash=token["at_hash"], scope=token["scope"], specific_claims=token["specific_claims"], ) token["id_token"] = id_token_dic await provider.tokens.save_token(token) await provider.codes.mark_used_by_id(params["code"]) id_token = await client.sign(id_token_dic, jwk_set=provider.jwk_set) dic = { "access_token": token["access_token"], "refresh_token": token["refresh_token"], "token_type": "bearer", "expires_in": provider.token_expire_time, "id_token": id_token, } return dic
async def jwk_handler( request: sanic.request.Request) -> sanic.response.BaseHTTPResponse: headers = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, OPTIONS" } if request.method == "OPTIONS": return sanic.response.HTTPResponse(headers=headers) provider = get_provider(request) keys = [] for key in provider.jwk_set: keys.append(key._public_params()) # so we dont get json strings async for client in provider.clients.all(): for key in client.jwk: keys.append(key._public_params()) # so we dont get json strings return sanic.response.json({"keys": keys})
async def client_register_handler( request: sanic.request.Request) -> sanic.response.BaseHTTPResponse: provider = get_provider(request) scheme = get_scheme(request) if "client_id" in request.args: # Client Read, check auth header try: token = request.headers["Authorization"].split("Bearer ")[-1] client = await provider.clients.get_client_by_access_token(token) except Exception: return sanic.response.text( body="", status=403, headers={"WWW-Authenticate": 'Bearer error="invalid_token"'}) result = { "client_id": client.id, "client_secret": client.secret, "client_secret_expires_at": client.expires_at, "subject_type": client.type, "application_type": client.application_type, "response_types": client.response_types, "redirect_uris": client.callback_urls, "grant_types": client.grant_types, "contacts": client.contacts, "jwks_uri": client.jwks_url, "post_logout_redirect_uris": client.post_logout_redirect_urls, "request_uris": client.request_urls, # 'registration_client_uri': request.app.url_for('client_register_handler', _scheme=scheme, # _external=True, _server=request.host, # client_id=client_id), # 'registration_access_token': client.access_token, } if client.sector_identifier_uri: result["sector_identifier_uri"] = client.sector_identifier_uri if client.jwt_algo: result["id_token_signed_response_alg"] = client.jwt_algo if client.userinfo_signed_response_alg: result[ "userinfo_signed_response_alg"] = client.userinfo_signed_response_alg status = 201 else: if not provider.open_client_registration and not await provider.clients.auth_client_registration( request): return sanic.response.HTTPResponse(status=403) if not request.json or "redirect_uris" not in request.json: logger.warning("Did not provide any JSON or redirect_uris") result = { "error": "invalid_client_metadata", "error_description": "Invalid metadata" } return sanic.response.json(result, status=400) client_id = uuid.uuid4().hex[:16] client_name = request.json.get("client_name", client_id) client_secret = uuid.uuid4().hex client_secret_expires_at = 1577858400 # 1st jan 2020 application_type = request.json.get("application_type") response_types = request.json.get("response_types", frozenset(["code"])) scopes = request.json.get("scope", ["openid"]) redirect_uris = request.json.get("redirect_uris", []) grant_types = request.json.get("grant_types") contacts = request.json.get("contacts") jwks_uri = request.json.get("jwks_uri") jwks = request.json.get("jwks") post_logout_redirect_uris = request.json.get( "post_logout_redirect_uris") request_uris = request.json.get("request_uris") prompt = request.json.get("prompt", frozenset(["none", "login", "consent"])) sector_identifier_uri = request.json.get("sector_identifier_uri") subject_type = request.json.get("subject_type", "public") logo_uri = request.json.get("logo_uri") policy_uri = request.json.get("policy_uri") tos_uri = request.json.get("tos_uri") if isinstance(scopes, str): scopes = set(scopes.split()) scopes.add("openid") # TODO request_object_signing_alg # require_consent = request.json.get("require_consent") is True reuse_consent = request.json.get("reuse_consent") is True id_token_signed_response_alg = request.json.get( "id_token_signed_response_alg") userinfo_signed_response_alg = request.json.get( "userinfo_signed_response_alg") userinfo_encrypted_response_alg = request.json.get( "userinfo_encrypted_response_alg") userinfo_encrypted_response_enc = request.json.get( "userinfo_encrypted_response_enc") for url in redirect_uris: if "#" in url: # NO BAD, shouldnt have fragments in url result = { "error": "invalid_redirect_uri", "error_description": "Bad redirect uri {0}".format(url) } return sanic.response.json(result, status=400) # Validate sector_identifier_uri, must contain a superset of redirect_uris if sector_identifier_uri: try: async with aiohttp.ClientSession() as session: logger.info("Getting Sector Identifier URI {0}".format( sector_identifier_uri)) async with session.get(sector_identifier_uri) as resp: sector_json = await resp.json() if not isinstance(sector_json, list): raise Exception( "sector identifier json is not a list") invalid_uris = set(redirect_uris) - set(sector_json) if invalid_uris: raise Exception( "Invalid redirect uris: {0}".format( invalid_uris)) except Exception as err: logger.warning( "Failed to get sector identifier uri: {0}".format(err)) result = { "error": "invalid_client_metadata", "error_description": "Failed to validate sector identifier uri, {0}".format( err), } return sanic.response.json(result, status=400) success, data = await provider.clients.add_client( id_=client_id, name=client_name, type_=subject_type, secret=client_secret, scopes=scopes, callback_urls=redirect_uris, require_consent=require_consent, reuse_consent=reuse_consent, response_types=response_types, application_type=application_type, contacts=contacts, expires_at=client_secret_expires_at, grant_types=grant_types, jwks_url=jwks_uri, jwt_algo=id_token_signed_response_alg, prompts=prompt, post_logout_redirect_urls=post_logout_redirect_uris, request_urls=request_uris, sector_identifier_uri=sector_identifier_uri, userinfo_signed_response_alg=userinfo_signed_response_alg, userinfo_encrypted_response_alg=userinfo_encrypted_response_alg, userinfo_encrypted_response_enc=userinfo_encrypted_response_enc, logo_uri=logo_uri, policy_uri=policy_uri, tos_uri=tos_uri, jwks=jwks, ) if success: client: Client = data result = { "client_id": client_id, "client_secret": client_secret, "client_secret_expires_at": client_secret_expires_at, "subject_type": "confidential", "application_type": application_type, "response_types": response_types, "redirect_uris": redirect_uris, "grant_types": grant_types, "contacts": contacts, "jwks_uri": jwks_uri, "post_logout_redirect_uris": post_logout_redirect_uris, "request_uris": request_uris, "registration_client_uri": request.app.url_for("client_register_handler", _scheme=scheme, _external=True, _server=request.host, client_id=client_id), "registration_access_token": client.access_token, # 'token_endpoint_auth_method': 'client_secret_basic' } if sector_identifier_uri: result["sector_identifier_uri"] = sector_identifier_uri if id_token_signed_response_alg: result[ "id_token_signed_response_alg"] = id_token_signed_response_alg if logo_uri: result["logo_uri"] = logo_uri if tos_uri: result["tos_uri"] = tos_uri if policy_uri: result["policy_uri"] = policy_uri status = 201 else: result = { "error": "invalid_client_metadata", "error_description": data } status = 500 return sanic.response.json(result, headers={ "Cache-Control": "no-store", "Pragma": "no-cache" }, status=status)
async def userinfo_handler( request: sanic.request.Request) -> sanic.response.BaseHTTPResponse: headers = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Headers": "Authorization", "Access-Control-Allow-Methods": "GET, POST, OPTIONS", } if request.method == "OPTIONS": return sanic.response.HTTPResponse(headers=headers) provider = get_provider(request) try: params = await validate_userinfo_params(request) token = params["token"] client = await provider.clients.get_client_by_id(token["client"]) if token.get("specific_claims", {}) is None: specific_claims = {} else: specific_claims = token["specific_claims"] specific_claims = specific_claims.get("userinfo", {}).keys() claims = await provider.users.get_claims_for_user_by_scope( token["user"], token["scope"], specific_claims) result = {"sub": token["user"]} result.update(claims) if client.userinfo_signed_response_alg: # Sign response result = await client.jws_sign( result, algo=client.userinfo_signed_response_alg, jwk_set=provider.jwk_set) if client.userinfo_encrypted_response_alg: # Encrypt response result = await client.jws_encrypt( result, alg=client.userinfo_encrypted_response_alg, enc=client.userinfo_encrypted_response_enc, jwk_set=None, ) if isinstance(result, str): headers.update({ "Cache-Control": "no-store", "Pragma": "no-cache", "Content-Type": "application/jwt" }) # If we no longer have plain json, its most likely a JWT of sorts return sanic.response.HTTPResponse(body=result, headers=headers) else: headers.update({"Cache-Control": "no-store", "Pragma": "no-cache"}) return sanic.response.json(result, headers=headers) except TokenError as error: return sanic.response.json(error.create_dict(), status=400, headers=headers)
async def create_authorize_response_params( request: sanic.request.Request, params: Dict[str, Any], user: Dict[str, Any]) -> Tuple[dict, dict]: provider = get_provider(request) client = params["client"] uri = urlsplit(params["redirect_uri"]) query_params = parse_qs(uri.query) query_fragment = {} try: if params["grant_type"] in ("authorization_code", "hybrid"): code = await provider.codes.create_code( client=client, user=user, scopes=params["scopes"], code_expire=int(provider.code_expire_time), nonce=params["nonce"], code_challenge=params["code_challenge"], code_challenge_method=params["code_challenge_method"], specific_claims=params["specific_claims"], ) if params["grant_type"] == "authorization_code": # noinspection PyUnboundLocalVariable query_params["code"] = code["code"] query_params["state"] = params["state"] elif params["grant_type"] in ["implicit", "hybrid"]: token = provider.tokens.create_token( user=user, client=client, auth_time=user["auth_time"], scope=params["scopes"], expire_delta=provider.token_expire_time, specific_claims=params["specific_claims"], ) # Check if response_type must include access_token in the response. if 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 "openid" in params["scopes"]: scheme = get_scheme(request) issuer = "{0}://{1}".format(scheme, request.host) kwargs = { "auth_time": token["auth_time"], "user": user, "client": client, "issuer": issuer, "expire_delta": provider.token_expire_time, "nonce": params["nonce"], "at_hash": token["at_hash"], "scope": params["scopes"], "specific_claims": params["specific_claims"], } # Include at_hash when access_token is being returned. if "access_token" in query_fragment: kwargs["at_hash"] = token["at_hash"] id_token_dic = provider.tokens.create_id_token(**kwargs) # Check if response_type must include id_token in the response. if params["response_type"] in ("id_token", "id_token token", "code id_token", "code id_token token"): query_fragment["id_token"] = await client.sign( id_token_dic, jwk_set=provider.jwk_set) else: id_token_dic = {} # Store the token. token["id_token"] = id_token_dic await provider.tokens.save_token(token) # Code parameter must be present if it's Hybrid Flow. if params["grant_type"] == "hybrid": # noinspection PyUnboundLocalVariable query_fragment["code"] = code["code"] query_fragment["token_type"] = "bearer" query_fragment["expires_in"] = provider.token_expire_time query_fragment["state"] = params["state"] except Exception as err: logger.exception("Failed whilst creating authorize response", exc_info=err) raise AuthorizeError(params["redirect_uri"], "server_error", params["grant_type"]) query_params = {key: value for key, value in query_params.items() if value} query_fragment = { key: value for key, value in query_fragment.items() if value } return query_params, query_fragment
async def authorize_handler( request: sanic.request.Request) -> sanic.response.BaseHTTPResponse: provider = get_provider(request) # TODO split out based on response_type try: params = await validate_authorize_params(request, provider) if await provider.users.is_authenticated(request): user = await provider.users.get_user(request) if "login" in params["prompt"]: if "none" in params["prompt"]: # If login and none in prompt arg logger.warning("login prompt along with none prompt") raise AuthorizeError(params["redirect_uri"], "login_required", params["grant_type"]) else: # If login is in prompt arg request.ctx.session.clear() next_page = strip_prompt_login(get_request_url(request)) return redirect( request.app.url_for(provider.login_function_name, next=next_page)) if "select_account" in params["prompt"]: if "none" in params["prompt"]: logger.warning( "select_account prompt along with none prompt") raise AuthorizeError(params["redirect_uri"], "account_selection_required", params["grant_type"]) else: request.ctx.session.clear() return redirect( request.app.url_for(provider.login_function_name, next=get_request_url(request))) if { "none", "consent" } <= params["prompt"]: # Tests if both none and consent in prompt logger.warning("consent prompt along with none prompt") raise AuthorizeError(params["redirect_uri"], "consent_required", params["grant_type"]) implicit_flow_resp_types = {"id_token", "id_token token"} allow_skipping_consent = ( params["client"].type in ("public", "pairwise") or params["response_type"] in implicit_flow_resp_types) if not params[ "client"].require_consent and allow_skipping_consent and "consent" not in params[ "prompt"]: # If you dont require consent, and consent is allowed to be skipped # (aka type=confidential), and consent hasn't been requested query_params, query_fragment = await create_authorize_response_params( request, params, user) if params["response_mode"] == "form_post": # We've been requested to auto-post form data logger.info( "skipped consent, doing form-autosubmit for {0}". format(params["client"].name)) return await request.app.extensions["jinja2"].render_async( provider.autosubmit_html, request, form_url=params["redirect_uri"], query_params=query_params, query_fragment=query_fragment, ) else: # Standard 302 redirect logger.info( "skipped consent, doing 302 redirect for {0}".format( params["client"].name)) return redirect( create_authorize_response_uri(params["redirect_uri"], query_params, query_fragment)) # We require consent if params["client"].reuse_consent: # Allowing prior consent to be reused if user["consent"] and allow_skipping_consent and "consent" not in params[ "prompt"]: # If user has already given consent, and consent not requested query_params, query_fragment = await create_authorize_response_params( request, params, user) if params["response_mode"] == "form_post": # We've been requested to auto-post form data logger.info( "reusing consent, doing form-autosubmit for {0}". format(params["client"].name)) return await request.app.extensions[ "jinja2"].render_async( provider.autosubmit_html, request, form_url=params["redirect_uri"], query_params=query_params, query_fragment=query_fragment, ) else: # Standard 302 redirect logger.info( "reusing consent, doing 302 redirect for {0}". format(params["client"].name)) return redirect( create_authorize_response_uri( params["redirect_uri"], query_params, query_fragment)) if "none" in params["prompt"]: # Return consent_required logger.info("none prompt, giving up") raise AuthorizeError(params["redirect_uri"], "consent_required", params["grant_type"]) # Generate hidden inputs for the form. hidden_params = { "client_id": params["client"].id, "redirect_uri": params["redirect_uri"], "grant_type": params["grant_type"], "response_type": params["response_type"], "scope": params["scopes"], "state": params["state"], "nonce": params["nonce"], "prompt": " ".join(list(params["prompt"])), } if params["code_challenge"]: hidden_params["code_challenge"] = params["code_challenge"] hidden_params["code_challenge_method"] = params[ "code_challenge_method"] if params["response_mode"]: hidden_params["response_mode"] = params["response_mode"] if params["max_age"]: hidden_params["max_age"] = params["max_age"] hidden_inputs = await request.app.extensions[ "jinja2"].render_string_async(provider.hidden_inputs_html, request, params=hidden_params) # Remove `openid` from scope list since we don't need to print it. try: params["scopes"].remove("openid") except ValueError: pass context = { "client_name": params["client"].name, "form_url": request.path, "hidden_inputs": hidden_inputs, "scopes": params["scopes"], } # Show authorize html page #TODO allow it to be customised logger.info("showing consent for {0}".format( params["client"].name)) return await request.app.extensions["jinja2"].render_async( provider.authorize_html, request, **context) else: # Not logged in if "none" in params["prompt"]: # Cant prompt, raise error logger.warning("Not logged in, prompt=none, error") raise AuthorizeError(params["redirect_uri"], "login_required", params["grant_type"]) logger.warning("Not logged in, redirecting to login page") if "login" in params["prompt"]: # Can prompt, redirect them to login page next_page = strip_prompt_login(get_request_url(request)) return redirect( request.app.url_for(provider.login_function_name, next=next_page)) # Nothing in prompt, so default to redirecting to login page return redirect( request.app.url_for(provider.login_function_name, next=get_request_url(request))) except (ClientIdError, RedirectUriError) as err: context = {"error": err.error, "description": err.description} return await request.app.extensions["jinja2"].render_async( provider.error_html, request, **context) except AuthorizeError as err: if request.method == "POST": req_dict = request.form else: req_dict = request.args state = req_dict.get("state") response_mode = req_dict.get("response_mode") if response_mode == "form_post": return await request.app.extensions["jinja2"].render_async( provider.autosubmit_html, request, form_url=err.redirect_uri, query_params={ "error": err.error, "error_description": err.description }, query_fragment={}, ) else: uri = err.create_uri(err.redirect_uri, state) return redirect(uri)
async def validate_token_params( request: sanic.request.Request) -> Dict[str, Any]: provider = get_provider(request) if request.method == "POST": req_dict = request.form else: req_dict = request.args client_assertion_type = req_dict.get("client_assertion_type") client_assertion = req_dict.get("client_assertion") if client_assertion_type and client_assertion_type == "urn:ietf:params:oauth:client-assertion-type:jwt-bearer": header = jwt.get_unverified_header(client_assertion) audience = "{0}://{1}{2}".format(get_scheme(request), request.host, request.path) # TODO maintain central collection of client jwts if "kid" in header: # Asymetric signing temp_jwt_token = jwt.decode(client_assertion, verify=False) client = await provider.clients.get_client_by_id( temp_jwt_token["sub"]) jwt_key = client.jwk.get_key(header.get("kid")) try: jwt.decode(client_assertion, jwt_key.export_to_pem(), algorithms=[header["alg"]], audience=audience) # By it not erroring, its successfully verified except Exception as err: logger.exception("Invalid key id", exc_info=err) raise TokenError("invalid_client") else: # HMAC with client secret temp_jwt_token = jwt.decode(client_assertion, verify=False) client = await provider.clients.get_client_by_id( temp_jwt_token["sub"]) try: jwt.decode(client_assertion, client.secret, algorithms=[header["alg"]], audience=audience) # By it not erroring, its successfully verified except Exception as err: logger.exception("Invalid key id", exc_info=err) raise TokenError("invalid_client") else: client_id = req_dict.get("client_id") client_secret = req_dict.get("client_secret", "") if "authorization" in request.headers and not client_id: hdr = request.headers["authorization"] if "Basic" in hdr: client_id, client_secret = base64.b64decode( hdr.split()[-1].encode()).decode().split(":") else: raise NotImplementedError(hdr) client = await provider.clients.get_client_by_id(client_id) if not client_id: raise TokenError("invalid_client") if not client: raise TokenError("invalid_client") specific_claims = req_dict.get("claims") if specific_claims: try: specific_claims = json.loads(specific_claims) except Exception as err: logger.exception("Failed to decode specific claims", exc_info=err) result = { "client": client, "grant_type": req_dict.get("grant_type", ""), "code": req_dict.get("code", ""), "state": req_dict.get("state", ""), "scope": req_dict.get("scope", ""), "redirect_uri": req_dict.get("redirect_uri", ""), "refresh_token": req_dict.get("refresh_token", ""), "code_verifier": req_dict.get("code_verifier"), "username": req_dict.get("username", ""), "password": req_dict.get("password", ""), "specific_claims": specific_claims, } # if client.type == 'confidential' and client_secret != client.secret: # raise TokenError('invalid_client') if result["grant_type"] == "authorization_code": if result["redirect_uri"] not in client.callback_urls: raise TokenError("invalid_client") code = await provider.codes.get_by_id(result["code"]) if not code: raise TokenError("invalid_grant") if code["used"]: await provider.tokens.delete_token_by_code(result["code"]) raise TokenError("invalid_grant") if code["client"] != client.id: raise TokenError("invalid_grant") if result["code_verifier"]: if code["code_challenge_method"] == "S256": new_code_challenge = (base64.urlsafe_b64encode( hashlib.sha256(result["code_verifier"].encode( "ascii")).digest()).decode("utf-8").replace("=", "")) else: new_code_challenge = result["code_verifier"] if new_code_challenge != code["code_challenge"]: raise TokenError("invalid_grant") result["code_obj"] = code elif result["grant_type"] == "password": if not provider.allow_grant_type_password: raise TokenError("unsupported_grant_type") # TODO authenticate username/password # result['username'] result['password'] user = False if not user: raise UserAuthError() result["user_obj"] = user elif result["grant_type"] == "client_credentials": # TODO not sure about this raise NotImplementedError() elif result["grant_type"] == "refresh_token": if not result["refresh_token"]: logger.warning("No refresh token") raise TokenError("invalid_grant") token = await provider.tokens.get_token_by_refresh_token( result["refresh_token"]) if not token: raise TokenError("invalid_grant") result["token_obj"] = token return result