Skip to content

feat(auth)!: multi provider authentication system#183

Open
markcrivera wants to merge 39 commits into
mainfrom
feat/multi-provider-auth
Open

feat(auth)!: multi provider authentication system#183
markcrivera wants to merge 39 commits into
mainfrom
feat/multi-provider-auth

Conversation

@markcrivera

@markcrivera markcrivera commented May 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

Replaces the single-provider OIDC/LDAP setup with a flexible multi-provider authentication architecture: multiple simultaneous instances per provider type, plus a new OAuth 2.0 provider.

Changes

Provider architecture

  • Provider registry and service manager reworked so multiple configured instances per type (Local, LDAP, OIDC, OAuth) coexist and resolve at login.
  • New OAuth 2.0 provider alongside the existing OIDC and LDAP providers.
  • Group-to-role claim extraction pulled into a dedicated extractor.
  • Session handling, auth cookies, and the global auth middleware updated for the multi-provider flow.

API

  • New OAuth login and callback route.
  • Test endpoints to validate a provider config before committing it: LDAP connection, LDAP login, OIDC, OAuth.

UI

  • Provider configuration on the admin auth page, with a dedicated editor per type: Local, LDAP, OIDC, OAuth.
  • Built-in connection and login test results so an admin can validate a provider before saving.
  • Add/remove provider instances with a remove-confirmation step and a save bar for pending changes.
  • New reusable form primitives (AppInput, AppSelect, AppTextarea); updated login form and navbar.

Data

  • Migration to update the stored auth-config key format.

Testing

Validated against real providers:

Related

closes #3
closes #171
BREAKING CHANGE: The AuthConfigKeyFormat migration changes how auth configuration is stored and is not backward compatible. Existing deployments must run the migration; rolling back to a pre-merge version after migrating is not supported.

Replaces single provider OIDC/LDAP with a flexible multi-provider
architecture supporting multiple simultaneous instances per type and
adding OAuth 2.0

Signed-off-by: Mark Rivera <mcrivera@gmail.com>
@Amndeep7 Amndeep7 temporarily deployed to tir-feat-multi-provider-racujz May 11, 2026 19:47 Inactive
Signed-off-by: Mark Rivera <mcrivera@gmail.com>
Signed-off-by: Mark Rivera <mcrivera@gmail.com>
@Amndeep7 Amndeep7 temporarily deployed to tir-feat-multi-provider-racujz May 12, 2026 14:55 Inactive
Comment thread server/auth/oidcAuthProvider.ts Dismissed
Move cookie string literals to AUTH_COOKIES constant.

Signed-off-by: Mark Rivera <mcrivera@gmail.com>
@Amndeep7 Amndeep7 temporarily deployed to tir-feat-multi-provider-racujz May 13, 2026 00:28 Inactive
Signed-off-by: Mark Rivera <mcrivera@gmail.com>
@Amndeep7 Amndeep7 temporarily deployed to tir-feat-multi-provider-racujz May 13, 2026 01:07 Inactive
Signed-off-by: Mark Rivera <mcrivera@gmail.com>
@Amndeep7 Amndeep7 temporarily deployed to tir-feat-multi-provider-racujz May 13, 2026 02:44 Inactive
@Amndeep7 Amndeep7 temporarily deployed to mitre-tir-staging May 15, 2026 18:52 Inactive
Signed-off-by: Mark Rivera <mcrivera@gmail.com>
@Amndeep7 Amndeep7 temporarily deployed to mitre-tir-staging May 15, 2026 20:07 Inactive
Signed-off-by: Mark Rivera <mcrivera@gmail.com>
@Amndeep7 Amndeep7 temporarily deployed to mitre-tir-staging May 15, 2026 20:29 Inactive
Comment thread pages/administration/auth.vue Outdated
Comment thread pages/administration/auth.vue Outdated
/>
</div>
<div class="flex items-center gap-4">
<label class="w-48 text-left text-sm font-medium">Callback URL</label>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you should consider adding a help tooltip to clarify that this default value is likely good enough and that you will need to provide this callback url to the auth provider

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 847dc40. Added callback URL help text for OIDC and OAuth.

