• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

codozor / authk / 19938297188

04 Dec 2025 05:39PM UTC coverage: 53.425% (+1.7%) from 51.731%
19938297188

push

github

web-flow
✨ feat(oidc): Refactor OIDC client with golang.org/x/oauth2 and go-oidc (#6)

* ✨ feat(oidc): Refactor OIDC client with golang.org/x/oauth2 and go-oidc

This commit refactors the `internal/oidc` package to leverage the
`golang.org/x/oauth2` and `github.com/coreos/go-oidc` libraries.

The manual implementation of OAuth2/OIDC flows, including discovery,
token requests, and JSON parsing, has been replaced by robust,
industry-standard libraries.

Key changes include:
- Replaced manual OIDC discovery with `oidc.NewProvider`.
- Updated token retrieval (password and client credentials grants)
  to use `oauth2.Config` and `clientcredentials.Config`.
- Refactored token refreshing to use `oauth2.TokenSource` mechanisms.
- Eliminated custom `TokenResponse` struct in favor of `oauth2.Token`.
- Enhanced test suite (`internal/oidc/client_test.go`) to reflect new
  implementation and ensure compatibility with `go-oidc` expectations.
- Updated `cmd/authk/root.go` to use `token.Expiry` for refresh
  timing, removing custom `ExpiresIn` logic.

This refactoring significantly improves security (due to strict
validation of OIDC specs by go-oidc), maintainability, and reduces
the amount of custom code.

* ✅ feat: Improve test coverage and fix linting issue

This commit addresses the recent drop in test coverage reported by Coveralls and fixes a linting issue.

Changes include:
- **internal/oidc/client_test.go:**
  - Added error checking for `w.Write` call in mock server to resolve an `errcheck` linting error.
  - Introduced `TestClient_GetToken_Password` to specifically test the Resource Owner Password Credentials flow, increasing coverage for `GetToken` function.
- **internal/env/env_test.go:**
  - Added `TestFind_NotFound` to verify error handling when a file is not found.
  - Added `TestFind_WithSeparator` to test `Find` function behavior with paths containing separators.

These changes collectively improve the overall test coverage and code quality.

* 🐛 feat(oidc): Su... (continued)

49 of 74 new or added lines in 2 files covered. (66.22%)

2 existing lines in 2 files now uncovered.

273 of 511 relevant lines covered (53.42%)

1.74 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

5.0
/cmd/authk/root.go
1
package main
2

3
import (
4
        "fmt"
5
        "os"
6
        "time"
7

8
        "github.com/codozor/authk/internal/config"
9
        "github.com/codozor/authk/internal/env"
10
        "github.com/codozor/authk/internal/oidc"
11
        "github.com/rs/zerolog"
12
        "github.com/rs/zerolog/log"
13
        "github.com/spf13/cobra"
14
)
15

16
var (
17
        cfgFile string
18
        envFile string
19
        debug   bool
20
)
21

22
var rootCmd = &cobra.Command{
23
        Use:   "authk",
24
        Short: "OIDC Token Maintainer",
25
        Long: `authk establishes and maintains an OIDC connection, 
26
updating a .env file with the valid token.`,
27
        SilenceUsage:  true,
28
        SilenceErrors: true,
29
        RunE: func(cmd *cobra.Command, args []string) error {
×
30
                printBanner()
×
31

×
32
                // Setup Logger with Pretty Print
×
33
                logLevel := zerolog.InfoLevel
×
34
                if debug {
×
35
                        logLevel = zerolog.DebugLevel
×
36
                }
×
37
                log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}).Level(logLevel)
×
38

×
39
                // Try to find config file
×
40
                if found, err := env.Find(cfgFile); err == nil {
×
41
                        cfgFile = found
×
42
                }
×
43

44
                // Load Config
45
                cfg, err := config.Load(cfgFile)
×
46
                if err != nil {
×
47
                        return fmt.Errorf("failed to load config: %w", err)
×
48
                }
×
49

50
                // Try to find .env file
51
                if found, err := env.Find(envFile); err == nil {
×
52
                        envFile = found
×
53
                }
×
54

55
                // Prepare targets
56
                var targets []config.Target
×
57
                if len(cfg.Targets) > 0 {
×
58
                        targets = cfg.Targets
×
59
                        log.Info().Int("count", len(targets)).Msg("Configured with multiple targets")
×
60
                } 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")
×
63
                }
×
64

65
                // Initialize OIDC Client
66
                client, err := oidc.NewClient(cfg)
×
67
                if err != nil {
×
68
                        return fmt.Errorf("failed to initialize OIDC client: %w", err)
×
69
                }
×
70

71
                // Initial Token Retrieval
72
                token, err := client.GetToken("", "")
×
73
                if err != nil {
×
74
                        return fmt.Errorf("failed to get initial token: %w", err)
×
75
                }
×
76

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")
×
84
                        }
×
85
                }
86

87
                // Maintenance Loop
88
                for {
×
NEW
89
                        // Calculate sleep time based on token expiry and a refresh buffer
×
NEW
90
                        refreshBuffer := 60 * time.Second // Refresh 60 seconds before expiry
×
NEW
91
                        sleepDuration := time.Until(token.Expiry) - refreshBuffer
×
NEW
92
                        if sleepDuration < 10*time.Second { // Ensure at least 10 seconds sleep
×
93
                                sleepDuration = 10 * time.Second
×
94
                        }
×
95

96
                        log.Info().Dur("sleep_duration", sleepDuration).Msg("Waiting for token refresh")
×
97
                        time.Sleep(sleepDuration)
×
98

×
NEW
99
                        // Attempt to refresh the token
×
NEW
100
                        newToken, err := client.RefreshToken(token)
×
101
                        if err != nil {
×
102
                                log.Error().Err(err).Msg("Failed to refresh token, attempting full re-authentication")
×
103

×
104
                                // Try full re-authentication
×
105
                                newToken, err = client.GetToken("", "")
×
106
                                if err != nil {
×
107
                                        log.Error().Err(err).Msg("Failed to re-authenticate")
×
108
                                        // Retry after short delay
×
109
                                        time.Sleep(10 * time.Second)
×
110

×
111
                                        // Force short sleep on next iteration to retry quickly
×
NEW
112
                                        // By setting token.Expiry to now, time.Until will be negative,
×
NEW
113
                                        // and sleepDuration will become 10s.
×
NEW
114
                                        token.Expiry = time.Now()
×
UNCOV
115
                                        continue
×
116
                                }
117
                        }
118

119
                        // Update token
120
                        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
                        }
131
                }
132
        },
133
}
134

135
func printBanner() {
×
136
        banner := `
×
137
   __ _ _   _| |_| |__ | | __
×
138
  / _' | | | | __| '_ \| |/ /
×
139
 | (_| | |_| | |_| | | |   < 
×
140
  \__,_|\__,_|\__|_| |_|_|\_\
×
141
`
×
142
        fmt.Println(banner)
×
143
}
×
144

145
func Execute() {
×
146
        if err := rootCmd.Execute(); err != nil {
×
147
                fmt.Fprintln(os.Stderr, err)
×
148
                os.Exit(1)
×
149
        }
×
150
}
151

152
func init() {
1✔
153
        rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "authk.cue", "config file (default is authk.cue)")
1✔
154
        rootCmd.PersistentFlags().StringVar(&envFile, "env", ".env", "env file (default is .env)")
1✔
155
        rootCmd.PersistentFlags().BoolVar(&debug, "debug", false, "enable debug logging")
1✔
156
}
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc