Skip to content

Commit 80105a9

Browse files
✨ feat: add optional ID Token support (#8)
* ✨ feat: add optional ID Token support - Update configuration schema and Go structs to include idTokenKey - Support saving ID tokens to .env files when configured - Add --id-token flag to get command for direct output - Add --id-token flag to inspect command for decoding - Make ID token verification non-fatal to increase robustness - Add unit tests for config loading and OIDC token handling - Update README.md with new configuration options and flags * ✨ feat: allow explicit token type in targets configuration - Refactor `targets` schema to use `type` field instead of `idTokenKey` - Update `inspect` command to intelligently resolve files and keys from the targets list - Improve documentation for multiple target files and fix invalid examples in README - Maintain backward compatibility for simple configuration via `tokenKey` and `idTokenKey`
1 parent b0a93ea commit 80105a9

10 files changed

Lines changed: 312 additions & 62 deletions

File tree

README.md

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ user: {
5757
5858
// Optional: Key to update in .env (default: "TOKEN")
5959
tokenKey: "MY_TOKEN"
60+
61+
// Optional: Key to update with ID Token in .env
62+
idTokenKey: "MY_ID_TOKEN"
6063
```
6164

6265
## Configuration Examples
@@ -88,11 +91,35 @@ oidc: {
8891
clientId: "my-client-id"
8992
clientSecret: "my-client-secret"
9093
scopes: ["openid", "profile", "email", "goauthentik.io/api"]
91-
}
92-
scopes: ["openid", "profile", "email", "goauthentik.io/api"]
9394
}
9495
```
9596

97+
## Advanced Targets Configuration
98+
99+
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.
100+
101+
```cue
102+
// Optional: Multiple targets configuration
103+
targets: [
104+
{
105+
file: ".env"
106+
key: "MY_ACCESS_TOKEN"
107+
type: "access_token" // Default type
108+
},
109+
{
110+
file: ".env"
111+
key: "MY_ID_TOKEN"
112+
type: "id_token"
113+
},
114+
{
115+
file: "apps/frontend/.env"
116+
key: "API_TOKEN"
117+
}
118+
]
119+
```
120+
121+
When `targets` is defined, the global `tokenKey` and `idTokenKey` are ignored.
122+
96123
## Secrets Management
97124

98125
`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.
@@ -167,15 +194,20 @@ Fetches a valid token and prints it to stdout. Useful for piping to other comman
167194
./authk get
168195
```
169196

197+
**Flags:**
198+
- `--id-token`: Print ID Token instead of Access Token
199+
170200
### Inspect Token
171201

172-
Reads the current token from the `.env` file and displays its decoded content (Header and Payload).
202+
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.
173203

174204
```bash
175205
./authk inspect
176206
```
177207

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

181213
## License

cmd/authk/get.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ import (
1111
"github.com/spf13/cobra"
1212
)
1313

14+
var (
15+
showIDToken bool
16+
)
17+
1418
var getCmd = &cobra.Command{
1519
Use: "get",
1620
Short: "Get a valid token",
@@ -43,11 +47,20 @@ var getCmd = &cobra.Command{
4347
return fmt.Errorf("failed to get token: %w", err)
4448
}
4549

46-
fmt.Println(token.AccessToken)
50+
if showIDToken {
51+
idToken, ok := token.Extra("id_token").(string)
52+
if !ok || idToken == "" {
53+
return fmt.Errorf("no ID Token found in response")
54+
}
55+
fmt.Println(idToken)
56+
} else {
57+
fmt.Println(token.AccessToken)
58+
}
4759
return nil
4860
},
4961
}
5062

5163
func init() {
64+
getCmd.Flags().BoolVar(&showIDToken, "id-token", false, "Print ID Token instead of Access Token")
5265
rootCmd.AddCommand(getCmd)
5366
}

cmd/authk/inspect.go

Lines changed: 64 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ import (
1414
"github.com/spf13/cobra"
1515
)
1616

17-
var jsonOutput bool
17+
var (
18+
jsonOutput bool
19+
inspectID bool
20+
)
1821

1922
var inspectCmd = &cobra.Command{
2023
Use: "inspect",
@@ -37,8 +40,59 @@ var inspectCmd = &cobra.Command{
3740
envFile = found
3841
}
3942

43+
// Determine which file and key to use
44+
targetFile := envFile
45+
targetKey := ""
46+
47+
requestedType := "access_token"
48+
if inspectID {
49+
requestedType = "id_token"
50+
}
51+
52+
found := false
53+
// 1. If --env is explicitly provided, try to find a target matching that file and type
54+
if cmd.Flags().Changed("env") {
55+
for _, t := range cfg.Targets {
56+
if t.File == envFile && t.Type == requestedType {
57+
targetKey = t.Key
58+
found = true
59+
break
60+
}
61+
}
62+
}
63+
64+
// 2. If not found yet, take the first target matching the requested type
65+
if !found {
66+
for _, t := range cfg.Targets {
67+
if t.Type == requestedType {
68+
targetFile = t.File
69+
targetKey = t.Key
70+
found = true
71+
break
72+
}
73+
}
74+
}
75+
76+
// 3. Fallback to legacy/default behavior
77+
if !found {
78+
targetFile = envFile
79+
if inspectID {
80+
if cfg.IDTokenKey == "" {
81+
return fmt.Errorf("idTokenKey not configured in config file")
82+
}
83+
targetKey = cfg.IDTokenKey
84+
} else {
85+
targetKey = cfg.TokenKey
86+
}
87+
}
88+
89+
// Final check to find the file on disk (it might be in a parent directory)
90+
if foundPath, err := env.Find(targetFile); err == nil {
91+
targetFile = foundPath
92+
}
93+
4094
// Initialize Env Manager
41-
envMgr := env.NewManager(envFile, cfg.TokenKey)
95+
envMgr := env.NewManager(targetFile, targetKey)
4296

4397
// Get Token
4498
token, err := envMgr.Get()
@@ -112,7 +166,6 @@ func printJSON(title, segment string) {
112166
return
113167
}
114168

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

155208
if isTimestamp {
156209
cleanVal := strings.TrimSuffix(valTrimmed, ",")
157-
if ts, err := strconv.ParseInt(cleanVal, 10, 64); err == nil {
158-
tm := time.Unix(ts, 0)
159-
dateColor := color.New(color.Faint).SprintFunc()
160-
fmt.Print(dateColor(fmt.Sprintf(" (%s)", tm.Format("2006-01-02 15:04:05 MST"))))
161-
} }
210+
if ts, err := strconv.ParseInt(cleanVal, 10, 64); err == nil {
211+
tm := time.Unix(ts, 0)
212+
dateColor := color.New(color.Faint).SprintFunc()
213+
fmt.Print(dateColor(fmt.Sprintf(" (%s)", tm.Format("2006-01-02 15:04:05 MST"))))
214+
}
215+
}
162216
fmt.Println()
163217
} else {
164218
fmt.Println(val)
@@ -174,4 +228,5 @@ func printJSON(title, segment string) {
174228
func init() {
175229
rootCmd.AddCommand(inspectCmd)
176230
inspectCmd.Flags().BoolVar(&jsonOutput, "json", false, "Output as valid JSON without colors")
177-
}
231+
inspectCmd.Flags().BoolVar(&inspectID, "id-token", false, "Inspect the ID token instead of the Access token")
232+
}

cmd/authk/root.go

Lines changed: 41 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/rs/zerolog"
1212
"github.com/rs/zerolog/log"
1313
"github.com/spf13/cobra"
14+
"golang.org/x/oauth2"
1415
)
1516

