diff --git a/lib/galaxy/authnz/custos_authnz.py b/lib/galaxy/authnz/custos_authnz.py index 8f7b65d5de8..3ff36fe5123 100644 --- a/lib/galaxy/authnz/custos_authnz.py +++ b/lib/galaxy/authnz/custos_authnz.py @@ -60,6 +60,7 @@ class CustosAuthnzConfiguration: redirect_uri: str ca_bundle: Optional[str] pkce_support: bool + accepted_audiences: List[str] extra_params: Optional[dict] authorization_endpoint: Optional[str] token_endpoint: Optional[str] @@ -68,6 +69,8 @@ class CustosAuthnzConfiguration: iam_client_secret: Optional[str] userinfo_endpoint: Optional[str] credential_url: Optional[str] + issuer: Optional[str] + jwks_uri: Optional[str] class OIDCAuthnzBase(IdentityProvider): @@ -84,6 +87,7 @@ class OIDCAuthnzBase(IdentityProvider): redirect_uri=oidc_backend_config["redirect_uri"], ca_bundle=oidc_backend_config.get("ca_bundle", None), pkce_support=oidc_backend_config.get("pkce_support", False), + accepted_audiences=oidc_backend_config.get("accepted_audiences", []), extra_params={}, authorization_endpoint=None, token_endpoint=None, @@ -92,6 +96,8 @@ class OIDCAuthnzBase(IdentityProvider): iam_client_secret=None, userinfo_endpoint=None, credential_url=None, + issuer=None, + jwks_uri=None, ) def _decode_token_no_signature(self, token): @@ -457,6 +463,12 @@ class OIDCAuthnzBase(IdentityProvider): self.config.token_endpoint = well_known_oidc_config["token_endpoint"] self.config.userinfo_endpoint = well_known_oidc_config["userinfo_endpoint"] self.config.end_session_endpoint = well_known_oidc_config.get("end_session_endpoint") + self.config.issuer = well_known_oidc_config.get("issuer") + self.config.jwks_uri = well_known_oidc_config.get("jwks_uri") + if self.config.jwks_uri: + self.jwks_client = jwt.PyJWKClient(self.config.jwks_uri, cache_jwk_set=True, lifespan=360) + else: + self.jwks_client = None def _get_verify_param(self): """Return 'ca_bundle' if 'verify_ssl' is true and 'ca_bundle' is configured.""" @@ -587,3 +599,25 @@ class OIDCAuthnzBaseCustos(OIDCAuthnzBase): params = {"client_id": self.config.client_id} self._load_config(headers, params) + + def match_access_token_to_user(self, sa_session, access_token): + signing_key = self.jwks_client.get_signing_key_from_jwt(access_token) + decoded_jwt = jwt.decode( + access_token, + signing_key.key, + algorithms=["RS256"], + issuer=self.config["issuer"], + audience=self.config["accepted_audiences"], + options={ + "verify_signature": True, + "verify_exp": True, + "verify_nbf": True, + "verify_iat": True, + "verify_aud": self.config["accepted_audiences"] is not None, + "verify_iss": True, + }, + ) + # jwt verified, we can now fetch the user + user_id = decoded_jwt["sub"] + custos_authnz_token = self._get_custos_authnz_token(sa_session, user_id, self.config["provider"]) + return custos_authnz_token.user diff --git a/lib/galaxy/authnz/managers.py b/lib/galaxy/authnz/managers.py index d766efcd111..655c1a43d2c 100644 --- a/lib/galaxy/authnz/managers.py +++ b/lib/galaxy/authnz/managers.py @@ -404,6 +404,33 @@ class AuthnzManager: log.exception(msg) return False, msg, (None, None) + def find_user_by_access_token_in_provider(self, sa_session, provider, access_token): + try: + success, message, backend = self._get_authnz_backend(provider) + if success is False: + msg = f"An error occurred when obtaining user by token with provider `{provider}`: {message}" + log.error(msg) + return None + user = backend.match_access_token_to_user(sa_session, access_token) + if user: + log.debug(f"Found user: {user} via `{provider}` identity provider") + return user + return None + except Exception as e: + msg = f"An error occurred when finding user by token: {e}" + log.error(msg) + return None + + def find_user_by_access_token(self, sa_session, access_token): + # decoded_token = jwt.decode(access_token, options={"verify_signature": False}) + # issuer = decoded_token["iss"] + # audience = decoded_token["aud"] + for provider in self.oidc_backends_config: + user = self.find_user_by_access_token_in_provider(sa_session, provider, access_token) + if user: + return user + return None + def logout(self, provider, trans, post_user_logout_href=None): """ Log the user out of the identity provider. diff --git a/lib/galaxy/managers/users.py b/lib/galaxy/managers/users.py index 65ef2a7919f..3aba815df54 100644 --- a/lib/galaxy/managers/users.py +++ b/lib/galaxy/managers/users.py @@ -295,6 +295,13 @@ class UserManager(base.ModelManager, deletable.PurgableManagerMixin): raise exceptions.AuthenticationFailed("Provided API key has expired.") return provided_key.user + def by_oidc_access_token(self, access_token: str): + if hasattr(self.app, "authnz_manager") and self.app.authnz_manager: + user = self.app.authnz_manager.find_user_by_access_token(self.app.model.session, access_token) # type: ignore[attr-defined] + return user + else: + return None + def check_bootstrap_admin_api_key(self, api_key): bootstrap_admin_api_key = getattr(self.app.config, "bootstrap_admin_api_key", None) if not bootstrap_admin_api_key: diff --git a/lib/galaxy/webapps/base/webapp.py b/lib/galaxy/webapps/base/webapp.py index 8fc17986afe..f727f091d15 100644 --- a/lib/galaxy/webapps/base/webapp.py +++ b/lib/galaxy/webapps/base/webapp.py @@ -532,6 +532,8 @@ class GalaxyWebTransaction(base.DefaultWebTransaction, context.ProvidesHistoryCo """ Authenticate for the API via key or session (if available). """ + oidc_access_token = self.request.headers.get("Authorization", None) + oidc_token_supplied = self.environ.get("is_api_request", False) and oidc_access_token and "Bearer " in oidc_access_token api_key = self.request.params.get("key", None) or self.request.headers.get("x-api-key", None) secure_id = self.get_cookie(name=session_cookie) api_key_supplied = self.environ.get("is_api_request", False) and api_key @@ -554,6 +556,14 @@ class GalaxyWebTransaction(base.DefaultWebTransaction, context.ProvidesHistoryCo ) self.user = None self.galaxy_session = None + elif oidc_token_supplied: + # Sessionless API transaction with oidc token, we just need to associate a user. + oidc_access_token = oidc_access_token.replace("Bearer ", "") + try: + user = self.user_manager.by_oidc_access_token(oidc_access_token) + except AuthenticationFailed as e: + return str(e) + self.set_user(user) else: # Anonymous API interaction -- anything but @expose_api_anonymous will fail past here. self.user = None diff --git a/lib/galaxy/webapps/galaxy/api/__init__.py b/lib/galaxy/webapps/galaxy/api/__init__.py index b6a44a1f43a..55735e901f6 100644 --- a/lib/galaxy/webapps/galaxy/api/__init__.py +++ b/lib/galaxy/webapps/galaxy/api/__init__.py @@ -36,6 +36,7 @@ from fastapi.security import ( APIKeyCookie, APIKeyHeader, APIKeyQuery, + HTTPBearer ) from fastapi_utils.cbv import cbv from fastapi_utils.inferring_router import InferringRouter @@ -80,6 +81,7 @@ from galaxy.work.context import ( api_key_query = APIKeyQuery(name="key", auto_error=False) api_key_header = APIKeyHeader(name="x-api-key", auto_error=False) api_key_cookie = APIKeyCookie(name="galaxysession", auto_error=False) +api_bearer_token = HTTPBearer() def get_app() -> StructuredApp: @@ -139,6 +141,7 @@ def get_api_user( user_manager: UserManager = depends(UserManager), key: str = Security(api_key_query), x_api_key: str = Security(api_key_header), + bearer_token: str = Security(api_bearer_token), run_as: Optional[DecodedDatabaseIdField] = Header( default=None, title="Run as User", @@ -149,9 +152,12 @@ def get_api_user( ), ) -> Optional[User]: api_key = key or x_api_key - if not api_key: + if api_key: + user = user_manager.by_api_key(api_key=api_key) + elif bearer_token: + user = user_manager.by_oidc_access_token(access_token=bearer_token.credentials) + else: return None - user = user_manager.by_api_key(api_key=api_key) if run_as: if user_manager.user_can_do_run_as(user): return user_manager.by_id(run_as) diff --git a/test/integration/oidc/keycloak-server.crt.pem b/test/integration/oidc/keycloak-server.crt.pem index fd4b489661a..d9bdfb3a6a7 100644 --- a/test/integration/oidc/keycloak-server.crt.pem +++ b/test/integration/oidc/keycloak-server.crt.pem @@ -1,20 +1,22 @@ -----BEGIN CERTIFICATE----- -MIIDVzCCAj+gAwIBAgIUCYRtnqxm/HMxGPpE6VpTmPacZMQwDQYJKoZIhvcNAQEL -BQAwOzELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxFzAVBgNVBAoM -DkdhbGF4eSBQcm9qZWN0MB4XDTIzMDkxMDE5MjAwOFoXDTMzMDkwNzE5MjAwOFow -OzELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxFzAVBgNVBAoMDkdh -bGF4eSBQcm9qZWN0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwF+F -iXMW2emcYCvSkiZCh00RDVCcKUfB/i7Q+Rxl0y0HG6sTJWN8ELf4SVR7Zaq3b30w -5Yx6tmXY0lfzHkfjRXG/WdAjPDeN8j54RK74UAOM72KFySm/CxX2w5peNyBHA4BX -mHKGagIgWH4VUCeJ0z7GkI0ESrdlwOix8Qi8n/l0hjmmS2NOvM1dq1ehNVfOtB4K -DF1AANffiCEz2OkAIWwczwVZrgHg7ILvga+g4GnX6SEioEHMCfyk7E6ASbLb2mDj -iaIVT1PpwtRLgL+yhC7QRWK66IuurcRAJsLL/isIeZGtF0xfvP5nfiWC07lzYior -J3M1I14I7LTpEwmEIwIDAQABo1MwUTAdBgNVHQ4EFgQUbAYXkXuWm8xrOWmsbN5w -rQoaLG4wHwYDVR0jBBgwFoAUbAYXkXuWm8xrOWmsbN5wrQoaLG4wDwYDVR0TAQH/ -BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAhvHjMe20W7TO299iI1fEhob4Ihyr -tm/4hPPBLC9HSiKokKYvgOU0dK2dh/X7ax9N+ouuDmWYCLCP7AcaWS+HHDVyCv+0 -j8YNVTaqoD/u9qnvx9/2Zv1TqGaLhw9UPmro0MnUUOMoQRYQKaH3fatidCMQJt2j -rzvyrkOuicOr1lRh8Hj1+pdKzar4rmskz9SLkoKRiXjsAUrC1JvQxVxORzf4CoD/ -eR+ZXeglmAZu8AbwFd/y5OPaHuqsXXHpzg8oJffymOo8L3jp3fZjHFhzUBIHZX2u -ZVmpT0COKu69yQ1EoHmKpVFCinYVqbfOsu7s4qWOIt8qpoHPdaElzMD/2w== +MIIDkzCCAnugAwIBAgIUQFOsGXws9PtauKu5YhjKynETHVowDQYJKoZIhvcNAQEL +BQAwWTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDESMBAGA1UEAwwJbG9jYWxob3N0MB4X +DTIzMTAxNTIwMzYyNloXDTMzMTAxMjIwMzYyNlowWTELMAkGA1UEBhMCQVUxEzAR +BgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5 +IEx0ZDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEA1rp3CEgQRHmpgK4NFwqxfnhfwJMJeH9rFlSQ1XjN1JPCoICSwNDO +g1NWoPGFIImRnZ34X9CLsLeZdlcE/RaThdzwLbwbExl0mKP+rDSLP5/90K3utVD8 +DWfNSWnCaeXcnhaosl5z8ZhpI5xWN20ajCj3VnrPEbhVDAChxlZvVh+a6uvsEMP0 +SHOcy0OKe0g2I+x1Yw5J62ApC21pDMuFrjyualkpFEx+b7JY9lNhDjSPW8Iu4wRC +7TK0q3MRyyw+er0P7lPobw/khfVTTFmDrc1V8ui+271Bu8L5EvlcOADGM47jgt5j +Y3Q77gy7fVKDwFSS6Y2jpQRBgocUq9qN1QIDAQABo1MwUTAdBgNVHQ4EFgQUHlYS +w56dunNy35FN3D1JG3ZaZRAwHwYDVR0jBBgwFoAUHlYSw56dunNy35FN3D1JG3Za +ZRAwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAIoEdTs1B1h9Q +WOO+zK5kcd6pbOJnfqvZh44TvQs0DnezENQb4y1lR0USPjgyWSF1VHxP1yrm3H64 +tx5E7ZeVMO/Uq2x2x9qgEjJfk9y88+RzYmZyR3PvmtTPaf7ftokGi9eZSsj3khP3 +8YqKDpty5siA+Wd5HkAE/z1xoBAzxQ+D9JNA8miiCkmSMUWuTHXbH8nYaS7a4rWx +YvJJXxdAv7JsoLvSVgMgaiee+MtKhMPxDBJoCeuk8nGt8HXZh615JqYSkNDrfzwQ +C74INkovM6emUW9ehQ6sOK5ROprM7fXlnrH4Klc9BGRgJu5Lsj0VkrBfN2LQ03D2 +77yHVwzrgQ== -----END CERTIFICATE----- diff --git a/test/integration/oidc/keycloak-server.key.pem b/test/integration/oidc/keycloak-server.key.pem index a58818417e8..bc06d6c86ee 100644 --- a/test/integration/oidc/keycloak-server.key.pem +++ b/test/integration/oidc/keycloak-server.key.pem @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDAX4WJcxbZ6Zxg -K9KSJkKHTRENUJwpR8H+LtD5HGXTLQcbqxMlY3wQt/hJVHtlqrdvfTDljHq2ZdjS -V/MeR+NFcb9Z0CM8N43yPnhErvhQA4zvYoXJKb8LFfbDml43IEcDgFeYcoZqAiBY -fhVQJ4nTPsaQjQRKt2XA6LHxCLyf+XSGOaZLY068zV2rV6E1V860HgoMXUAA19+I -ITPY6QAhbBzPBVmuAeDsgu+Br6DgadfpISKgQcwJ/KTsToBJstvaYOOJohVPU+nC -1EuAv7KELtBFYrroi66txEAmwsv+Kwh5ka0XTF+8/md+JYLTuXNiKisnczUjXgjs -tOkTCYQjAgMBAAECggEAR4Jah8z11hwuSluP408AlP3zVEE+Q2QOuzdY6/j/6t+G -kGTn8ZKJGEfirhOO6SxsEq/1QKcXKH00e4BA+eUcGQDe8fpaVrSJ+d355AIsgMXJ -GmVDuRLhFrRb7bsQwaooa6dGKH+N8pMGxg5cStbft1xTAIHC8ik5VFvLZX9raoVw -DyfLPg3CtWQID9Jbb4lODqHpjSP5J5v+IskCbbk1sihelEoyFQxka8y1UYglSB7E -ntz5j83OqH7qFes/J/g3K8eNvhd3RQZCozzCwjaUUNHpAeTOEmQqdu9joVvQu1pp -/iQcwqCb0TSzezUssCRGJNHCVzb0UwBgqCH4stt+8QKBgQD/NZSj0ofY8GA1XMcK -s1R9dc/iz6FTeFOz4/36xoTSWoLUFVUpP+1XVVzjV98j65D+CtC6RDBso3tf5Jeo -JB0u4Jy86iyVVN8VMgxDLmNsNSPWOdQCfa6ASQRjnlSwa99L+fZ3ITjz71fDR+0E -RKMEzRAu9CYS7X6dy2/6ccRP0QKBgQDA+Bo7VE3HMeSQkT8hJWZpEmFseJ8AdM+/ -2+dwQRFvyBO25hlaaedF3fv52D5S3hiM40tHxSyRmd0qJBQAF5gXVMAtsDD/SZru -maAQXGR0gorGuWkBkyKHDpZmYnK4TWAE5Tikp2638Zrtik6gSZtJVaBqz9oj1Z56 -8elxUxSlswKBgA6lU4dnQJaJjOvjDCUoD3Q3u5tnkPmKUTtknSG0HNE5koJ7SuzY -F1XIt1DwrQSRsztxAXN/EExbwWvKYY2rrOUQzM3mnnJfcYH04lFEqz39erYDkZAn -CEmiZxcUcgXB2wmwKZ3CkVMmbCHv0cFJ8xUGc6ROLJUvffGuSBnWR47RAoGBAJr0 -4IOyD/8cVpGswUgJs3I+m1MyjWQdaVKE8RXYensL7KDbp5JOJrSZLKWV+34p8Qb5 -4tLQSBjnuI0FibYFPFHH8cb+jGoEHlDptnpM5zIUPgKUkvj7wSc5FJdFCSBsqeVk -eJboCOXXMvT7MmZOlU9PXlFHdcF+4hOSPUaOU683AoGAdj+MsVxRgnCEmUCl05EJ -kd5H8bBFB/vq+Rn3erJFXKLM3vpjBV3Yi2RGC06YGe4L1+krrMIrnEFno3G5yi4Z -UWuakpvEDV+esu9s/EcZxv4WtHkQQ+2g3CGqy5BqDefe5YOJrQaCVuwIga1WIV0q -JNY3E9bDmpvvDBw+SEzcJug= +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDWuncISBBEeamA +rg0XCrF+eF/Akwl4f2sWVJDVeM3Uk8KggJLA0M6DU1ag8YUgiZGdnfhf0Iuwt5l2 +VwT9FpOF3PAtvBsTGXSYo/6sNIs/n/3Qre61UPwNZ81JacJp5dyeFqiyXnPxmGkj +nFY3bRqMKPdWes8RuFUMAKHGVm9WH5rq6+wQw/RIc5zLQ4p7SDYj7HVjDknrYCkL +bWkMy4WuPK5qWSkUTH5vslj2U2EONI9bwi7jBELtMrSrcxHLLD56vQ/uU+hvD+SF +9VNMWYOtzVXy6L7bvUG7wvkS+Vw4AMYzjuOC3mNjdDvuDLt9UoPAVJLpjaOlBEGC +hxSr2o3VAgMBAAECggEAAVbB4RtEyGmqE9WvtexIygrgP68UKxw/tgwl8QtjASwc +HsenmXV4bC2ix7ey9E573FBhWQS9hIcRsBxtglIzN22JCGyLDmOlUNRWqdQUC6CW +i+yht5+Rb1g/2af6qdsXa7buBshpRjyW1hjvq9SpHO4wv3uCsgapbjzxV5PmQIlt +H59GPNcMJTnBLktPfLQ3fNZRjOiE/aFvGoe1YyFNbl5RADzq5YkmuUSFhkBbyNz4 +wOzhfPr8cnthStFY17GGKmu9nueFoiQhmaiUIIIgeHuSg+0tMZa/PrGsnbfdhoKZ +ifvv5Fyi44COEpzcDjfQqQlHAMRPzWnJNdI4phRzoQKBgQDZDJYAt+Eq8sU6ZLVG +W47k6WouDro9ndf7ZLd0j39KwQ2GAxyHvL8JNmphVa2T8yrvG6QRTrUJ/t2WpXYl +sOfTwPXZh4udMLA+ia0fBHB1X7c2grPlQILLhcvqP5ZUAT3+iOg2j17Aa91B+36g +w/OEwgBWfIt4rW8BBfFfjpXtZQKBgQD9Q0KhqoFs2YZk4Dq5tgl5YMFPBKJUbmED +B4cRGDqPtI7R9kGDTQ06boLZ1W72iX4eroPKhosYVIJ0ILRUvdApbc+ZVi5iRp7Q +lR3fkHdGwJznPH4SLgl6XU3O0JRTSeELDGT5dgJSM2hY7kHLSyZVIsrhsfIxwxVr +sLa0hd6PsQKBgCRmjvWZ4QJh6p/LafddvY44cx08TRGuWh2IG5hJxJBV5h22gd9l +0SgLXzXpt42bd3TYTuC0MXp2qtTr9O+HRqnlr5WGvOxk0Pn7/vO3u+CDZ+eVbfQh +qI3XZ4ZmmaCHAM2iSkd0LfDZZib9tZPiqQF3w7S9eGqJs5cZVwyujghdAoGBAM75 +g9/JD4KcdtfD5FpXKYD3kKN6HwuvNTsbkMdorJs6rSlr/fhHJRNmsytA4TE8BkUe +y/fcYppcnkw3WgiTIyZoZqtiof+QxUOjScmmL8Qzr0bOyh0jYH2O+QKWtVEn5HF6 +DJ9OUPFKr/FVrZFtdgNHrtQp78u5Ka1busTnTo7RAoGAQFdl7sIDPaO0OIXJEuk9 +89OBMPQbuMdyQ63FFl8SADCBLuqZLxxNQArVdelPPCVCgi6sL3w23GrGG0LibfrM +Ts8FQ5cXXFwa+qvYjskazBsiDAJ5LjHWXFQ9ZAg1dpYwToTalaXKer4TBgxMTnte +e05IvlRad2fy9xnaBHoQ0wY= -----END PRIVATE KEY----- diff --git a/test/integration/oidc/test_auth_oidc.py b/test/integration/oidc/test_auth_oidc.py index 8b3e3bfc213..1dfd4524666 100644 --- a/test/integration/oidc/test_auth_oidc.py +++ b/test/integration/oidc/test_auth_oidc.py @@ -18,12 +18,13 @@ KEYCLOAK_ADMIN_USERNAME = "admin" KEYCLOAK_ADMIN_PASSWORD = "admin" KEYCLOAK_TEST_USERNAME = "gxyuser" KEYCLOAK_TEST_PASSWORD = "gxypass" +KEYCLOAK_URL = "https://localhost:8443/realms/gxyrealm" -OIDC_BACKEND_CONFIG_TEMPLATE = """ +OIDC_BACKEND_CONFIG_TEMPLATE = f""" - https://localhost:8443/realms/gxyrealm + {KEYCLOAK_URL} gxyclient gxytestclientsecret $galaxy_url/authnz/keycloak/callback @@ -125,6 +126,8 @@ class AbstractTestCases: if "OAUTHLIB_INSECURE_TRANSPORT" in os.environ: cls.saved_oauthlib_insecure_transport = os.environ["OAUTHLIB_INSECURE_TRANSPORT"] os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "true" + os.environ["REQUESTS_CA_BUNDLE"] = os.path.dirname(__file__) + "/keycloak-server.crt.pem" + os.environ["SSL_CERT_FILE"] = os.path.dirname(__file__) + "/keycloak-server.crt.pem" @classmethod def restoreOauthlibHttps(cls): @@ -189,3 +192,22 @@ class TestGalaxyOIDCLoginIntegration(AbstractTestCases.BaseKeycloakIntegrationTe # make sure we can no longer request the user response = session.get(self._api_url("users/current")) self._assert_status_code_is(response, 400) + + def test_auth_by_access_token(self): + # login at least once + self._login_via_keycloak(KEYCLOAK_TEST_USERNAME, KEYCLOAK_TEST_PASSWORD) + access_token = self.get_keycloak_access_token() + response = self._get("users/current", headers={"Authorization": f"Bearer {access_token}"}) + self._assert_status_code_is(response, 200) + assert response.json()["email"] == "gxyuser@galaxy.org" + + def get_keycloak_access_token(self, username=KEYCLOAK_TEST_USERNAME, password=KEYCLOAK_TEST_PASSWORD): + data = { + "client_id": "gxyclient", + "client_secret": "gxytestclientsecret", + "grant_type": "password", + "username": username, + "password": password + } + response = requests.post(f"{KEYCLOAK_URL}/protocol/openid-connect/token", data=data, verify=False) + return response.json()["access_token"]