Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
59a9c9e
Remove defunct custos auth provider
nuwang Oct 16, 2025
666491a
Also remove defunct custos vault
nuwang Oct 16, 2025
3811bf1
Remove unused variable and fix test
nuwang Oct 17, 2025
2e54f52
Run make client-format
nuwang Oct 17, 2025
bb6bbd2
Fix schema entry
nuwang Nov 3, 2025
5ddc596
Merge keycloak and cilogon implementations into PSA
nuwang Nov 2, 2025
c18cd60
Replace get_jwks_keys with PSA implementation
nuwang Nov 2, 2025
0fbc404
Refactor and introduce generic oidc.py
nuwang Nov 2, 2025
cd910f2
Port over fixed_delegated_auth
nuwang Nov 2, 2025
a157583
Refactor and simplify oidc tests
nuwang Nov 2, 2025
017909f
Add more tests for fixed_delegated_auth
nuwang Nov 2, 2025
9c4cd95
Add tests to ensure that account linking works with logged in user wi…
nuwang Nov 2, 2025
6d1e230
Add misisng test file auth_conf_empty.xml
nuwang Nov 2, 2025
e99445c
Reformat code
nuwang Nov 2, 2025
8a146ab
Fix linting errors
nuwang Nov 3, 2025
4073719
Fix mypy type errors
nuwang Nov 3, 2025
a088b1c
Add missing property in test
nuwang Nov 3, 2025
25c669a
Add migration scripts and tests
nuwang Nov 3, 2025
73fcd29
Update doc/source/releases/22.05_announce.rst
nuwang Nov 3, 2025
f8d07f6
Fix alembic down revision and lint errors
nuwang Nov 3, 2025
7cad991
Fix mypy type errors in migration scripts
nuwang Nov 3, 2025
b7006d4
Run isort to fix import error
nuwang Nov 4, 2025
a4b3a1d
Refactor migration scripts
nuwang Nov 6, 2025
b4e9045
Drop custos table on upgrade and restore data on downgrade
nuwang Nov 6, 2025
f6c00a8
Remove CustosAuthnzToken and references from model
nuwang Nov 6, 2025
106cd47
Fix formatting error in custos_to_psa.py
nuwang Nov 6, 2025
f26f16d
Simplify migration by reusing psa table from model
nuwang Nov 10, 2025
9564663
Fix table return type to satisfy mypy
nuwang Nov 10, 2025
e087a17
Fix issue where account linked message is displayed repeatedly
nuwang Nov 11, 2025
d5aedfa
Fix incorrect logout param
nuwang Nov 11, 2025
ee8cde7
Make sure expires is migrated from custps
nuwang Nov 11, 2025
1177e7e
Use iat frmo acccess_token if possible and add more tests for migration
nuwang Nov 11, 2025
dd51d9a
Refactor code to better separate oidc and non-oidc providers
nuwang Nov 12, 2025
e7cc7e9
Remove unused imports and make format
nuwang Nov 12, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion client/src/components/Login/LoginForm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ describe("LoginForm", () => {
axiosMock = new MockAdapter(axios);
server.use(
http.get("/api/configuration", ({ response }) => {
return response.untyped(HttpResponse.json({ oidc: { cilogon: false, custos: false } }));
return response.untyped(HttpResponse.json({ oidc: { cilogon: false } }));
}),
);
});
Expand Down
16 changes: 8 additions & 8 deletions client/src/components/Register/RegisterForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ interface Props {
enableOidc?: boolean;
mailingJoinAddr?: string;
oidcIdps?: OIDCConfig;
preferCustosLogin?: boolean;
preferOidcLogin?: boolean;
redirect?: string;
registrationWarningMessage?: string;
serverMailConfigured?: boolean;
Expand All @@ -58,8 +58,8 @@ const labelSubscribe = ref(localize("Stay in the loop and join the galaxy-announ

const idpsWithRegistration = computed(() => (props.oidcIdps ? getOIDCIdpsWithRegistration(props.oidcIdps) : {}));

const custosPreferred = computed(() => {
return props.enableOidc && props.preferCustosLogin;
const oidcPreferred = computed(() => {
return props.enableOidc && props.preferOidcLogin;
});

/** This decides if all register options should be displayed in column style
Expand Down Expand Up @@ -107,30 +107,30 @@ async function submit() {

<BForm id="registration" @submit.prevent="submit()">
<BCard no-body>
<!-- OIDC and Custos enabled and prioritized: encourage users to use it instead of local registration -->
<span v-if="custosPreferred">
<!-- OIDC enabled and prioritized: encourage users to use it instead of local registration -->
<span v-if="oidcPreferred">
<BCardHeader v-b-toggle.accordion-oidc role="button">
Register using institutional account
</BCardHeader>

<BCollapse id="accordion-oidc" visible role="tabpanel" accordion="registration_acc">
<BCardBody>
Create a Galaxy account using an institutional account (e.g.:Google/JHU). This will
redirect you to your institutional login through Custos.
redirect you to your institutional login through OIDC.
<ExternalLogin class="mt-2" />
</BCardBody>
</BCollapse>
</span>

<!-- Local Galaxy Registration -->
<BCardHeader v-if="!custosPreferred" v-localize>Create a Galaxy account</BCardHeader>
<BCardHeader v-if="!oidcPreferred" v-localize>Create a Galaxy account</BCardHeader>
<BCardHeader v-else v-localize v-b-toggle.accordion-register role="button">
Or, register with email
</BCardHeader>

<BCollapse
id="accordion-register"
:visible="!custosPreferred"
:visible="!oidcPreferred"
role="tabpanel"
accordion="registration_acc">
<BCardBody :class="{ 'd-flex w-100': !registerColumnDisplay }">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export type OIDCConfigWithRegistration = Record<

/** Return the per-IDP config, minus anything the caller wants to hide. */
export function getFilteredOIDCIdps(oidcConfig: OIDCConfig, exclude: string[] = []): OIDCConfig {
const blacklist = new Set(["cilogon", "custos", ...exclude]);
const blacklist = new Set(["cilogon", ...exclude]);
const filtered: OIDCConfig = {};
Object.entries(oidcConfig).forEach(([idp, cfg]) => {
if (!blacklist.has(idp)) {
Expand All @@ -51,11 +51,11 @@ export function getOIDCIdpsWithRegistration(oidcConfig: OIDCConfig): OIDCConfigW

/** Do we need to show the institution picker at all? */
export const getNeedShowCilogonInstitutionList = (cfg: OIDCConfig): boolean => {
return Boolean(cfg.cilogon || cfg.custos);
return Boolean(cfg.cilogon);
};

/**
* Generic OIDC login (all providers *except* CILogon/Custos).
* Generic OIDC login (all providers *except* CILogon).
* Returns the redirect URI Galaxy gives back, or throws.
*/
export async function submitOIDCLogon(idp: string, redirectParam: string | null = null): Promise<string | null> {
Expand All @@ -73,8 +73,8 @@ export async function submitOIDCLogon(idp: string, redirectParam: string | null
}

/**
* CILogon/Custos login.
* @param idp "cilogon" | "custos"
* CILogon login.
* @param idp "cilogon"
* @param useIDPHint If true, append ?idphint=
* @param idpHint The entityID to hint with (ignored when useIDPHint = false)
*/
Expand Down Expand Up @@ -108,7 +108,7 @@ export async function redirectToSingleProvider(config: OIDCConfig): Promise<stri
throw new Error("OIDC provider key is undefined.");
}

if (idp === "cilogon" || idp === "custos") {
if (idp === "cilogon") {
const redirectUri = await submitCILogon(idp, false);
return redirectUri;
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,6 @@ export default {
doomedItem: null,
errorMessage: null,
enable_oidc: galaxy.config.enable_oidc,
cilogonOrCustos: null,
userEmail: galaxy.user.get("email"),
};
},
Expand Down
40 changes: 12 additions & 28 deletions client/src/components/User/ExternalIdentities/ExternalLogin.vue
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ const messageVariant = ref<string | null>(null);
const cILogonIdps = ref<Idp[]>([]);
const selected = ref<Idp | null>(null);
const rememberIdp = ref(false);
const cilogonOrCustos = ref<"cilogon" | "custos" | null>(null);
const cilogon = ref<"cilogon" | null>(null);
const toggleCilogon = ref(false);

const oIDCIdps = computed<OIDCConfig>(() => (isConfigLoaded.value ? config.value.oidc : {}));
Expand All @@ -60,22 +60,21 @@ const filteredOIDCIdps = computed(() => getFilteredOIDCIdps(oIDCIdps.value, prop
const cilogonListShow = computed(() => getNeedShowCilogonInstitutionList(oIDCIdps.value));

const cILogonEnabled = computed(() => oIDCIdps.value.cilogon);
const custosEnabled = computed(() => oIDCIdps.value.custos);

onMounted(async () => {
rememberIdp.value = getIdpPreference() !== null;

// Only fetch CILogonIDPs if custos/cilogon configured
// Only fetch CILogonIDPs if cilogon configured
if (cilogonListShow.value) {
await getCILogonIdps();
}
});

function toggleCILogon(idp: "cilogon" | "custos") {
if (cilogonOrCustos.value === idp || cilogonOrCustos.value === null) {
function toggleCILogon(idp: "cilogon") {
if (cilogon.value === idp || cilogon.value === null) {
toggleCilogon.value = !toggleCilogon.value;
}
cilogonOrCustos.value = toggleCilogon.value ? idp : null;
cilogon.value = toggleCilogon.value ? idp : null;
}

async function clickOIDCLogin(idp: string) {
Expand Down Expand Up @@ -186,7 +185,7 @@ function getIdpPreference() {
<!-- OIDC login-->
<BForm v-if="cilogonListShow" id="externalLogin" class="cilogon">
<div v-if="props.loginPage">
<!--Only Display if CILogon/Custos is configured-->
<!--Only Display if CILogon is configured-->
<BFormGroup label="Use existing institutional login">
<Multiselect
v-model="selected"
Expand All @@ -212,31 +211,16 @@ function getIdpPreference() {
<LoadingSpan v-if="loading" message="Signing In" />
<span v-else>Sign in with Institutional Credentials*</span>
</GButton>
<!--convert to v-else-if to allow only one or the other. if both enabled, put the one that should be default first-->
<GButton
v-if="Object.prototype.hasOwnProperty.call(oIDCIdps, 'custos')"
:disabled="loading || selected === null"
@click="clickCILogin('custos')">
<LoadingSpan v-if="loading" message="Signing In" />
<span v-else>Sign in with Custos*</span>
</GButton>
</div>

<div v-else>
<GButtonGroup class="w-100">
<GButton
v-if="cILogonEnabled"
:pressed="cilogonOrCustos === 'cilogon'"
:pressed="cilogon === 'cilogon'"
@click="toggleCILogon('cilogon')">
Sign in with Institutional Credentials*
</GButton>

<GButton
v-if="custosEnabled"
:pressed="cilogonOrCustos === 'custos'"
@click="toggleCILogon('custos')">
Sign in with Custos*
</GButton>
</GButtonGroup>

<BFormGroup v-if="toggleCilogon" class="mt-1">
Expand All @@ -254,18 +238,18 @@ function getIdpPreference() {
v-if="toggleCilogon"
class="mt-1"
:disabled="loading || selected === null"
@click="clickCILogin(cilogonOrCustos)">
Login via {{ cilogonOrCustos === "cilogon" ? "CILogon" : "Custos" }} *
@click="clickCILogin(cilogon)">
Login via CILogon *
</GButton>
</BFormGroup>
</div>

<p class="mt-3">
<small class="text-muted">
* Galaxy uses CILogon via Custos to enable you to log in from this organization. By clicking
'Sign In', you agree to the
* Galaxy uses CILogon to enable you to log in from this organization. By clicking 'Sign In', you
agree to the
<a href="https://ca.cilogon.org/policy/privacy">CILogon</a> privacy policy and you agree to
share your username, email address, and affiliation with CILogon, Custos, and Galaxy.
share your username, email address, and affiliation with CILogon and Galaxy.
</small>
</p>
</BForm>
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/User/ExternalIdentities/service.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const getUrl = (path) => getAppRoot() + path;
export async function disconnectIdentity(doomed) {
if (doomed) {
let url;
if (doomed.provider === "custos" || doomed.provider === "cilogon") {
if (doomed.provider === "cilogon") {
url = getUrl(`authnz/${doomed.provider}/disconnect/${doomed.email}`);
} else {
url = getUrl(`authnz/${doomed.provider}/disconnect/`);
Expand Down
2 changes: 1 addition & 1 deletion client/src/entry/analysis/modules/Login.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ beforeEach(() => {
allow_local_account_creation: true,
enable_oidc: true,
mailing_join_addr: "mailing_join_addr",
prefer_custos_login: true,
prefer_oidc_login: true,
registration_warning_message: "registration_warning_message",
server_mail_configured: true,
show_welcome_with_login: true,
Expand Down
4 changes: 2 additions & 2 deletions client/src/entry/analysis/modules/Register.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ beforeEach(() => {
allow_local_account_creation: true,
enable_oidc: true,
mailing_join_addr: "mailing_join_addr",
prefer_custos_login: true,
prefer_oidc_login: true,
registration_warning_message: "registration_warning_message",
server_mail_configured: true,
show_welcome_with_login: true,
Expand Down Expand Up @@ -59,7 +59,7 @@ describe("Register", () => {
expect(props.sessionCsrfToken).toBe("session_csrf_token");
expect(props.enableOidc).toBe(true);
expect(props.mailingJoinAddr).toBe("mailing_join_addr");
expect(props.preferCustosLogin).toBe(true);
expect(props.preferOidcLogin).toBe(true);
expect(props.serverMailConfigured).toBe(true);
expect(props.registrationWarningMessage).toBe("registration_warning_message");
expect(props.termsUrl).toBe("terms_url");
Expand Down
2 changes: 1 addition & 1 deletion client/src/entry/analysis/modules/Register.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const sessionCsrfToken = computed(() => {
:enable-oidc="config.enable_oidc"
:mailing-join-addr="config.mailing_join_addr"
:oidc-idps="config.oidc"
:prefer-custos-login="config.prefer_custos_login"
:prefer-oidc-login="config.prefer_oidc_login"
:registration-warning-message="config.registration_warning_message"
:server-mail-configured="config.server_mail_configured"
:session-csrf-token="sessionCsrfToken"
Expand Down
4 changes: 2 additions & 2 deletions doc/source/admin/galaxy_options.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3817,11 +3817,11 @@


~~~~~~~~~~~~~~~~~~~~~~~
``prefer_custos_login``
``prefer_oidc_login``
~~~~~~~~~~~~~~~~~~~~~~~

:Description:
Controls the order of the login page to prefer Custos-based login
Controls the order of the login page to prefer OIDC-based login
and registration.
:Default: ``false``
:Type: bool
Expand Down
16 changes: 1 addition & 15 deletions doc/source/admin/special_topics/vault.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ There are currently 3 supported backends.
| Backend | Description |
|-------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| hashicorp | Hashicorp Vault is a secrets and encryption management system. https://www.vaultproject.io/ |
| custos | Custos is an NSF-funded project, backed by open source software that provides science gateways such as Galaxy with single sign-on, group management, and management of secrets such as access keys and OAuth2 access tokens. Custos secrets management is backed by Hashicorp's vault, but provides a convenient, always-on ReST API service. |
| database | The database backend stores secrets in an encrypted table in the Galaxy database itself. It is a convenient way to get started with a vault, and while it supports basic key rotation, we recommend using one of the other options in production. |

## Configuring Galaxy
Expand All @@ -36,7 +35,7 @@ path_prefix: /galaxy # optional
...
```

The `type` must be a valid backend type: `hashicorp`, `custos`, or `database`. At present, only a single vault backend
The `type` must be a valid backend type: `hashicorp`, or `database`. At present, only a single vault backend
is supported. The `path_prefix` property indicates the root path under which to store all vault keys. If multiple
Galaxy instances are using the same vault, a prefix can be used to uniquely identify the Galaxy instance.
If no path_prefix is provided, the prefix defaults to `/galaxy`.
Expand All @@ -50,19 +49,6 @@ vault_address: http://localhost:8200
vault_token: vault_application_token
```

## Vault configuration for Custos

```yaml
type: custos
custos_host: service.staging.usecustos.org
custos_port: 30170
custos_client_id: custos-jeREDACTEDye-10000001
custos_client_sec: OGREDACTEDBSUDHn
```

Obtaining the Custos client id and client secret requires first registering your Galaxy instance with Custos.
Visit [usecustos.org](http://usecustos.org/) for more information.

## Vault configuration for database

```yaml
Expand Down
8 changes: 0 additions & 8 deletions doc/source/lib/galaxy.authnz.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,6 @@ galaxy.authnz package
Submodules
----------

galaxy.authnz.custos\_authnz module
-----------------------------------

.. automodule:: galaxy.authnz.custos_authnz
:members:
:undoc-members:
:show-inheritance:

galaxy.authnz.managers module
-----------------------------

Expand Down
61 changes: 61 additions & 0 deletions lib/galaxy/authnz/cilogon.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""
CILogon OpenID Connect backend for Galaxy.

This backend extends Galaxy's base OIDC implementation with CILogon-specific features.
"""

from galaxy.authnz.oidc import GalaxyOpenIdConnect


class CILogonOpenIdConnect(GalaxyOpenIdConnect):
"""
CILogon OIDC backend for Galaxy.

Inherits PKCE support, localhost development mode, and refresh token support
from GalaxyOpenIdConnect. Adds CILogon-specific configuration:
- CILogon-specific scopes including org.cilogon.userinfo
- Custom URL handling to strip /authorize suffix
- CILogon-specific IDP hint parameter (idphint)
"""

name = "cilogon"

# CILogon-specific scopes
DEFAULT_SCOPE = ["openid", "email", "profile", "org.cilogon.userinfo"]

def auth_params(self, state=None):
"""
Add CILogon-specific parameters to the authorization request.

Adds idphint parameter for CILogon IDP selection.
"""
params = super().auth_params(state)

# Add CILogon IDP hint (default: "cilogon")
idphint = self.setting("IDPHINT", "cilogon")
if idphint:
params["idphint"] = idphint

return params

def oidc_endpoint(self):
"""
Return the OIDC endpoint for configuration discovery.

CILogon URLs may include /authorize in examples, which needs to be
stripped to find the correct base URL.

Example CILogon URL:
https://cilogon.org/authorize -> https://cilogon.org
"""
# Check if custom URL is configured
base_url = self.setting("URL")
if base_url:
# Backwards compatibility: CILogon URL is sometimes given with /authorize
# Remove it to get the correct openid configuration endpoint
if base_url.endswith("/authorize"):
base_url = "/".join(base_url.split("/")[:-1])
# Remove potential trailing slash
return base_url.rstrip("/")
# Fall back to default OIDC endpoint discovery
return super().oidc_endpoint()
Loading
Loading