Comment thread pages/administration/auth.vue Outdated
Comment thread pages/administration/auth.vue Outdated
@@ -1,20 +1,52 @@
<template>
<dl class="mb-6 space-y-6 divide-y divide-gray-100 border-b border-t border-gray-200 pb-6 pt-6 text-sm leading-6">
<!-- Default Login Tab -->

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

have a tooltip that explains that the 'default tab' is for when you have to type in your username/password, and that the remainder of the SSO options will show up on the list below

@markcrivera markcrivera Jun 2, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

I can definitely summarize more but it is getting a bit wordy. Maybe stop at buttons for SSO. @Amndeep7

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"
Default Login Tab
Choose the tab shown when users open the login page. Only Local and LDAP providers appear as tabs; OAuth and OIDC providers are always visible as buttons so they cannot be set as the default.
"

As a test, I sent this pic and a screencap of the login page of TIR to the AI and it recommended the above (slightly tweaked by me) language as a more concise version.

I think this new text is better cause a) it avoids unnecessary details like why local/ldap providers are tabs - users won't care so why bother telling them, b) removes stuff like where things are positioned (ex. 'below that form') which is just tech debt for the next style update, and c) makes it a verb at the beginning "Choose to do X" instead of passive voice.

Feel free to use the above or come up with alternative text you like more.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 897e9e8 - went with your suggested wording for the Default Login Tab description. Thanks!

Comment thread components/login/LoginForm.vue
Comment thread pages/administration/auth.vue Outdated
</div>
</dd>

<!-- Local Auth -->

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the default password requirements should probably match the AS&D (i.e. 15 chars, 1 of each character class)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 1ef555c. Local password defaults are now 15 chars with one of each character class.

Comment thread pages/administration/auth.vue Outdated
@@ -1,20 +1,52 @@
<template>
<dl class="mb-6 space-y-6 divide-y divide-gray-100 border-b border-t border-gray-200 pb-6 pt-6 text-sm leading-6">

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it feels weird that the divider between the different auth options in their list is the same as the divider between all the other UI bits that appear vertically

note the horizontal line with same styling + size for everything

Image Image

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tracked in #194, folded into a broader provider layout rework.

Signed-off-by: Mark Rivera <mcrivera@gmail.com>
Groups are read from a configurable LDAP attribute (default: memberOf),
CNs are extracted from DNs, and mapped to roles using the same
groupName:roleId format as OIDC/OAuth. Access is denied when mappings
are configured but the user matches no group.

Signed-off-by: Mark Rivera <mcrivera@gmail.com>
Removes CN extraction so groups with identical CNs in different OUs are
unambiguous. Pipe delimiter avoids conflicts with commas inside DNs.
OIDC/OAuth mapping parsing is unchanged (still comma-delimited).

Signed-off-by: Mark Rivera <mcrivera@gmail.com>
@Amndeep7

Copy link
Copy Markdown
Contributor

@markcrivera is this good to start reviewing yet?

@markcrivera

Copy link
Copy Markdown
Collaborator Author

@markcrivera is this good to start reviewing yet?

@Amndeep7 We are getting close. I decided to make the test buttons more robust instead of removing them. We're also doing our final round of integration testing for the auth methods.

OIDC and OAuth use a popup window with postMessage to run the full auth
flow without affecting the current admin session. LDAP uses an inline
credential form with a dedicated test endpoint that validates credentials
and resolves group mappings without creating a session or user record.

Signed-off-by: Mark Rivera <mcrivera@gmail.com>
When no group mappings are set, userRoleId is null but the real login
defaults to role 2 (User). Display now uses denied flag as the signal
for None rather than checking for an explicit roleId of 2.

