Add Audience(s) to Variable#18
Add Audience(s) to Variable#18pfzetto wants to merge 2 commits intohaproxytech:masterfrom pfzetto:master
Conversation
|
Thanks @pfz4 I will take a look at your changes. |
|
Multiple audiences seems like something that would be good to support, but I am not sure that this is the best approach because we already have a function named
Instead, I propose:
|
|
As for validating a substring...if we do this, then it would need to be clear that that is going to happen, such as by having the user put an asterisk onto the end or beginning of the string. In the past, I have not been in favor of adding stuff like that because it's not part of the spec, and I would prefer to keep the library simple. |
|
Ok, I will change the audienceIsValid Function an add a custom HaProxy Function to Validadte the Audience (https://www.haproxy.com/de/blog/5-ways-to-extend-haproxy-with-lua/):
This way we don't have the substring matching problems |
|
Implemented Multiple Audiences (seperated by Added an validateAudience Fetch |
|
@NickMRamirez Implemented the requested changes |
|
Hmm, the changes seem more complex than I thought they would be. I'll see if I can condense this down. Also, I want to see if I can expose all of the claims in a token as variables, since there's value in being able to see, for example, the client ID. We already store the "scope" claim, so the framework is there, and we shouldn't need to add a new fetch to get them. |
|
I haven't found a way to Match for a Array Variable in Haproxy other than Substring Matching, which can lead to false positives. Maybe I can look into converting the string in size 1 Arrays before checking |
|
@pfz4 I tested this code with the following combinations:
It converts' aud' and OAUTH_AUDIENCE to tables, even when there is only one value. Then it loops through those tables to compare if they contain a match. I think that's all of the possible combinations. local function contains(items, test_str)
for _,item in pairs(items) do
-- strip whitespace
item = item:gsub("%s+", "")
test_str = test_str:gsub("%s+", "")
if item == test_str then
return true
end
end
return false
end
local function audienceIsValid(token, expectedAudienceParam)
-- Convert OAUTH_AUDIENCE environment variable to a table,
-- even if it contains only one value
local expectedAudiences = expectedAudienceParam
if type(expectedAudiences) == "string" then
expectedAudiences = core.tokenize(expectedAudienceParam, ",")
end
-- Convert 'aud' claim to a table, even if it contains only one value
local receivedAudiences = token.payloaddecoded.aud
if type(token.payloaddecoded.aud) == "string" then
receivedAudiences = core.tokenize(token.payloaddecoded.aud, ",")
end
for _, receivedAudience in ipairs(receivedAudiences) do
if contains(expectedAudiences, receivedAudience) then
return true
end
end
return false
endNext I will investigate returning all of the token claims as variables that you can access in the haproxy.cfg. |
|
@NickMRamirez looks good! But I would change From: https://openid.net/specs/openid-connect-core-1_0.html
Will you keep the validateAudience Fetch? |
|
Do you mean, change the code to match the specification so that it adds the client_id to the audience? It must be left up to the auth token providers to send a valid token. Since the token is signed, I won't be able to alter it, such as to add the client ID to the audience.The token will be passed to the backend servers as the Authorization header as-is. But, we can add code to parse the information into variables so that HAProxy can read it. Interestingly, I don't think that all of the auth providers send the Client ID in the audience. For example, Keycloak does that, but you can remove it. Auth0 doesn't, as far as I remember. We won't need the ValidateAudience fetch, since the validation logic will all be kept contained in the audienceIsValid function. |
|
This function seems to work for setting the variables. local function setVariablesFromPayload(txn, decodedPayload)
for key, value in pairs(decodedPayload) do
txn:set_var("txn.oauth." .. key, dump(value))
end
endThen you can access / log the variables like this in the haproxy.cfg: For my token, it captures these fields: If this works for you, I will commit it. |
Sorry for not being clear about what i meant. This way the code matches the specification of the AUD Claim.
How would you use that variable in an acl? I don't know a acl method like for arrays like contains. |
|
Bear with me as I think this through... If I understand you correctly, you are asking whether it is incorrect to treat a comma as a delimiter when it appears in a single string audience value, since, theoretically, a token could send an 'aud' that is a string, contains a comma, but is meant to be one value. The specification that you linked to is for OpenID Connect, but this Lua library is for OAuth 2.0 only (so far). So, I looked at this OAuth 2.0 document to learn what it says about how to treat audiences. It says that the audience needs to be an absolute URI that refers to the resource server. The question is, can an absolute URI contain a comma, and still be considered a single value? According to RFC3986, the answer is yes. A comma is a reserved character that can be used as a delimiter. So, I think you are correct. I should not tokenize a comma-delimited string, but instead should leave it as a single string. Your way seems better: I will update my code. As for your second question...
Help me to understand what you want to do in the ACL, because it seems like the Also, if you need to parse the 'aud' variable, you would need to treat it as a string. So, you can use any of the string matching operators in HAProxy. The string will look like this: Do you prefer a different format for a table that is converted to a string? |
I use one HaProy HA Cluster to handle all my API Routing. So it think it would be nice if it would be possible to restrict api access as far as possible prior to the api endpoints. My Config would look something like this (using fetch): I liked the Fetch function because it validates the absolute values and leaves no error for false positives. For example in a public auth envoirement. But I guess if the URLs are surrounded by a character that can't be used in urls it would also work. |
|
You can use the variable I will create. I have also read that some people use scopes alone to restrict access. Then you could make 'aud' generic: http://localhost/api/. |
|
Hmmm....maybe that won't work because if the received audience has two audiences, then the first ACL will always win (it will always go to weatherservice). Then again the fetch would have the same problem. You would need to check the host header, like in your example, to be sure where they are trying to go. In the end, I think that the host header would tell you where they are trying to go, but the 'aud' doesn't add much value as a way to restrict. The token either contains an allowed audience or it doesn't. But it isn't exclusive if there are multiple values. It doesn't tell you any more by calling a fetch or adding it as a variable check. The only way to be exclusive about an audience and restrict access by it would be to issue two different tokens. That is, if the token contains multiple audiences. Scopes would further refine the access level though. |
|
I would See the scope and audience validation as a way to preauthenticate requests. My only concern with the Variable solution is that there are false positives like Using aud in ACL would be perfect when having multiple apis over the same haproxy instance. So you would add the multiple audiences to the global filter and the filter again for the different backends. |
|
I don't think there's much chance of a malicious audience, since the token will be coming from your trusted token issuer server, which will be verified by checking the cryptographic signature on the token. The signature is the linchpin in determining "do we trust this token?". After that, we trust all the claims and are just verifying that it's the correct token for the service they're calling. But tampering with the token is discovered early on by the signature check. |
|
Here are my changes, if you want to try them to see if they will work for your use case. |
|
Closing because the changes were done in #20. |
When using HaProxy as an API Gateway it might be required to have differnet Audiences for different backends. This Commit added a Variable called
txn.audience. This can be used to validate the Audience by an ACL Rule.Bsp.:
http-request deny unless { var(txn.audience) -m sub " audienceA " }The leading and trailing whitespaces are there to prevent false flags by the substring method.
Bsp.:
http-request deny unless { var(txn.audience) -m sub "audienceA" }audienceA-> passfooaudienceA-> passaudienceAbar-> passfooaudienceAbar-> passhttp-request deny unless { var(txn.audience) -m sub " audienceA " }audienceA-> passfooaudienceA-> denyaudienceAbar-> denyfooaudienceAbar-> denyThis change only works when no Audience is set as the Env Variable.