I am facing an issue while working with Redirect Url and getting the data from QueryParams in Python using FastApi. I am using Azure AD Authorization Grant Flow to log in, below is the code which generates the RedirectResponse
@app.get("/auth/oauth/{provider_id}")
async def oauth_login(provider_id: str, request: Request):
if config.code.oauth_callback is None:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="No oauth_callback defined",
)
provider = get_oauth_provider(provider_id)
if not provider:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Provider {provider_id} not found",
)
random = random_secret(32)
params = urllib.parse.urlencode(
{
"client_id": provider.client_id,
"redirect_uri": f"{get_user_facing_url(request.url)}/callback",
"state": random,
**provider.authorize_params,
}
)
response = RedirectResponse(
url=f"{provider.authorize_url}?{params}")
samesite = os.environ.get("CHAINLIT_COOKIE_SAMESITE", "lax") # type: Any
secure = samesite.lower() == "none"
response.set_cookie(
"oauth_state",
random,
httponly=True,
samesite=samesite,
secure=secure,
max_age=3 * 60,
)
return response
And this is where I am receiving the Redirect URL.
@app.get("/auth/oauth/{provider_id}/callback")
async def oauth_callback(
provider_id: str,
request: Request,
error: Optional[str] = None,
code: Optional[str] = None,
state: Optional[str] = None,
):
if config.code.oauth_callback is None:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="No oauth_callback defined",
)
provider = get_oauth_provider(provider_id)
if not provider:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Provider {provider_id} not found",
)
if not code or not state:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Missing code or state",
)
response.delete_cookie("oauth_state")
return response
This redirect works fine when the QueryParams are with ? but the issue right now is that the redirect callback from Azure AD is with # and due to that I am not able to get the Code
& State
QueryParams from the Url
#
http://localhost/callback#code=xxxxxx&state=yyyyyy
Any thoughts on how to fix this issue.
Getting get the text (or key-value pairs, in your case) after the hash mark #
on server side— the #
in a URL is also known as the URI fragment (see Text fragments on MDN documentation as well)—is currently not possible. This is simply because the fragment is never sent to the server (related posts can be found here and here).
I would suggest using the question mark in the URL ?
, which is the proper way to send query parameters in an HTTP request. If this was a complex path parameter, you could instead follow the approach described in this answer, which would allow you to capture the whole URL path, including characters such as /
and %
, but still not text/values after #
.
Since the "fragment" is only available/accessible on client side, you could use JavaScript to obtain the hash
property of the Location
interface, i.e., window.location.hash
. To do that, you could have a /callback_init
endpoint that is initially called and used as the redirect_uri
for the authorization server, and which will return the relevant JavaScript code to read the fragment and pass it in the query string of the URL to the final /callback
endpoint. This can easily be done by replaing the #
with ?
in the URL, as follows:
@app.get('/callback_init', response_class=HTMLResponse)
async def callback_init(request: Request):
html_content = """
<html>
<head>
<script>
var url = window.location.href;
newUrl = url.replace('/callback_init', '/callback').replace("#", "?");
window.location.replace(newUrl);
</script>
</head>
</html>
"""
return HTMLResponse(content=html_content, status_code=200)
However, the approach above would not respect the possibility of query parameters already present in the URL (even though this is not an issue in your case); hence, one could instead use the below.
The example below also takes into account that you had set a cookie, which you had to remove afterwards; hence, this is demonstrated below as well. Also, note that for replacing the URL in the browser's address bar (in other words, sending a request to the /callback
endpoint), window.location.replace()
is used, which, as explained in this answer, won't let the current page (before navigating to the next one) to be saved in session history, meaning that the user won't be able to use the back button in the browser to navigate back to it (if, for some reason, you had to allow the user going back, you could use window.location.href
or window.location.assign()
instead).
If you would like hiding the path and/or query parameters from the URL, you can use an approach similar to this answer and this answer. However, this wouldn't mean that the URL, including such path/query parameters would not make it to the browsing history, etc., already. Hence, you should be aware that sending sensitive information in the query string is not safe—please refer to this answer for more details on that subject.
To trigger the redirection, please go to your browser and call http://localhost:8000/
.
from fastapi import FastAPI, Request, Response
from fastapi.responses import RedirectResponse, HTMLResponse
from typing import Optional
app = FastAPI()
@app.get("/")
async def main():
redirect_url = 'http://localhost:8000/callback_init?msg=Hello#code=1111&state=2222'
response = RedirectResponse(redirect_url)
response.set_cookie(key='some-cookie', value='some-cookie-value', httponly=True)
return response
@app.get('/callback_init', response_class=HTMLResponse)
async def callback_init(request: Request):
html_content = """
<html>
<head>
<script>
var url = window.location.href;
const fragment = window.location.hash;
const searchParams = new URLSearchParams(fragment.substring(1));
url = url.replace('/callback_init', '/callback').replace(fragment, "");
const newUrl = new URL(url);
for (const [key, value] of searchParams) {
newUrl.searchParams.append(key, value);
}
window.location.replace(newUrl);
</script>
</head>
</html>
"""
return HTMLResponse(content=html_content, status_code=200)
@app.get("/callback")
async def callback(
request: Request,
response: Response,
code: Optional[str] = None,
state: Optional[str] = None,
):
print(request.url.query)
print(request.cookies)
response.delete_cookie("some-cookie")
return {"code": code, "state": state}