Using flask-jwt-extended, I have a situation where the API has to serve both users, and also a series of web-applications (for example, one of the latter is a chatbot).
For the users, the package functionality out of the box is just perfect, however, for the web applications, I would like the JWT tokens to behave more like API-keys, where they don't necessarily expire after a period of time.
So what I would like to do, is to suppress the checks for 'expiry' provided the request has come from a predefined and trusted IP address.
I have a sqlalchemy model that stores trusted 'ip addresses', and this has a foreign key relationship with the users model, meaning that a user can specify one (or more) whitelisted ip addresses.
Now the decode_token function:
https://flask-jwt-extended.readthedocs.io/en/stable/_modules/flask_jwt_extended/utils.html#decode_token
Has an argument allow_expired, which permits the expiry to be overridden, however, this is not utilised in any way within the _decode_jwt_from_request(...) function, which seems to be instrumental when validating JWT tokens.
Ultimately, I am after a decorator replacement to #jwt_required, which permits expired tokens to be used, provided the request is coming from the whitelisted IP address.
My questions are twofold:
Is the above structure ok from a security point of view?, and,
Without having to duplicate (and slightly modify) entire functions from the library, how might I go about the above?
Unless anyone tells me a better way to do it, I ended up monkey patching the decode_token function:
I have highlighted the 'patched' region, which intercepts the 'ExpiredSignatureError', and checks if the ip address is in the user ip-whitelist, and if so, permits business as usual.
def decode_token(encoded_token, csrf_value=None, allow_expired=False):
"""
Returns the decoded token (python dict) from an encoded JWT. This does all
the checks to insure that the decoded token is valid before returning it.
:param encoded_token: The encoded JWT to decode into a python dict.
:param csrf_value: Expected CSRF double submit value (optional)
:param allow_expired: Options to ignore exp claim validation in token
:return: Dictionary containing contents of the JWT
"""
jwt_manager = _get_jwt_manager()
unverified_claims = jwt.decode(
encoded_token, verify=False, algorithms=config.decode_algorithms
)
unverified_headers = jwt.get_unverified_header(encoded_token)
# Attempt to call callback with both claims and headers, but fallback to just claims
# for backwards compatibility
try:
secret = jwt_manager._decode_key_callback(unverified_claims, unverified_headers)
except TypeError:
msg = (
"The single-argument (unverified_claims) form of decode_key_callback ",
"is deprecated. Update your code to use the two-argument form ",
"(unverified_claims, unverified_headers)."
)
warn(msg, DeprecationWarning)
secret = jwt_manager._decode_key_callback(unverified_claims)
try:
return decode_jwt(
encoded_token=encoded_token,
secret=secret,
algorithms=config.decode_algorithms,
identity_claim_key=config.identity_claim_key,
user_claims_key=config.user_claims_key,
csrf_value=csrf_value,
audience=config.audience,
issuer=config.issuer,
leeway=config.leeway,
allow_expired=allow_expired
)
except ExpiredSignatureError:
expired_token = decode_jwt(
encoded_token=encoded_token,
secret=secret,
algorithms=config.decode_algorithms,
identity_claim_key=config.identity_claim_key,
user_claims_key=config.user_claims_key,
csrf_value=csrf_value,
audience=config.audience,
issuer=config.issuer,
leeway=config.leeway,
allow_expired=True
)
# ------------------------------------------------------------
# Author: Nicholas E. Hamilton
# Date: 25th August 2019
# Patch: Check if ip address is in the whitelist,
# and if so, permit an expired token
# ------------------------------------------------------------
user = user_loader(expired_token[config.identity_claim_key])
ip_address = request.remote_addr
if user and ip_address:
ip_whitelist = [x.ip_address for x in user.ip_whitelist]
if ip_address in ip_whitelist:
return expired_token
# >>>> END PATCH
# Proceed as normal
ctx_stack.top.expired_jwt = expired_token
raise
flask_jwt_extended.view_decorators.decode_token = flask_jwt_extended.utils.decode_token = decode_token
Related
I follow stackoverflow for quite some time now. In most cases the problems I encountered were already mentioned and addressed by people before me. Now, I have an issue I have not found an applicable solution to yet. It may result from my humble understanding of the issue and not knowing what I actually am looking for, so I hope you can help me to at least better understand what happens. If additional info is required to make sense, please do not hesitate to ask.
Synopsis: One user of a program I built often (not always, interestingly) gets an InvalidAuthenticationToken error from the request python package when requesting calendar events with a token
generated by the msal package while none of the other users have any issues at all.
The situation is as follows:
I built a program for a small company which has to read out the events of some of its employees. I wrote it in python and used the msal and requests packages for the part of the interaction with MS Outlook:
import msal
import requests
class OutlookClient():
def __init__(self, client_id, authority):
# client_id and authority are the respective
# aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee style ids of the app I registered at azure.
self.app = msal.PublicClientApplication(
client_id = client_id,
client_credential = None,
authority = msal.authority.AuthorityBuilder(msal.authority.AZURE_PUBLIC,authority)
)
def getToken(self, username, pw):
# credentials of some dummy employee being authenticated to access
# the employees' calendars
self.auth = self.app.acquire_token_by_username_password(username,pw,
scopes=["Calendars.Read","Calendars.Read.Shared","People.Read"]
)
return
def getCalendar(self, agentCal, startDate, endDate):
# agentCal is the id of the employee in question obtained somewhere else.
graph_data = None
if 'access_token' in self.auth:
req = "https://graph.microsoft.com/v1.0/users/"+agentCal+"/calendar/calendarView"+\
"?startDateTime="+ startDate.strftime("%Y-%m-%dT02:00")+\
"&endDateTime="+ endDate.strftime("%Y-%m-%dT23:00")+\
graph_data = requests.get(req,
headers={'Authorization': 'Bearer ' + self.auth['access_token'], 'content-type': 'application/json'}
).json()
try:
return graph_data['value']
except KeyError:
return []
Currently, three employees are testing the program in the field. One of them faces a recurring error which neither of the other users nor I can reproduce. When getCalendar gets called the request gets answered as
graph_data =
{'error':
{'code': 'InvalidAuthenticationToken',
'message': 'Access token has expired or is not yet valid.',
'innerError':
{'date': '2022-10-27T05:56:39',
'request-id': 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
'client-request-id': 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
}
}
}
whereas all other users and the specific user also sometimes should get a list of events. The token, however, looked fine to me:
self.auth =
{'token_type': 'Bearer',
'scope': 'Calendars.Read Calendars.Read.Shared Calendars.ReadWrite Mail.ReadWrite Mail.Send openid People.Read profile User.Read email',
'expires_in': 4581,
'ext_expires_in': 4581,
'access_token': 'eyJ0eXAiOiJKV1Q...',
'refresh_token': '0.AREA...',
'id_token': 'eyJ0eXAiOiJKV1Q...',
'client_info': 'eyJ1aWQ...',
'id_token_claims': {...}
}
I have limited opportunity to identify the issue at the user's computer, unfortunately, as they are currently overwhelmed with work and therefore not very responsive. So, before I bother them and myself with many trial and error approaches I hoped you could share some ideas.
The problem persists, as I was told, even when the program is closed and restarted.
I let the program create a log-file which stores the relevant variables such as the token etc. to see if any pattern arises but everytime a token is generated independent of whether the request for the calendars is answered correctly or incorrectly.
I thought that maybe the program gets started and after some time the token expires but in the log-file it seems to still be valid.
Sorry, it was as expected and I initially just did not check the correct things.
Indeed, the token expired and I did not see it. One solution is to check whether a request gets answered properly and if not get a token by the refresh token
if 'error' in graph_data:
self.auth = self.app.acquire_token_by_refresh_token(\
self.auth['refresh_token'],scopes)
and request again.
I'd like my discord bot to send a user (captain) a message, when another user on the server wants to join their team.
I have the DiscordID of the captain stored in my SQLite database and can retrieve it. The problem is that when I pass this variable as an argument to the client.users.get() method, it returns undefined. I've tried converting the DiscordID to a string, but it still returns undefined. When I hardcode the DiscordID it works fine.
I'm not very experienced with discord.js so I'm hoping this is just a trivial issue which can be quickly resolved. Or is this intentionally not possible to avoid bots messaging users without directly receiving a messages from them?
const client = new Discord.Client();
client.users.get('12345'); <-- This returns the user object
const capt = 12345;
client.users.get(capt); <-- returns undefined
const capt = 12345;
const captString = capt.toString();
client.users.get(captString); <-- returns undefined
Discord IDs need to be stored as a string, because the number is too big for Node.js and would get rounded. By checking Number.MAX_SAFE_INTEGER, you'll see that it's less than 18 digits and therefore won't properly store IDs.
Bad: 189855563893571595
Good: '189855563893571595'
The CashPaymentFlow has a parameter anonymous to say whether the recipient should be replaced by a confidential identity. In my case, I don't want the confidential identity, so I set it to false.
This works fine for the recipient output (Contains the well known party) but not for the change output to the sender (Contains an anonymous Party).
This happens even if the original sender input is not anonymous
Im on Corda Release 4 on a ubuntu azure VM.
fun sendPayment(amount: Amount<Currency>, recipient: Party): AbstractCashFlow.Result? {
val anonymous = false
val handle = proxy.startTrackedFlow(::CashPaymentFlow, amount, recipient, anonymous)
return handle.returnValue.get()
}
Assume we are sending from A to B and have one unspent output with owner A and value 1000 EUR. Now call sendPayment with amount 800 EUR and recipient B.
I would expect two outputs:
800 EUR, owner B
200 EUR, owner A
Instead I get
800 EUR, owner B
200 EUR, Anonymous(SDFOMWEORHEWLRKHWLEKR...)
Historically, change from cash spends in Corda have been returned using confidential identities. This is the default functionality when generating a spend with either confidential or well-known identities and can't be overridden through CashPaymentFlow.
The Tokens SDK will be replacing Cash (and the rest of the finance module) in due course through which you'll be able to make anonymous change keys optional.
The simulator now displays an error message trying to access request.writeFields.
Before that writeFields in Firestore Security Rules did just not work in real requests.
The message states the following:
The simulator only simulates client SDK calls; request.writeFields is always null for these simulations
Does this mean that writeFields are only specified in HTTP requests?
The documentation only states this:
writeFields: List of fields being written in a write request.
A problem that arises from this
I am searching for something that replaces this property because it is "always null".
request.resource.data in update also contains fields that are not in the requests, but already in the document to my knowledge.
Example
// Existing document:
document:
- name: "Peter"
- age: 52
- profession: "Baker"
// Update call:
document:
- age: 53
// request.resource.data in allow update contains the following:
document:
- name: "Peter"
- age: 53
- profession: "Baker"
But I only want age.
EDIT Mar 4, 2020: Map.diff() replaces writeFields functionality
The Map.diff() function gives the difference between two maps:
https://firebase.google.com/docs/reference/rules/rules.Map#diff
To use it in rules:
// Returns a MapDiff object
map1.diff(map2)
A MapDiff object has the following methods
addedKeys() // a set of strings of keys that are in after but not before
removedKeys() // a set of strings of keys that are in before but not after
changedKeys() // a set of strings of keys that are in both maps but have different values
affectedKeys() // a set of strings that's the union of addedKeys() + removedKeys() + updatedKeys()
unchangedKeys() // a set of strings of keys that are in both maps and have the same value in both
For example:
// This rule only allows updates where "a" is the only field affected
request.resource.data.diff(resource.data).affectedKeys().hasOnly(["a"])
EDIT Oct 4, 2018: writeFields is no longer supported by Firestore and its functionality will eventually be removed.
writeFields is still valid, as you can see from the linked documentation. What the error message in the simulator is telling you is that it's unable to simulate writeFields, as it only works with requests coming from client SDKs. The simulator itself seems to be incapable of simulating requests exactly as required in order for writeFields to be tested. So, if you write rules that use writeFields, you'll have to test them by using a client SDK to perform the read or write that would trigger the rule.
I'm working with the SAFE stack (https://safe-stack.github.io/) and through the example dojo. It's great so far.
I'd like to extend the example to include a button to login/auth via Google. So I looked at an example on the Google website (https://developers.google.com/identity/sign-in/web/build-button). And then I had a look how to do authentication using ASP.NET (https://learn.microsoft.com/en-us/aspnet/core/security/authentication/social/google-logins?view=aspnetcore-2.1&tabs=aspnetcore2x) As a result I ended up confused as to how to integrate this into a SAFE project. Can someone tell me what they would do? SHould I be trying to use ASP.NET Identity or should I be using the JWT approach? I don't even know if they are the same since I'm very new to web frameworks.....
The other question I have is how would one inject raw Javascript into the client side of a SAFE project. The google example above shows raw JS/CSS/HTML code? Should I be injecting that as is or should I look in React for some button that does this and map that idea back through Fable?
Setting up OAuth
The easiest way to use Google OAuth is to wait until the next release of Saturn, at which point Saturn will include the use_google_oauth feature that I just added. :-) See the source code if you're interested in how it works, though I'm afraid you can't implement this yourself with use_custom_oauth because you'll run into a type error (the underlying ASP.NET code has a GoogleOptions class, and use_custom_oauth wants an OAuthOptions class, and they aren't compatible).
To use it, add the following to your application CE:
use_google_oauth googleClientId googleClientSecret "/oauth_callback_google" []
The last parameter should be a sequence of string * string pairs that represent keys and values: you could use a list of tuples, or a Map passed through Map.toSeq, or whatever. The keys of that sequence are keys in the JSON structure that Google returns for the "get more details about this person" API call, and the values are the claim types that those keys should be mapped to in ASP.NET's claims system. The default mapping that use_google_oauth already does is:
id → ClaimTypes.NameIdentifier
displayName → ClaimTypes.Name
emails[] (see note) → ClaimTypes.Email
Those three are automatically mapped by ASP.NET. I added a fourth mapping:
avatar.url → `"urn:google:avatar:url"
There's no standard ClaimTypes name for this one, so I picked an arbitrary URN. Caution: this feature hasn't been released yet, and it's possible (though unlikely) that this string might change between now and when the feature is released in the next version of Saturn.
With those four claim types mapped automatically, I found that I didn't need to specify any additional claims, so I left the final parameter to use_google_oauth as an empty list in my demo app. But if you want more (say you want to get the user's preferred language to use in your localization) then just add them to that list, e.g.:
use_google_oauth googleClientId googleClientSecret "/oauth_callback_google" ["language", "urn:google:language"]
And then once someone has logged in, look in the User.Claims seq for a claim of type "urn:google:language".
Note re: the emails[] list in the JSON: I haven't tested this with a Google account that has multiple emails, so I don't know how ASP.NET picks an email to put in the ClaimTypes.Email claim. It might just pick the first email in the list, or it might pick the one with a type of account; I just don't know. Some experimentation might be needed.
Also note that third-party OAuth, including GitHub and Google, has been split into a new Saturn.Extensions.Authorization package. It will be released on NuGet at the same time that Saturn's next version (probably 0.7.0) is released.
Making the button
Once you have the use_google_oauth call in your application, create something like the following:
let googleUserIdForRmunn = "106310971773596475579"
let matchUpUsers : HttpHandler = fun next ctx ->
// A real implementation would match up user identities with something stored in a database, not hardcoded in Users.fs like this example
let isRmunn =
ctx.User.Claims |> Seq.exists (fun claim ->
claim.Issuer = "Google" && claim.Type = ClaimTypes.NameIdentifier && claim.Value = googleUserIdForRmunn)
if isRmunn then
printfn "User rmunn is an admin of this demo app, adding admin role to user claims"
ctx.User.AddIdentity(new ClaimsIdentity([Claim(ClaimTypes.Role, "Admin", ClaimValueTypes.String, "MyApplication")]))
next ctx
let loggedIn = pipeline {
requires_authentication (Giraffe.Auth.challenge "Google")
plug matchUpUsers
}
let isAdmin = pipeline {
plug loggedIn
requires_role "Admin" (RequestErrors.forbidden (text "Must be admin"))
}
And now in your scope (NOTE: "scope" will probably be renamed to "router" in Saturn 0.7.0), do something like this:
let loggedInView = scope {
pipe_through loggedIn
get "/" (htmlView Index.layout)
get "/index.html" (redirectTo false "/")
get "/default.html" (redirectTo false "/")
get "/admin" (isAdmin >=> htmlView AdminPage.layout)
}
And finally, let your main router have a URL that passes things to the loggedInView router:
let browserRouter = scope {
not_found_handler (htmlView NotFound.layout) //Use the default 404 webpage
pipe_through browser //Use the default browser pipeline
forward "" defaultView //Use the default view
forward "/members-only" loggedInView
}
Then your login button can just go to the /members-only route and you'll be fine.
Note that if you want multiple OAuth buttons (Google, GitHub, Facebook, etc) you'll probably need to tweak that a bit, but this answer is long enough already. When you get to the point of wanting multiple OAuth buttons, go ahead and ask another question.