def get_request_url(request: sanic.request.Request) -> str: url = request.url scheme = get_scheme(request) if not url.startswith(scheme + ":"): url = scheme + ":" + url.split(":", 1)[-1] return url
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)
def get_callback_url(request: sanic.request): scheme = get_scheme(request) callback_url = list( urllib.parse.urlparse(request.app.url_for("handle_callback"))) callback_url[0] = scheme callback_url[1] = request.host return urllib.parse.urlunparse(callback_url)
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 well_known_openid_config_handler( request: sanic.request.Request) -> sanic.response.BaseHTTPResponse: scheme = get_scheme(request) response = { "issuer": "{0}://{1}".format(scheme, request.host), "authorization_endpoint": request.app.url_for("authorize_handler", _scheme=scheme, _external=True, _server=request.host), "token_endpoint": request.app.url_for("token_handler", _scheme=scheme, _external=True, _server=request.host), "userinfo_endpoint": request.app.url_for("userinfo_handler", _scheme=scheme, _external=True, _server=request.host), "jwks_uri": request.app.url_for("jwk_handler", _scheme=scheme, _external=True, _server=request.host), "registration_endpoint": request.app.url_for("client_register_handler", _scheme=scheme, _external=True, _server=request.host), "login_hint": "N/A", # TODO code_challenge_methods_supported "request_parameter_supported": True, "response_types_supported": [ "code", "id_token", "id_token token", "code token", "code id_token", "code id_token token", ], "id_token_signing_alg_values_supported": ["HS256", "RS256", "ES256"], "subject_types_supported": ["public", "pairwise"], # or pairwise "token_endpoint_auth_methods_supported": [ "client_secret_post", "client_secret_basic", "private_key_jwt", "client_secret_jwt", ], "claims_supported": [ "name", "family_name", "given_name", "middle_name", "nickname", "preferred_username", "profile", "picture", "website", "gender", "birthdate", "zoneinfo", "locale", "updated_at", "email", "email_verified", "address", "phone_number", "phone_number_verified", ], "grant_types_supported": [ "authorization_code", "implicit", "refresh_token", "password", "client_credentials" ], "scopes_supported": list(SCOPES.keys()), } return sanic.response.json(response, headers={"Access-Control-Allow-Origin": "*"})
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 well_known_oauth_config_handler( request: sanic.request.Request) -> sanic.response.BaseHTTPResponse: scheme = get_scheme(request) response = { "issuer": "{0}://{1}".format(scheme, request.host), "authorization_endpoint": request.app.url_for("authorize_handler", _scheme=scheme, _external=True, _server=request.host), "token_endpoint": request.app.url_for("token_handler", _scheme=scheme, _external=True, _server=request.host), "jwks_uri": request.app.url_for("jwk_handler", _scheme=scheme, _external=True, _server=request.host), "registration_endpoint": request.app.url_for("client_register_handler", _scheme=scheme, _external=True, _server=request.host), "scopes_supported": list(SCOPES.keys()), "response_types_supported": [ "code", "id_token", "id_token token", "code token", "code id_token", "code id_token token", ], "grant_types_supported": [ "authorization_code", "implicit", "refresh_token", "password", "client_credentials" ], "token_endpoint_auth_methods_supported": [ "client_secret_post", "client_secret_basic", "private_key_jwt", "client_secret_jwt", "none", ], "token_endpoint_auth_signing_alg_values_supported": ["HS256", "RS256", "ES256"], "introspection_endpoint": request.app.url_for("introspection_handler", _scheme=scheme, _external=True, _server=request.host), "introspection_endpoint_auth_methods_supported": [ "client_secret_post", "client_secret_basic", "private_key_jwt", "client_secret_jwt", "none", ], "code_challenge_methods_supported": ["S256"], } return sanic.response.json(response, headers={"Access-Control-Allow-Origin": "*"})
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 decorated_function( request: sanic.request, *args, **kwargs) -> sanic.response.BaseHTTPResponse: if "user" in request.ctx.session: if request.ctx.session["user"][ "expires_at"] > datetime.datetime.now().timestamp( ): response = await f(request, *args, **kwargs) return response # Try refreshing access token to avoid a redirect if request.ctx.session["user"]["refresh_token"]: try: await self.post_token_endpoint( request=request, refresh_token=request.ctx.session["user"] ["refresh_token"]) logger.debug("Successfully refreshed tokens") except Exception: pass # We've tried, we encountered an error, redirect the user else: # Success, now process the response response = await f(request, *args, **kwargs) return response del request.ctx.session[ "user"] # User has expired, remove data current_url = list(urllib.parse.urlparse(request.url)) current_url[0] = get_scheme(request) current_url = urllib.parse.urlunparse(current_url) state = str(uuid.uuid4()) nonce = str(uuid.uuid4()) request.ctx.session["oicp_state"] = state request.ctx.session["oicp_redirect"] = current_url request.ctx.session["oicp_nonce"] = nonce params = { "scope": self.string_scopes, "response_type": "code", "client_id": self.id, "redirect_uri": self.get_callback_url(request), "state": state, "nonce": nonce, } if not self.authorize_url: if self.autodiscover_url: success = await self.autodiscover_settings() if not success: return sanic.response.text( "SSO client library failed to autodiscover settings" ) else: # Settings not passed during setup. return sanic.response.text( "SSO client library not setup correctly, authorize_url not provided" ) redirect_url = list(urllib.parse.urlparse(self.authorize_url)) redirect_url[4] = urllib.parse.urlencode(params) redirect_url = urllib.parse.urlunparse(redirect_url) return redirect(redirect_url)
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