Skip to content

Commit c74a381

Browse files
author
Christian
committed
chore: update module dependencies and configs
- Upgrade `go-authx` to v1.1.0, `openai-go` to v3.18.0, `gosec` to v2.23.0, `golang.org` libraries, and others. - Enhance `docker-compose.yml` with additional environment variables for token handling. - Add `go/yaml/v3` dependency for extended YAML support.
1 parent 85d051d commit c74a381

9 files changed

Lines changed: 394 additions & 56 deletions

File tree

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ METRICS_PORT=9091
33
LOG_LEVEL=info
44
AUTH_ENABLED=false
55
AUTH_TOKEN_TYPE=jwt
6+
AUTH_INTROSPECTION_AUTH_METHOD=client_secret_basic

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,5 @@ experiments/
4646
*.log
4747
build
4848
.env
49-
.cache
49+
.cache
50+
.gocache

README.md

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,34 @@ Key environment variables:
6262
- `AUTH_TOKEN_TYPE` - Token mode: `jwt` (default) or `opaque`
6363
- `AUTH_JWKS_URL` - Optional custom JWKS endpoint override (JWT mode)
6464
- `AUTH_INTROSPECTION_URL` - OAuth2 introspection endpoint (required in opaque mode)
65-
- `AUTH_INTROSPECTION_CLIENT_ID` / `AUTH_INTROSPECTION_CLIENT_SECRET` - Introspection client credentials (required in opaque mode)
65+
- `AUTH_INTROSPECTION_AUTH_METHOD` - Introspection client auth (`client_secret_basic` default or `private_key_jwt`)
66+
- `AUTH_INTROSPECTION_CLIENT_ID` / `AUTH_INTROSPECTION_CLIENT_SECRET` - Introspection client credentials (required for `client_secret_basic`)
67+
- `AUTH_INTROSPECTION_PRIVATE_KEY` - Private key content for `private_key_jwt` (PEM, JWK JSON, or Zitadel key JSON)
68+
- `AUTH_INTROSPECTION_PRIVATE_KEY_FILE` - Alternative file path for `AUTH_INTROSPECTION_PRIVATE_KEY` (mutually exclusive)
69+
- `AUTH_INTROSPECTION_PRIVATE_KEY_JWT_KID` - Optional JWT header `kid` override for `private_key_jwt`
70+
- `AUTH_INTROSPECTION_PRIVATE_KEY_JWT_ALG` - Optional signing alg override (`RS256` or `ES256`)
6671
- `MAX_UPLOAD_SIZE` / `TIMEOUT` / `LOG_LEVEL` - Upload limit, server timeouts, and logging level
6772

73+
ZITADEL `private_key_jwt` examples:
74+
75+
```bash
76+
# PEM (inline or from file)
77+
AUTH_TOKEN_TYPE=opaque
78+
AUTH_INTROSPECTION_URL=https://<zitadel-domain>/oauth/v2/introspect
79+
AUTH_INTROSPECTION_AUTH_METHOD=private_key_jwt
80+
AUTH_INTROSPECTION_CLIENT_ID=<client-id>
81+
AUTH_INTROSPECTION_PRIVATE_KEY_FILE=/run/secrets/zitadel-private-key.pem
82+
AUTH_INTROSPECTION_PRIVATE_KEY_JWT_ALG=RS256
83+
```
84+
85+
```bash
86+
# Zitadel key JSON envelope (contains keyId/key/clientId)
87+
AUTH_TOKEN_TYPE=opaque
88+
AUTH_INTROSPECTION_URL=https://<zitadel-domain>/oauth/v2/introspect
89+
AUTH_INTROSPECTION_AUTH_METHOD=private_key_jwt
90+
AUTH_INTROSPECTION_PRIVATE_KEY='{"keyId":"...","key":"-----BEGIN PRIVATE KEY-----\n...","clientId":"..."}'
91+
```
92+
6893
## Usage
6994

7095
### CLI

