Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ Controller v3.8 is a **greenfield** release aligned with **Edgelet**. There is *

### Added

- Embedded OIDC identity service with TOTP MFA (mandatory for `admin` group).
- Embedded OIDC identity service with TOTP MFA (`mfaRequired` per auth group; all system groups default to MFA off on install).
- **`GET /api/v3/user/profile`** (embedded mode) — includes **`mfaEnabled`** for the authenticated user.
- **`POST /api/v3/auth/migration/export`** — one-way embedded → external IdP migration.
- **`POST /api/v3/auth/jwks/rotate`** — manual JWKS rotation (embedded mode).
- Built-in rate limiting on auth endpoints.
Expand Down Expand Up @@ -105,6 +106,7 @@ Controller v3.8 is a **greenfield** release aligned with **Edgelet**. There is *

### Changed

- Embedded auth login MFA challenge — users with enrolled TOTP (**`mfaEnabled`**) are always prompted for TOTP at login, including voluntary My Account enrollment when all groups have **`mfaRequired: false`**. Group **`mfaRequired`** still forces enrollment for members who have not enrolled.
- OpenTelemetry SDK dependencies bumped to **0.219.x**; telemetry init simplified (removed custom resource detector wrapper).
- **`js-yaml`** bumped to **4.2.0**.

Expand Down
64 changes: 38 additions & 26 deletions docs/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3131,6 +3131,11 @@ paths:
tags:
- User
summary: Get current user profile data
description: |
In **embedded** auth mode, returns a flat profile object derived from the access token
plus embedded MFA enrollment state (`mfaEnabled`).