1617
var (
@@ -58,8 +59,19 @@ updating a .env file with the valid token.`,
5859
targets = cfg.Targets
5960
log.Info().Int("count", len(targets)).Msg("Configured with multiple targets")
6061
} else {
61-
targets = []config.Target{{File: envFile, Key: cfg.TokenKey}}
62-
log.Info().Str("env_file", envFile).Str("token_key", cfg.TokenKey).Msg("Configured with single target")
62+
targets = append(targets, config.Target{
63+
File: envFile,
64+
Key: cfg.TokenKey,
65+
Type: "access_token",
66+
})
67+
if cfg.IDTokenKey != "" {
68+
targets = append(targets, config.Target{
69+
File: envFile,
70+
Key: cfg.IDTokenKey,
71+
Type: "id_token",
72+
})
73+
}
74+
log.Info().Str("env_file", envFile).Msg("Configured with default targets")
6375
}
6476

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

77-
// Update all targets
78-
for _, target := range targets {
79-
mgr := env.NewManager(target.File, target.Key)
80-
if err := mgr.Update(token.AccessToken); err != nil {
81-
log.Error().Err(err).Str("file", target.File).Msg("Failed to update target")
82-
} else {
83-
log.Info().Str("file", target.File).Msg("Target updated")
89+
// Function to update all targets with current tokens
90+
updateTargets := func(t *oauth2.Token) {
91+
for _, target := range targets {
92+
var tokenValue string
93+
switch target.Type {
94+
case "id_token":
95+
idToken, ok := t.Extra("id_token").(string)
96+
if !ok || idToken == "" {
97+
log.Warn().Str("file", target.File).Msg("ID Token requested but not found in response")
98+
continue
99+
}
100+
tokenValue = idToken
101+
default: // access_token
102+
tokenValue = t.AccessToken
103+
}
104+
105+
mgr := env.NewManager(target.File, target.Key)
106+
if err := mgr.Update(tokenValue); err != nil {
107+
log.Error().Err(err).Str("file", target.File).Str("type", target.Type).Msg("Failed to update token")
108+
} else {
109+
log.Info().Str("file", target.File).Str("type", target.Type).Msg("Token updated")
110+
}
84111
}
85112
}
86113

114+
// Initial update
115+
updateTargets(token)
116+
87117
// Maintenance Loop
88118
for {
89119
// Calculate sleep time based on token expiry and a refresh buffer
@@ -116,18 +146,9 @@ updating a .env file with the valid token.`,
116146
}
117147
}
118148

119-
// Update token
149+
// Update token and targets
120150
token = newToken
121-
122-
// Update all targets
123-
for _, target := range targets {
124-
mgr := env.NewManager(target.File, target.Key)
125-
if err := mgr.Update(token.AccessToken); err != nil {
126-
log.Error().Err(err).Str("file", target.File).Msg("Failed to update target")
127-
} else {
128-
log.Info().Str("file", target.File).Msg("Target updated")
129-
}
130-
}
151+
updateTargets(token)
131152
}
132153
},
133154
}

internal/config/config.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,17 @@ import (
1818
var schemaContent []byte
1919

2020
type Config struct {
21-
OIDC OIDCConfig `json:"oidc"`
22-
User UserConfig `json:"user"`
23-
TokenKey string `json:"tokenKey"`
24-
Targets []Target `json:"targets,omitempty"`
21+
OIDC OIDCConfig `json:"oidc"`
22+
User UserConfig `json:"user"`
23+
TokenKey string `json:"tokenKey"`
24+
IDTokenKey string `json:"idTokenKey,omitempty"`
25+
Targets []Target `json:"targets,omitempty"`
2526
}
2627

2728
type Target struct {
2829
File string `json:"file"`
2930
Key string `json:"key"`
31+
Type string `json:"type"`
3032
}
3133

3234
type OIDCConfig struct {

0 commit comments

Comments
 (0)