feat: add OIDC authentication for server mode
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
67
service/session.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user