Files
ollama/auth/auth.go
ParthSareen eed58a312d auth: add local sign-in state storage
Add functions to cache sign-in state locally at ~/.ollama/signin.json,
enabling offline sign-in status checks without network calls.

- SignInState struct with Name, Email, and CachedAt fields
- GetSignInState() reads cached state from disk
- SetSignInState() atomically writes state (temp file + rename)
- ClearSignInState() removes the cached state file
- IsSignedIn() quick check for valid cached sign-in
2026-01-07 15:35:48 -08:00

168 lines
3.6 KiB
Go

package auth
import (
"bytes"
"context"
"crypto/rand"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"log/slog"
"os"
"path/filepath"
"strings"
"time"
"golang.org/x/crypto/ssh"
)
const defaultPrivateKey = "id_ed25519"
const signInStateFile = "signin.json"
// SignInState represents the locally cached sign-in state
type SignInState struct {
Name string `json:"name"`
Email string `json:"email"`
CachedAt time.Time `json:"cached_at"`
}
func GetPublicKey() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
keyPath := filepath.Join(home, ".ollama", defaultPrivateKey)
privateKeyFile, err := os.ReadFile(keyPath)
if err != nil {
slog.Info(fmt.Sprintf("Failed to load private key: %v", err))
return "", err
}
privateKey, err := ssh.ParsePrivateKey(privateKeyFile)
if err != nil {
return "", err
}
publicKey := ssh.MarshalAuthorizedKey(privateKey.PublicKey())
return strings.TrimSpace(string(publicKey)), nil
}
func NewNonce(r io.Reader, length int) (string, error) {
nonce := make([]byte, length)
if _, err := io.ReadFull(r, nonce); err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(nonce), nil
}
func Sign(ctx context.Context, bts []byte) (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
keyPath := filepath.Join(home, ".ollama", defaultPrivateKey)
privateKeyFile, err := os.ReadFile(keyPath)
if err != nil {
slog.Info(fmt.Sprintf("Failed to load private key: %v", err))
return "", err
}
privateKey, err := ssh.ParsePrivateKey(privateKeyFile)
if err != nil {
return "", err
}
// get the pubkey, but remove the type
publicKey := ssh.MarshalAuthorizedKey(privateKey.PublicKey())
parts := bytes.Split(publicKey, []byte(" "))
if len(parts) < 2 {
return "", errors.New("malformed public key")
}
signedData, err := privateKey.Sign(rand.Reader, bts)
if err != nil {
return "", err
}
// signature is <pubkey>:<signature>
return fmt.Sprintf("%s:%s", bytes.TrimSpace(parts[1]), base64.StdEncoding.EncodeToString(signedData.Blob)), nil
}
// GetSignInState reads the locally cached sign-in state from ~/.ollama/signin.json
func GetSignInState() (*SignInState, error) {
home, err := os.UserHomeDir()
if err != nil {
return nil, err
}
statePath := filepath.Join(home, ".ollama", signInStateFile)
data, err := os.ReadFile(statePath)
if err != nil {
return nil, err
}
var state SignInState
if err := json.Unmarshal(data, &state); err != nil {
return nil, err
}
return &state, nil
}
// SetSignInState atomically writes the sign-in state to ~/.ollama/signin.json
func SetSignInState(state *SignInState) error {
home, err := os.UserHomeDir()
if err != nil {
return err
}
ollamaDir := filepath.Join(home, ".ollama")
statePath := filepath.Join(ollamaDir, signInStateFile)
tmpPath := statePath + ".tmp"
state.CachedAt = time.Now()
data, err := json.MarshalIndent(state, "", " ")
if err != nil {
return err
}
// Write to temp file first
if err := os.WriteFile(tmpPath, data, 0o600); err != nil {
return err
}
// Atomic rename
return os.Rename(tmpPath, statePath)
}
// ClearSignInState removes the locally cached sign-in state
func ClearSignInState() error {
home, err := os.UserHomeDir()
if err != nil {
return err
}
statePath := filepath.Join(home, ".ollama", signInStateFile)
err = os.Remove(statePath)
if errors.Is(err, os.ErrNotExist) {
return nil // Already cleared
}
return err
}
// IsSignedIn returns true if there is a valid locally cached sign-in state
func IsSignedIn() bool {
state, err := GetSignInState()
if err != nil {
return false
}
return state.Name != ""
}