initial working version
This commit is contained in:
parent
db6b90134d
commit
d986a0b31a
19 changed files with 1430 additions and 0 deletions
107
cmd/refresh.go
Normal file
107
cmd/refresh.go
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
gooidc "github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"guardianproject.dev/ops/nix-cache-login/internal/config"
|
||||
"guardianproject.dev/ops/nix-cache-login/internal/netrc"
|
||||
"guardianproject.dev/ops/nix-cache-login/internal/token"
|
||||
)
|
||||
|
||||
var refreshCmd = &cobra.Command{
|
||||
Use: "refresh",
|
||||
Short: "Refresh the access token using stored refresh token",
|
||||
Long: `Reads the stored refresh token, exchanges it for a new access token,
|
||||
and updates the netrc file. Exits 0 on success, 1 on failure.
|
||||
Designed to be called from a systemd timer.`,
|
||||
RunE: runRefresh,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(refreshCmd)
|
||||
}
|
||||
|
||||
func runRefresh(cmd *cobra.Command, args []string) error {
|
||||
ctx := context.Background()
|
||||
|
||||
// Read stored refresh token
|
||||
rtPath := config.RefreshTokenPath()
|
||||
rtData, err := os.ReadFile(rtPath)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "No refresh token found. Run `nix-cache-login login` first.")
|
||||
os.Exit(1)
|
||||
}
|
||||
refreshToken := string(rtData)
|
||||
|
||||
if refreshToken == "" {
|
||||
fmt.Fprintln(os.Stderr, "Refresh token is empty. Run `nix-cache-login login` first.")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// OIDC discovery
|
||||
provider, err := gooidc.NewProvider(ctx, cfg.Issuer)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "OIDC discovery failed: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
oauthCfg := oauth2.Config{
|
||||
ClientID: cfg.ClientID,
|
||||
Endpoint: provider.Endpoint(),
|
||||
Scopes: []string{gooidc.ScopeOpenID},
|
||||
}
|
||||
|
||||
// Create a token source with the refresh token
|
||||
oldToken := &oauth2.Token{
|
||||
RefreshToken: refreshToken,
|
||||
Expiry: time.Now().Add(-1 * time.Hour), // force refresh
|
||||
}
|
||||
|
||||
ts := oauthCfg.TokenSource(ctx, oldToken)
|
||||
newToken, err := ts.Token()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Session expired. Run `nix-cache-login login`.\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Update netrc with new access token
|
||||
if err := netrc.Upsert(cfg.NetrcPath, cfg.CacheHost, newToken.AccessToken); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to update netrc: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Update refresh token if rotated
|
||||
if newToken.RefreshToken != "" && newToken.RefreshToken != refreshToken {
|
||||
if err := os.MkdirAll(filepath.Dir(rtPath), 0700); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to create config directory: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := os.WriteFile(rtPath, []byte(newToken.RefreshToken), 0600); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to update refresh token: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Show expiry info
|
||||
claims, err := token.DecodePayload(newToken.AccessToken)
|
||||
if err == nil {
|
||||
exp, remaining := token.ExpiryInfo(claims)
|
||||
if !exp.IsZero() {
|
||||
fmt.Fprintf(os.Stderr, "Token refreshed. Valid until %s (%s remaining).\n",
|
||||
exp.Local().Format(time.RFC1123),
|
||||
remaining.Round(time.Minute))
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintln(os.Stderr, "Token refreshed successfully.")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue