feat: add OIDC authentication for server mode

This commit is contained in:
2026-04-01 19:33:15 +02:00
parent 7bce56384f
commit 52a975b66d
13 changed files with 515 additions and 7 deletions

View File

@@ -28,13 +28,27 @@ func (c *apiClient) do(method, path string, body any) (*http.Response, error) {
if err != nil {
return nil, err
}
req.Header.Set("X-Ax-User", c.user)
if err := c.setAuth(req); err != nil {
return nil, err
}
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
return c.http.Do(req)
}
// setAuth attaches either a Bearer token (when a session exists) or the
// X-Ax-User header (no session / non-OIDC servers).
func (c *apiClient) setAuth(req *http.Request) error {
sess, err := LoadSession()
if err != nil || sess == nil || sess.Token == "" {
req.Header.Set("X-Ax-User", c.user)
return nil
}
req.Header.Set("Authorization", "Bearer "+sess.Token)
return nil
}
func apiDecode[T any](resp *http.Response) (T, error) {
var v T
defer resp.Body.Close()

View File

@@ -11,6 +11,16 @@ type ServerConfig struct {
Port int `json:"port"`
}
type OIDCConfig struct {
Issuer string `json:"issuer"`
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret"`
// PublicURL is the externally reachable base URL of this server, used to
// construct the OIDC redirect URI (e.g. "https://ax.example.com:7000").
PublicURL string `json:"public_url"`
UserClaim string `json:"user_claim"` // default "preferred_username"
}
type Config interface {
GetUser() string
SetUser(username string) error
@@ -21,5 +31,7 @@ type Config interface {
GetServerConfig() ServerConfig
// GetRemoteConfig returns the remote server address and whether remote mode is enabled.
GetRemoteConfig() (ServerConfig, bool)
// GetOIDCConfig returns the OIDC configuration and whether OIDC is enabled.
GetOIDCConfig() (OIDCConfig, bool)
Save() error
}

View File

@@ -15,6 +15,7 @@ type fileConfig struct {
UserAliases []*Alias `json:"aliases"`
Serve ServerConfig `json:"serve"`
Remote ServerConfig `json:"remote"`
OIDC OIDCConfig `json:"oidc"`
}
var defaultAliases = []*Alias{
@@ -142,6 +143,17 @@ func (c *fileConfig) ListAliases() ([]*Alias, error) {
return result, nil
}
func (c *fileConfig) GetOIDCConfig() (OIDCConfig, bool) {
if c.OIDC.Issuer == "" {
return OIDCConfig{}, false
}
cfg := c.OIDC
if cfg.UserClaim == "" {
cfg.UserClaim = "preferred_username"
}
return cfg, true
}
func (c *fileConfig) GetRemoteConfig() (ServerConfig, bool) {
if c.Remote.Host == "" {
return ServerConfig{}, false

View File

@@ -92,7 +92,7 @@ func GetNodeServiceForUser(user string) (NodeService, error) {
if user == "" {
return nil, fmt.Errorf("user is required")
}
st, err := store.FindAndOpenSQLiteStore()
st, err := store.FindOrInitSQLiteStore()
if err != nil {
return nil, err
}

67
service/session.go Normal file
View File

@@ -0,0 +1,67 @@
package service
import (
"encoding/json"
"os"
"path/filepath"
)
// Session holds the server-issued token returned by POST /auth/poll.
// The ax server owns the full OIDC flow; the client only needs this token.
type Session struct {
Token string `json:"token"`
}
func sessionPath() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
return filepath.Join(home, ".config", "ax", "session.json"), nil
}
func LoadSession() (*Session, error) {
path, err := sessionPath()
if err != nil {
return nil, err
}
data, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, err
}
var s Session
if err := json.Unmarshal(data, &s); err != nil {
return nil, err
}
return &s, nil
}
func SaveSession(s *Session) error {
path, err := sessionPath()
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
return err
}
data, err := json.MarshalIndent(s, "", " ")
if err != nil {
return err
}
return os.WriteFile(path, data, 0600)
}
func ClearSession() error {
path, err := sessionPath()
if err != nil {
return err
}
err = os.Remove(path)
if os.IsNotExist(err) {
return nil
}
return err
}