kontron / redmine_oauth

Redmine authentication through OAuth.
GNU General Public License v2.0
57 stars 27 forks source link

Require a certain role or group membership to allow login #36

Closed col-panic closed 1 month ago

col-panic commented 5 months ago

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)

ok2uec commented 2 months ago

I need that as well. The only thing I'm missing is role mapping after login.

picman commented 2 months ago

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

col-panic commented 1 month ago

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:

  1. Required the client role user to 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)
  2. 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.
picman commented 1 month ago

Could you test roles branch with your Keycloak configuration?

col-panic commented 1 month ago

@picman thank you for your response, I added some comments to https://github.com/kontron/redmine_oauth/commit/e1f6a6808e5366e5f84c05c20e94388270c94b27

picman commented 1 month ago

Updated accordingly.

col-panic commented 1 month ago

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": "tester@tester.at"
}
picman commented 1 month ago
  1. How does look the token if "none of the roles is mapped"?
  2. What do you mean with "the same happens for the access token"?
col-panic commented 1 month ago
picman commented 1 month ago

Fixed. (The error "contained more than one 'token' key" comes from an external library. I can't change it in my code.)

col-panic commented 1 month ago

thanks - will test on Monday!

col-panic commented 1 month ago

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 'tester@whatever.at' 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": "tester@whatever.at"
}

Is there a means to show what access token redmine_oauth effectively gets? Maybe there's a problem in parsing the correct entry?

picman commented 1 month ago

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/additionalenvironment.rb)

In my case I see:

1) 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 Karel.Picman@kontron.com
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=>"tester@whatever.at"}
INFO -- : Successful authentication for 'kpicman'

2) 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 'Karel.Picman@kontron.com' 
col-panic commented 1 month ago

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)

grafik

col-panic commented 1 month ago

Thats the client mapper setting for client roles grafik

col-panic commented 1 month ago

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?

picman commented 1 month ago

After studying documentation and source codes I came to a conclusion that the roles information are a part of _idtoken not _accesstoken. 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 _accesstoken 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 _idtoken only and not _accesstoken?

col-panic commented 1 month ago

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)

  1. 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
  2. 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

1) 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 2) The code entry is the authorization code which the application uses to obtain the ID token and the refresh token. 3) 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) 4) see book right bottom Groß (IMG_8250)

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.

picman commented 1 month ago

I've added a new option into the plugin's settings. You can now chose a preferable token type. Please give it a try.

col-panic commented 1 month ago

removed (content was clearly wrong)

col-panic commented 1 month ago

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":"tester@medevit.at"}
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 'tester@medevit.at' 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": "tester@medevit.at"
}

clearly the value is in there - what is wrong here??

picman commented 1 month ago

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 _userroles. Do you happen to enter user_roles with double quotation marks in your plugin's settings?

col-panic commented 1 month ago

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
picman commented 1 month ago

I've added a debug message with _userinfo content. git pull roles branch and post your log again, please.

col-panic commented 1 month ago
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"=>"tester@medevit.at", "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
picman commented 1 month ago

Once again, please.

col-panic commented 1 month ago

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:

grafik

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)

grafik

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)

grafik

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!

picman commented 1 month ago

All right. I reverted the patch. Can you test it with _roles_withoutpatch branch, please?

col-panic commented 1 month ago

The branch roles_without_patch works like a charm, and the log info OAuth2::AccessToken.from_hash:hashcontained 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! 👍

picman commented 1 month ago

Merged into devel.

Aurel004 commented 1 month ago

Hi,

Tested also with Zitadel (devel branch), works well ! One thing, even if that's exactly what the text says below the field, but when an account has only the "admin" role without the "user" role, it denies the auth. Maybe "admin" role should be enough to log in ?

Thanks

picman commented 1 month ago

It makes sense.

col-panic commented 1 month ago

Do I get your propsal right @Aurel004 ? There is only user for default access OR admin for access with admin rights anything else will deny access? What if oauth2 despite that delivers roles [ user, admin ] how should the plugin react?

picman commented 1 month ago

I'd say: user => access granted user, admin => access + admin rights granted admin => access + admin rights granted

Aurel004 commented 1 month ago

I'd say:

user => access granted

user, admin => access + admin rights granted

admin => access + admin rights granted

Exactly, and atm only ["user"] and ["user","admin"] give access