Skip to content

Commit cd38715

Browse files
committed
Add support for path based auth
1 parent 4cf6e77 commit cd38715

3 files changed

Lines changed: 221 additions & 18 deletions

File tree

authentication/mtls.go

Lines changed: 118 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"fmt"
99
"net/http"
1010
"os"
11+
"regexp"
1112

1213
"github.com/go-kit/log"
1314
grpc_middleware_auth "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/auth"
@@ -28,9 +29,11 @@ func init() {
2829
}
2930

3031
type mTLSConfig struct {
31-
RawCA []byte `json:"ca"`
32-
CAPath string `json:"caPath"`
33-
CAs []*x509.Certificate
32+
RawCA []byte `json:"ca"`
33+
CAPath string `json:"caPath"`
34+
PathPatterns []string `json:"pathPatterns"`
35+
CAs []*x509.Certificate
36+
pathMatchers []*regexp.Regexp
3437
}
3538

3639
type MTLSAuthenticator struct {
@@ -83,6 +86,15 @@ func newMTLSAuthenticator(c map[string]interface{}, tenant string, registrationR
8386
config.CAs = cas
8487
}
8588

89+
// Compile path patterns
90+
for _, pattern := range config.PathPatterns {
91+
matcher, err := regexp.Compile(pattern)
92+
if err != nil {
93+
return nil, fmt.Errorf("failed to compile mTLS path pattern %q: %v", pattern, err)
94+
}
95+
config.pathMatchers = append(config.pathMatchers, matcher)
96+
}
97+
8698
return MTLSAuthenticator{
8799
tenant: tenant,
88100
logger: logger,
@@ -93,6 +105,29 @@ func newMTLSAuthenticator(c map[string]interface{}, tenant string, registrationR
93105
func (a MTLSAuthenticator) Middleware() Middleware {
94106
return func(next http.Handler) http.Handler {
95107
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
108+
// Check if mTLS is required for this path
109+
if len(a.config.pathMatchers) > 0 {
110+
pathMatches := false
111+
for _, matcher := range a.config.pathMatchers {
112+
if matcher.MatchString(r.URL.Path) {
113+
pathMatches = true
114+
break
115+
}
116+
}
117+
118+
// If path doesn't match, skip mTLS enforcement
119+
if !pathMatches {
120+
next.ServeHTTP(w, r)
121+
return
122+
}
123+
}
124+
125+
// Path matches or no paths configured, enforce mTLS
126+
if r.TLS == nil {
127+
httperr.PrometheusAPIError(w, "mTLS required but no TLS connection", http.StatusBadRequest)
128+
return
129+
}
130+
96131
caPool := x509.NewCertPool()
97132
for _, ca := range a.config.CAs {
98133
caPool.AddCert(ca)
@@ -157,3 +192,83 @@ func (a MTLSAuthenticator) GRPCMiddleware() grpc.StreamServerInterceptor {
157192
func (a MTLSAuthenticator) Handler() (string, http.Handler) {
158193
return "", nil
159194
}
195+
196+
// PathAwareMiddleware creates a middleware that only enforces mTLS on matching paths
197+
func (a MTLSAuthenticator) PathAwareMiddleware(pathMatchers []*regexp.Regexp) Middleware {
198+
return func(next http.Handler) http.Handler {
199+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
200+
// Check if the request path matches any of the configured patterns
201+
pathMatches := false
202+
for _, matcher := range pathMatchers {
203+
if matcher.MatchString(r.URL.Path) {
204+
pathMatches = true
205+
break
206+
}
207+
}
208+
209+
// If no path matches, skip mTLS enforcement
210+
if !pathMatches {
211+
next.ServeHTTP(w, r)
212+
return
213+
}
214+
215+
// Path matches, enforce mTLS
216+
if r.TLS == nil {
217+
httperr.PrometheusAPIError(w, "mTLS required but no TLS connection", http.StatusBadRequest)
218+
return
219+
}
220+
221+
caPool := x509.NewCertPool()
222+
for _, ca := range a.config.CAs {
223+
caPool.AddCert(ca)
224+
}
225+
226+
if len(r.TLS.PeerCertificates) == 0 {
227+
httperr.PrometheusAPIError(w, "client certificate required for this path", http.StatusUnauthorized)
228+
return
229+
}
230+
231+
opts := x509.VerifyOptions{
232+
Roots: caPool,
233+
Intermediates: x509.NewCertPool(),
234+
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
235+
}
236+
237+
if len(r.TLS.PeerCertificates) > 1 {
238+
for _, cert := range r.TLS.PeerCertificates[1:] {
239+
opts.Intermediates.AddCert(cert)
240+
}
241+
}
242+
243+
if _, err := r.TLS.PeerCertificates[0].Verify(opts); err != nil {
244+
if errors.Is(err, x509.CertificateInvalidError{}) {
245+
httperr.PrometheusAPIError(w, err.Error(), http.StatusUnauthorized)
246+
return
247+
}
248+
httperr.PrometheusAPIError(w, err.Error(), http.StatusInternalServerError)
249+
return
250+
}
251+
252+
var sub string
253+
switch {
254+
case len(r.TLS.PeerCertificates[0].EmailAddresses) > 0:
255+
sub = r.TLS.PeerCertificates[0].EmailAddresses[0]
256+
case len(r.TLS.PeerCertificates[0].URIs) > 0:
257+
sub = r.TLS.PeerCertificates[0].URIs[0].String()
258+
case len(r.TLS.PeerCertificates[0].DNSNames) > 0:
259+
sub = r.TLS.PeerCertificates[0].DNSNames[0]
260+
case len(r.TLS.PeerCertificates[0].IPAddresses) > 0:
261+
sub = r.TLS.PeerCertificates[0].IPAddresses[0].String()
262+
default:
263+
httperr.PrometheusAPIError(w, "could not determine subject", http.StatusBadRequest)
264+
return
265+
}
266+
ctx := context.WithValue(r.Context(), subjectKey, sub)
267+
268+
// Add organizational units as groups.
269+
ctx = context.WithValue(ctx, groupsKey, r.TLS.PeerCertificates[0].Subject.OrganizationalUnit)
270+
271+
next.ServeHTTP(w, r.WithContext(ctx))
272+
})
273+
}
274+
}

authentication/oidc.go

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"net/http"
1111
"os"
1212
"path"
13+
"regexp"
1314
"strings"
1415
"time"
1516

@@ -40,15 +41,16 @@ func init() {
4041

4142
// oidcConfig represents the oidc authenticator config.
4243
type oidcConfig struct {
43-
ClientID string `json:"clientID"`
44-
ClientSecret string `json:"clientSecret"`
45-
GroupClaim string `json:"groupClaim"`
46-
IssuerRawCA []byte `json:"issuerCA"`
47-
IssuerCAPath string `json:"issuerCAPath"`
44+
ClientID string `json:"clientID"`
45+
ClientSecret string `json:"clientSecret"`
46+
GroupClaim string `json:"groupClaim"`
47+
IssuerRawCA []byte `json:"issuerCA"`
48+
IssuerCAPath string `json:"issuerCAPath"`
4849
issuerCA *x509.Certificate
49-
IssuerURL string `json:"issuerURL"`
50-
RedirectURL string `json:"redirectURL"`
51-
UsernameClaim string `json:"usernameClaim"`
50+
IssuerURL string `json:"issuerURL"`
51+
RedirectURL string `json:"redirectURL"`
52+
UsernameClaim string `json:"usernameClaim"`
53+
PathPatterns []string `json:"pathPatterns"`
5254
}
5355

5456
type oidcAuthenticator struct {
@@ -62,6 +64,7 @@ type oidcAuthenticator struct {
6264
redirectURL string
6365
oauth2Config oauth2.Config
6466
handler http.Handler
67+
pathMatchers []*regexp.Regexp
6568
}
6669

6770
func newOIDCAuthenticator(c map[string]interface{}, tenant string,
@@ -146,6 +149,16 @@ func newOIDCAuthenticator(c map[string]interface{}, tenant string,
146149

147150
verifier := provider.Verifier(&oidc.Config{ClientID: config.ClientID, SkipClientIDCheck: true})
148151

152+
// Compile path patterns
153+
var pathMatchers []*regexp.Regexp
154+
for _, pattern := range config.PathPatterns {
155+
matcher, err := regexp.Compile(pattern)
156+
if err != nil {
157+
return nil, fmt.Errorf("failed to compile OIDC path pattern %q: %v", pattern, err)
158+
}
159+
pathMatchers = append(pathMatchers, matcher)
160+
}
161+
149162
oidcProvider := &oidcAuthenticator{
150163
tenant: tenant,
151164
logger: logger,
@@ -156,6 +169,7 @@ func newOIDCAuthenticator(c map[string]interface{}, tenant string,
156169
client: client,
157170
cookieName: fmt.Sprintf("observatorium_%s", tenant),
158171
redirectURL: path.Join("/", tenant),
172+
pathMatchers: pathMatchers,
159173
}
160174

161175
r := chi.NewRouter()
@@ -275,6 +289,24 @@ func (a oidcAuthenticator) Handler() (string, http.Handler) {
275289
func (a oidcAuthenticator) Middleware() Middleware {
276290
return func(next http.Handler) http.Handler {
277291
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
292+
// Check if OIDC is required for this path
293+
if len(a.pathMatchers) > 0 {
294+
pathMatches := false
295+
for _, matcher := range a.pathMatchers {
296+
if matcher.MatchString(r.URL.Path) {
297+
pathMatches = true
298+
break
299+
}
300+
}
301+
302+
// If path doesn't match, skip OIDC enforcement
303+
if !pathMatches {
304+
next.ServeHTTP(w, r)
305+
return
306+
}
307+
}
308+
309+
// Path matches or no paths configured, enforce OIDC
278310
var token string
279311

280312
authorizationHeader := r.Header.Get("Authorization")

main.go

Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -237,9 +237,11 @@ type tenant struct {
237237
IssuerRawCA []byte `json:"issuerCA"`
238238
IssuerCAPath string `json:"issuerCAPath"`
239239
issuerCA *x509.Certificate
240-
IssuerURL string `json:"issuerURL"`
241-
RedirectURL string `json:"redirectURL"`
242-
UsernameClaim string `json:"usernameClaim"`
240+
IssuerURL string `json:"issuerURL"`
241+
RedirectURL string `json:"redirectURL"`
242+
UsernameClaim string `json:"usernameClaim"`
243+
Paths []string `json:"paths"`
244+
pathMatchers []*regexp.Regexp
243245
config map[string]interface{}
244246
} `json:"oidc"`
245247
OpenShift *struct {
@@ -255,16 +257,19 @@ type tenant struct {
255257
} `json:"authenticator"`
256258

257259
MTLS *struct {
258-
RawCA []byte `json:"ca"`
259-
CAPath string `json:"caPath"`
260-
cas []*x509.Certificate
261-
config map[string]interface{}
260+
RawCA []byte `json:"ca"`
261+
CAPath string `json:"caPath"`
262+
Paths []string `json:"paths"`
263+
cas []*x509.Certificate
264+
pathMatchers []*regexp.Regexp
265+
config map[string]interface{}
262266
} `json:"mTLS"`
263267
OPA *struct {
264268
Query string `json:"query"`
265269
Paths []string `json:"paths"`
266270
URL string `json:"url"`
267271
WithAccessToken bool `json:"withAccessToken"`
272+
pathMatchers []*regexp.Regexp
268273
authorizer rbac.Authorizer
269274
} `json:"opa"`
270275
RateLimits []*struct {
@@ -362,6 +367,23 @@ func main() {
362367
continue
363368
}
364369

370+
// Compile OIDC path matchers
371+
for _, pathPattern := range t.OIDC.Paths {
372+
matcher, err := regexp.Compile(pathPattern)
373+
if err != nil {
374+
skip.Log("msg", "failed to compile OIDC path pattern", "pattern", pathPattern, "err", err, "tenant", t.Name)
375+
skippedTenants.WithLabelValues(t.Name).Inc()
376+
tenantsCfg.Tenants[i] = nil
377+
break
378+
}
379+
t.OIDC.pathMatchers = append(t.OIDC.pathMatchers, matcher)
380+
}
381+
if tenantsCfg.Tenants[i] == nil {
382+
continue
383+
}
384+
385+
// Add path patterns to the config that will be passed to the authenticator
386+
oidcConfig["pathPatterns"] = t.OIDC.Paths
365387
t.OIDC.config = oidcConfig
366388
}
367389

@@ -373,6 +395,24 @@ func main() {
373395
tenantsCfg.Tenants[i] = nil
374396
continue
375397
}
398+
399+
// Compile mTLS path matchers
400+
for _, pathPattern := range t.MTLS.Paths {
401+
matcher, err := regexp.Compile(pathPattern)
402+
if err != nil {
403+
skip.Log("msg", "failed to compile mTLS path pattern", "pattern", pathPattern, "err", err, "tenant", t.Name)
404+
skippedTenants.WithLabelValues(t.Name).Inc()
405+
tenantsCfg.Tenants[i] = nil
406+
break
407+
}
408+
t.MTLS.pathMatchers = append(t.MTLS.pathMatchers, matcher)
409+
}
410+
if tenantsCfg.Tenants[i] == nil {
411+
continue
412+
}
413+
414+
// Add path patterns to the config that will be passed to the authenticator
415+
mTLSConfig["pathPatterns"] = t.MTLS.Paths
376416
t.MTLS.config = mTLSConfig
377417
}
378418

@@ -397,6 +437,21 @@ func main() {
397437
}
398438

399439
if t.OPA != nil {
440+
// Compile OPA path matchers
441+
for _, pathPattern := range t.OPA.Paths {
442+
matcher, err := regexp.Compile(pathPattern)
443+
if err != nil {
444+
skip.Log("msg", "failed to compile OPA path pattern", "pattern", pathPattern, "err", err, "tenant", t.Name)
445+
skippedTenants.WithLabelValues(t.Name).Inc()
446+
tenantsCfg.Tenants[i] = nil
447+
break
448+
}
449+
t.OPA.pathMatchers = append(t.OPA.pathMatchers, matcher)
450+
}
451+
if tenantsCfg.Tenants[i] == nil {
452+
continue
453+
}
454+
400455
if t.OPA.URL != "" {
401456
u, err := url.Parse(t.OPA.URL)
402457
if err != nil {
@@ -1546,6 +1601,7 @@ func tenantAuthenticatorConfig(t *tenant) (map[string]interface{}, string, error
15461601
}
15471602
}
15481603

1604+
15491605
type otelErrorHandler struct {
15501606
logger log.Logger
15511607
}

0 commit comments

Comments
 (0)