In **external** auth mode, returns the upstream IdP UserInfo payload (shape depends on the IdP).
operationId: getUserProfile
security:
- authToken: []
Expand Down Expand Up @@ -3882,8 +3887,12 @@ paths:
patch:
tags:
- Auth Groups
summary: Update custom auth group
summary: Update auth group
operationId: updateAuthGroup
description: >
Updates a custom auth group name and/or `mfaRequired`. System groups (`admin`, `sre`,
`developer`, `viewer`) allow `mfaRequired` changes only; rename is forbidden. Requires RBAC
`authGroups` resource with `patch` verb.
security:
- authToken: []
parameters:
Expand Down Expand Up @@ -8946,32 +8955,27 @@ components:
type: string
UserProfileDetailsResponse:
type: object
description: |
Embedded auth mode response. External auth mode returns the IdP UserInfo object instead
(may include different fields; `mfaEnabled` is not supplied by Controller in external mode).
properties:
userinfo:
sub:
type: string
email:
type: string
description: Omitted when the login identifier has no `@` (e.g. bootstrap username).
preferred_username:
type: string
groups:
type: array
items:
properties:
sub:
type: string
SubscriptionKey:
type: string
email_verified:
type: string
name:
type: string
preferred_username:
type: string
locale:
type: string
given_name:
type: string
family_name:
type: string
email:
type: string
password_change_required:
type: boolean
description: Present when JWT claim `password_change_required` is true
type: string
password_change_required:
type: boolean
description: Present when JWT claim `password_change_required` is true
mfaEnabled:
type: boolean
description: Embedded auth only — whether the user has enrolled TOTP MFA (login requires TOTP when true, except bootstrap)
ChangePasswordRequest:
type: object
required:
Expand Down Expand Up @@ -9067,13 +9071,19 @@ components:
properties:
name:
type: string
mfaRequired:
type: boolean
default: false
description: When true, members of this group must enroll and use TOTP MFA at login
AuthGroupUpdateRequest:
type: object
required:
- name
description: At least one of `name` or `mfaRequired` is required
properties:
name:
type: string
mfaRequired:
type: boolean
description: When true, members of this group must enroll and use TOTP MFA at login
AuthGroupResponse:
type: object
properties:
Expand All @@ -9083,6 +9093,8 @@ components:
type: string
isSystem:
type: boolean
mfaRequired:
type: boolean
createdAt:
type: string
format: date-time
Expand Down
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@
"serialize-javascript": "7.0.5",
"diff": "8.0.3",
"form-data": "^4.0.6",
"js-yaml": "4.2.0"
"js-yaml": "4.2.0",
"undici": "7.28.0"
}
}
1 change: 1 addition & 0 deletions src/data/migrations/mysql/db_migration_mysql_v3.8.0.sql
Original file line number Diff line number Diff line change
Expand Up @@ -991,6 +991,7 @@ CREATE TABLE IF NOT EXISTS AuthGroups (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL UNIQUE,
is_system BOOLEAN DEFAULT false,
mfa_required BOOLEAN NOT NULL DEFAULT false,
created_at DATETIME,
updated_at DATETIME
);
Expand Down
1 change: 1 addition & 0 deletions src/data/migrations/postgres/db_migration_pg_v3.8.0.sql
Original file line number Diff line number Diff line change
Expand Up @@ -983,6 +983,7 @@ CREATE TABLE IF NOT EXISTS "AuthGroups" (
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY NOT NULL,
name VARCHAR(255) NOT NULL UNIQUE,
is_system BOOLEAN DEFAULT false,
mfa_required BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMP(0),
updated_at TIMESTAMP(0)
);
Expand Down
1 change: 1 addition & 0 deletions src/data/migrations/sqlite/db_migration_sqlite_v3.8.0.sql
Original file line number Diff line number Diff line change
Expand Up @@ -982,6 +982,7 @@ CREATE TABLE IF NOT EXISTS AuthGroups (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
name VARCHAR(255) NOT NULL UNIQUE,
is_system BOOLEAN DEFAULT false,
mfa_required BOOLEAN NOT NULL DEFAULT false,
created_at DATETIME,
updated_at DATETIME
);
Expand Down
6 changes: 6 additions & 0 deletions src/data/models/authGroup.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ module.exports = (sequelize, DataTypes) => {
allowNull: false,
defaultValue: false,
field: 'is_system'
},
mfaRequired: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
field: 'mfa_required'
}
}, {
tableName: 'AuthGroups',
Expand Down
10 changes: 5 additions & 5 deletions src/data/seeders/mysql/db_seeder_mysql_v3.8.0.sql
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,12 @@ INSERT IGNORE INTO AuthPolicy (
)
VALUES (1, 12, true, true, true, 0, 5, 5, 15, 900, 3600, true, NULL);

INSERT IGNORE INTO AuthGroups (name, is_system)
INSERT IGNORE INTO AuthGroups (name, is_system, mfa_required)
VALUES
('admin', true),
('sre', true),
('developer', true),
('viewer', true);
('admin', true, false),
('sre', true, false),
('developer', true, false),
('viewer', true, false);

INSERT IGNORE INTO AuthBootstrapMeta (id, completed_at)
VALUES (1, NULL);
Expand Down
10 changes: 5 additions & 5 deletions src/data/seeders/postgres/db_seeder_pg_v3.8.0.sql
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,12 @@ INSERT INTO "AuthPolicy" (
VALUES (1, 12, true, true, true, 0, 5, 5, 15, 900, 3600, true, NULL)
ON CONFLICT (id) DO NOTHING;

INSERT INTO "AuthGroups" (name, is_system)
INSERT INTO "AuthGroups" (name, is_system, mfa_required)
VALUES
('admin', true),
('sre', true),
('developer', true),
('viewer', true)
('admin', true, false),
('sre', true, false),
('developer', true, false),
('viewer', true, false)
ON CONFLICT (name) DO NOTHING;

INSERT INTO "AuthBootstrapMeta" (id, completed_at)
Expand Down
10 changes: 5 additions & 5 deletions src/data/seeders/sqlite/db_seeder_sqlite_v3.8.0.sql
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,12 @@ INSERT OR IGNORE INTO AuthPolicy (
)
VALUES (1, 12, true, true, true, 0, 5, 5, 15, 900, 3600, true, NULL);

INSERT OR IGNORE INTO AuthGroups (name, is_system)
INSERT OR IGNORE INTO AuthGroups (name, is_system, mfa_required)
VALUES
('admin', true),
('sre', true),
('developer', true),
('viewer', true);
('admin', true, false),
('sre', true, false),
('developer', true, false),
('viewer', true, false);

INSERT OR IGNORE INTO AuthBootstrapMeta (id, completed_at)
VALUES (1, NULL);
7 changes: 4 additions & 3 deletions src/schemas/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,8 @@ const createAuthGroup = {
id: '/createAuthGroup',
type: 'object',
properties: {
name: { type: 'string', minLength: 1 }
name: { type: 'string', minLength: 1 },
mfaRequired: { type: 'boolean' }
},
required: ['name'],
additionalProperties: true
Expand All @@ -130,9 +131,9 @@ const updateAuthGroup = {
id: '/updateAuthGroup',
type: 'object',
properties: {
name: { type: 'string', minLength: 1 }
name: { type: 'string', minLength: 1 },
mfaRequired: { type: 'boolean' }
},
required: ['name'],
additionalProperties: true
}

Expand Down
6 changes: 5 additions & 1 deletion src/services/auth-bootstrap-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,11 @@ async function ensureSystemGroups (transaction) {
for (const name of SYSTEM_GROUPS) {
await db.AuthGroup.findOrCreate({
where: { name },
defaults: { name, isSystem: true },
defaults: {
name,
isSystem: true,
mfaRequired: false
},
transaction
})
}
Expand Down
9 changes: 8 additions & 1 deletion src/services/auth-login-service.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
'use strict'

const { decodeJwt } = require('jose')
const db = require('../data/models')
const Errors = require('../helpers/errors')
const { withTransaction } = require('../helpers/app-helper')
const AuthPolicyService = require('./auth-policy-service')
const AuthPasswordService = require('./auth-password-service')
const AuthTokenService = require('./auth-token-service')
Expand Down Expand Up @@ -54,12 +56,17 @@ async function profile (req, transaction) {
const accessToken = req.headers.authorization.replace('Bearer ', '')
const claims = decodeJwt(accessToken)

const mfaRecord = claims.sub
? await db.AuthMfa.findOne(withTransaction(transaction, { where: { userId: claims.sub } }))
: null

return {
sub: claims.sub,
email: claims.email,
preferred_username: claims.preferred_username,
groups: claims.groups || [],
password_change_required: claims.password_change_required === true
password_change_required: claims.password_change_required === true,
mfaEnabled: Boolean(mfaRecord && mfaRecord.enabled)
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/services/auth-mfa-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ function isMfaExempt (user) {
}

function userRequiresMfa (groups) {
return normalizeGroupNames(groups).includes(ADMIN_GROUP)
return (groups || []).some((group) => group.mfaRequired === true)
}

function userMustEnrollMfa (user, groups, mfaRecord) {
Expand All @@ -38,7 +38,7 @@ function userRequiresMfaChallenge (user, groups, mfaRecord) {
if (isMfaExempt(user)) {
return false
}
return userRequiresMfa(groups) && Boolean(mfaRecord && mfaRecord.enabled)
return Boolean(mfaRecord && mfaRecord.enabled)
}

async function loadUserAuthContext (email, transaction) {
Expand Down
Loading
Loading