opensearch-project / security-dashboards-plugin

🔐 Manage your internal users, roles, access control, and audit logs from OpenSearch Dashboards
https://opensearch.org/docs/latest/security-plugin/index/
Apache License 2.0
68 stars 148 forks source link

[RFC] Remove confusion about conflicting token expiration and cookie + session config settings #1711

Open jochen-kressin opened 6 months ago

jochen-kressin commented 6 months ago

Background

The OpenSearch Dashboard configuration contains multiple settings that affect how long user credentials are saved and valid in the browser cookie:

In addition, we may have credentials with an expiration time set on runtime, or rather provided by an external authentication, such as an Identity Provider (IdP). Examples of this are when using JWT, OpenID, or SAML. The credentials in these cases contain a token, which may contain an "exp" property that denotes the lifespan of the given token.

The multiple settings for the credentials lifespan have lead to confusion and sometimes misconfigurations, as seen reported in multiple issues and forum discussions.

Related issues

Examples of issues related this are:

The settings in detail

cookie.ttl

This setting doesn't seem to have any effect. The expected result of using this setting would be to control the browser cookie's lifespan, but the setting is never passed on to the underlying cookie mechanism (hapi cookie). Instead, the browser cookie will always be "Session", and the cookie will exists until the browser session is closed.

session.ttl

This setting adds a lifespan (time to live) in the form of a property - expiryTime - that is stored in the authentication cookie. On each request, this ttl is validated against the current time.

session.keepalive

If this setting is true, the lifespan (expiryTime) of the credentials are extended whenever Dashboards detects an authenticated request. If the user is idle and no request is sent, the lifespan will not be extended.

The .exp property in a token

This denotes the lifespan of the token in seconds. More info: https://openid.net/specs/openid-connect-core-1_0.html#IDToken

The underlying problem

First of all - the fact that the cookie.ttl setting does not have any effect may be unexpected for users looking to really limit the browser cookie's lifetime. While this isn't the main problem in the issues referenced, it might be worth fixing.

The main source of confusion comes when using OpenID, SAML and likely also JWT.

User expectancy: the IdP's token lifespan is not (always) used

The token lifespan can be set within the IdP, but that lifespan may be overridden by session.ttl

A user is logged out too early

If session.ttl is shorter that the token's actual lifespan, the user will be unauthenticated even though the token is still valid.

Differences between the authentication types

SAML does use a token's exp property when the user authenticates, and the cookie's expiryTime will be set with the token's exp value. However, if session.keepalive is true, this value will be overriden with session.ttl upon the first request

OpenID does not use expiryTime for storing the token's exp value.

JWT does not use exp at all.

JWT throws a 500 error if the token has expired

If session.ttl is greater than the token's actual lifespan, Dashboards will pass an expired token to the backend. The backend validates and throws an authentication error. This error is not handled in Dashboards at the moment, and the user will see a 500 error message.

The security backend's role in regards to the lifespan (exp)

Based on my tests, the backend also validates the token lifespan and correctly returns a 401 if the token has expired.

In most cases, Dashboards also validates the token lifespan, and an expired token will not reach the backend. An exception to this is JWT, as mentioned earlier in this issue.

Somewhat confusingly, the backend then seems to log this message:

Algorithm of JWT does not match algorithm of JWK (HS512 != RS256)

in addition to

BadCredentialsException: The token has expired

Should we decide to remove the frontend (Dashboards) validation and just rely on the backend validation, we need to make sure a 401 is always handled gracefully by Dashboards. This is a bit different to unauthenticated requests because Dashboards has what it "thinks" are valid credentials.

Based on my tests, the only thing needed is to make sure to catch a 401 here: https://github.com/opensearch-project/security-dashboards-plugin/blob/2.11.1.0/server/auth/types/authentication_type.ts#L207 (If multitenancy is enabled, this is already handled)

A note about differences between IdPs

One thing worth to mention, but probably out of scope for this issue, is that there may be differences between how IdPs pass on the token lifespan. Related user comment: https://github.com/opensearch-project/security-dashboards-plugin/issues/159#issuecomment-1022438420

NB: In the case of SAML, OpenSearch security converts the SAMLResponse to a token.

Recommendations

  1. Make sure that the cookie.ttl setting is either used, or removed from the configuration options. I believe having a mechanism to control the browser cookie's lifetime may be expected by users, and the browser session as lifetime is probably a sensible default. Fixing this option is probably to be considered a breaking change though, since the default value of an hour would then start being applied. Hapi cookie also supports a cookie.keepAlive setting, which should be investigated.
  2. Make sure that the exp property is handled in the same way for SAML, OpenID and JWT in the code.
  3. Using session.ttl and session.keepalive may be redundant if cookie.ttl (and possibly cookie.keepAlive) are implemented. However, if there is information in the cookie that should exist even though the credentials are to be considered expired, using the session configuration may provide extra flexibility. Another option would be to introduce support for more than one cookie, and we could add a cookie with a longer lifespan.
  4. Either way, using the session config settings together with SAML, OpenID and JWT is probably redundant. One possible use case could be to use the session config settings as a fallback if the token for some reason does not contain the exp property. This would mean removing the general auth type agnostic check for expiryTime property, and leave the validation to each authentication type (which is already implemented, except for with the JWT authentication type)
  5. If we keep the session configuration settings, it may make sense to have them "off" by default - especially if we fix the cookie.ttl setting.

Request for comments

Please feel very welcome to share your thoughts about this. Are there missing considerations? Do you have suggestions for further improvements? Do you have questions?

