Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 35 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ user: {

// Optional: Key to update in .env (default: "TOKEN")
tokenKey: "MY_TOKEN"

// Optional: Key to update with ID Token in .env
idTokenKey: "MY_ID_TOKEN"
```

## Configuration Examples
Expand Down Expand Up @@ -88,11 +91,35 @@ oidc: {
clientId: "my-client-id"
clientSecret: "my-client-secret"
scopes: ["openid", "profile", "email", "goauthentik.io/api"]
}
scopes: ["openid", "profile", "email", "goauthentik.io/api"]
}
```

## Advanced Targets Configuration

By default, `authk` updates a single `.env` file. For more complex setups, you can define multiple targets to update different files or keys with different token types.

```cue
// Optional: Multiple targets configuration
targets: [
{
file: ".env"
key: "MY_ACCESS_TOKEN"
type: "access_token" // Default type
},
{
file: ".env"
key: "MY_ID_TOKEN"
type: "id_token"
},
{
file: "apps/frontend/.env"
key: "API_TOKEN"
}
]
```

When `targets` is defined, the global `tokenKey` and `idTokenKey` are ignored.

## Secrets Management

`authk` integrates with [vals](https://github.com/helmfile/vals) to support loading secrets securely from various sources. You can use special URI schemes in your configuration file to reference secrets instead of hardcoding them.
Expand Down Expand Up @@ -167,15 +194,20 @@ Fetches a valid token and prints it to stdout. Useful for piping to other comman
./authk get
```

**Flags:**
- `--id-token`: Print ID Token instead of Access Token

### Inspect Token

Reads the current token from the `.env` file and displays its decoded content (Header and Payload).
Reads the current token from the `.env` file and displays its decoded content (Header and Payload). It automatically uses the file and key defined in your `targets` if available.

```bash
./authk inspect
```

**Flags:**
- `--id-token`: Inspect the ID token instead of the Access token (searches for a target of type `id_token`)
- `--env`: Path to .env file. If multiple targets exist for different files, use this to specify which one to inspect.
- `--json`: Output as valid JSON without colors (useful for parsing)

## License
Expand Down
15 changes: 14 additions & 1 deletion cmd/authk/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import (
"github.com/spf13/cobra"
)

var (
showIDToken bool
)

var getCmd = &cobra.Command{
Use: "get",
Short: "Get a valid token",
Expand Down Expand Up @@ -43,11 +47,20 @@ var getCmd = &cobra.Command{
return fmt.Errorf("failed to get token: %w", err)
}

fmt.Println(token.AccessToken)
if showIDToken {
idToken, ok := token.Extra("id_token").(string)
if !ok || idToken == "" {
return fmt.Errorf("no ID Token found in response")
}
fmt.Println(idToken)
} else {
fmt.Println(token.AccessToken)
}
return nil
},
}

func init() {
getCmd.Flags().BoolVar(&showIDToken, "id-token", false, "Print ID Token instead of Access Token")
rootCmd.AddCommand(getCmd)
}
73 changes: 64 additions & 9 deletions cmd/authk/inspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ import (
"github.com/spf13/cobra"
)

var jsonOutput bool
var (
jsonOutput bool
inspectID bool
)

var inspectCmd = &cobra.Command{
Use: "inspect",
Expand All @@ -37,8 +40,59 @@ var inspectCmd = &cobra.Command{
envFile = found
}

// Determine which file and key to use
targetFile := envFile
targetKey := ""

requestedType := "access_token"
if inspectID {
requestedType = "id_token"
}

found := false
// 1. If --env is explicitly provided, try to find a target matching that file and type
if cmd.Flags().Changed("env") {
for _, t := range cfg.Targets {
if t.File == envFile && t.Type == requestedType {
targetKey = t.Key
found = true
break
}
}
}

// 2. If not found yet, take the first target matching the requested type
if !found {
for _, t := range cfg.Targets {
if t.Type == requestedType {
targetFile = t.File
targetKey = t.Key
found = true
break
}
}
}

// 3. Fallback to legacy/default behavior
if !found {
targetFile = envFile
if inspectID {
if cfg.IDTokenKey == "" {
return fmt.Errorf("idTokenKey not configured in config file")
}
targetKey = cfg.IDTokenKey
} else {
targetKey = cfg.TokenKey
}
}

// Final check to find the file on disk (it might be in a parent directory)
if foundPath, err := env.Find(targetFile); err == nil {
targetFile = foundPath
}

// Initialize Env Manager
envMgr := env.NewManager(envFile, cfg.TokenKey)
envMgr := env.NewManager(targetFile, targetKey)

// Get Token
token, err := envMgr.Get()
Expand Down Expand Up @@ -112,7 +166,6 @@ func printJSON(title, segment string) {
return
}


// Simple syntax highlighting for JSON keys
jsonStr := string(pretty)
lines := strings.Split(jsonStr, "\n")
Expand Down Expand Up @@ -154,11 +207,12 @@ func printJSON(title, segment string) {

if isTimestamp {
cleanVal := strings.TrimSuffix(valTrimmed, ",")
if ts, err := strconv.ParseInt(cleanVal, 10, 64); err == nil {
tm := time.Unix(ts, 0)
dateColor := color.New(color.Faint).SprintFunc()
fmt.Print(dateColor(fmt.Sprintf(" (%s)", tm.Format("2006-01-02 15:04:05 MST"))))
} }
if ts, err := strconv.ParseInt(cleanVal, 10, 64); err == nil {
tm := time.Unix(ts, 0)
dateColor := color.New(color.Faint).SprintFunc()
fmt.Print(dateColor(fmt.Sprintf(" (%s)", tm.Format("2006-01-02 15:04:05 MST"))))
}
}
fmt.Println()
} else {
fmt.Println(val)
Expand All @@ -174,4 +228,5 @@ func printJSON(title, segment string) {
func init() {
rootCmd.AddCommand(inspectCmd)
inspectCmd.Flags().BoolVar(&jsonOutput, "json", false, "Output as valid JSON without colors")
}
inspectCmd.Flags().BoolVar(&inspectID, "id-token", false, "Inspect the ID token instead of the Access token")
}
61 changes: 41 additions & 20 deletions cmd/authk/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"golang.org/x/oauth2"
)

var (
Expand Down Expand Up @@ -58,8 +59,19 @@ updating a .env file with the valid token.`,
targets = cfg.Targets
log.Info().Int("count", len(targets)).Msg("Configured with multiple targets")
} else {
targets = []config.Target{{File: envFile, Key: cfg.TokenKey}}
log.Info().Str("env_file", envFile).Str("token_key", cfg.TokenKey).Msg("Configured with single target")
targets = append(targets, config.Target{
File: envFile,
Key: cfg.TokenKey,
Type: "access_token",
})
if cfg.IDTokenKey != "" {
targets = append(targets, config.Target{
File: envFile,
Key: cfg.IDTokenKey,
Type: "id_token",
})
}
log.Info().Str("env_file", envFile).Msg("Configured with default targets")
}

// Initialize OIDC Client
Expand All @@ -74,16 +86,34 @@ updating a .env file with the valid token.`,
return fmt.Errorf("failed to get initial token: %w", err)
}

// Update all targets
for _, target := range targets {
mgr := env.NewManager(target.File, target.Key)
if err := mgr.Update(token.AccessToken); err != nil {
log.Error().Err(err).Str("file", target.File).Msg("Failed to update target")
} else {
log.Info().Str("file", target.File).Msg("Target updated")
// Function to update all targets with current tokens
updateTargets := func(t *oauth2.Token) {
for _, target := range targets {
var tokenValue string
switch target.Type {
case "id_token":
idToken, ok := t.Extra("id_token").(string)
if !ok || idToken == "" {
log.Warn().Str("file", target.File).Msg("ID Token requested but not found in response")
continue
}
tokenValue = idToken
default: // access_token
tokenValue = t.AccessToken
}

mgr := env.NewManager(target.File, target.Key)
if err := mgr.Update(tokenValue); err != nil {
log.Error().Err(err).Str("file", target.File).Str("type", target.Type).Msg("Failed to update token")
} else {
log.Info().Str("file", target.File).Str("type", target.Type).Msg("Token updated")
}
}
}

// Initial update
updateTargets(token)

// Maintenance Loop
for {
// Calculate sleep time based on token expiry and a refresh buffer
Expand Down Expand Up @@ -116,18 +146,9 @@ updating a .env file with the valid token.`,
}
}

// Update token
// Update token and targets
token = newToken

// Update all targets
for _, target := range targets {
mgr := env.NewManager(target.File, target.Key)
if err := mgr.Update(token.AccessToken); err != nil {
log.Error().Err(err).Str("file", target.File).Msg("Failed to update target")
} else {
log.Info().Str("file", target.File).Msg("Target updated")
}
}
updateTargets(token)
}
},
}
Expand Down
10 changes: 6 additions & 4 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,17 @@ import (
var schemaContent []byte

type Config struct {
OIDC OIDCConfig `json:"oidc"`
User UserConfig `json:"user"`
TokenKey string `json:"tokenKey"`
Targets []Target `json:"targets,omitempty"`
OIDC OIDCConfig `json:"oidc"`
User UserConfig `json:"user"`
TokenKey string `json:"tokenKey"`
IDTokenKey string `json:"idTokenKey,omitempty"`
Targets []Target `json:"targets,omitempty"`
}

type Target struct {
File string `json:"file"`
Key string `json:"key"`
Type string `json:"type"`
}

type OIDCConfig struct {
Expand Down
Loading