Signed-off-by: Mark Rivera <mcrivera@gmail.com>
Signed-off-by: Mark Rivera <mcrivera@gmail.com>
…onent

Signed-off-by: Mark Rivera <mcrivera@gmail.com>
…ponents

Reduces auth.vue from over 1000 lines. Each provider component manages
its own test state and functions. OIDC and OAuth components properly clean
up their postMessage listeners via onUnmounted.

Signed-off-by: Mark Rivera <mcrivera@gmail.com>
Replace the per-provider .input-field styles with ring-based AppInput,
AppSelect, and AppTextarea wrappers that match the rest of the app, and
extract Local Auth into its own AuthLocalProvider component. The wrappers
expose a size prop (xs/sm), and AppInput honors the number model modifier
so v-model.number keeps working on the password-policy fields.

Signed-off-by: Mark Rivera <mcrivera@gmail.com>
…nput sizing

Add dirty tracking via JSON snapshot comparison, saving state, and discard
handler. Restore AppInput/AppSelect/AppTextarea to original border/padding
style (py-1, border) and fix LocalProvider pt-6 regression from extraction.

Signed-off-by: Mark Rivera <mcrivera@gmail.com>
Signed-off-by: Mark Rivera <mcrivera@gmail.com>
@markcrivera markcrivera force-pushed the feat/multi-provider-auth branch from 937e495 to f56dc86 Compare May 29, 2026 21:24
Add for/id associations to the auth provider config labels so screen
readers announce each control's name, resolving the SonarCloud
accessibility findings (Web:S6853). UISlideSwitch gains an optional id
prop forwarded to the underlying switch so toggles can be targeted by
their labels.

Signed-off-by: Mark Rivera <mcrivera@gmail.com>
Replace global parseInt/parseFloat/isNaN with their Number.* equivalents
and mark never-reassigned class members as readonly, resolving the
SonarCloud findings S7773 and S2933. No behavior change.

Signed-off-by: Mark Rivera <mcrivera@gmail.com>
Use globalThis.window/globalThis.location instead of window for the
location and SSR-guard accesses, resolving SonarCloud finding S7764.
Window-specific APIs (addEventListener, open) are left unchanged. No
behavior change.

Signed-off-by: Mark Rivera <mcrivera@gmail.com>
Extract nested ternary operations into if/else blocks and guard clauses
for role selection, TLS options, request body typing, and JWKS
pluralization, resolving SonarCloud finding S3358. No behavior change.

Signed-off-by: Mark Rivera <mcrivera@gmail.com>
Add the 'RoleId 1=Admin, 2=User' legend to the OAuth provider's group
mappings helper text so it matches the OIDC and LDAP providers, per PR
review feedback.

Signed-off-by: Mark Rivera <mcrivera@gmail.com>
Default the local password policy to 15 characters with at least one
upper, lower, number, and special character, per PR review feedback.

Signed-off-by: Mark Rivera <mcrivera@gmail.com>
Add helper text under the OIDC and OAuth callback URL fields noting the
default is usually fine and must be registered as an allowed callback/
redirect URL with the identity provider. Addresses PR review feedback.

Signed-off-by: Mark Rivera <mcrivera@gmail.com>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: What happens when you do not have local auth nor LDAP (ex. github auth only)?

Like what happens to that complete ui section for the horizontal tabs + all the username/password fields?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@markcrivera see this comment from late yesterday

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Amndeep7 I did forget this.

Two issues are arising.

  1. When disabling username/password auth, it is not being removed from the selectable items.
  2. Username/password is still presented on the login screen with no username/password auth providers selected.

Working a change now.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in db880d5 - when no Local or LDAP provider is enabled, the login page no longer renders the tab bar or the username/password form. Only the SSO buttons show, under a "Sign in with" header (instead of "or sign in with"). The consent control stays visible since the SSO buttons gate on it.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related follow-up in 82b072c: disabled Local/LDAP providers can no longer be set as the default. Their radios are disabled and greyed, and if the current default provider is later disabled or removed, the default reassigns to the first enabled provider.

