Require a certain role or group membership to allow login
Optionally require a certain role or group membership to allow login. (e.g. the user must have the client role redmine-user to be allowed login in the system)
I need that as well. The only thing I'm missing is role mapping after login.
What OAuth provider do you use? In Azure it should be possible in general. If you specify groupMembershipClaims, then we should get the access token with a list of groups. If you have the option to set the groupMembershipClaims in your application, I can create a branch where I log the access token to see whether some groups are present and in in format. I can't test it myself as I have no direct access to Azure administration. https://stackoverflow.com/questions/54868974/getting-security-groups-in-jwt-access-token
We have a Keycloak Realm with multiple users. Now I only want a subset of this users to be able to login to Redmine. Keyloak is an Authentication tool, while the task of authorization is done by the Resource Provider (in this case Redmine).
At the moment redmine_oauth does only require a user to successfully authenticate against a realm to be able to login as user. This already leaves projects marked as "public" open to this user.
(There exists an extension to block a user from getting a client token in https://github.com/sventorben/keycloak-restrict-client-auth - but as mentioned, this is considered to be a task of the resource provider)
The basic approach would be, that a client role user is introduced, and if the token represented by the user does not contain a role like resource_access.redmine.user then access is not granted.
In the following IDToken I added myself to a newly required client role user
{
"exp": 1724998286,
"iat": 1724997986,
"jti": "fb1d6fb5-1734-4c16-ab98-4e008b9ffefd",
"iss": "https://***/realms/***",
"aud": "redmine",
"sub": "8e82f27a-39cf-4540-a308-ddd8c94e6a5d",
"typ": "ID",
"azp": "redmine",
"sid": "dddeb1de-ebd8-4c6f-913e-290ae5d5f8ac",
"acr": "1",
"resource_access": {
"redmine": {
"roles": [
"user"
]
}
},
"email_verified": true,
"name": "Marco Descher",
"preferred_username": "mdescher",
"given_name": "Marco",
"family_name": "Descher",
"email": "***@***"
}
the mapping to resource_access.redmine.roles is keycloaks default to map a users client.role. The location
should however be configurable. This role could also be mapped to the access token. I asumme, however, that
you are reading the IDToken.
So the following extensions could be realize:
- Required the client role
userto be authorized to login to redmine. (This feature could be optional. If someone has a setup where having a valid user on the openid server is already enough, then we would not need to check this) - EXTENSION Connection to https://github.com/kontron/redmine_oauth/issues/37 if e.g. the token has a value like this
"resource_access": {
"redmine": {
"roles": [
"user", "admin"
]
}
}
then the user could already be granted the Administrator privileges. Some way round - if admin is not part of the token anymore, then the admin rights should be removed.
3. EXTENSION Introducing groups which could then contain memberships to groups as used within redmine. But I don't elaborate on this here. If we come to this point, I'll create a separate issue explaining in detail.
Could you test roles branch with your Keycloak configuration?
@picman thank you for your response, I added some comments to https://github.com/kontron/redmine_oauth/commit/e1f6a6808e5366e5f84c05c20e94388270c94b27
Updated accordingly.
Validate user roles = resource_access.mis.roles
If none of the roles is mapped, then this happens:
mis-echo-1 | OAuth2::AccessToken.from_hash: `hash` contained more than one 'token' key (["access_token", "id_token"]); using "access_token".
mis-echo-1 | E, [2024-09-05T13:23:35.291165 #1] ERROR -- : undefined method `[]' for nil:NilClass
it is correct not to let me in - but there should be a better message :)
if user role is mapped the same happens for the access token
{
"exp": 1725543182,
"iat": 1725542882,
"jti": "433d0ae4-3821-499c-9eee-06f061685acd",
"iss": "https://keycloak.medelexis.ch/realms/Medelexis",
"sub": "c64712b8-a25b-4368-82e0-a73d5fb7c944",
"typ": "Bearer",
"azp": "mis",
"sid": "b9957d40-8c9e-4881-ba03-92eb636de2eb",
"acr": "1",
"allowed-origins": [
"/*"
],
"resource_access": {
"mis": {
"roles": [
"user"
]
}
},
"scope": "openid profile email",
"email_verified": true,
"name": "Test flawil",
"preferred_username": "tester",
"locale": "de",
"given_name": "Test",
"family_name": "flawil",
"email": "[email protected]"
}
- How does look the token if "none of the roles is mapped"?
- What do you mean with "the same happens for the access token"?
- Keycloak seems to deliver both tokens. I think you should focus for the
id_tokencurrently it seems to select theaccess_token - With no role granted at all, the
id_tokenlooks like this
{
"exp": 1725547546,
"iat": 1725547246,
"jti": "1e939b4e-9df2-412d-89a7-89b77e5e9363",
"iss": "https://keycloak.medelexis.ch/realms/Medelexis",
"aud": "mis",
"sub": "c64712b8-a25b-4368-82e0-a73d5fb7c944",
"typ": "ID",
"azp": "mis",
"sid": "a83e1b4e-c707-4a8b-b14a-ee806bf35708",
"acr": "1",
"email_verified": true,
"name": "Test flawil",
"preferred_username": "tester",
"locale": "de",
"given_name": "Test",
"family_name": "flawil",
"email": "[email protected]"
}
the access-token like this
{
"exp": 1725547546,
"iat": 1725547246,
"jti": "c201968e-4c78-4124-a244-5b37e1ea6064",
"iss": "https://keycloak.medelexis.ch/realms/Medelexis",
"sub": "c64712b8-a25b-4368-82e0-a73d5fb7c944",
"typ": "Bearer",
"azp": "mis",
"sid": "7efbb610-e786-43c4-aaee-9cc02dc1f02a",
"acr": "1",
"allowed-origins": [
"/*"
],
"scope": "openid profile email",
"email_verified": true,
"name": "Test flawil",
"preferred_username": "tester",
"locale": "de",
"given_name": "Test",
"family_name": "flawil",
"email": "[email protected]"
}
- With the role
usergranted for the client, theid_tokenlooks like this
{
"exp": 1725547853,
"iat": 1725547553,
"jti": "99f42891-d424-4bc7-b403-3a41967ef884",
"iss": "https://keycloak.medelexis.ch/realms/Medelexis",
"aud": "mis",
"sub": "c64712b8-a25b-4368-82e0-a73d5fb7c944",
"typ": "ID",
"azp": "mis",
"sid": "ae6b6e47-a4f3-4d35-841b-27bdc395119b",
"acr": "1",
"resource_access": {
"mis": {
"roles": [
"user"
]
}
},
"email_verified": true,
"name": "Test flawil",
"preferred_username": "tester",
"locale": "de",
"given_name": "Test",
"family_name": "flawil",
"email": "[email protected]"
}
the access_token like this
{
"exp": 1725547853,
"iat": 1725547553,
"jti": "107e0d71-7bb8-4de0-b759-85245ace0623",
"iss": "https://keycloak.medelexis.ch/realms/Medelexis",
"sub": "c64712b8-a25b-4368-82e0-a73d5fb7c944",
"typ": "Bearer",
"azp": "mis",
"sid": "c6377cd2-2303-4984-b2b3-86585d1eaec7",
"acr": "1",
"allowed-origins": [
"/*"
],
"resource_access": {
"mis": {
"roles": [
"user"
]
}
},
"scope": "openid profile email",
"email_verified": true,
"name": "Test flawil",
"preferred_username": "tester",
"locale": "de",
"given_name": "Test",
"family_name": "flawil",
"email": "[email protected]"
}
for all these combinations I receive
mis-echo-1 | OAuth2::AccessToken.from_hash: `hash` contained more than one 'token' key (["access_token", "id_token"]); using "access_token".
mis-echo-1 | E, [2024-09-05T14:47:40.218602 #1] ERROR -- : undefined method `[]' for nil:NilClass
Fixed. (The error "contained more than one 'token' key" comes from an external library. I can't change it in my code.)
thanks - will test on Monday!
If I set resource_access.mis.roles then it won't let me login at all.
Both with and without the role set, I will see
mis-echo-1 | OAuth2::AccessToken.from_hash: `hash` contained more than one 'token' key (["access_token", "id_token"]); using "access_token".
mis-echo-1 | I, [2024-09-09T06:41:45.064722 #1] INFO -- : Authentication failed due to a missing role in the token
mis-echo-1 | W, [2024-09-09T06:45:17.847620 #1] WARN -- : Failed login for '[email protected]' from 80.108.2.151 at 2024-09-09 06:45:17 UTC
mis-echo-1 | E, [2024-09-09T06:45:17.847757 #1] ERROR -- : Benutzer oder Passwort ist ungültig.
But Keycloak tells me that the content of the access token should be
{
"exp": 1725864532,
"iat": 1725864232,
"jti": "a165eff7-17f6-4f42-87bd-618cc093d013",
"iss": "https://keycloak.medelexis.ch/realms/Medelexis",
"sub": "c64712b8-a25b-4368-82e0-a73d5fb7c944",
"typ": "Bearer",
"azp": "mis",
"sid": "1173cc58-9397-41b9-b9c5-c1e2406b5376",
"acr": "1",
"allowed-origins": [
"/*"
],
"resource_access": {
"mis": {
"roles": [
"user"
]
}
},
"scope": "openid profile email",
"email_verified": true,
"name": "Test flawil",
"preferred_username": "tester",
"locale": "de",
"given_name": "Test",
"family_name": "flawil",
"email": "[email protected]"
}
Is there a means to show what access token redmine_oauth effectively gets? Maybe there's a problem in parsing the correct entry?
I've added some debug log messages. Could you switch your log level to debug and post your output?
(config.log_level = :debug in config/additional_environment.rb)
In my case I see:
- Validate user roles set to 'resource_access.mis.roles' nad roles present in the access_token:
DEBUG -- : Setting.validate_user_roles = 'resource_access.mis.roles'
DEBUG -- : key: resource_access
DEBUG -- : key: mis
DEBUG -- : key: roles
DEBUG -- : Roles: user
DEBUG -- : admin = false
DEBUG -- : try_to_log_in [email protected]
DEBUG -- : {:exp=>1725864532, :iat=>1725864232, :jti=>"a165eff7-17f6-4f42-87bd-618cc093d013", :iss=>"https://keycloak.medelexis.ch/realms/Medelexis", :sub=>"c64712b8-a25b-4368-82e0-a73d5fb7c944", :typ=>"Bearer", :azp=>"mis", :sid=>"1173cc58-9397-41b9-b9c5-c1e2406b5376", :acr=>"1", :"allowed-origins"=>["/*"], :resource_access=>{:mis=>{:roles=>["user"]}}, :scope=>"openid profile email", :email_verified=>true, :name=>"Test flawil", :preferred_username=>"tester", :locale=>"de", :given_name=>"Test", :family_name=>"flawil", :email=>"[email protected]"}
INFO -- : Successful authentication for 'kpicman'
- Roles are not present in the access_token:
DEBUG -- : Setting.validate_user_roles = 'resource_access.mis.roles'
DEBUG -- : key: resource_access
DEBUG -- : Key not found => access denied
DEBUG -- : Roles:
DEBUG -- : user role not found => access denied
INFO -- : Authentication failed due to a missing role in the token
WARN -- : Failed login for '[email protected]'
I don't get it - on the left hand side you see the Evaluate Client Scope entry for access token,
which clearly states that the resp. resource_access entry is part of it. Yet it seems not to
pick it up?!
The interesting bit seems
mis-echo-1 | D, [2024-09-11T13:59:12.396448 #1] DEBUG -- : Setting.validate_user_roles = 'resource_access.mis.roles'
mis-echo-1 | D, [2024-09-11T13:59:12.396493 #1] DEBUG -- : key: resource_access
mis-echo-1 | D, [2024-09-11T13:59:12.396507 #1] DEBUG -- : Key not found => access denied
it's not even finding the base element?!
(Nevermind the No filters fit line .. thats a separate plugin. I removed it also to test, and it didn't change the behavior)
Thats the client mapper setting for client roles
Maybe the problem is located in this line
mis-echo-1 | OAuth2::AccessToken.from_hash: `hash` contained more than one 'token' key (["access_token", "id_token"]); using "access_token".
the info I transport is either part of access_token or id_token but these are encoded as JWT. I'm not sure you are analyzing this. Could you output the response you get at this point?
After studying documentation and source codes I came to a conclusion that the roles information are a part of id_token not access_token. Despite your option Add to access token. Problem is that if both tokens are present in the response, the OAuth2 library takes the first one. In your case it is access_token that doesn't contain roles. From oauth2 gem:
TOKEN_KEYS_STR = %w[access_token id_token token accessToken idToken].freeze
TOKEN_KEYS_SYM = %i[access_token id_token token accessToken idToken].freeze
TOKEN_KEY_LOOKUP = TOKEN_KEYS_STR + TOKEN_KEYS_SYM
supported_keys = TOKEN_KEY_LOOKUP & fresh.keys
key = supported_keys[0]
So they simply takes the first available token and there is no option to change it. Can't you somehow set in your configuration to the response contains id_token only and not access_token?
After checking your calls, I don't seem there is a way for me to intervene in the response.
Lets fix the calls that happen (from my live debugging)
- Redirected to https://keycloak.medelexis.ch/realms/Medelexis/protocol/openid-connect/auth?client_id=mis&redirect_uri=https%3A%2F%2Fmis-echo.medelexis.ch%2Foauth2callback&response_type=code&scope=openid+email&state=92hNDQ7yPNGPt%2F%2FD%2BLazs5xZx6g6tUn2Tbx66TJrlfM%3D
- Started
GET "/oauth2callback?state=92hNDQ7yPNGPt%2F%2FD%2BLazs5xZx6g6tUn2Tbx66TJrlfM%3D&session_state=c72ab908-32b4-41a0-a774-f1145e0af2fd&iss=https%3A%2F%2Fkeycloak.medelexis.ch%2Frealms%2FMedelexis&code=2a630c94-47e8-44c0-b7fa-52bba6372e81.c72ab908-32b4-41a0-a774-f1145e0af2fd.b7d66e22-37d0-4915-b3cc-13b21c65f233" for 80.108.2.151 at 2024-09-12 07:50:13 +0000
- Is an "Authentication Request" (see https://www.amazon.com/Keycloak-Management-Applications-protocols-applications/dp/1800562497 page 44) which creates an authorization code which is returned to the application in 2
- The
codeentry is the authorization code which the application uses to obtain the ID token and the refresh token. - This code you would use to connect to the token endpoint to get the actual token (I'm confused, because I don't see this request to https://keycloak.medelexis.ch/realms/Medelexis/protocol/openid-connect/token happen in the redmine log) which would look like 4)
- see book right bottom
This token response is not meant to contain the respective info, but the id_token is (afaik the access token is only in keycloak a JWT, it is not required to be of any known kind to other oauth2 idps) - hence the content of it should be analyzed.
But I guess another approach would be to use the authorization code access token to query the userinfo endpoint.
I've added a new option into the plugin's settings. You can now chose a preferable token type. Please give it a try.
removed (content was clearly wrong)
I added the following line Rails.logger.debug { "Decoded token: #{user_info.to_json}" } after https://github.com/kontron/redmine_oauth/blob/bc7e4e40c85680943b8761dae95f50cbd74f1ab7/app/controllers/redmine_oauth_controller.rb#L103 which now leads to the following ouput
-echo-1 | I, [2024-09-13T10:54:43.718583 #1] INFO -- : Current user: anonymous
mis-echo-1 | OAuth2::AccessToken.from_hash: `hash` contained more than one 'token' key (["access_token", "id_token"]); using "id_token".
mis-echo-1 | D, [2024-09-13T10:54:43.763172 #1] DEBUG -- : Decoded token: {"exp":1726225183,"iat":1726224883,"auth_time":1726223264,"jti":"6ee9e5f5-fca1-4b2e-a882-7bd83102c1c7","iss":"https://keycloak.medelexis.ch/realms/Medelexis","aud":"mis","sub":"c64712b8-a25b-4368-82e0-a73d5fb7c944","typ":"ID","azp":"mis","sid":"81765aa5-87b8-4666-8c55-cf7f8cdf657e","at_hash":"piggBPy814R-XJvOE36EIg","acr":"0","email_verified":true,"name":"Test flawil","preferred_username":"tester","user_roles":["user"],"locale":"de","given_name":"Test","family_name":"flawil","email":"[email protected]"}
mis-echo-1 | D, [2024-09-13T10:54:43.763218 #1] DEBUG -- : Setting.validate_user_roles = '"user_roles"'
mis-echo-1 | D, [2024-09-13T10:54:43.763241 #1] DEBUG -- : key: "user_roles"
mis-echo-1 | D, [2024-09-13T10:54:43.763285 #1] DEBUG -- : Key not found => access denied
mis-echo-1 | D, [2024-09-13T10:54:43.763312 #1] DEBUG -- : Roles:
mis-echo-1 | D, [2024-09-13T10:54:43.763325 #1] DEBUG -- : user role not found => access denied
mis-echo-1 | I, [2024-09-13T10:54:43.763338 #1] INFO -- : Authentication failed due to a missing role in the token
mis-echo-1 | W, [2024-09-13T10:54:43.763392 #1] WARN -- : Failed login for '[email protected]' from 80.108.2.151 at 2024-09-13 10:54:43 UTC
For easier analysis i formatted the decoded token:
{
"exp": 1726225183,
"iat": 1726224883,
"auth_time": 1726223264,
"jti": "6ee9e5f5-fca1-4b2e-a882-7bd83102c1c7",
"iss": "https://keycloak.medelexis.ch/realms/Medelexis",
"aud": "mis",
"sub": "c64712b8-a25b-4368-82e0-a73d5fb7c944",
"typ": "ID",
"azp": "mis",
"sid": "81765aa5-87b8-4666-8c55-cf7f8cdf657e",
"at_hash": "piggBPy814R-XJvOE36EIg",
"acr": "0",
"email_verified": true,
"name": "Test flawil",
"preferred_username": "tester",
"user_roles": [
"user"
],
"locale": "de",
"given_name": "Test",
"family_name": "flawil",
"email": "[email protected]"
}
clearly the value is in there - what is wrong here??
Here seems to be a problem:
my environment: DEBUG -- : Setting.validate_user_roles = 'user_roles'
your environment: DEBUG -- : Setting.validate_user_roles = '"user_roles"'
Notice the double quotation marks around user_roles. Do you happen to enter user_roles with double quotation marks in your plugin's settings?
you are right, I removed the quotation marks, yet the problem persists:
is-echo-1 | I, [2024-09-13T11:52:36.748956 #1] INFO -- : Current user: anonymous
mis-echo-1 | OAuth2::AccessToken.from_hash: `hash` contained more than one 'token' key (["access_token", "id_token"]); using "access_token".
mis-echo-1 | D, [2024-09-13T11:52:36.799044 #1] DEBUG -- : Setting.validate_user_roles = 'user_roles'
mis-echo-1 | D, [2024-09-13T11:52:36.799201 #1] DEBUG -- : key: user_roles
mis-echo-1 | D, [2024-09-13T11:52:36.799428 #1] DEBUG -- : Key not found => access denied
mis-echo-1 | D, [2024-09-13T11:52:36.799514 #1] DEBUG -- : Roles:
mis-echo-1 | D, [2024-09-13T11:52:36.799730 #1] DEBUG -- : user role not found => access denied
mis-echo-1 | I, [2024-09-13T11:52:36.799774 #1] INFO -- : Authentication failed due to a missing role in the token
I've added a debug message with user_info content. git pull roles branch and post your log again, please.
mis-echo-1 | OAuth2::AccessToken.from_hash: `hash` contained more than one 'token' key (["access_token", "id_token"]); using "id_token".
mis-echo-1 | D, [2024-09-13T12:03:15.060896 #1] DEBUG -- : Setting.validate_user_roles = 'user_roles.'
mis-echo-1 | D, [2024-09-13T12:03:15.060943 #1] DEBUG -- : {"exp"=>1726229295, "iat"=>1726228995, "auth_time"=>1726227416, "jti"=>"b62a791b-5c6c-47cc-880b-7b2fa7e4204d", "iss"=>"https://keycloak.medelexis.ch/realms/Medelexis", "aud"=>"mis", "sub"=>"c64712b8-a25b-4368-82e0-a73d5fb7c944", "typ"=>"ID", "azp"=>"mis", "sid"=>"1f8783d5-a7db-4861-b37f-f65f72bc9bad", "at_hash"=>"vpCP80GKDS7qrmO95Rgr-g", "acr"=>"0", "email_verified"=>true, "name"=>"Test flawil", "preferred_username"=>"tester", "user_roles"=>["user"], "locale"=>"de", "given_name"=>"Test", "family_name"=>"flawil", "email"=>"[email protected]", "login"=>"tester"}
mis-echo-1 | D, [2024-09-13T12:03:15.061015 #1] DEBUG -- : key: user_roles
mis-echo-1 | D, [2024-09-13T12:03:15.061047 #1] DEBUG -- : Key not found => access denied
mis-echo-1 | D, [2024-09-13T12:03:15.061064 #1] DEBUG -- : Roles:
mis-echo-1 | D, [2024-09-13T12:03:15.061076 #1] DEBUG -- : user role not found => access denied
mis-echo-1 | I, [2024-09-13T12:03:15.061103 #1] INFO -- : Authentication failed due to a missing role in the token
Once again, please.
This looks very good now @picman
It worked with the id_token and the key user_roles - but as this was an adaptation, i fixed the defaults of keycloak.
So it should directly work with keycloak if:
The keycloak instance creates a client and adds the following client roles:
Now the user role has to be set for a user. But the roles are only transported by default with the access_token,
if the scope roles is either requested by the client (which is NOT the case for redmine_oauth at the moment) or set to Default in keycloak (in which case the info will be transmitted without requesting the scope)
The keycloak scope roles will then automatically add the info to the access_token (!NOT! the id_token) by the key resource_access.${client_id}.roles which can be analysed as follows (my client is called mis)
I also tested dynamically assigning the administrator - and it worked as expected!
Thank you very much! I'm not sure about the requirement of selecting the token you patched in https://github.com/kontron/redmine_oauth/commit/fddd607753f1b8d4f05bba78f958d27423e5c735 - the default behaviour for keycloak should be to select the access_tokenas described!
All right. I reverted the patch. Can you test it with roles_without_patch branch, please?
The branch roles_without_patch works like a charm, and the log info OAuth2::AccessToken.from_hash: hash contained more than one 'token' key (["access_token", "id_token"]); using "access_token". is just correct for the Keycloak setting.
Thank you very much for your work - maybe we should somehow link the info in this issue to the documentation! 👍
Merged into devel.