cmd/server/main.go

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -177,11 +177,25 @@ func buildUnaryInterceptors(cfg *config.Config) ([]grpc.UnaryServerInterceptor,
177177

178178
validatorBuilder := grpcserver.NewValidatorBuilder(cfg.AuthIssuer, cfg.AuthAudience)
179179
if cfg.AuthTokenType == "opaque" {
180-
validatorBuilder = validatorBuilder.WithOpaqueTokenIntrospection(
181-
cfg.AuthIntrospectionURL,
182-
cfg.AuthIntrospectionClientID,
183-
cfg.AuthIntrospectionClientSecret,
184-
)
180+
if cfg.AuthIntrospectionAuthMethod == "private_key_jwt" {
181+
authConfig := grpcserver.IntrospectionClientAuthConfig{
182+
Method: grpcserver.IntrospectionClientAuthMethodPrivateKeyJWT,
183+
ClientID: cfg.AuthIntrospectionClientID,
184+
PrivateKey: cfg.AuthIntrospectionPrivateKey,
185+
PrivateKeyJWTKeyID: cfg.AuthIntrospectionPrivateKeyJWTKeyID,
186+
PrivateKeyJWTAlgorithm: cfg.AuthIntrospectionPrivateKeyJWTAlgorithm,
187+
}
188+
validatorBuilder = validatorBuilder.WithOpaqueTokenIntrospectionAuth(
189+
cfg.AuthIntrospectionURL,
190+
authConfig,
191+
)
192+
} else {
193+
validatorBuilder = validatorBuilder.WithOpaqueTokenIntrospection(
194+
cfg.AuthIntrospectionURL,
195+
cfg.AuthIntrospectionClientID,
196+
cfg.AuthIntrospectionClientSecret,
197+
)
198+
}
185199
} else if cfg.AuthJWKSURL != "" {
186200
validatorBuilder = validatorBuilder.WithJWKSURL(cfg.AuthJWKSURL)
187201
}
@@ -197,6 +211,7 @@ func buildUnaryInterceptors(cfg *config.Config) ([]grpc.UnaryServerInterceptor,
197211
Str("audience", cfg.AuthAudience).
198212
Str("jwks_url", cfg.AuthJWKSURL).
199213
Str("introspection_url", cfg.AuthIntrospectionURL).
214+
Str("introspection_auth_method", cfg.AuthIntrospectionAuthMethod).
200215
Msg("gRPC authentication enabled")
201216

202217
return append(interceptors, grpcserver.UnaryServerInterceptor(

cmd/server/main_test.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@ package main
22

33
import (
44
"context"
5+
"crypto/rand"
6+
"crypto/rsa"
7+
"crypto/x509"
8+
"encoding/json"
9+
"encoding/pem"
510
"fmt"
611
"net"
712
"net/http"
@@ -117,6 +122,50 @@ func TestBuildUnaryInterceptors_WithOpaqueAuth(t *testing.T) {
117122
assert.Len(t, interceptors, 3)
118123
}
119124

125+
func TestBuildUnaryInterceptors_WithOpaqueAuthPrivateKeyJWTPEM(t *testing.T) {
126+
cfg := &config.Config{
127+
AuthEnabled: true,
128+
AuthIssuer: "https://issuer.example.com",
129+
AuthAudience: "nist-entropy",
130+
AuthTokenType: "opaque",
131+
AuthIntrospectionURL: "https://issuer.example.com/oauth2/introspect",
132+
AuthIntrospectionAuthMethod: "private_key_jwt",
133+
AuthIntrospectionClientID: "svc-client",
134+
AuthIntrospectionPrivateKey: mustGenerateRSAPrivateKeyPEM(t),
135+
AuthIntrospectionPrivateKeyJWTKeyID: "kid-1",
136+
AuthIntrospectionPrivateKeyJWTAlgorithm: "RS256",
137+
}
138+
139+
interceptors, err := buildUnaryInterceptors(cfg)
140+
require.NoError(t, err)
141+
assert.Len(t, interceptors, 3)
142+
}
143+
144+
func TestBuildUnaryInterceptors_WithOpaqueAuthPrivateKeyJWTZitadelJSON(t *testing.T) {
145+
privateKeyPEM := mustGenerateRSAPrivateKeyPEM(t)
146+
zitadelEnvelope := map[string]string{
147+
"keyId": "zitadel-kid",
148+
"clientId": "svc-client",
149+
"key": privateKeyPEM,
150+
}
151+
zitadelKeyJSONBytes, err := json.Marshal(zitadelEnvelope)
152+
require.NoError(t, err)
153+
154+
cfg := &config.Config{
155+
AuthEnabled: true,
156+
AuthIssuer: "https://issuer.example.com",
157+
AuthAudience: "nist-entropy",
158+
AuthTokenType: "opaque",
159+
AuthIntrospectionURL: "https://issuer.example.com/oauth2/introspect",
160+
AuthIntrospectionAuthMethod: "private_key_jwt",
161+
AuthIntrospectionPrivateKey: string(zitadelKeyJSONBytes),
162+
}
163+
164+
interceptors, err := buildUnaryInterceptors(cfg)
165+
require.NoError(t, err)
166+
assert.Len(t, interceptors, 3)
167+
}
168+
120169
func TestRunFailsOnBadConfig(t *testing.T) {
121170
// Invalid port should cause config validation failure
122171
os.Setenv("SERVER_PORT", "-1")
@@ -213,3 +262,20 @@ func mustListen(t *testing.T) net.Listener {
213262
}
214263
return ln
215264
}
265+
266+
func mustGenerateRSAPrivateKeyPEM(t *testing.T) string {
267+
t.Helper()
268+
269+
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
270+
require.NoError(t, err)
271+
272+
privateKeyDER, err := x509.MarshalPKCS8PrivateKey(privateKey)
273+
require.NoError(t, err)
274+
275+
privateKeyPEM := pem.EncodeToMemory(&pem.Block{
276+
Type: "PRIVATE KEY",
277+
Bytes: privateKeyDER,
278+
})
279+
280+
return string(privateKeyPEM)
281+
}

docker-compose.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,13 @@ services:
2222
- AUTH_JWKS_URL=${AUTH_JWKS_URL:-}
2323
# Opaque mode (RFC 7662 introspection)
2424
- AUTH_INTROSPECTION_URL=${AUTH_INTROSPECTION_URL:-}
25+
- AUTH_INTROSPECTION_AUTH_METHOD=${AUTH_INTROSPECTION_AUTH_METHOD:-client_secret_basic}
2526
- AUTH_INTROSPECTION_CLIENT_ID=${AUTH_INTROSPECTION_CLIENT_ID:-}
2627
- AUTH_INTROSPECTION_CLIENT_SECRET=${AUTH_INTROSPECTION_CLIENT_SECRET:-}
28+
- AUTH_INTROSPECTION_PRIVATE_KEY=${AUTH_INTROSPECTION_PRIVATE_KEY:-}
29+
- AUTH_INTROSPECTION_PRIVATE_KEY_FILE=${AUTH_INTROSPECTION_PRIVATE_KEY_FILE:-}
30+
- AUTH_INTROSPECTION_PRIVATE_KEY_JWT_KID=${AUTH_INTROSPECTION_PRIVATE_KEY_JWT_KID:-}
31+
- AUTH_INTROSPECTION_PRIVATE_KEY_JWT_ALG=${AUTH_INTROSPECTION_PRIVATE_KEY_JWT_ALG:-}
2732
- TLS_ENABLED=${TLS_ENABLED:-false}
2833
- TLS_CERT_FILE=${TLS_CERT_FILE:-}
2934
- TLS_KEY_FILE=${TLS_KEY_FILE:-}

docs/architecture.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -312,8 +312,13 @@ The `internal/config` package provides environment-variable-based configuration
312312
| `AUTH_TOKEN_TYPE` | `jwt` | Token mode (`jwt` or `opaque`) |
313313
| `AUTH_JWKS_URL` | (empty) | Custom JWKS endpoint (JWT mode) |
314314
| `AUTH_INTROSPECTION_URL` | (empty) | OAuth2 introspection endpoint (opaque mode) |
315-
| `AUTH_INTROSPECTION_CLIENT_ID` | (empty) | Introspection client ID (opaque mode) |
316-
| `AUTH_INTROSPECTION_CLIENT_SECRET` | (empty) | Introspection client secret (opaque mode) |
315+
| `AUTH_INTROSPECTION_AUTH_METHOD` | `client_secret_basic` | Introspection client auth method (`client_secret_basic` or `private_key_jwt`) |
316+
| `AUTH_INTROSPECTION_CLIENT_ID` | (empty) | Introspection client ID (`client_secret_basic`; optional for Zitadel key JSON with `private_key_jwt`) |
317+
| `AUTH_INTROSPECTION_CLIENT_SECRET` | (empty) | Introspection client secret (`client_secret_basic`) |
318+
| `AUTH_INTROSPECTION_PRIVATE_KEY` | (empty) | Introspection private key content for `private_key_jwt` (PEM, JWK JSON, or Zitadel key JSON) |
319+
| `AUTH_INTROSPECTION_PRIVATE_KEY_FILE` | (empty) | File path alternative for `AUTH_INTROSPECTION_PRIVATE_KEY` |
320+
| `AUTH_INTROSPECTION_PRIVATE_KEY_JWT_KID` | (empty) | Optional `kid` override for `private_key_jwt` assertions |
321+
| `AUTH_INTROSPECTION_PRIVATE_KEY_JWT_ALG` | (empty) | Optional assertion signing algorithm (`RS256` or `ES256`) |
317322
| `MAX_UPLOAD_SIZE` | `104857600` | Maximum upload size in bytes (100 MB) |
318323
| `TIMEOUT` | `5m` | HTTP read/write timeout |
319324
| `LOG_LEVEL` | `info` | Log verbosity (debug, info, warn, error) |
@@ -349,7 +354,7 @@ The service supports three security mechanisms, all of which are optional and in
349354

350355
**mTLS (Mutual TLS)**: Controlled by `TLS_CLIENT_AUTH`, the server can require clients to present and verify X.509 certificates against a trusted CA bundle specified in `TLS_CA_FILE`.
351356

352-
**OIDC Authentication**: When `AUTH_ENABLED=true`, a token validation interceptor is appended to the gRPC interceptor chain. In `AUTH_TOKEN_TYPE=jwt` mode, `go-authx` validates JWT access tokens via JWKS auto-discovery or `AUTH_JWKS_URL`. In `AUTH_TOKEN_TYPE=opaque` mode, `go-authx` validates opaque access tokens via RFC 7662 introspection (`AUTH_INTROSPECTION_URL` + client credentials). Health check endpoints (`/grpc.health.v1.Health/Check` and `/grpc.health.v1.Health/Watch`) are exempted from authentication.
357+
**OIDC Authentication**: When `AUTH_ENABLED=true`, a token validation interceptor is appended to the gRPC interceptor chain. In `AUTH_TOKEN_TYPE=jwt` mode, `go-authx` validates JWT access tokens via JWKS auto-discovery or `AUTH_JWKS_URL`. In `AUTH_TOKEN_TYPE=opaque` mode, `go-authx` validates opaque access tokens via RFC 7662 introspection (`AUTH_INTROSPECTION_URL`). Introspection client authentication supports both `client_secret_basic` and RFC 7523 `private_key_jwt` (PEM/JWK/Zitadel key JSON). Health check endpoints (`/grpc.health.v1.Health/Check` and `/grpc.health.v1.Health/Watch`) are exempted from authentication.
353358

354359
## 5. Build Architecture
355360

@@ -488,7 +493,7 @@ The `DataGuard` class in `wrapper.cpp` implements RAII (Resource Acquisition Is
488493
| Native Code | C++11 with OpenMP |
489494
| RPC Framework | gRPC (google.golang.org/grpc v1.78.0) |
490495
| Protocol Definitions | Protocol Buffers v3 |
491-
| Authentication | OIDC JWT + opaque token introspection via go-authx v1.0.1 |
496+
| Authentication | OIDC JWT + opaque token introspection (client_secret_basic/private_key_jwt) via go-authx v1.1.0 |
492497
| Logging | zerolog (structured JSON) |
493498
| Metrics | Prometheus client_golang v1.23.2 with promauto |
494499
| Testing | testify, gotestsum, teststub build tag |

internal/config/config.go

Lines changed: 97 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,19 @@ type Config struct {
4242
MetricsEnabled bool
4343

4444
// Authentication
45-
AuthEnabled bool
46-
AuthIssuer string
47-
AuthAudience string
48-
AuthJWKSURL string
49-
AuthTokenType string
50-
AuthIntrospectionURL string
51-
AuthIntrospectionClientID string
52-
AuthIntrospectionClientSecret string
45+
AuthEnabled bool
46+
AuthIssuer string
47+
AuthAudience string
48+
AuthJWKSURL string
49+
AuthTokenType string
50+
AuthIntrospectionURL string
51+
AuthIntrospectionAuthMethod string
52+
AuthIntrospectionClientID string
53+
AuthIntrospectionClientSecret string
54+
AuthIntrospectionPrivateKey string
55+
AuthIntrospectionPrivateKeyFile string
56+
AuthIntrospectionPrivateKeyJWTKeyID string
57+
AuthIntrospectionPrivateKeyJWTAlgorithm string
5358
}
5459

5560
// LoadConfig reads configuration from environment variables, applies default
@@ -58,28 +63,33 @@ type Config struct {
5863
func LoadConfig() (*Config, error) {
5964
config := &Config{
6065
// Defaults
61-
ServerPort: getEnvAsInt("METRICS_PORT", getEnvAsInt("SERVER_PORT", 9091)),
62-
ServerHost: getEnv("SERVER_HOST", "0.0.0.0"),
63-
GRPCEnabled: getEnvAsBool("GRPC_ENABLED", false),
64-
GRPCPort: getEnvAsInt("GRPC_PORT", 9090),
65-
TLSEnabled: getEnvAsBool("TLS_ENABLED", false),
66-
TLSCertFile: getEnv("TLS_CERT_FILE", ""),
67-
TLSKeyFile: getEnv("TLS_KEY_FILE", ""),
68-
TLSCAFile: getEnv("TLS_CA_FILE", ""),
69-
TLSClientAuth: getEnv("TLS_CLIENT_AUTH", "none"),
70-
TLSMinVersion: getEnv("TLS_MIN_VERSION", "1.2"),
71-
LogLevel: getEnv("LOG_LEVEL", "info"),
72-
MaxUploadSize: getEnvAsInt64("MAX_UPLOAD_SIZE", 100*1024*1024), // 100MB default
73-
Timeout: getEnvAsDuration("TIMEOUT", 5*time.Minute),
74-
MetricsEnabled: getEnvAsBool("METRICS_ENABLED", true),
75-
AuthEnabled: getEnvAsBool("AUTH_ENABLED", false),
76-
AuthIssuer: getEnv("AUTH_ISSUER", ""),
77-
AuthAudience: getEnv("AUTH_AUDIENCE", ""),
78-
AuthJWKSURL: getEnv("AUTH_JWKS_URL", ""),
79-
AuthTokenType: getEnv("AUTH_TOKEN_TYPE", "jwt"),
80-
AuthIntrospectionURL: getEnv("AUTH_INTROSPECTION_URL", ""),
81-
AuthIntrospectionClientID: getEnv("AUTH_INTROSPECTION_CLIENT_ID", ""),
82-
AuthIntrospectionClientSecret: getEnv("AUTH_INTROSPECTION_CLIENT_SECRET", ""),
66+
ServerPort: getEnvAsInt("METRICS_PORT", getEnvAsInt("SERVER_PORT", 9091)),
67+
ServerHost: getEnv("SERVER_HOST", "0.0.0.0"),
68+
GRPCEnabled: getEnvAsBool("GRPC_ENABLED", false),
69+
GRPCPort: getEnvAsInt("GRPC_PORT", 9090),
70+
TLSEnabled: getEnvAsBool("TLS_ENABLED", false),
71+
TLSCertFile: getEnv("TLS_CERT_FILE", ""),
72+
TLSKeyFile: getEnv("TLS_KEY_FILE", ""),
73+
TLSCAFile: getEnv("TLS_CA_FILE", ""),
74+
TLSClientAuth: getEnv("TLS_CLIENT_AUTH", "none"),
75+
TLSMinVersion: getEnv("TLS_MIN_VERSION", "1.2"),
76+
LogLevel: getEnv("LOG_LEVEL", "info"),
77+
MaxUploadSize: getEnvAsInt64("MAX_UPLOAD_SIZE", 100*1024*1024), // 100MB default
78+
Timeout: getEnvAsDuration("TIMEOUT", 5*time.Minute),
79+
MetricsEnabled: getEnvAsBool("METRICS_ENABLED", true),
80+
AuthEnabled: getEnvAsBool("AUTH_ENABLED", false),
81+
AuthIssuer: getEnv("AUTH_ISSUER", ""),
82+
AuthAudience: getEnv("AUTH_AUDIENCE", ""),
83+
AuthJWKSURL: getEnv("AUTH_JWKS_URL", ""),
84+
AuthTokenType: getEnv("AUTH_TOKEN_TYPE", "jwt"),
85+
AuthIntrospectionURL: getEnv("AUTH_INTROSPECTION_URL", ""),
86+
AuthIntrospectionAuthMethod: getEnv("AUTH_INTROSPECTION_AUTH_METHOD", "client_secret_basic"),
87+
AuthIntrospectionClientID: getEnv("AUTH_INTROSPECTION_CLIENT_ID", ""),
88+
AuthIntrospectionClientSecret: getEnv("AUTH_INTROSPECTION_CLIENT_SECRET", ""),
89+
AuthIntrospectionPrivateKey: getEnv("AUTH_INTROSPECTION_PRIVATE_KEY", ""),
90+
AuthIntrospectionPrivateKeyFile: getEnv("AUTH_INTROSPECTION_PRIVATE_KEY_FILE", ""),
91+
AuthIntrospectionPrivateKeyJWTKeyID: getEnv("AUTH_INTROSPECTION_PRIVATE_KEY_JWT_KID", ""),
92+
AuthIntrospectionPrivateKeyJWTAlgorithm: getEnv("AUTH_INTROSPECTION_PRIVATE_KEY_JWT_ALG", ""),
8393
}
8494

8595
if err := config.Validate(); err != nil {
@@ -135,11 +145,44 @@ func (c *Config) Validate() error {
135145
if c.AuthIntrospectionURL == "" {
136146
return fmt.Errorf("invalid auth introspection URL: required when AUTH_TOKEN_TYPE=opaque")
137147
}
138-
if c.AuthIntrospectionClientID == "" {
139-
return fmt.Errorf("invalid auth introspection client ID: required when AUTH_TOKEN_TYPE=opaque")
148+
authMethod, err := parseAuthIntrospectionAuthMethod(c.AuthIntrospectionAuthMethod)
149+
if err != nil {
150+
return err
140151
}
141-
if c.AuthIntrospectionClientSecret == "" {
142-
return fmt.Errorf("invalid auth introspection client secret: required when AUTH_TOKEN_TYPE=opaque")
152+
c.AuthIntrospectionAuthMethod = authMethod
153+
154+
switch c.AuthIntrospectionAuthMethod {
155+
case "client_secret_basic":
156+
if c.AuthIntrospectionClientID == "" {
157+
return fmt.Errorf("invalid auth introspection client ID: required when AUTH_INTROSPECTION_AUTH_METHOD=client_secret_basic")
158+
}
159+
if c.AuthIntrospectionClientSecret == "" {
160+
return fmt.Errorf("invalid auth introspection client secret: required when AUTH_INTROSPECTION_AUTH_METHOD=client_secret_basic")
161+
}
162+
case "private_key_jwt":
163+
c.AuthIntrospectionPrivateKey = strings.TrimSpace(c.AuthIntrospectionPrivateKey)
164+
c.AuthIntrospectionPrivateKeyFile = strings.TrimSpace(c.AuthIntrospectionPrivateKeyFile)
165+
if c.AuthIntrospectionPrivateKey != "" && c.AuthIntrospectionPrivateKeyFile != "" {
166+
return fmt.Errorf("invalid auth introspection private key config: AUTH_INTROSPECTION_PRIVATE_KEY and AUTH_INTROSPECTION_PRIVATE_KEY_FILE are mutually exclusive")
167+
}
168+
if c.AuthIntrospectionPrivateKey == "" && c.AuthIntrospectionPrivateKeyFile == "" {
169+
return fmt.Errorf("invalid auth introspection private key: required when AUTH_INTROSPECTION_AUTH_METHOD=private_key_jwt")
170+
}
171+
if c.AuthIntrospectionPrivateKeyFile != "" {
172+
privateKeyBytes, readErr := os.ReadFile(c.AuthIntrospectionPrivateKeyFile)
173+
if readErr != nil {
174+
return fmt.Errorf("invalid auth introspection private key file: %w", readErr)
175+
}
176+
c.AuthIntrospectionPrivateKey = strings.TrimSpace(string(privateKeyBytes))
177+
if c.AuthIntrospectionPrivateKey == "" {
178+
return fmt.Errorf("invalid auth introspection private key file: empty file")
179+
}
180+
}
181+
privateKeyJWTAlgorithm, parseErr := parseAuthIntrospectionPrivateKeyJWTAlgorithm(c.AuthIntrospectionPrivateKeyJWTAlgorithm)
182+
if parseErr != nil {
183+
return parseErr
184+
}
185+
c.AuthIntrospectionPrivateKeyJWTAlgorithm = privateKeyJWTAlgorithm
143186
}
144187
}
145188
}
@@ -238,6 +281,26 @@ func parseAuthTokenType(tokenType string) (string, error) {
238281
}
239282
}
240283

284+
func parseAuthIntrospectionAuthMethod(method string) (string, error) {
285+
switch strings.ToLower(strings.TrimSpace(method)) {
286+
case "", "client_secret_basic":
287+
return "client_secret_basic", nil
288+
case "private_key_jwt":
289+
return "private_key_jwt", nil
290+
default:
291+
return "", fmt.Errorf("invalid AUTH_INTROSPECTION_AUTH_METHOD: %s (use client_secret_basic or private_key_jwt)", method)
292+
}
293+
}
294+
295+
func parseAuthIntrospectionPrivateKeyJWTAlgorithm(algorithm string) (string, error) {
296+
switch strings.ToUpper(strings.TrimSpace(algorithm)) {
297+
case "", "RS256", "ES256":
298+
return strings.ToUpper(strings.TrimSpace(algorithm)), nil
299+
default:
300+
return "", fmt.Errorf("invalid AUTH_INTROSPECTION_PRIVATE_KEY_JWT_ALG: %s (use RS256 or ES256)", algorithm)
301+
}
302+
}
303+
241304
func getEnvAsInt64(key string, defaultValue int64) int64 {
242305
valueStr := os.Getenv(key)
243306
if valueStr == "" {

0 commit comments

Comments
 (0)