mirror of
https://github.com/sol1/rustguac.git
synced 2026-10-25 13:09:56 +00:00
oidc: add option to disable automatic account creation
With `[oidc] auto_create_users = false`, OIDC login no longer creates
an account for a user who isn't in the database yet. Only users an
admin has added beforehand can log in. The option defaults to true, so
existing deployments behave as before.
An admin can add users ahead of their first login with any of:
- CLI: `rustguac add-user --email <email> --role <role> [--name <name>]`
- API: `POST /api/users` (admin only), returns 201, or 409 if the user exists
- Admin page: an "Add User" form under the users table
A user added this way keeps the role the admin gave them; default_role
is not applied. Group-to-role mappings still apply on every login.
A rejected SSO login now redirects to the login page with an error code
instead of returning JSON. This covers a user with no account
(sso_error=no_account) and a disabled account (sso_error=disabled).
SSO errors are now shown under the SSO button: they used to go into the
API key form, which is hidden when SSO is on, so "SSO login failed" was
never visible. The "provider unavailable" notice uses the same element,
and all SSO messages are defined in one place in index.html.
(cherry picked from commit 9bfdc52155)
This commit is contained in:
committed by
Dave Kempe
parent
68bce184df
commit
c25ee93b16
@@ -147,6 +147,12 @@ web_allowed_networks = ["127.0.0.0/8", "::1/128"]
|
||||
# # Default: "operator"
|
||||
# default_role = "operator"
|
||||
#
|
||||
# # Create an account automatically on first OIDC login (default: true).
|
||||
# # Set to false to let in only users an admin has added beforehand
|
||||
# # (`rustguac add-user --email ... --role ...`, POST /api/users, or the
|
||||
# # Admin page). The email must match what the identity provider sends.
|
||||
# auto_create_users = true
|
||||
#
|
||||
# # OIDC claim name containing group memberships (default: "groups").
|
||||
# groups_claim = "groups"
|
||||
#
|
||||
|
||||
@@ -110,6 +110,7 @@ Enables OpenID Connect authentication. When configured, the web UI shows a login
|
||||
| `client_secret` | — | OIDC client secret (or use `OIDC_CLIENT_SECRET` env var) |
|
||||
| `redirect_uri` | — | Redirect URI: `https://your-host/auth/callback` (required) |
|
||||
| `default_role` | `operator` | Role assigned to new users on first login |
|
||||
| `auto_create_users` | `true` | Create an account on first OIDC login. When `false`, only users already in the database can log in; add them with `rustguac add-user`, `POST /api/users` or the Admin page |
|
||||
| `groups_claim` | `groups` | JWT claim name containing group memberships |
|
||||
| `extra_scopes` | `[]` | Additional OIDC scopes to request |
|
||||
| `ca_cert` | — | Path to CA certificate (PEM) for verifying the OIDC provider |
|
||||
|
||||
@@ -49,6 +49,8 @@ OIDC users are assigned a role through three mechanisms (in order of precedence)
|
||||
2. **Manual role assignment** — admins can set a user's role via CLI, API, or the Admin page.
|
||||
3. **Default role** — new users get the `default_role` from OIDC config on first login (default: `operator`).
|
||||
|
||||
With `auto_create_users = false` in `[oidc]`, rustguac does not create accounts on login: a user must be added first via `rustguac add-user --email <email> --role <role>`, `POST /api/users` (`{"email": "...", "role": "...", "name": "..."}`), or the Admin page. Anyone else who authenticates with the identity provider gets a 403. The pre-assigned role is kept; `default_role` is not applied, but group-to-role mappings still are.
|
||||
|
||||
## Endpoint access control
|
||||
|
||||
### Session management
|
||||
|
||||
+63
@@ -1569,6 +1569,69 @@ pub async fn list_users(
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CreateUserRequest {
|
||||
pub email: String,
|
||||
#[serde(default)]
|
||||
pub name: String,
|
||||
pub role: String,
|
||||
}
|
||||
|
||||
/// POST /api/users — Pre-provision a user for OIDC login (needed when
|
||||
/// `auto_create_users = false`). Admin only.
|
||||
pub async fn create_user(
|
||||
identity: Option<Extension<AuthIdentity>>,
|
||||
Extension(database): Extension<Db>,
|
||||
Json(req): Json<CreateUserRequest>,
|
||||
) -> impl IntoResponse {
|
||||
if !identity
|
||||
.as_ref()
|
||||
.map(|Extension(id)| id.has_role("admin"))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return (
|
||||
StatusCode::FORBIDDEN,
|
||||
Json(json!({"error": "admin role required"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let email = req.email.trim().to_string();
|
||||
if email.is_empty() {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(json!({"error": "email is required"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
if !["admin", "poweruser", "operator", "viewer"].contains(&req.role.as_str()) {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(json!({"error": "role must be admin, poweruser, operator, or viewer"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let db_clone = database.clone();
|
||||
let name = req.name.trim().to_string();
|
||||
let role = req.role.clone();
|
||||
match tokio::task::spawn_blocking(move || db::create_user(&db_clone, &email, &name, &role))
|
||||
.await
|
||||
{
|
||||
Ok(Ok(user)) => (StatusCode::CREATED, Json(json!(user))).into_response(),
|
||||
Ok(Err(e)) if e.to_string().contains("UNIQUE") => (
|
||||
StatusCode::CONFLICT,
|
||||
Json(json!({"error": "user already exists"})),
|
||||
)
|
||||
.into_response(),
|
||||
_ => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "failed to create user"})),
|
||||
)
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct SetRoleRequest {
|
||||
pub role: String,
|
||||
|
||||
@@ -26,6 +26,11 @@ pub struct OidcConfig {
|
||||
pub redirect_uri: String,
|
||||
#[serde(default = "default_oidc_default_role")]
|
||||
pub default_role: String,
|
||||
/// Create a user account on first OIDC login (default: true). When false,
|
||||
/// only users already in the database (pre-provisioned with `add-user`,
|
||||
/// `POST /api/users` or the Admin page) can log in.
|
||||
#[serde(default = "default_true")]
|
||||
pub auto_create_users: bool,
|
||||
/// Name of the OIDC claim containing group memberships (default: "groups").
|
||||
#[serde(default = "default_groups_claim")]
|
||||
pub groups_claim: String,
|
||||
@@ -53,6 +58,7 @@ impl std::fmt::Debug for OidcConfig {
|
||||
)
|
||||
.field("redirect_uri", &self.redirect_uri)
|
||||
.field("default_role", &self.default_role)
|
||||
.field("auto_create_users", &self.auto_create_users)
|
||||
.field("groups_claim", &self.groups_claim)
|
||||
.field("extra_scopes", &self.extra_scopes)
|
||||
.field("tls_skip_verify", &self.tls_skip_verify)
|
||||
@@ -1535,6 +1541,7 @@ mod tests {
|
||||
client_secret: Some("sensitive-value".into()),
|
||||
redirect_uri: "https://console.example.com/oidc/callback".into(),
|
||||
default_role: default_oidc_default_role(),
|
||||
auto_create_users: true,
|
||||
groups_claim: default_groups_claim(),
|
||||
extra_scopes: vec![],
|
||||
tls_skip_verify: false,
|
||||
|
||||
@@ -513,7 +513,10 @@ impl std::fmt::Display for AuthError {
|
||||
|
||||
// ── User management ──
|
||||
|
||||
/// Upsert a user from OIDC login. Creates on first login, updates last_login_at on subsequent.
|
||||
/// Record an OIDC login. Updates an existing user's profile and last_login_at.
|
||||
/// A user not yet in the database is created with `default_role` when
|
||||
/// `create_if_missing` is set; otherwise `Ok(None)` is returned and nothing is
|
||||
/// written (OIDC `auto_create_users = false`).
|
||||
pub fn upsert_user(
|
||||
db: &Db,
|
||||
email: &str,
|
||||
@@ -521,18 +524,64 @@ pub fn upsert_user(
|
||||
oidc_subject: Option<&str>,
|
||||
default_role: &str,
|
||||
groups: &[String],
|
||||
) -> rusqlite::Result<User> {
|
||||
create_if_missing: bool,
|
||||
) -> rusqlite::Result<Option<User>> {
|
||||
let groups_str = groups.join(",");
|
||||
let conn = db.lock().unwrap();
|
||||
if create_if_missing {
|
||||
conn.execute(
|
||||
"INSERT INTO users (email, name, oidc_subject, role, oidc_groups)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5)
|
||||
ON CONFLICT(email) DO UPDATE SET
|
||||
name = excluded.name,
|
||||
oidc_subject = COALESCE(excluded.oidc_subject, users.oidc_subject),
|
||||
oidc_groups = excluded.oidc_groups,
|
||||
last_login_at = datetime('now')",
|
||||
params![email, name, oidc_subject, default_role, groups_str],
|
||||
)?;
|
||||
} else {
|
||||
let changed = conn.execute(
|
||||
"UPDATE users SET
|
||||
name = ?2,
|
||||
oidc_subject = COALESCE(?3, oidc_subject),
|
||||
oidc_groups = ?4,
|
||||
last_login_at = datetime('now')
|
||||
WHERE email = ?1",
|
||||
params![email, name, oidc_subject, groups_str],
|
||||
)?;
|
||||
if changed == 0 {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
conn.query_row(
|
||||
"SELECT id, email, name, oidc_subject, role, disabled, created_at, last_login_at, oidc_groups
|
||||
FROM users WHERE email = ?1",
|
||||
params![email],
|
||||
|row| {
|
||||
Ok(User {
|
||||
id: row.get(0)?,
|
||||
email: row.get(1)?,
|
||||
name: row.get(2)?,
|
||||
oidc_subject: row.get(3)?,
|
||||
role: row.get(4)?,
|
||||
disabled: row.get::<_, i32>(5)? != 0,
|
||||
created_at: row.get(6)?,
|
||||
last_login_at: row.get(7)?,
|
||||
oidc_groups: row.get(8)?,
|
||||
})
|
||||
},
|
||||
)
|
||||
.map(Some)
|
||||
}
|
||||
|
||||
/// Pre-provision a user so they can log in via OIDC when automatic account
|
||||
/// creation is disabled. The OIDC subject, name and groups are filled in on
|
||||
/// first login. Fails with a UNIQUE constraint error if the email exists.
|
||||
pub fn create_user(db: &Db, email: &str, name: &str, role: &str) -> rusqlite::Result<User> {
|
||||
let conn = db.lock().unwrap();
|
||||
conn.execute(
|
||||
"INSERT INTO users (email, name, oidc_subject, role, oidc_groups)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5)
|
||||
ON CONFLICT(email) DO UPDATE SET
|
||||
name = excluded.name,
|
||||
oidc_subject = COALESCE(excluded.oidc_subject, users.oidc_subject),
|
||||
oidc_groups = excluded.oidc_groups,
|
||||
last_login_at = datetime('now')",
|
||||
params![email, name, oidc_subject, default_role, groups_str],
|
||||
"INSERT INTO users (email, name, role) VALUES (?1, ?2, ?3)",
|
||||
params![email, name, role],
|
||||
)?;
|
||||
conn.query_row(
|
||||
"SELECT id, email, name, oidc_subject, role, disabled, created_at, last_login_at, oidc_groups
|
||||
@@ -1922,4 +1971,72 @@ mod tests {
|
||||
let k = add_admin(&db, "none", None, None).unwrap();
|
||||
assert!(validate_api_key(&db, &k, None).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_upsert_user_creates_when_allowed() {
|
||||
let db = test_db();
|
||||
let groups = vec!["ops".to_string()];
|
||||
let user = upsert_user(
|
||||
&db,
|
||||
"a@example.com",
|
||||
"A",
|
||||
Some("sub-a"),
|
||||
"operator",
|
||||
&groups,
|
||||
true,
|
||||
)
|
||||
.unwrap()
|
||||
.expect("user created");
|
||||
assert_eq!(user.role, "operator");
|
||||
assert_eq!(user.oidc_subject.as_deref(), Some("sub-a"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_upsert_user_without_create_rejects_unknown() {
|
||||
let db = test_db();
|
||||
let res = upsert_user(
|
||||
&db,
|
||||
"x@example.com",
|
||||
"X",
|
||||
Some("sub-x"),
|
||||
"admin",
|
||||
&[],
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(res.is_none());
|
||||
assert!(get_user_by_email(&db, "x@example.com").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_upsert_user_without_create_logs_in_provisioned() {
|
||||
let db = test_db();
|
||||
create_user(&db, "p@example.com", "", "viewer").unwrap();
|
||||
let groups = vec!["devs".to_string()];
|
||||
let user = upsert_user(
|
||||
&db,
|
||||
"p@example.com",
|
||||
"P",
|
||||
Some("sub-p"),
|
||||
"admin",
|
||||
&groups,
|
||||
false,
|
||||
)
|
||||
.unwrap()
|
||||
.expect("provisioned user logs in");
|
||||
// Pre-provisioned role is kept, default_role is not applied.
|
||||
assert_eq!(user.role, "viewer");
|
||||
assert_eq!(user.name, "P");
|
||||
assert_eq!(user.oidc_subject.as_deref(), Some("sub-p"));
|
||||
assert_eq!(user.oidc_groups, "devs");
|
||||
assert!(user.last_login_at.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_user_duplicate_fails() {
|
||||
let db = test_db();
|
||||
create_user(&db, "d@example.com", "", "viewer").unwrap();
|
||||
let err = create_user(&db, "d@example.com", "", "admin").unwrap_err();
|
||||
assert!(err.to_string().contains("UNIQUE"));
|
||||
}
|
||||
}
|
||||
|
||||
+39
-1
@@ -127,6 +127,20 @@ enum Command {
|
||||
/// List all OIDC users
|
||||
ListUsers,
|
||||
|
||||
/// Add an OIDC user ahead of their first login (required to let anyone
|
||||
/// in when `[oidc] auto_create_users = false`)
|
||||
AddUser {
|
||||
/// User email, exactly as the identity provider sends it
|
||||
#[arg(long)]
|
||||
email: String,
|
||||
/// Role: admin, poweruser, operator, or viewer
|
||||
#[arg(long)]
|
||||
role: String,
|
||||
/// Display name (replaced by the provider's name on login)
|
||||
#[arg(long, default_value = "")]
|
||||
name: String,
|
||||
},
|
||||
|
||||
/// Set a user's role
|
||||
SetRole {
|
||||
/// User email
|
||||
@@ -270,6 +284,9 @@ async fn main() {
|
||||
cmd_generate_cert(&hostname, &out_dir, &extra_sans);
|
||||
}
|
||||
Some(Command::ListUsers) => cmd_list_users(&database),
|
||||
Some(Command::AddUser { email, role, name }) => {
|
||||
cmd_add_user(&database, &email, &role, &name)
|
||||
}
|
||||
Some(Command::SetRole { email, role }) => cmd_set_role(&database, &email, &role),
|
||||
Some(Command::DisableUser { email }) => cmd_disable_user(&database, &email),
|
||||
Some(Command::DeleteUser { email }) => cmd_delete_user(&database, &email),
|
||||
@@ -516,6 +533,27 @@ fn cmd_list_users(database: &Db) {
|
||||
}
|
||||
}
|
||||
|
||||
fn cmd_add_user(database: &Db, email: &str, role: &str, name: &str) {
|
||||
if !["admin", "poweruser", "operator", "viewer"].contains(&role) {
|
||||
eprintln!("Role must be admin, poweruser, operator, or viewer.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
match db::create_user(database, email, name, role) {
|
||||
Ok(_) => {
|
||||
println!("User '{}' added with role '{}'.", email, role);
|
||||
audit_cli(database, "add_user", email, Some(&format!("role={}", role)));
|
||||
}
|
||||
Err(e) if e.to_string().contains("UNIQUE") => {
|
||||
eprintln!("User '{}' already exists.", email);
|
||||
std::process::exit(1);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Error: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn cmd_set_role(database: &Db, email: &str, role: &str) {
|
||||
if !["admin", "poweruser", "operator", "viewer"].contains(&role) {
|
||||
eprintln!("Role must be admin, poweruser, operator, or viewer.");
|
||||
@@ -1247,7 +1285,7 @@ async fn run_server(config: Config, database: Db) {
|
||||
.route("/api/reports/top-users", get(api::report_top_users))
|
||||
.route("/api/reports/summary", get(api::report_summary))
|
||||
.route("/api/system/status", get(api::system_status))
|
||||
.route("/api/users", get(api::list_users))
|
||||
.route("/api/users", get(api::list_users).post(api::create_user))
|
||||
.route("/api/users/{email}/role", put(api::set_user_role))
|
||||
.route(
|
||||
"/api/users/{email}/sessions",
|
||||
|
||||
+11
-7
@@ -508,8 +508,10 @@ pub async fn callback(
|
||||
_ => None,
|
||||
};
|
||||
|
||||
// Upsert user in DB (sets default_role only on first login INSERT, not on subsequent updates)
|
||||
// Upsert user in DB (sets default_role only on first login INSERT, not on subsequent updates).
|
||||
// With auto_create_users = false, only already-provisioned users get through.
|
||||
let default_role = oidc.config.default_role.clone();
|
||||
let auto_create = oidc.config.auto_create_users;
|
||||
let db_clone = database.clone();
|
||||
let email_clone = email.clone();
|
||||
let name_clone = name.clone();
|
||||
@@ -523,11 +525,16 @@ pub async fn callback(
|
||||
Some(&subject_clone),
|
||||
&default_role,
|
||||
&groups,
|
||||
auto_create,
|
||||
)
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(Ok(user)) => user,
|
||||
Ok(Ok(Some(user))) => user,
|
||||
Ok(Ok(None)) => {
|
||||
tracing::warn!(email = %email, "OIDC login rejected: no account and auto_create_users is disabled");
|
||||
return Redirect::to("/?sso_error=no_account").into_response();
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
tracing::error!("Failed to upsert user: {}", e);
|
||||
return (
|
||||
@@ -562,11 +569,8 @@ pub async fn callback(
|
||||
};
|
||||
|
||||
if user.disabled {
|
||||
return (
|
||||
StatusCode::FORBIDDEN,
|
||||
axum::Json(json!({"error": "account is disabled"})),
|
||||
)
|
||||
.into_response();
|
||||
tracing::warn!(email = %email, "OIDC login rejected: account is disabled");
|
||||
return Redirect::to("/?sso_error=disabled").into_response();
|
||||
}
|
||||
|
||||
// Create auth session
|
||||
|
||||
@@ -45,6 +45,17 @@
|
||||
</tr></thead>
|
||||
<tbody id="users-body"></tbody>
|
||||
</table>
|
||||
<div class="add-form">
|
||||
<input type="text" id="new-user-email" placeholder="user@example.com" style="width:200px;">
|
||||
<input type="text" id="new-user-name" placeholder="Name (optional)">
|
||||
<select id="new-user-role">
|
||||
<option value="viewer">viewer</option>
|
||||
<option value="operator" selected>operator</option>
|
||||
<option value="poweruser">poweruser</option>
|
||||
<option value="admin">admin</option>
|
||||
</select>
|
||||
<button class="btn-primary" id="add-user-btn">Add User</button>
|
||||
</div>
|
||||
|
||||
<h2>Group-to-Role Mappings</h2>
|
||||
<p style="color:#888;font-size:0.85em;margin:0.3em 0;">OIDC groups are matched to roles on every login. The highest matching role wins.</p>
|
||||
@@ -314,6 +325,30 @@
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('add-user-btn').addEventListener('click', function() {
|
||||
var email = document.getElementById('new-user-email').value.trim();
|
||||
var name = document.getElementById('new-user-name').value.trim();
|
||||
var role = document.getElementById('new-user-role').value;
|
||||
if (!email) { showError('Email is required'); return; }
|
||||
|
||||
fetch('/api/users', {
|
||||
method: 'POST',
|
||||
headers: apiHeaders({'Content-Type': 'application/json'}),
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({email: email, name: name, role: role})
|
||||
})
|
||||
.then(function(res) {
|
||||
if (!res.ok) return res.json().then(function(d){ showError(d.error); throw new Error(); });
|
||||
return res.json();
|
||||
})
|
||||
.then(function() {
|
||||
document.getElementById('new-user-email').value = '';
|
||||
document.getElementById('new-user-name').value = '';
|
||||
loadUsers();
|
||||
})
|
||||
.catch(function() {});
|
||||
});
|
||||
|
||||
function td(text) {
|
||||
var el = document.createElement('td');
|
||||
el.textContent = text || '';
|
||||
|
||||
+20
-8
@@ -24,13 +24,13 @@
|
||||
color: var(--text-muted);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
#sso-unavailable {
|
||||
display: none;
|
||||
#sso-error {
|
||||
margin-top: var(--s-2);
|
||||
font-size: var(--fz-sm);
|
||||
color: var(--text-muted);
|
||||
text-align: center;
|
||||
}
|
||||
#sso-error:empty { display: none; }
|
||||
.api-key-toggle {
|
||||
color: var(--text-muted);
|
||||
font-size: var(--fz-sm);
|
||||
@@ -77,7 +77,7 @@
|
||||
<div class="login-container">
|
||||
<div id="sso-section">
|
||||
<button id="sso-btn">Sign in with SSO</button>
|
||||
<div id="sso-unavailable">SSO provider is temporarily unavailable — retrying…</div>
|
||||
<div id="sso-error" role="alert"></div>
|
||||
</div>
|
||||
|
||||
<div class="api-key-toggle" id="api-key-toggle">
|
||||
@@ -95,9 +95,21 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Messages shown under the SSO button. Keys other than `unavailable`
|
||||
// match the sso_error codes the OIDC callback redirects with.
|
||||
var ssoMessages = {
|
||||
failed: 'SSO login failed — please try again.',
|
||||
no_account: 'You do not have an account here. Ask an administrator to add you.',
|
||||
disabled: 'Your account is disabled. Contact an administrator.',
|
||||
unavailable: 'SSO provider is temporarily unavailable — retrying…'
|
||||
};
|
||||
var ssoErrorEl = document.getElementById('sso-error');
|
||||
|
||||
// Show SSO error if redirected back from a failed callback
|
||||
if (new URLSearchParams(window.location.search).get('sso_error')) {
|
||||
document.getElementById('error').textContent = 'SSO login failed — please try again.';
|
||||
var ssoError = new URLSearchParams(window.location.search).get('sso_error');
|
||||
if (ssoError) {
|
||||
ssoErrorEl.textContent = Object.prototype.hasOwnProperty.call(ssoMessages, ssoError)
|
||||
? ssoMessages[ssoError] : ssoMessages.failed;
|
||||
history.replaceState(null, '', '/');
|
||||
}
|
||||
|
||||
@@ -121,7 +133,6 @@
|
||||
var chevron = document.getElementById('api-key-chevron');
|
||||
|
||||
var ssoBtn = document.getElementById('sso-btn');
|
||||
var ssoUnavailable = document.getElementById('sso-unavailable');
|
||||
var ssoPollTimer = null;
|
||||
|
||||
// Reflect live SSO availability: when the OIDC provider is reachable the
|
||||
@@ -132,12 +143,13 @@
|
||||
if (available) {
|
||||
ssoBtn.disabled = false;
|
||||
ssoBtn.textContent = 'Sign in with SSO';
|
||||
ssoUnavailable.style.display = 'none';
|
||||
// Clear only our own notice; keep a login error from the callback.
|
||||
if (ssoErrorEl.textContent === ssoMessages.unavailable) ssoErrorEl.textContent = '';
|
||||
if (ssoPollTimer) { clearInterval(ssoPollTimer); ssoPollTimer = null; }
|
||||
} else {
|
||||
ssoBtn.disabled = true;
|
||||
ssoBtn.textContent = 'SSO unavailable';
|
||||
ssoUnavailable.style.display = 'block';
|
||||
ssoErrorEl.textContent = ssoMessages.unavailable;
|
||||
if (!ssoPollTimer) {
|
||||
ssoPollTimer = setInterval(function() {
|
||||
fetch('/api/auth/status')
|
||||
|
||||
Reference in New Issue
Block a user