I am trying to create an API for our organization using FastAPI. It has a KeyCloak server that is used for all authentication, and OpenID Connect and JWTs in the way that is considered best practice.
In the simplest case, someone else takes care of acquiring a valid JWT token so that FastAPI then can simply decode and read the user and permissions.
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
async def get_current_user(token: str = Depends(oauth2_scheme)):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
jwt_token = jwt.decode(token, key=env.keycloak_server_public_key, audience='myorg')
return jwt_token['preferred_username']
except jwt.exceptions.ExpiredSignatureError:
raise credentials_exception
Life is simple!
I do, however, want to try to let users explore the API using the Swagger page. I have created this function that lets users login using the UI:
#app.post("/token")
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
login_request = requests.post(
"https://mygreatorg.com/auth/realms/master/protocol/openid-connect/token",
data={
"grant_type": "password",
"username": form_data.username,
"password": form_data.password,
"client_id": "fastapi-application",
},
)
raw_response = json.loads(login_request.content.decode('utf-8'))
raw_response['acquire_time'] = time.time()
TOKEN_CACHE[form_data.username] = raw_response
return {"access_token": raw_response['access_token'], "token_type": "bearer"}
This works fine. The auth header in Swagger is now the token, and it validates, for about a minute. The expire time for the tokens is set to a very short time. One is then expected to refresh them using the refresh_token provided in the raw_response payload.
I can very easily make another request to get a new valid access token given the refresh_token. But I am unable to get Swagger to change the token of the request in the UI. The only way I find is to log out and log in again, but users will be very annoyed if they only allow a minute without being kicked out.
One workaround would be to simply cache the token and ignore the expiration time and let the user be logged in for a while longer, but that defeats the purpose of the entire security setup and feels like a bad idea.
Any ideas on how to let the UI of FastAPI update the bearer token when it needs a refresh, without letting the user log in again?
This is far from an answer and I will likely delete this later. It is a only a placeholder for outlining my research into this question
USE CASE: Swagger UI need to auto refresh with updated JWT token without closing the UI.
Systems/Applications:
KeyCloak
FastApi
Swagger
OpenID Connect and JWTs
When I looking into this question I noted that the issue in this question was raised in the issues at both FastApi and Swagger.
Swagger
When looking through the code base of Swagger, I noted an authorization parameter named persistAuthorization. According to the documentation this parameter will maintain authorization data and this data will not be lost on browser close/refresh.
In the Swagger code base I see this item:
# source: /src/plugins/topbar/topbar.jsx
#
flushAuthData() {
const { persistAuthorization } = this.props.getConfigs()
if (persistAuthorization)
{
return
}
this.props.authActions.restoreAuthorization({
authorized: {}
})
}
The code above makes calls to /src/core/plugins/auth/actions.js.
In the Swagger pull requests there is a pending feature named configs.preserveAuthorization This scope of this feature:
Refreshing or closing/reopening the the page will preserve authorization if we have configs.preserveAuthorization set to true.
It's unclear based on the comments how the features preserveAuthorization and persistAuthorization are different.
FastApi
When I was looking at the FastApi code base, I noted a OAuthFlowsModel named refreshUrl. I looked through the FastApi document and didn't see this mentioned.
# source: /fastapi/security/oauth2.py
#
class OAuth2AuthorizationCodeBearer(OAuth2):
def __init__(
self,
authorizationUrl: str,
tokenUrl: str,
refreshUrl: Optional[str] = None,
scheme_name: Optional[str] = None,
scopes: Optional[Dict[str, str]] = None,
auto_error: bool = True,
):
if not scopes:
scopes = {}
flows = OAuthFlowsModel(
authorizationCode={
"authorizationUrl": authorizationUrl,
"tokenUrl": tokenUrl,
"refreshUrl": refreshUrl,
"scopes": scopes,
}
)
super().__init__(flows=flows, scheme_name=scheme_name, auto_error=auto_error)
async def __call__(self, request: Request) -> Optional[str]:
authorization: str = request.headers.get("Authorization")
scheme, param = get_authorization_scheme_param(authorization)
if not authorization or scheme.lower() != "bearer":
if self.auto_error:
raise HTTPException(
status_code=HTTP_401_UNAUTHORIZED,
detail="Not authenticated",
headers={"WWW-Authenticate": "Bearer"},
)
else:
return None # pragma: nocover
return param
Looking through the issues for FastApi I noted one asking for OAuth2RerefreshRequestForm to be added. The scope of this issue is token refresh.
I also noted another issue refresh token with Keycloak and Fastapi
I apologize for not being able to provide a solid answer.
Related
I have been struggling with this problem for two weeks, Basically I have configured the auth0 settings with my Flask app which runs on local host.
So, I have the following two endpoints in my flask app:
A public endpoint which requires no authentication process:
#APP.route("/api/public")
#cross_origin(headers=["Content-Type", "Authorization"])
def public():
# No access token required to access this route
response = "Hello from a public endpoint! You don't need to be authenticated to see this."
return jsonify(message=response)
A private endpoint which requires authentication:
#APP.route("/api/private")
#cross_origin(headers=["Content-Type", "Authorization"])
#cross_origin(headers=["Access-Control-Allow-Origin", "http://localhost:3000"])
#requires_auth
def private():
# A valid access token is required to access this route
response = "Hello from a private endpoint! You need to be authenticated to see this."
return jsonify(message=response)
So here is the following scenario:
I try to log in with the auth0 url that redirects me to universal login page for my app. After successful login, it redirects me to the private end point and I get 401 Un-authorized error.
Whenever I make this request with valid token from the browser it throws 401 error.
With the same token, I call this endpoint using postman it works !!
The reason for that is this request from the browser is not including the Authorization header, unlike postman.
I really don't understand why the browser is not including the Authorization header.
Can someone explains ?
NOTE: At first it was working using browser without any problems but suddenly it appeared somehow.
Since the private endpoint requires authentication, whenever I try to access the private end point this function is called:
def get_token_auth_header():
"""Obtains the access token from the Authorization Header
"""
auth = request.headers.get("Authorization", None) # HERE IS THE PROBLEM OCCURRS
print("REQUEST HEADERS: \n", request.headers)
if not auth:
raise AuthError({"code": "authorization_header_missing",
"description":
"Authorization header is expected"}, 401)
parts = auth.split()
if parts[0].lower() != "bearer":
raise AuthError({"code": "invalid_header",
"description":
"Authorization header must start with"
" Bearer"}, 401)
elif len(parts) == 1:
raise AuthError({"code": "invalid_header",
"description": "Token not found"}, 401)
elif len(parts) > 2:
raise AuthError({"code": "invalid_header",
"description":
"Authorization header must be"
" Bearer token"}, 401)
token = parts[1]
return token
I have been struggling with this for almost two weeks, I tried everything.
I would really appreciate your help.
I'm copying here the same answer I provided in the community forum in case you still need it ;)
It seems you are missing the authlib configuration ;)
You can see here how to configure that and use it on your app
From reading various documents it seems like authorization is optionally required by oauth2 providers for refresh token requests. I'm working with the FitBit API that appears to require authorization.
I'm following the instructions here for refreshing a token with requests-oauthlib:
https://requests-oauthlib.readthedocs.io/en/latest/oauth2_workflow.html#refreshing-tokens
Some setup code (not what I am using, but you get the idea:
>>> token = {
... 'access_token': 'eswfld123kjhn1v5423',
... 'refresh_token': 'asdfkljh23490sdf',
... 'token_type': 'Bearer',
... 'expires_in': '-30', # initially 3600, need to be updated by you
... }
>>> client_id = r'foo'
>>> refresh_url = 'https://provider.com/token'
>>> protected_url = 'https://provider.com/secret'
>>> # most providers will ask you for extra credentials to be passed along
>>> # when refreshing tokens, usually for authentication purposes.
>>> extra = {
... 'client_id': client_id,
... 'client_secret': r'potato',
... }
>>> # After updating the token you will most likely want to save it.
>>> def token_saver(token):
... # save token in database / session
from requests_oauthlib import OAuth2Session
client = OAuth2Session(client_id, token=token, auto_refresh_url=refresh_url,
auto_refresh_kwargs=extra, token_updater=token_saver)
r = client.get(protected_url)
However, with this call I'm getting:
MissingTokenError: (missing_token) Missing access token parameter.
I know my token is expired, but why isn't the refresh working?
The library is broken in this regard. See #379.
You can work around it something like this:
def _wrap_refresh(func):
def wrapper(*args, **kwargs):
kwargs['auth'] = (client_id, client_secret)
kwargs.pop('allow_redirects', None)
return func(*args, **kwargs)
return wrapper
client = OAuth2Session(client_id, token=token,
auto_refresh_url=refresh_url,
token_updater=token_saver)
client.refresh_token = _wrap_refresh(client.refresh_token)
Edit: There is still some useful info below but overriding the auth function means that my actual API requests are now failing (i.e. below is not a correct answer) I'm not sure how I got the one request I tried last time to work. It may have just returned an error (in json) rather than throwing an error, and I just assumed no raised error meant it was actually working. See a correct workaround by OrangeDog (until the library is fixed).
Well, I examined the FitBit server response, just before the MissingTokenError was being thrown. It turns out I was getting an error saying that the authentication was incorrect.
This is perhaps a useful point on its own to dwell on for a sec. The MissingTokenError seems to occur when the response doesn't contain the expected token. If you can debug and look at the response more closely, you may find the server is providing a bit more detail as to why your request was malformed. I went to the location of the error and added a print statement. This allowed me to see the JSON message from FitBit. Anyway, this approach may be useful for others getting the MissingTokenError.
if not 'access_token' in params:
print(params)
raise MissingTokenError(description="Missing access token parameter.")
Anyway, after some further debugging, the authentication was not set. Additionally my client id and secret were being posted in the body (this may not have been a problem). In the FitBit examples, the client id and secret were not posted in the body but were passed via authentication. So I needed the client to pass authentication to FitBit.
So then the question was, how do I authenticate. The documentation is currently lacking in this respect. However, looking at the session object I found a .auth property that was being set and a reference to an issue (#278). In that issue a workaround is provided (shown below with my code) for manual authentication setting: https://github.com/requests/requests-oauthlib/issues/278
Note, the oauth session inherits from the requests session, so for someone that knows requests really well, this may be obvious ...
Anyway, the solution was just to set the auth parameter after initializing the session. Since FitBit doesn't need the client id and secret in the body, I removed passing in the extras as well (again, this may be a minor issue and not really impact things):
import os
import json
from requests.auth import HTTPBasicAuth
from requests_oauthlib import OAuth2Session
client_id = ""
client_secret = ""
with open("tokens.json", "r") as read_file:
token = json.load(read_file)
save_file_path = os.path.abspath('tokens.json')
refresh_url = 'https://api.fitbit.com/oauth2/token'
def token_saver(token):
with open(save_file_path, "w") as out_file:
json.dump(token, out_file, indent = 6)
#Note, I've removed the 'extras' input
client = OAuth2Session(client_id, token=token, auto_refresh_url=refresh_url, token_updater=token_saver)
#This was the magic line ...
auth = HTTPBasicAuth(client_id, client_secret)
client.auth = auth
url = 'https://api.fitbit.com/1.2/user/-/sleep/date/2021-01-01-2021-01-23.json'
wtf = client.get(url)
OK, I think I copied that code correctly, it is currently a bit of a mess on my end. The key part was simply the line of:
client.auth = auth
After the client was initiated.
Note, my token contains an expires_at field. I don't think the session handles the expires_in in terms of exact timing. In other words, I think expires_in only causes a refresh if its value is less than 0. I don't think it looks at the time the object was created and starts a timer or sets a property to know what expires_in is relative to. The expires_at field on the other hand seems to provide (I think) a field that is checked to ensure that the token hasn't expired at the time of the request, since expires_at is a real world, non-relative, time. Here's my token dict (with fake tokens and user_id):
{'access_token': '1234',
'expires_in': 28800,
'refresh_token': '5678',
'scope': ['heartrate',
'profile',
'settings',
'nutrition',
'location',
'weight',
'activity',
'sleep',
'social'],
'token_type': 'Bearer',
'user_id': 'abcd',
'expires_at': 1611455442.4566112}
I'm trying to run unit tests in Python for my flask application for routes that depend on the userID which is obtained from the access_token.
Is there a way to call the auth0 authorize API, in Python, to obtain an access_token for a user given their username and password?
If not, then what is an automated way of calling the authorize API to give it a username and password and obtain an access_token?
A code snippet would be best.
Thanks to #Jerdog, I've constructed the required piece of code:
import json
import requests
# testing user password database:
testingUsers = {
'testingUser2#funnymail.com': 'BuQ3tUS3 :jbFAL',
'testingUser3w#funnymail.com': 'y(1726854(b(-KY'
}
def getUserToken(userName):
# client id and secret come from LogIn (Test Client)! which has password enabled under "Client > Advanced > Grant Types > Tick Password"
url = 'https://YOUR_AUTH0_DOMAIN/oauth/token'
headers = {'content-type': 'application/json'}
password = testingUsers[userName]
parameter = { "client_id":"Jfjrl12w55uqcJswWmMhSm5IG2Qov8w2e",
"client_secret": "3E5ZnqLFbPUppBLQiGDjB0H2GtXaLyaD26sdk2HmHrBXQaDYE453UCUoUHmt5nWWh",
"audience": 'AUTH0_AUDIENCE',
"grant_type": "password",
"username": userName,
"password": password, "scope": "openid" }
# do the equivalent of a CURL request from https://auth0.com/docs/quickstart/backend/python/02-using#obtaining-an-access-token-for-testing
responseDICT = json.loads(requests.post(url, json=parameter, headers=headers).text)
return responseDICT['access_token']
#memoize # memoize code from: https://stackoverflow.com/a/815160
def getUserTokenHeaders(userName='testingUser2#funnymail.com'):
return { 'authorization': "Bearer " + getUserToken(userName)}
The #memoize decorator is to avoid multiple calls to get a token over many tests. The tenant has to have a default database specified for the above call to work (see this answer). It was a bit cryptic as to what the database name was supposed to be (the default_directory), but for me, with only Auth0 users, the database was Username-Password-Authentication, which seems to be the default for new accounts.
Have you looked at the https://auth0.com/docs/quickstart/backend/python/01-authorization walkthrough? The full quickstart for Python should give you a good start
I am trying to use test the API request by using tokens. I was able to extract the token but I struggle to find a way to use it.
This is how I get my token:
#pytest.mark.django_db
class TestUserAPI(APITestCase):
def setUp(self):
self.created_user = UserFactory()
User.objects.create_user(username='test', password='pass1234')
def test_authentification(self):
request = self.client.post('http://localhost:8000/api/v1/auth/',
{
"username": "test",
"password": "pass1234"
})
TestUserAPI.token = request.data["token"]
assert request.status_code == 200
and this is how I use it:
def test_profile(self):
request = self.client.get('http://localhost:8000/api/v1/profile/',
TokenAuthentication = 'token {}'.format(TestUserAPI.token))
assert request.status_code == status.HTTP_200_OK
It gives me 401 error. What is the correct way of using token?
The solution is simple and it's because of my inexperience with testing. The test_profile function doesn't register that a token has been asked in the test_authentification function. So I had to put both of them in the SetUp function for the token to be registered for every function in the class.
According to documentation correct header format is Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b
According to official documentation the the token key should be Authentication and the token should be should be trailing Token and a white space.
In your code change token {} to Token {}.
If the problem persists try to print request headers in you view by printing request.META to check the key.
Quick question: I'm trying to use the Discord API to make a backup of all the messages on a server (or a guild, if you use the official term).
So I implemented OAuth without any problems, I have my access token and I can query some endpoints (I tried /users/#me, /users/#me/guilds). Though, most of them don't work. For example, if I query /users/#me/channels (which is supposed to be the DMs) I get a 401 Unauthorized response from the API. It's the same if I gather a guild id from /users/#me/guilds and then try to list the channels in it with /guilds/guild.id/channels.
The really weird thing is that I do have all the scopes required (I think so, I didn't take the RPC ones since I don't think it's required for what I want to do) and I can't figure it out myself... What is also weird is that on the OAuth authorization screen, I have those two things:
It kind of counterdicts itself... :(
Do you have any ideas you'd like to share ?
Thanks!
Note: I'm using Python but I don't think it's related here, since some endpoints do work with the headers and tokens I have...
Here is my "authentication code":
baseUrl = "https://discordapp.com/api"
def authorize():
scopes = [
"guilds",
"email",
"identify",
"messages.read",
"guilds.join",
"gdm.join",
"connections"
]
urlAuthorize = "{}/oauth2/authorize?client_id={}&scope={}&response_type=code".format(baseUrl, clientid, ('+'.join(scopes)))
pyperclip.copy(urlAuthorize)
code = input("Code: ")
return code
def getAccessToken(code):
url = "{}/oauth2/token".format(baseUrl)
params = {
"client_id" : clientid,
"client_secret" : clientsecret,
"redirect_uri" : "http://localhost",
"grant_type":"authorization_code",
"code" : code,
}
req = requests.post(url, params = params)
return json.loads(req.text)
And the code related to an API request:
def getHeaders():
return {
"Authorization" : "{} {}".format("Bearer", config["accessToken"]),
# "user-agent" : "DiscordBackup/0.0.1"
}
def getRequest(endpoint, asJson = True, additional = None):
url = "{}/{}".format(baseUrl, endpoint)
req = requests.get(url, headers = getHeaders())
print()
print(getHeaders())
print(url)
print(req.text)
if asJson:
return json.loads(req.text)
else:
return req.text
def getMe(): # this works
endpoint = "users/#me"
return getRequest(endpoint)
def getMyDMs(): # this gives me a code 401 Unauthorized
endpoint = "/users/#me/channels"
return getRequest(endpoint)
I came across this post when encountering this issue, and to put it bluntly, there's no way to resolve it.
The messages.read permission is for a local RPC server; https://discordapp.com/developers/docs/topics/oauth2#shared-resources-oauth2-scopes
However, local RPC servers are in private beta and you must sign up/get accepted to use this.
I wanted to create a DM exporter, but that doesn't look likely now.