Compare commits
5 Commits
6c193ee9c9
...
a28b6cfa80
| Author | SHA1 | Date | |
|---|---|---|---|
| a28b6cfa80 | |||
| 37cc8052a9 | |||
| ef6b857ac6 | |||
| 36b09b9853 | |||
| 7021c55e1b |
@@ -46,6 +46,7 @@ func scanAuthProvider(row interface {
|
||||
&item.OIDCGroupsClaim,
|
||||
&item.OIDCAdmissionExpr,
|
||||
&item.OIDCEndSessionURL,
|
||||
&item.OIDCPostLogoutRedirect,
|
||||
&item.GroupSyncMode,
|
||||
&defaultUserGroupID,
|
||||
&item.UserCount,
|
||||
@@ -65,7 +66,7 @@ const authProviderSelectCols = `
|
||||
p.ldap_tls_insecure_skip_verify,
|
||||
p.oidc_client_id, p.oidc_client_secret, p.oidc_authorize_url, p.oidc_token_url,
|
||||
p.oidc_userinfo_url, p.oidc_redirect_url, p.oidc_scopes, p.oidc_tls_insecure_skip_verify,
|
||||
p.oidc_groups_claim, p.oidc_admission_expr, p.oidc_end_session_url, p.group_sync_mode,
|
||||
p.oidc_groups_claim, p.oidc_admission_expr, p.oidc_end_session_url, p.oidc_post_logout_redirect, p.group_sync_mode,
|
||||
COALESCE(ug.public_id, ''),
|
||||
(SELECT COUNT(*) FROM users WHERE auth_provider_id = p.id),
|
||||
p.created_at, p.updated_at`
|
||||
@@ -307,16 +308,16 @@ func (s *Store) CreateAuthProvider(item models.AuthProvider) (models.AuthProvide
|
||||
ldap_tls_insecure_skip_verify,
|
||||
oidc_client_id, oidc_client_secret, oidc_authorize_url, oidc_token_url,
|
||||
oidc_userinfo_url, oidc_redirect_url, oidc_scopes, oidc_tls_insecure_skip_verify,
|
||||
oidc_groups_claim, oidc_admission_expr, oidc_end_session_url, group_sync_mode,
|
||||
oidc_groups_claim, oidc_admission_expr, oidc_end_session_url, oidc_post_logout_redirect, group_sync_mode,
|
||||
default_user_group_id,
|
||||
created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CASE WHEN ? = '' THEN NULL ELSE (SELECT id FROM user_groups WHERE public_id = ?) END, ?, ?)`,
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CASE WHEN ? = '' THEN NULL ELSE (SELECT id FROM user_groups WHERE public_id = ?) END, ?, ?)`,
|
||||
item.ID, item.Name, item.Type, item.Enabled,
|
||||
item.LDAPUrl, item.LDAPBindDN, item.LDAPBindPassword, item.LDAPUserBaseDN, item.LDAPUserFilter,
|
||||
item.LDAPTLSInsecureSkipVerify,
|
||||
item.OIDCClientID, item.OIDCClientSecret, item.OIDCAuthorizeURL, item.OIDCTokenURL,
|
||||
item.OIDCUserInfoURL, item.OIDCRedirectURL, item.OIDCScopes, item.OIDCTLSInsecureSkipVerify,
|
||||
item.OIDCGroupsClaim, item.OIDCAdmissionExpr, item.OIDCEndSessionURL, item.GroupSyncMode,
|
||||
item.OIDCGroupsClaim, item.OIDCAdmissionExpr, item.OIDCEndSessionURL, item.OIDCPostLogoutRedirect, item.GroupSyncMode,
|
||||
item.DefaultUserGroupID, item.DefaultUserGroupID,
|
||||
item.CreatedAt, item.UpdatedAt,
|
||||
)
|
||||
@@ -367,7 +368,7 @@ func (s *Store) UpdateAuthProvider(item models.AuthProvider) (models.AuthProvide
|
||||
ldap_tls_insecure_skip_verify = ?,
|
||||
oidc_client_id = ?, oidc_client_secret = ?, oidc_authorize_url = ?, oidc_token_url = ?,
|
||||
oidc_userinfo_url = ?, oidc_redirect_url = ?, oidc_scopes = ?, oidc_tls_insecure_skip_verify = ?,
|
||||
oidc_groups_claim = ?, oidc_admission_expr = ?, oidc_end_session_url = ?, group_sync_mode = ?,
|
||||
oidc_groups_claim = ?, oidc_admission_expr = ?, oidc_end_session_url = ?, oidc_post_logout_redirect = ?, group_sync_mode = ?,
|
||||
default_user_group_id = CASE WHEN ? = '' THEN NULL ELSE (SELECT id FROM user_groups WHERE public_id = ?) END,
|
||||
updated_at = ?
|
||||
WHERE public_id = ?`,
|
||||
@@ -376,7 +377,7 @@ func (s *Store) UpdateAuthProvider(item models.AuthProvider) (models.AuthProvide
|
||||
item.LDAPTLSInsecureSkipVerify,
|
||||
item.OIDCClientID, item.OIDCClientSecret, item.OIDCAuthorizeURL, item.OIDCTokenURL,
|
||||
item.OIDCUserInfoURL, item.OIDCRedirectURL, item.OIDCScopes, item.OIDCTLSInsecureSkipVerify,
|
||||
item.OIDCGroupsClaim, item.OIDCAdmissionExpr, item.OIDCEndSessionURL, item.GroupSyncMode,
|
||||
item.OIDCGroupsClaim, item.OIDCAdmissionExpr, item.OIDCEndSessionURL, item.OIDCPostLogoutRedirect, item.GroupSyncMode,
|
||||
item.DefaultUserGroupID, item.DefaultUserGroupID,
|
||||
item.UpdatedAt,
|
||||
strings.TrimSpace(item.ID),
|
||||
|
||||
@@ -647,14 +647,21 @@ func (s *Store) GetUserByAPIKeyHash(tokenHash string) (models.User, error) {
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (s *Store) CreateSession(userID string, token string, expiresAt time.Time, totpVerified bool) error {
|
||||
func (s *Store) CreateSession(userID string, token string, expiresAt time.Time, totpVerified bool, oidcIDToken string) error {
|
||||
var err error
|
||||
_, err = s.Exec(`INSERT INTO sessions (id, user_id, token, expires_at, created_at, totp_verified)
|
||||
VALUES (?, (SELECT id FROM users WHERE public_id = ?), ?, ?, ?, ?)`,
|
||||
mustID(), userID, token, expiresAt.Unix(), time.Now().UTC().Unix(), totpVerified)
|
||||
_, err = s.Exec(`INSERT INTO sessions (id, user_id, token, expires_at, created_at, totp_verified, oidc_id_token)
|
||||
VALUES (?, (SELECT id FROM users WHERE public_id = ?), ?, ?, ?, ?, ?)`,
|
||||
mustID(), userID, token, expiresAt.Unix(), time.Now().UTC().Unix(), totpVerified, oidcIDToken)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Store) GetSessionOIDCIDToken(token string) (string, error) {
|
||||
var idToken string
|
||||
var err error
|
||||
err = s.QueryRow(`SELECT oidc_id_token FROM sessions WHERE token = ?`, token).Scan(&idToken)
|
||||
return idToken, err
|
||||
}
|
||||
|
||||
func (s *Store) SetSessionTOTPVerified(token string) error {
|
||||
var err error
|
||||
_, err = s.Exec(`UPDATE sessions SET totp_verified = 1 WHERE token = ?`, token)
|
||||
|
||||
@@ -11,6 +11,7 @@ import "fmt"
|
||||
import "io"
|
||||
import "mime/multipart"
|
||||
import "net/http"
|
||||
import "net/url"
|
||||
import "os"
|
||||
import "path/filepath"
|
||||
import "sort"
|
||||
@@ -537,7 +538,7 @@ func (api *API) Login(w http.ResponseWriter, r *http.Request, _ map[string]strin
|
||||
WriteJSONWithErrorReason(w, r, http.StatusUnauthorized, "invalid credentials")
|
||||
}
|
||||
|
||||
func (api *API) issueSession(w http.ResponseWriter, r *http.Request, user models.User, totpVerified bool) {
|
||||
func (api *API) issueSession(w http.ResponseWriter, r *http.Request, user models.User, totpVerified bool, oidcIDToken string) {
|
||||
var token string
|
||||
var err error
|
||||
var expires time.Time
|
||||
@@ -548,7 +549,7 @@ func (api *API) issueSession(w http.ResponseWriter, r *http.Request, user models
|
||||
|
||||
expires = auth.SessionExpiry(api.Cfg)
|
||||
_ = api.store(r).DeleteExpiredSessions(time.Now().UTC())
|
||||
_ = api.store(r).CreateSession(user.ID, token, expires, totpVerified)
|
||||
_ = api.store(r).CreateSession(user.ID, token, expires, totpVerified, oidcIDToken)
|
||||
cookie = &http.Cookie{
|
||||
Name: api.sessionCookieName(),
|
||||
Value: token,
|
||||
@@ -562,6 +563,7 @@ func (api *API) issueSession(w http.ResponseWriter, r *http.Request, user models
|
||||
func (api *API) Logout(w http.ResponseWriter, r *http.Request, _ map[string]string) {
|
||||
var cookie *http.Cookie
|
||||
var endSessionURL string
|
||||
var oidcIDToken string
|
||||
var err error
|
||||
|
||||
cookie, err = r.Cookie(api.sessionCookieName())
|
||||
@@ -572,9 +574,14 @@ func (api *API) Logout(w http.ResponseWriter, r *http.Request, _ map[string]stri
|
||||
if userErr == nil && strings.TrimSpace(user.AuthProviderID) != "" {
|
||||
var provider models.AuthProvider
|
||||
var provErr error
|
||||
var tokenErr error
|
||||
provider, provErr = api.store(r).GetAuthProvider(user.AuthProviderID)
|
||||
if provErr == nil {
|
||||
endSessionURL = strings.TrimSpace(provider.OIDCEndSessionURL)
|
||||
oidcIDToken, tokenErr = api.store(r).GetSessionOIDCIDToken(cookie.Value)
|
||||
if tokenErr != nil {
|
||||
oidcIDToken = ""
|
||||
}
|
||||
endSessionURL = api.buildOIDCEndSessionURL(r, provider, oidcIDToken)
|
||||
}
|
||||
}
|
||||
_ = api.store(r).DeleteSession(cookie.Value)
|
||||
@@ -594,6 +601,68 @@ func (api *API) Logout(w http.ResponseWriter, r *http.Request, _ map[string]stri
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (api *API) buildOIDCEndSessionURL(r *http.Request, provider models.AuthProvider, idToken string) string {
|
||||
var raw string
|
||||
var parsed *url.URL
|
||||
var values url.Values
|
||||
var redirectURL string
|
||||
var err error
|
||||
|
||||
raw = strings.TrimSpace(provider.OIDCEndSessionURL)
|
||||
if raw == "" {
|
||||
return ""
|
||||
}
|
||||
parsed, err = url.Parse(raw)
|
||||
if err != nil {
|
||||
return raw
|
||||
}
|
||||
values = parsed.Query()
|
||||
if strings.TrimSpace(idToken) != "" && strings.TrimSpace(values.Get("id_token_hint")) == "" {
|
||||
values.Set("id_token_hint", strings.TrimSpace(idToken))
|
||||
}
|
||||
if provider.OIDCPostLogoutRedirect {
|
||||
redirectURL = api.postLogoutRedirectURL(r)
|
||||
if redirectURL != "" && strings.TrimSpace(values.Get("post_logout_redirect_uri")) == "" {
|
||||
values.Set("post_logout_redirect_uri", redirectURL)
|
||||
}
|
||||
}
|
||||
parsed.RawQuery = values.Encode()
|
||||
return parsed.String()
|
||||
}
|
||||
|
||||
func (api *API) postLogoutRedirectURL(r *http.Request) string {
|
||||
var base string
|
||||
var host string
|
||||
var scheme string
|
||||
|
||||
base = strings.TrimRight(strings.TrimSpace(api.Cfg.PublicBaseURL), "/")
|
||||
if base != "" {
|
||||
return base + "/login"
|
||||
}
|
||||
host = strings.TrimSpace(r.Header.Get("X-Forwarded-Host"))
|
||||
if host != "" {
|
||||
host = strings.TrimSpace(strings.Split(host, ",")[0])
|
||||
}
|
||||
if host == "" {
|
||||
host = strings.TrimSpace(r.Host)
|
||||
}
|
||||
if host == "" {
|
||||
return ""
|
||||
}
|
||||
scheme = strings.TrimSpace(r.Header.Get("X-Forwarded-Proto"))
|
||||
if scheme != "" {
|
||||
scheme = strings.TrimSpace(strings.Split(scheme, ",")[0])
|
||||
}
|
||||
if scheme == "" {
|
||||
if r.TLS != nil {
|
||||
scheme = "https"
|
||||
} else {
|
||||
scheme = "http"
|
||||
}
|
||||
}
|
||||
return scheme + "://" + host + "/login"
|
||||
}
|
||||
|
||||
func (api *API) Me(w http.ResponseWriter, r *http.Request, _ map[string]string) {
|
||||
var user models.User
|
||||
var permissions []string
|
||||
@@ -616,9 +685,24 @@ func (api *API) Me(w http.ResponseWriter, r *http.Request, _ map[string]string)
|
||||
if ok {
|
||||
response.TOTPVerifyRequired = user.TOTPEffectiveRequired && user.TOTPEnabled && !session.TOTPVerified
|
||||
}
|
||||
response.LogoutRequiresConfirmation = api.logoutRequiresConfirmation(r, user)
|
||||
WriteJSON(w, http.StatusOK, response)
|
||||
}
|
||||
|
||||
func (api *API) logoutRequiresConfirmation(r *http.Request, user models.User) bool {
|
||||
var provider models.AuthProvider
|
||||
var err error
|
||||
|
||||
if strings.TrimSpace(user.AuthProviderID) == "" {
|
||||
return false
|
||||
}
|
||||
provider, err = api.store(r).GetAuthProvider(user.AuthProviderID)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return provider.Type == db.AuthProviderTypeOIDC && strings.TrimSpace(provider.OIDCEndSessionURL) != ""
|
||||
}
|
||||
|
||||
func (api *API) UpdateMe(w http.ResponseWriter, r *http.Request, _ map[string]string) {
|
||||
var ctxUser models.User
|
||||
var ok bool
|
||||
|
||||
@@ -157,7 +157,7 @@ func (api *API) OIDCCallbackWithProvider(w http.ResponseWriter, r *http.Request,
|
||||
user, err = api.oidcGetOrCreateUser(r.Context(), claims, provider)
|
||||
if err != nil {
|
||||
api.Logger.Write(logIDAuth, codit_logger.LOG_WARN, "oidc user mapping failed provider=%s err=%v", provider.Name, err)
|
||||
WriteJSONWithErrorReason(w, r, http.StatusUnauthorized, "oidc user mapping failed")
|
||||
WriteJSONWithErrorReason(w, r, http.StatusUnauthorized, fmt.Sprintf("oidc user mapping failed - %s", err.Error()))
|
||||
return
|
||||
}
|
||||
required, err = api.store(r).UserRequiresTOTP(user.ID)
|
||||
@@ -173,7 +173,7 @@ func (api *API) OIDCCallbackWithProvider(w http.ResponseWriter, r *http.Request,
|
||||
user.TOTPEffectiveRequired = required
|
||||
user.TOTPSetupRequired = required && !user.TOTPEnabled
|
||||
sessionVerified = !required
|
||||
api.issueSession(w, r, user, sessionVerified)
|
||||
api.issueSession(w, r, user, sessionVerified, token.IDToken)
|
||||
api.Logger.Write(logIDAuth, codit_logger.LOG_INFO, "oidc login success username=%s provider=%s", user.Username, provider.Name)
|
||||
http.Redirect(w, r, "/", http.StatusFound)
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ type meResponse struct {
|
||||
TOTPEffectiveRequired bool `json:"totp_effective_required"`
|
||||
TOTPSetupRequired bool `json:"totp_setup_required"`
|
||||
TOTPVerifyRequired bool `json:"totp_verify_required"`
|
||||
LogoutRequiresConfirmation bool `json:"logout_requires_confirmation"`
|
||||
AvatarURL string `json:"avatar_url"`
|
||||
AvatarUpdatedAt int64 `json:"avatar_updated_at"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
|
||||
@@ -61,7 +61,7 @@ func (api *API) finishLogin(w http.ResponseWriter, r *http.Request, user models.
|
||||
if required && !user.TOTPEnabled {
|
||||
sessionVerified = false
|
||||
}
|
||||
api.issueSession(w, r, user, sessionVerified)
|
||||
api.issueSession(w, r, user, sessionVerified, "")
|
||||
WriteJSON(w, http.StatusOK, user)
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ type AuthProvider struct {
|
||||
OIDCGroupsClaim string `json:"oidc_groups_claim"`
|
||||
OIDCAdmissionExpr string `json:"oidc_admission_expr"`
|
||||
OIDCEndSessionURL string `json:"oidc_end_session_url"`
|
||||
OIDCPostLogoutRedirect bool `json:"oidc_post_logout_redirect"`
|
||||
GroupSyncMode string `json:"group_sync_mode"`
|
||||
GroupMappings []AuthProviderGroupMapping `json:"group_mappings,omitempty"`
|
||||
DefaultUserGroupID string `json:"default_user_group_id"`
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE auth_providers ADD COLUMN oidc_post_logout_redirect INTEGER NOT NULL DEFAULT 0;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE sessions ADD COLUMN oidc_id_token TEXT NOT NULL DEFAULT '';
|
||||
@@ -10,6 +10,7 @@ export interface User {
|
||||
totp_effective_required?: boolean
|
||||
totp_setup_required?: boolean
|
||||
totp_verify_required?: boolean
|
||||
logout_requires_confirmation?: boolean
|
||||
avatar_url?: string
|
||||
avatar_updated_at?: number
|
||||
disabled?: boolean
|
||||
@@ -467,6 +468,7 @@ export interface AuthProvider {
|
||||
oidc_groups_claim: string
|
||||
oidc_admission_expr: string
|
||||
oidc_end_session_url: string
|
||||
oidc_post_logout_redirect: boolean
|
||||
group_sync_mode: 'off' | 'first_login' | 'sync'
|
||||
group_mappings?: AuthProviderGroupMapping[]
|
||||
default_user_group_id: string
|
||||
|
||||
@@ -4,6 +4,9 @@ import {
|
||||
Box,
|
||||
Button,
|
||||
Container,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Divider,
|
||||
Drawer,
|
||||
IconButton,
|
||||
@@ -18,6 +21,7 @@ import {
|
||||
} from '@mui/material'
|
||||
import { Link, Outlet, useLocation, useNavigate } from 'react-router-dom'
|
||||
import { api, User } from '../api'
|
||||
import ModalDialog from '../components/ModalDialog'
|
||||
import { useBrand } from '../branding'
|
||||
import MenuIcon from '@mui/icons-material/Menu'
|
||||
import DashboardIcon from '@mui/icons-material/Dashboard'
|
||||
@@ -70,6 +74,8 @@ export default function Layout() {
|
||||
const [drawerOpen, setDrawerOpen] = useState(true)
|
||||
const [appearanceMenuAnchor, setAppearanceMenuAnchor] = useState<HTMLElement | null>(null)
|
||||
const [accountMenuAnchor, setAccountMenuAnchor] = useState<HTMLElement | null>(null)
|
||||
const [logoutDialogOpen, setLogoutDialogOpen] = useState(false)
|
||||
const [logoutBusy, setLogoutBusy] = useState(false)
|
||||
const themeMode = useContext(ThemeModeContext)
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
@@ -128,10 +134,28 @@ export default function Layout() {
|
||||
]
|
||||
}, [user])
|
||||
|
||||
const handleLogout = async () => {
|
||||
const openLogoutDialog = () => {
|
||||
closeAccountMenu()
|
||||
const result = await api.logout()
|
||||
if (!user?.logout_requires_confirmation) {
|
||||
setLogoutBusy(true)
|
||||
handleLogout().catch(() => {
|
||||
setLogoutBusy(false)
|
||||
})
|
||||
return
|
||||
}
|
||||
setLogoutDialogOpen(true)
|
||||
}
|
||||
|
||||
const closeLogoutDialog = () => {
|
||||
if (logoutBusy) return
|
||||
setLogoutDialogOpen(false)
|
||||
}
|
||||
|
||||
const handleLogout = async () => {
|
||||
const result: { oidc_end_session_url?: string } | null = await api.logout()
|
||||
setUser(null)
|
||||
setLogoutDialogOpen(false)
|
||||
setLogoutBusy(false)
|
||||
if (result?.oidc_end_session_url) {
|
||||
window.location.href = result.oidc_end_session_url
|
||||
} else {
|
||||
@@ -139,6 +163,13 @@ export default function Layout() {
|
||||
}
|
||||
}
|
||||
|
||||
const confirmLogout = () => {
|
||||
setLogoutBusy(true)
|
||||
handleLogout().catch(() => {
|
||||
setLogoutBusy(false)
|
||||
})
|
||||
}
|
||||
|
||||
const openAccountMenu = (event: React.MouseEvent<HTMLElement>) => {
|
||||
setAccountMenuAnchor(event.currentTarget)
|
||||
}
|
||||
@@ -247,7 +278,7 @@ export default function Layout() {
|
||||
<MenuItem disabled>{user.username}</MenuItem>
|
||||
<Divider />
|
||||
<MenuItem onClick={openAccountPage}>Account</MenuItem>
|
||||
<MenuItem onClick={handleLogout}>Logout</MenuItem>
|
||||
<MenuItem onClick={openLogoutDialog}>Sign out</MenuItem>
|
||||
</Menu>
|
||||
</>
|
||||
) : null}
|
||||
@@ -330,6 +361,21 @@ export default function Layout() {
|
||||
<Outlet context={{ user, setUser }} />
|
||||
</Container>
|
||||
</Box>
|
||||
|
||||
<ModalDialog open={logoutDialogOpen} onClose={closeLogoutDialog} maxWidth="xs" fullWidth>
|
||||
<DialogTitle>Sign out?</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
This will sign you out of {brand.site_name}. If your account uses OIDC, it may also sign you out of your identity provider session.
|
||||
</Typography>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={confirmLogout} disabled={logoutBusy} variant="contained" color="primary">
|
||||
{logoutBusy ? 'Signing out...' : 'Sign out'}
|
||||
</Button>
|
||||
<Button onClick={closeLogoutDialog} disabled={logoutBusy}>Cancel</Button>
|
||||
</DialogActions>
|
||||
</ModalDialog>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -94,6 +94,7 @@ export default function AuthProviderDetailsDialog(props: AuthProviderDetailsDial
|
||||
</Box>
|
||||
<TextField label="Admission Expression" value={item.oidc_admission_expr || '-'} InputProps={{ readOnly: true }} multiline minRows={2} />
|
||||
<TextField label="End Session URL" value={item.oidc_end_session_url || '-'} InputProps={{ readOnly: true }} />
|
||||
<TextField label="Post Logout Redirect" value={yesNo(item.oidc_post_logout_redirect)} InputProps={{ readOnly: true }} />
|
||||
</>
|
||||
) : null}
|
||||
<TextField label="Created At" value={fmt(item?.created_at)} InputProps={{ readOnly: true }} />
|
||||
|
||||
@@ -40,6 +40,7 @@ export type AuthProviderFormState = {
|
||||
oidc_groups_claim: string
|
||||
oidc_admission_expr: string
|
||||
oidc_end_session_url: string
|
||||
oidc_post_logout_redirect: boolean
|
||||
group_sync_mode: 'off' | 'first_login' | 'sync'
|
||||
group_mappings: AuthProviderGroupMapping[]
|
||||
default_user_group_id: string
|
||||
@@ -80,6 +81,7 @@ export function emptyAuthProviderForm(): AuthProviderFormState {
|
||||
oidc_groups_claim: 'groups',
|
||||
oidc_admission_expr: '',
|
||||
oidc_end_session_url: '',
|
||||
oidc_post_logout_redirect: false,
|
||||
group_sync_mode: 'off',
|
||||
group_mappings: [],
|
||||
default_user_group_id: ''
|
||||
@@ -108,6 +110,7 @@ export function authProviderToForm(item: AuthProvider): AuthProviderFormState {
|
||||
oidc_groups_claim: item.oidc_groups_claim || 'groups',
|
||||
oidc_admission_expr: item.oidc_admission_expr || '',
|
||||
oidc_end_session_url: item.oidc_end_session_url || '',
|
||||
oidc_post_logout_redirect: item.oidc_post_logout_redirect,
|
||||
group_sync_mode: (item.group_sync_mode as 'off' | 'first_login' | 'sync') || 'off',
|
||||
group_mappings: Array.isArray(item.group_mappings) ? item.group_mappings.map((mapping: AuthProviderGroupMapping) => ({
|
||||
claim_value: mapping.claim_value || '',
|
||||
@@ -369,6 +372,18 @@ export default function AuthProviderFormDialog(props: AuthProviderFormDialogProp
|
||||
onChange={(event) => props.setForm((prev) => ({ ...prev, oidc_end_session_url: event.target.value }))}
|
||||
helperText="RP-initiated logout endpoint (end_session_endpoint). When set, users are redirected here on logout to also clear their SSO session."
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={props.form.oidc_post_logout_redirect}
|
||||
onChange={(event) => props.setForm((prev) => ({ ...prev, oidc_post_logout_redirect: event.target.checked }))}
|
||||
/>
|
||||
}
|
||||
label="Append post-logout redirect URI"
|
||||
/>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
When enabled, logout appends post_logout_redirect_uri back to the login page. The ID token is also sent as id_token_hint when available.
|
||||
</Typography>
|
||||
</Box>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
@@ -88,7 +88,9 @@ export default function BoardCardContent(props: BoardCardContentProps) {
|
||||
))}
|
||||
{descriptionText ? (
|
||||
<Tooltip title={descriptionText} arrow>
|
||||
<BadgeBox><DescriptionIcon sx={{ fontSize: 13 }} /></BadgeBox>
|
||||
<span>
|
||||
<BadgeBox><DescriptionIcon sx={{ fontSize: 13 }} /></BadgeBox>
|
||||
</span>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
{checklistTotal > 0 ? (
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -33,7 +33,7 @@ export default function LoginPage() {
|
||||
await api.login(username, password, otpRequired ? otpCode : '')
|
||||
navigate('/')
|
||||
} catch (err) {
|
||||
message = err instanceof Error ? err.message : 'Login failed'
|
||||
message = err instanceof Error ? err.message : 'Sign in failed'
|
||||
if (message === 'totp_required') {
|
||||
setOTPRequired(true)
|
||||
setOTPCode('')
|
||||
@@ -69,7 +69,7 @@ export default function LoginPage() {
|
||||
</Typography>
|
||||
)}
|
||||
<Button type="submit" variant="contained" fullWidth sx={{ mt: 2 }}>
|
||||
{otpRequired ? 'Verify' : 'Login'}
|
||||
{otpRequired ? 'Verify' : 'Sign in'}
|
||||
</Button>
|
||||
{oidcProviders.map((p) => (
|
||||
<Button
|
||||
@@ -82,7 +82,7 @@ export default function LoginPage() {
|
||||
window.location.assign(`/api/auth/oidc/${p.id}/login`)
|
||||
}}
|
||||
>
|
||||
Login with {p.name}
|
||||
Sign in with {p.name}
|
||||
</Button>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
Reference in New Issue
Block a user