~bigbes/lethe

ref: 50654f96ce59bec2ca3200cd54806704472ff21f lethe/internal/server/auth/devstub.go -rw-r--r-- 4.4 KiB
50654f96 — Eugene Blikh web: wire display settings UI a month ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
// Package auth — OIDCDevStub provides a steward-managed in-process OIDC
// provider for local development. It is registered before OIDCVerifier so its
// listener accepts traffic by the time the verifier's Init calls
// oidc.NewProvider (IV2). When DevStub.Enabled is false the component is a
// no-op: no listener, no goroutines, no banner (IV3).
package auth

import (
	"context"
	"log/slog"
	"net"
	"net/http"
	"time"

	"go.bigb.es/auxilia/culpa"

	"sourcecraft.dev/bigbes/lethe/internal/config"
	"sourcecraft.dev/bigbes/lethe/internal/platform/observability"
	"sourcecraft.dev/bigbes/lethe/internal/testutil/oidcstub"
)

// OIDCDevStub is a steward-managed in-process OIDC identity provider for
// local development. Construct as a zero value with Cfg and Log set, then
// call Init. When OIDC or DevStub are not enabled, Init is a no-op.
//
// Fields are exported for steward injection.
type OIDCDevStub struct {
	Cfg config.AuthConfig      `config:""`
	Log *observability.Logger  `inject:""`

	stub *oidcstub.Stub
	srv  *http.Server
}

// Init starts the stub OIDC provider. It is a no-op when either
// cfg.Auth.OIDC.Enabled or cfg.Auth.OIDC.DevStub.Enabled is false.
//
// When enabled it:
//  1. Builds an oidcstub.Stub from the shared OIDC config (IV4).
//  2. Starts an http.Server on Cfg.OIDC.DevStub.Bind in a goroutine.
//  3. Polls the bind address (1 s deadline, 100 ms dial timeout) so Init
//     returns only once the listener accepts connections (IV2).
//  4. Mints one token per AllowedUsers entry and logs a banner.
func (d *OIDCDevStub) Init(ctx context.Context) error {
	// Defense-in-depth guard; main.go also gates registration (IV3).
	if !d.Cfg.OIDC.Enabled || !d.Cfg.OIDC.DevStub.Enabled {
		return nil
	}

	ttl := d.Cfg.OIDC.DevStub.TokenTTL
	if ttl == 0 {
		ttl = 24 * time.Hour
	}

	// Build the stub using the same issuer/audience/username_claim that the
	// production OIDCVerifier will use (IV4).
	stub, err := oidcstub.New(oidcstub.Options{
		Issuer:        d.Cfg.OIDC.Issuer,
		Audience:      d.Cfg.OIDC.Audience,
		UsernameClaim: d.Cfg.OIDC.UsernameClaim,
		DefaultTTL:    ttl,
	})
	if err != nil {
		return culpa.WithCode(culpa.Wrap(err, "oidcstub init"), "OIDC_DEVSTUB_INIT")
	}
	d.stub = stub

	srv := &http.Server{
		Addr:    d.Cfg.OIDC.DevStub.Bind,
		Handler: stub.Handler(),
	}
	d.srv = srv

	// Channel to capture fatal startup errors from the server goroutine.
	// Buffered so the goroutine never blocks if Init has already returned.
	errCh := make(chan error, 1)
	go func() {
		if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			errCh <- err
		}
	}()

	// Readiness probe: poll until the listener accepts connections or the
	// deadline expires.
	bind := d.Cfg.OIDC.DevStub.Bind
	deadline := time.Now().Add(time.Second)
	for time.Now().Before(deadline) {
		// Check if the goroutine reported a fatal startup error.
		select {
		case startErr := <-errCh:
			return culpa.WithCode(
				culpa.Wrap(startErr, "oidcstub server failed to start"),
				"OIDC_DEVSTUB_INIT",
			)
		default:
		}

		conn, dialErr := net.DialTimeout("tcp", bind, 100*time.Millisecond)
		if dialErr == nil {
			conn.Close()
			goto ready
		}
		time.Sleep(20 * time.Millisecond)
	}

	// One final check for a startup error before returning timeout.
	select {
	case startErr := <-errCh:
		return culpa.WithCode(
			culpa.Wrap(startErr, "oidcstub server failed to start"),
			"OIDC_DEVSTUB_INIT",
		)
	default:
	}

	return culpa.WithCode(
		culpa.New("oidcstub listener readiness timeout on "+bind),
		"OIDC_DEVSTUB_INIT",
	)

ready:
	// Mint one dev token per allowed user and emit the banner.
	for _, user := range d.Cfg.AllowedUsers {
		tok, expiresAt, mintErr := stub.Mint(user, ttl, nil)
		if mintErr != nil {
			return culpa.WithCode(culpa.Wrap(mintErr, "mint dev token for "+user), "OIDC_DEVSTUB_INIT")
		}
		d.Log.L.Info("oidc dev stub token",
			slog.String("user", user),
			slog.String("issuer", d.Cfg.OIDC.Issuer),
			slog.String("audience", d.Cfg.OIDC.Audience),
			slog.Time("expires_at", expiresAt),
			slog.String("token", tok),
		)
	}

	d.Log.L.Info("oidc dev stub started",
		slog.String("bind", bind),
		slog.String("issuer", d.Cfg.OIDC.Issuer),
		slog.Int("allowed_users", len(d.Cfg.AllowedUsers)),
	)

	return nil
}

// Destroy shuts the stub HTTP server down using the caller's context budget.
// It is safe to call when the stub was never started (no-op path).
func (d *OIDCDevStub) Destroy(ctx context.Context) error {
	if d.srv == nil {
		return nil
	}
	return d.srv.Shutdown(ctx)
}