Feature #43352
openProvide an option to authenticate in Redmine through an OAuth provider.
Added by Marius BĂLTEANU about 1 year ago. Updated 1 day ago.
Description
This feature will allow users to securely log in using their accounts from popular services like Google, Microsoft, Okta, or Keylock, providing an alternative to the traditional username/password login or LDAP login.
From what I checked, there is very well maintained plugin that brings this capability, but I think that it should a core feature.
Files
| native-openid-connect-design.md (33.5 KB) native-openid-connect-design.md | Jan Opravil, 2026-05-24 21:33 | ||
| redmine-oidc-native-support.patch.gz (298 KB) redmine-oidc-native-support.patch.gz | Jan Opravil, 2026-09-21 18:13 |
Related issues
Updated by Marius BĂLTEANU about 1 year ago
- Related to Feature #37363: Add native support for OIDC added
Updated by Marius BĂLTEANU about 1 year ago
I've added this to 7.0.0 for now to discuss it, but I strongly believe we should implement this in some way.
Updated by Anonymous about 1 year ago
- File clipboard-202510151400-sn8gk.png added
Updated by Go MAEDA about 1 year ago
- File deleted (
clipboard-202510151400-sn8gk.png)
Updated by Jan Catrysse about 1 year ago
https://github.com/jcatrysse/bless-this-redmine-sso/tree/feature_version_2.0.0
This is what I have been using (I added a version 2.0.0 feature branch).
Updated by Quentin Aymard 12 months ago
Yes please. The lack of support for modern auth / Single Sign On (SSO) is really a problem for Redmine to move forward.
The OAuth side of the force is a bit disorderly, with several plugins more or less trying to do the same thing, most of the time confusing OAuth and its subset OpenID Connect. Kontron/redmine_oauth is the only one I was able to use properly.
The SAML side of the force was, until recently, somewhat clearer : a bunch of abandoned plugins, and the last one standing being the one from AlphaNodes : https://github.com/AlphaNodes/redmine_saml. But this last one went in public archive mode on Github this fall, so I think this is not an option anymore.
Updated by Jan Opravil 5 months ago
Hello Marius Ionescu BĂLTEANU and team,
I'd like to propose a design for native OpenID Connect support and seek your direction before opening any patches. I've spent time analysing the four community OIDC plugins (`enricohuang/redmine_oidc`, `devopskube/redmine_openid_connect`, `nanego/redmine_omniauth_oidc`, `kontron/redmine_oauth`), read this issue and #37363 in detail, and produced a full design document covering data model, authentication flow, session lifecycle, logout (RP-initiated + Back-Channel), claim mapping, admin UI, security checklist, and testing strategy.
Full proposal: in attachement
The proposed shape is a native OIDC 1.0 implementation in Redmine core (no plugin layer), targeting 7.0.0. Architecture highlights:
Dedicated `OidcProvider` ActiveRecord model (not `AuthSource` STI) supporting multiple active providers simultaneously, with presets for Keycloak, Microsoft Entra ID, Okta, Auth0, Google Cloud Identity / Workspace, and a fully-configurable Custom (generic OIDC) profile.
Stable user identity via an `oidc_user_links(issuer, sub)` join table; first-time email matching gated by `email_verified=true`.
Mandatory PKCE S256 and JWKS-based ID token signature verification — the critical security gap in three of the four existing community plugins.
Three universal claim-mapping strategies (`group` / `project_role` / `none`) driven by a configurable dot-notation claim path that works across all major IdPs without special-casing. Admin flag is sync-controlled only when the admin explicitly opts in via an `admin_role_values` whitelist. Manually-assigned groups and memberships are never overwritten.
Full SSO logout propagation: RP-initiated logout via `end_session_endpoint`, Back-Channel Logout endpoint, opt-in periodic introspection for sub-token-TTL revocation latency.
Reactive token lifecycle: lazy refresh-on-expiry with a configurable buffer (default 300s), serialised under a pessimistic row lock to handle refresh-token rotation safely. No background scheduler in MVP.
2FA delegated to the IdP when the `amr` or `acr` claim signals MFA; auto-provisioned users without TOTP are routed through Redmine's existing setup wizard when global policy requires 2FA but the IdP did not assert MFA.
One new gem dependency: `jwt` (~> 2.x) for signature verification. Alternatives are either heavier (`omniauth_openid_connect` pulls OmniAuth middleware) or unsafe (manual base64 decoding, which is what three of the four existing plugins currently do).
EN + CS locales in MVP; comprehensive test pyramid (unit / functional / integration with WebMock-stubbed IdP); security checklist as a patch-submission gate.
Before I commit time to implementation, I'd appreciate your direction on the following:
1. Scope: OIDC-only vs OIDC + plain OAuth2 in MVP? The title of this issue says "OAuth Authentication", and Felix Singer's comment in #37363 (note 2) asks for plain OAuth2 providers (GitHub, GitLab, Google OAuth). I propose OIDC-only for MVP because plain OAuth2 lacks a signed identity assertion (no ID token, no JWKS), but this is genuinely your call — extending to plain OAuth2 doubles the surface area and we akens the security baseline.
2. AuthSource vs. dedicated model? OIDC's browser-redirect flow doesn't fit the `AuthSource#authenticate(login, password)` contract, and multi-provider with per-provider quirks complicates STI. I propose a dedicated `OidcProvider` model. Confirm or redirect.
3. Patch series shape? Acceptable as one large patch, or do you want it split into a series (schema + models → discovery/JWKS primitives → login flow → role mapping → admin UI → BCL → tests)?
4. Gem dependency: is `jwt` (~> 2.x) acceptable? It's the minimum to do JWKS signature verification properly.
5. Server-side session store as a BCL prerequisite? Redmine defaults to cookie-only sessions; Back-Channel Logout needs server-side sessions to be able to destroy sessions on demand. Document this as a prerequisite admins must enable for BCL to be effective, or different approach?
6. Prior art — note 5 above (Jan Catrysse) links to a v2.0.0 feature branch of an alternative SSO implementation. I didn't have the URL when drafting the proposal — could you point me at it if it should inform the design?
The full proposal documents 21 design decisions with their rationale, and lists exactly what's deliberately out of scope for MVP (background refresh, plugin migration importers, SAML, plain OAuth2, SCIM, Front-Channel Logout, real-IdP integration tests in CI, step-up auth, mobile/native client support). Happy to break any section into a separate discussion if useful, and to iterate the design before any code lands.
Thanks for your time.
Jan Opravil
Updated by Marius BĂLTEANU 4 months ago
- Target version changed from 7.0.0 to 7.1.0
Updated by Jan Opravil about 1 month ago
Native OIDC implementation — patch attached
Following up on my design proposal above (comment #7): the implementation is complete and attached as redmine-oidc-native-support.patch (single consolidated diff, applies cleanly with patch -p1 or git apply against current trunk, r25125).
Scope (120 files, ~22.9k lines added):
- OidcProvider — dedicated model (not AuthSource STI), multiple providers can be enabled simultaneously. Built-in presets for Keycloak, Microsoft Entra ID, Okta, Auth0 and Google Cloud Identity/Workspace, plus a generic "custom" option for any spec-compliant IdP.
- OidcSession / OidcUserLink — per-device session tracking (refresh/access/ID tokens encrypted at rest via the existing Redmine::Ciphering), separate from the identity link so one identity can hold several concurrent device sessions.
- Auth code flow with mandatory PKCE (S256) and JWKS-based ID token signature verification (lib/redmine/oidc/id_token_validator.rb, jwks.rb, discovery.rb) — no fallback to unsigned or alg=none tokens.
- Login-CSRF defence via a signed, user-agent-bound handshake cookie (handshake_binding.rb) plus server-side handshake store, per RFC 9700 §4.7.
- RFC 9207 mix-up mitigation, iss checking, nonce, and standard claim validation (issuer/audience/exp/iat).
- Back-Channel and Front-Channel Logout support (RFC/OIDC spec), both revoking the stored refresh token at the provider, not just dropping the local session.
- Session lifecycle: periodic revalidation (prompt=none), optional token introspection (RFC 7662), token revocation (RFC 7009) on logout/unlink/deactivation/account deletion.
- Three role-mapping strategies: none, group (claim → Redmine group) and project_role (claim → project membership + role), plus a separate admin_role_values mapping — all reconciled against actual current access on every login, not just an internal bookkeeping table, and fully reversible when a provider or link is removed.
- Delegated 2FA: optional "trust the IdP's MFA claim (amr/acr) within a configurable max_age" policy, falling back to Redmine's own 2FA.
- CLI tooling: rake redmine:oidc:{providers,create,delete,revoke_grants,prune_sessions,refresh_metadata}, plus one-shot importers from two existing community plugins' data shapes (enricohuang/redmine_oidc, jcatrysse/bless-this-redmine-sso).
- Admin UI (OidcProvidersController + views) and end-user UI (login buttons, linked-accounts panel, unlink flow).
- Full English + Czech i18n.
Dependency: one new gem, jwt (~> 3.2), for signature verification.
Tests: 989 test runs (functional/unit/integration), all green, covering the full auth/logout/refresh/revocation lifecycle, role-mapping reconciliation, concurrency (row-locking against races between refresh and logout/BCL), and a dedicated security-regression suite (audit_r17_findings_test.rb) pinning specific findings from an independent security review of the branch (claim-extraction privilege escalation, weak-key JWKS acceptance, session-fixation-adjacent issues, etc.).
Patch attached below.
Updated by Marco Descher 9 days ago
Jan, will your plugin support OIDC access for the Rest API?
That is, will it allow to use the access token via bearer authorization?
See https://github.com/kontron/redmine_oauth/issues/34 for the discussion!
Updated by Jan Opravil 8 days ago
Short answer: not in the current patch — but there's a well-scoped way to get there, and I'd like feedback on it.
The current implementation is intentionally scoped to browser-based login only (OAuth2 Authorization Code + PKCE, the interactive flow). The access token Redmine obtains from the IdP during that flow is never accepted back as a credential against Redmine's own REST API — it's used only for session revalidation and revocation at the provider, never presented to a resource server. That's a deliberate scope boundary, not an oversight; it's recorded as such in this branch's own audit notes.
So: no, you can't currently take the access token issued during a user's interactive login and use it as Authorization: Bearer <token> against /issues.json etc. REST API auth stays exactly as it is today (API key, or Redmine's own Doorkeeper OAuth2 provider if enabled) — this patch doesn't touch that path at all.
That said, I don't think "Bearer auth for the REST API" and "OIDC login" should be the same mechanism anyway, and I think that's part of why this keeps coming up unresolved elsewhere too — see kontron/redmine_oauth#34, which is the same request against a different plugin and has sat open for over a year without an implementation.
What I think would actually fit well — as a separate, follow-up feature, not part of this patch — is the OAuth2 Client Credentials grant for machine-to-machine access:
- An admin registers a confidential client in the IdP (e.g. Keycloak) with service-account/client_credentials enabled, and hands that client_id/secret to the external service — no user, no browser, no redirect involved at any point.
- The external service calls the IdP's own token endpoint directly to get a short-lived access token, and sends it to Redmine as a Bearer token.
- Redmine validates the token's signature against the same JWKS already fetched for the provider, checks iss/aud/exp, and maps the client identity to a Redmine service-account user via a small new mapping (client_id → user), conceptually similar to an API key but centrally revocable from the IdP.
This reuses most of the existing JWKS/discovery machinery, keeps the interactive login flow untouched, and — unlike a static API key — lets an admin kill access centrally by disabling the client at the IdP. I think it's worth proposing as its own issue once this patch has landed, rather than bundling two different grant types into one review.
Happy to write that up properly if there's interest.
Updated by Marco Descher 8 days ago
Jan thank you for your feedback.
Why confine it to a client credentials grant? It wouldn't matter anyway - as long as one can provide an
access token with a valid issuer and the redmine client mentioned as aud it should not be relevant which way the
token was created. (well you could dive deeper and confine it with checking the azp but i don't really
see this happening on most projects).
Why do you see a separate service-account user? We e.g. don't use it that way. If have standard users, that
currently have to use the redmine api key, which are using a keycloak idp for everything else. That is
we have a small app, that communicates with Redmine directly using the REST API, but it needs to be done
with the identity of the user.
- There exists a user on a separate IdP
- Redmine should accept this user's bearer token either by verifying the JWT against the IdP issuer (or calling the introspection endpoint, and optionally verifying the
audclaim - The
preferred_usernameshould map to a possibly existing user (if not - leave option to create or no create) - The roles or groups should map to
groupsmembership on redmine with the IdP being the authoritative roles
If one would you could add a special role that maps to admin rights?!
Updated by Jan Opravil 8 days ago
Thanks for pushing back on this, it's a fair point and I think it actually resolves cleanly.
You're right that, mechanically, it doesn't matter which grant produced the token — Redmine would only need to check iss/aud against a configured provider and resolve identity from claims, same as the interactive login path already does. And there's a detail that makes your proposal and mine converge more than I first thought: when a Keycloak client has "Service Accounts Enabled", the client_credentials token it issues still carries a resolvable identity (sub, preferred_username: service-account-<client-id>), assignable to groups/roles like any other user. So the mechanism — accept a valid bearer token, resolve it via the existing claim-mapping logic (uid_claim → user, groups/roles → Redmine groups/roles, IdP authoritative) — can cover both "delegated on behalf of a logged-in human" and "pure machine access" without the Redmine-side code needing to distinguish how the token was minted.
Where I think your example specifically assumes something that won't generally hold: it requires Redmine and the calling application to already sit behind the same IdP, so that extending an existing client with an audience mapper is enough. In practice that's the exception, not the rule — most integrations are either on a different IdP entirely, have their own auth mechanism with no OIDC relationship at all, or are backed by something like LDAP. For those, there's no existing client to extend; you'd have to provision one specifically for the Redmine integration regardless, which is exactly the client-credentials/service-account pattern.
So I'd frame it this way: a general solution has to support provisioning a dedicated client purely for the integration, independent of however the calling application authenticates its own end users (or whether it does that via OIDC at all). "Accept any valid bearer token from the same IdP Redmine already uses for login" is then a narrower convenience for the specific case where an organization happens to be IdP-aligned already — worth allowing, since it's nearly free once the claim-resolution path exists, but not sufficient as the answer on its own.
Concretely, I'd design it as: Redmine validates a Bearer token's signature against the JWKS of whichever configured provider's iss it matches, requires aud to name this Redmine instance, and resolves the resulting claims through the exact same uid_claim/role-mapping machinery the login flow already uses — regardless of whether the token came from an authorization_code flow (your delegated case), or client_credentials against a dedicated service-account client (the general case, and the one I'd treat as primary since it doesn't assume any pre-existing identity relationship between the calling app and Redmine's IdP).
Happy to write this up properly as a follow-up proposal once the current patch lands.
Updated by Jonathan Cormier 7 days ago
1. Scope: OIDC-only vs OIDC + plain OAuth2 in MVP? The title of this issue says "OAuth Authentication", and Felix Singer's comment in #37363 (note 2) asks for plain OAuth2 providers (GitHub, GitLab, Google OAuth). I propose OIDC-only for MVP because plain OAuth2 lacks a signed identity assertion (no ID token, no JWKS), but this is genuinely your call — extending to plain OAuth2 doubles the surface area and we akens the security baseline.
Supporting OAuth2 providers should be a pretty important feature. Even with redmine.org, its be great being able to login via Google OAuth instead of via password.
Updated by Jan Opravil 6 days ago
Thanks for the input. I'd like to push back gently on "OAuth2 providers" as the framing here, because Google specifically is actually already covered by the OIDC-only scope we're proposing — it's not a reason to widen it.
Google's OAuth2 implementation is fully OIDC-compliant: it has a discovery document (accounts.google.com/.well-known/openid-configuration), issues signed ID tokens, and publishes a JWKS. Our OidcProvider config is fully generic (issuer URL, client_id/secret, scopes, claim mappings — nothing Keycloak-specific), and "Google" is already a built-in preset in this patch, tested against the real Google endpoints and covered in the test fixtures. So logging into redmine.org via Google is something this MVP already supports, purely through configuration — no code changes needed, and no scope widening required.
The actual scope question is narrower than "Google vs. no Google": it's whether to also support IdPs that only do plain OAuth2 with no ID token and no JWKS — GitHub's OAuth being the clearest example (GitLab and Google both do publish OIDC metadata, so they're not actually in this bucket). For those, there's no signed identity assertion to validate at all; you'd have to call a provider-specific profile endpoint (e.g. GitHub's /user REST API) and trust its response over plain TLS instead of a cryptographically signed token. That's a materially different trust model, not just "fewer fields to fill in" — it needs its own validation path per provider rather than one standards-based OIDC client, and it removes the signature-verification guarantee the rest of this design relies on.
So I'd still keep this MVP OIDC-only, since the concrete examples raised (redmine.org wanting Google login) are already satisfied — and treat broader plain-OAuth2 support (GitHub-style) as a separate follow-up issue, scoped and reviewed on its own, rather than folding a weaker trust model into this patch.
Updated by Marco Descher 1 day ago
I have implemented a solution, that uses an intermediate nginx server, using nginx-auth-jwt by adding a location like this
location = /jwks_uri {
internal;
proxy_cache default_cache;
proxy_cache_valid 200 60m;
proxy_read_timeout 5s;
proxy_pass_request_body off;
# Using the SSL a resolution problem was faced, where the DNS is resolved to IP
# when there is a body, which fails, so we contact keycloak via the internal net
proxy_pass http://keycloak:8080/realms/my-realm/protocol/openid-connect/certs;
}
# Allow to use an access token to connect to the redmine api
location /redmine/ {
auth_jwt "closed site";
auth_jwt_key_request /jwks_uri;
auth_jwt_require_claim resource_access.redmine.roles intersect json=["user"];
satisfy all;
include /etc/access/admin_key.conf;
proxy_pass http://redmine:3000/; # the real redmine service
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Redmine-API-Key $admin_api_key;
proxy_set_header X-Redmine-Switch-User $jwt_claim_preferred_username;
}
where the content of
/etc/access/admin_key.conf is set $admin_api_key "some-boggling-long-redmine-api-key";
Now using an access_token against this endpoint and some redmine rest api url will lead to an admin api key call,
switched to the resp. user of the access token.
What I will do now, is adding a cron script that regularily refreshes the apikey of the admin user used for this
process, updates admin_key.conf and then restarts nginx. This should be a solid security solution.
Any thoughts?