Private
Public Access
1
0

feat: Complete Azure SSO implementation (v0.1.3)

- Add SSO session cleanup task (10-min expiry, 60s purge interval)
- Change callback to redirect to frontend with tokens as query params
- Add sso_callback_url to SecurityConfig with serde default
- Add SsoCallbackPage.tsx for handling SSO callback redirects
- Add /auth/sso/callback public route to App.tsx
- Add Sign in with Microsoft Azure button to LoginPage
- Replace insecure decode_jwt_payload with verify_id_token
- Implement JWKS caching (1-hour TTL) and RSA signature verification
- Validate iss, aud, exp claims on id_token
- Add jsonwebtoken dependency to pm-web crate
- Update config.example.toml with sso_callback_url setting
- Add sso_callback_url to settings response (read-only from TOML)
This commit is contained in:
2026-05-12 17:01:20 +00:00
parent 08add28b80
commit 86a6c714d4
18 changed files with 561 additions and 239 deletions

View File

@ -1,4 +1,5 @@
import { useEffect, useState, useMemo } from 'react'
import { useNavigate } from 'react-router-dom'
import {
Box, Button, Chip, CircularProgress, Container, Dialog, DialogActions,
DialogContent, DialogContentText, DialogTitle, FormControlLabel, IconButton,
@ -75,6 +76,7 @@ function PasswordStrengthIndicator({ password }: { password: string }) {
export default function UsersPage() {
const currentUser = useAuthStore(s => s.user)
const navigate = useNavigate()
const isAdmin = currentUser?.role === 'admin'
const [users, setUsers] = useState<User[]>([])
@ -327,8 +329,17 @@ export default function UsersPage() {
color={u.role === 'admin' ? 'primary' : 'default'} />
</TableCell>
<TableCell>
<Chip size="small" label={u.mfa_enabled ? 'On' : 'Off'}
color={u.mfa_enabled ? 'success' : 'warning'} />
{u.mfa_enabled ? (
<Chip size="small" label="On" color="success" />
) : currentUser?.id === u.id ? (
<Tooltip title="Enable MFA">
<Chip size="small" label="Off" color="warning"
sx={{ cursor: 'pointer', '&:hover': { opacity: 0.8 } }}
onClick={() => navigate('/mfa/setup')} />
</Tooltip>
) : (
<Chip size="small" label="Off" color="default" />
)}
</TableCell>
<TableCell>
<Chip size="small" label={u.is_active ? 'Active' : 'Disabled'}
@ -460,24 +471,41 @@ export default function UsersPage() {
/>
</Box>
{/* MFA status & disable */}
{/* MFA status */}
<Box sx={{ mt: 2, display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="body2" color="text.secondary">MFA Status:</Typography>
<Chip size="small"
label={editUser?.mfa_enabled ? 'Enabled' : 'Disabled'}
color={editUser?.mfa_enabled ? 'success' : 'default'}
/>
{editUser?.mfa_enabled && (
{editUser?.mfa_enabled ? (
<Button size="small" color="error" variant="outlined"
onClick={() => editUser && handleMfaDisable(editUser)}>
Disable MFA
</Button>
) : (
currentUser?.id === editUser?.id ? (
<Button size="small" color="primary" variant="outlined"
onClick={() => navigate('/mfa/setup')}>
Enable MFA
</Button>
) : (
<Typography variant="caption" color="text.secondary">
User must enable MFA from their own profile settings.
</Typography>
)
)}
</Box>
{editUser?.mfa_enabled && (
{editUser?.mfa_enabled ? (
<Typography variant="caption" color="warning.main" sx={{ display: 'block', mt: 0.5 }}>
Disabling MFA reduces account security for this user.
</Typography>
) : (
currentUser?.id === editUser?.id && (
<Typography variant="caption" color="info.main" sx={{ display: 'block', mt: 0.5 }}>
You will be guided through authenticator app setup.
</Typography>
)
)}
</>
)}