@markcrivera markcrivera marked this pull request as ready for review June 4, 2026 17:54
@markcrivera markcrivera requested a review from Amndeep7 June 4, 2026 17:54
@Amndeep7

Amndeep7 commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

@markcrivera review is likely going to be started next week - have a dr's apt tomorrow that'll take up most of the day

Reword the Default Login Tab description per review: explain that only Local
and LDAP appear as tabs while OAuth and OIDC are always shown as buttons and
cannot be the default.

Signed-off-by: Mark Rivera <mcrivera@gmail.com>
When no Local or LDAP provider is enabled, the login page rendered an empty
tab bar, a non-functional username/password form, and an 'or sign in with'
divider. Hide the tab selector and credential fields when there is no
credential provider, keep the consent control visible (the SSO buttons gate on
it), and label the SSO section 'Sign in with' instead of 'or sign in with'.

Signed-off-by: Mark Rivera <mcrivera@gmail.com>
Disable and grey the Default Login Tab radios for Local/LDAP providers that are
not enabled so they cannot be selected. When the stored default points at a
provider that is disabled or removed, reassign it to the first enabled provider.

Signed-off-by: Mark Rivera <mcrivera@gmail.com>
Replace typeof globalThis.window === "undefined" with a direct === undefined
check in the callbackUrl helpers (SonarCloud S7741). The property access cannot
throw, so typeof is unnecessary.

Signed-off-by: Mark Rivera <mcrivera@gmail.com>
Drop a redundant `as Exclude<OAuthProviderType, "custom">` cast (the else
branch already narrows providerType, matching the unasserted lookup elsewhere)
and an `as any` on an index into an any-typed options object (SonarCloud S4325).

Signed-off-by: Mark Rivera <mcrivera@gmail.com>
Replace !email || !email.includes("@") with !email?.includes("@") in the
LDAP/AD email fallback (SonarCloud S6582); nullish email short-circuits the
optional call to undefined, preserving the original behavior.

Signed-off-by: Mark Rivera <mcrivera@gmail.com>
Deduplicate escapeFilter, domainFromBaseDn, firstAttr, allAttrs, and the UAC constant across ldapAuthProvider and the ldap-login test route. Rewrite filter escaping to replaceAll/String.raw, clearing SonarCloud S7780/S7781.

Signed-off-by: Mark Rivera <mcrivera@gmail.com>
Invert if/else and ternary tests so the positive branch leads,
removing six "unexpected negated condition" smells across the
OIDC/LDAP test routes, groupClaimExtractor, and authConfig.
Behavior preserved.

Signed-off-by: Mark Rivera <mcrivera@gmail.com>
Merge two consecutive checks.push() calls into one (S7778) in the
OAuth test route, and lift the inner template literal in resolveRole
out into a mappingSummary variable so the debug message is no longer
a nested template (S4624). Behavior unchanged.

Signed-off-by: Mark Rivera <mcrivera@gmail.com>
Replace the innermost forEach callback in insecureFetch with a
for...of loop so header collection is no longer a function nested
five levels deep. Behavior unchanged.

Signed-off-by: Mark Rivera <mcrivera@gmail.com>
Extract the three near-identical ldap/oidc/oauth provider-loading
loops into a typed loadProviderGroup helper, and flatten the local
fallback by naming the noProvidersEnabled guard instead of nesting
an if. The helper's secretField is constrained to "password" |
"secret".  Attempts to address SonarCloud S3776.

Signed-off-by: Mark Rivera <mcrivera@gmail.com>
@sonarqubecloud

sonarqubecloud Bot commented Jun 5, 2026

Copy link
Copy Markdown

@markcrivera markcrivera changed the title feat(auth): multi provider authentication system feat(auth)!: multi provider authentication system Jun 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature request: allow multiple SSO login methods at the same time Implement Auth in TIR

3 participants