peternied commented 6 months ago

Thanks for the detailed problem statement and recommendations, looks solid; here are some thoughts:

Somewhat confusingly, the backend then seems to log this message:

I think it would be very useful to correctly handle these cases and log actionable messages for OpenSearch cluster operators - should there be another recommendation or is it wrapped in one of the other bullets?

Make sure that the cookie.ttl setting is either used, or removed from the configuration options.

I think deprecating this setting in 2.x and removing in the 3.0 versions are a good path forward to remove this extraneous and non-functional option. If limiting the session length is an option we want to expose, it should be handled by the backend system to clearly separate responsibilities between FE/BE.

If we keep the session configuration settings...

I'm not as familiar with the different operator scenarios where different configurations are desirable. Maybe it would be good to connect with the user base via a community meeting or outreach of some kind. I'd love to work backward from what cluster operators need vs what is already implemented.

scrawfor99 commented 6 months ago

[Triage] Hi @jochen-kressin, thank you for filing this issue. This seems like a good change and something that should be improved.

shree1999 commented 3 months ago

Hi @derek-ho reaching out to you after integrating the latest release 2.13 into our build hoping to fix the session issue. But it seems keepAlive property under openearch_security.session doesn't keep the user session active even after OIDC token expiry time has reached. The user logs out from the opensearch exactly after 15 minutes, and also looses all the filters applied during the time

IDP Used: PingID ID Token expiry time: 15 minutes Refresh token expiry time: 90 days

Below is the config used in the opensearch opensearch_dashboard.yml

logging:
      verbose: true
    server:
      basePath: "/dashboards"
      rewriteBasePath: true
      host: 'https://some_url.com'
      ssl:
        enabled: false
    opensearch:
      username: ${KIBANA_USER}
      password: ${KIBANA_PASSWORD}
      hosts: [https://opensearch-cluster-master:9200]
      ssl:
        verificationMode: none
      requestHeadersWhitelist: ["Authorization", "security_tenant", "WWW-Authenticate"]
    opensearch_security:
      cookie:
        secure: true
        ttl: 86400000
      session:
        keepalive: true
        ttl: 86400000
      auth:
        type: "openid"
        multiple_auth_enabled: false
      openid:
        base_redirect_url: "https://some_url.com/dashboards"
        connect_url: "xxx"
        scope: openid offline_access
        client_id: ${CLIENT_ID}
        client_secret: ${CLIENT_SECRET}
        refresh_tokens: true
      multitenancy:
        enabled: false
      readonly_mode:
        roles: [kibana_read_only]

opensearch -> security config.yaml

_meta:
  type: "config"
  config_version: 2
config:
  dynamic:
    http:
      anonymous_auth_enabled: false
    authc:
      basic_internal_auth_domain:
        http_enabled: true
        transport_enabled: true
        order: 0
        http_authenticator:
          type: basic
          challenge: false
        authentication_backend:
          type: internal
      openid_auth_domain:
        http_enabled: true
        transport_enabled: true
        order: 1
        http_authenticator:
          type: openid
          challenge: false
          config:
            openid_connect_idp:
              enable_ssl: true
              verify_hostnames: false
              pemtrustedcas_filepath: "/usr/share/opensearch/config/certs/rootCa.pem"
            subject_key: sub
            openid_connect_url: "xxx"
        authentication_backend:
          type: noop
    authz: {}

Please let me know how to ensure the user session doesn't end after the token expiry has reached and he can continue until the ttl has reached.

javad87 commented 3 weeks ago

Versions : 2.12.0

Describe the issue: Is this Azure idp SAML expiration resolved in new OS? I'm using version 2.12.0 and Azure idp SAML, with SSO session gets expired after some times and needs log in. these are my dashboard.yml:

opensearch_security.cookie.ttl: 604800000 opensearch_security.session.ttl: 604800000 opensearch_security.session.keepalive: true

and in security config.yml file for saml_auth_domain, sp section I set: sp: forceAuthn: false

what should I do to extend session expiration to aviod relogin?

besides that anyone can help me how to decipher security_authentication_saml1, security_authentication token (btw I do not want to use log for seeing inside token, I already did it), as far as I know OS cahnges SAML response with light weight jwt and encrypt it with the exchange_key, but I could not decipher it with follwing python script:


import hashlib
import hmac
import json

def base64_url_decode(input):

input += '=' * (4 - (len(input) % 4))
return base64.urlsafe_b64decode(input)

def decode_jwt(token, key):
try:
 header_b64, payload_b64, signature_b64 = token.split('.')
 header = json.loads(base64_url_decode(header_b64).decode('utf-8'))
 payload = json.loads(base64_url_decode(payload_b64).decode('utf-8'))

 message = f"{header_b64}.{payload_b64}".encode('utf-8')
 expected_signature = base64_url_decode(signature_b64)
 computed_signature = hmac.new(key.encode('utf-8'), message, hashlib.sha256).digest()

 if not hmac.compare_digest(expected_signature, computed_signature):
   raise ValueError("Invalid token signature")

 return payload

except Exception as e:
 print("Error decoding the token:", str(e))
 return None

exchange_key = "...=="
token = "Fe26.2**c8ab634f300f834bc5db597554f904ca2117fdbff85afa1bff2bac1290ab779e*pL-KTe0QtxtJGjN5FSLbMw*X2s..."

decrypted_message = decode_jwt(token, exchange_key)
if decrypted_message:
 print("Decrypted message:", json.dumps(decrypted_message, indent=2))
else:
 print("Failed to decode the token.")```