Feature: Add admin-only SSO bridge to web UI

Bridge an already-authenticated portal session into a YAMA web session via
a new /sso endpoint, so portal administrators can enter the remote-desktop
UI without a second login. Only the "admin" portal role is bridged; any
other role (viewer, unknown) still falls back to the built-in login form,
so a low-privileged portal account cannot bypass authentication.

- wsauth: extract IssueToken from VerifyLogin so an out-of-band-authenticated
  identity (SSO) can mint a session token without the challenge/response.
- web: add the SSODetector interface, a fail-closed JWT detector (HMAC-only),
  and the /sso handler gated to the admin role.
- cmd: enable the JWT detector via YAMA_SSO_JWT_SECRET, the shared HS256
  secret matching the parent portal.
- web UI: attempt SSO before showing the login form, falling back to login
  when the server returns 401.
- docs: document SSO (admin-only) and license trial-vs-paid troubleshooting
  in README and the env example.

Co-Authored-By: deepseek-v4-pro
This commit is contained in:
yuanyuanxianganddeepseek-v4-pro committed 2026-10-13 15:46:04 +02:00
1 parent d2149851b2
commit 53b7151fb7
9 files changed
+499 -11

No files matched your search

+16 -5
View File
@@ -258,7 +258,6 @@ func (a *Authenticator) fakeSalt(username string) string {
func (a *Authenticator) VerifyLogin(username, response, nonce string) (token, role string, err error) {
a.mu.RLock()
u, ok := a.users[username]
expire := a.tokenExpire
a.mu.RUnlock()
if !ok {
return "", "", errors.New("invalid credentials")
@@ -268,18 +267,30 @@ func (a *Authenticator) VerifyLogin(username, response, nonce string) (token, ro
return "", "", errors.New("invalid credentials")
}
token, err = randomHex(tokenBytes)
token, err = a.IssueToken(username, u.Role)
if err != nil {
return "", "", err
}
return token, u.Role, nil
}
// IssueToken mints and stores a fresh session token for an identity that has
// already been authenticated out-of-band (SSO), skipping the challenge
// response. username is used only for audit/logging; authorization keys on
// Role. The caller must pass a role already normalized to "admin" or "viewer".
func (a *Authenticator) IssueToken(username, role string) (string, error) {
token, err := randomHex(tokenBytes)
if err != nil {
return "", err
}
a.mu.Lock()
a.tokens[token] = &Session{
Username: username,
Role: u.Role,
ExpiresAt: time.Now().Add(expire),
Role: role,
ExpiresAt: time.Now().Add(a.tokenExpire),
}
a.mu.Unlock()
return token, u.Role, nil
return token, nil
}
// usersFileEntry is the on-disk shape of one record in users.json. Field