Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
7fd1d7e
decode only once in middleware
MariemBaccari Sep 9, 2025
03a06f3
fix formatting error
MariemBaccari Sep 9, 2025
b20dc4c
[logging] Add subject field to log output (#1263)
MariemBaccari Sep 9, 2025
fc27a16
remove merge messages
MariemBaccari Sep 9, 2025
625f684
fix unit test
MariemBaccari Sep 9, 2025
1ef90ba
edit error message
MariemBaccari Sep 9, 2025
5860a2e
Revert changes and improve middleware declarations
MariemBaccari Sep 18, 2025
30dd60f
address pr comments
MariemBaccari Oct 21, 2025
06b3ae2
fix context key nit
MariemBaccari Oct 21, 2025
86bb4ca
Merge branch 'master' into 1051-dup-token
MariemBaccari Oct 21, 2025
363c8da
add missing context
MariemBaccari Oct 21, 2025
9fd7ae0
clarify middleware doc
MariemBaccari Oct 21, 2025
36df8df
decode only once in middleware
MariemBaccari Sep 9, 2025
929fbe6
fix formatting error
MariemBaccari Sep 9, 2025
0621a00
[logging] Add subject field to log output (#1263)
MariemBaccari Sep 9, 2025
7316590
remove merge messages
MariemBaccari Sep 9, 2025
1a74ef7
fix unit test
MariemBaccari Sep 9, 2025
1325293
edit error message
MariemBaccari Sep 9, 2025
c11145a
Revert changes and improve middleware declarations
MariemBaccari Sep 18, 2025
d78dc68
address pr comments
MariemBaccari Oct 21, 2025
33cca92
fix context key nit
MariemBaccari Oct 21, 2025
09f7926
add missing context
MariemBaccari Oct 21, 2025
26eae64
clarify middleware doc
MariemBaccari Oct 21, 2025
ac80360
Merge branch '1051-dup-token' of github.com:Orbitalize/dss into 1051-…
MariemBaccari Oct 21, 2025
6e443ca
fix auth_test.go
MariemBaccari Oct 21, 2025
ed2de91
address review comments
MariemBaccari Dec 15, 2025
71210a2
use keyclaims var name
MariemBaccari Dec 15, 2025
62ed06c
remove added newline
MariemBaccari Dec 15, 2025
97891bb
move ctxkey
MariemBaccari Dec 15, 2025
817115b
use t.context in all testing
MariemBaccari Dec 15, 2025
32e429e
address review comments
MariemBaccari Dec 26, 2025
e33f6b4
log value from claims.FromContext
MariemBaccari Dec 26, 2025
1e5755f
remove unused ctxkey
MariemBaccari Dec 26, 2025
4fdf748
nit
MariemBaccari Dec 30, 2025
60f3db1
Update pkg/logging/http.go
MariemBaccari Jan 6, 2026
799821b
address review comment
MariemBaccari Jan 6, 2026
bcdebdb
nit
MariemBaccari Jan 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 25 additions & 12 deletions cmds/core-service/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"github.com/interuss/dss/pkg/build"
"github.com/interuss/dss/pkg/datastore"
"github.com/interuss/dss/pkg/datastore/flags" // Force command line flag registration
dsserr "github.com/interuss/dss/pkg/errors"
"github.com/interuss/dss/pkg/logging"
"github.com/interuss/dss/pkg/rid/application"
rid_v1 "github.com/interuss/dss/pkg/rid/server/v1"
Expand Down Expand Up @@ -314,12 +315,10 @@ func RunHTTPServer(ctx context.Context, ctxCanceler func(), address, locality st
multiRouter.Routers = append(multiRouter.Routers, &scdV1Router)
}

handler := logging.HTTPMiddleware(logger, *dumpRequests,
healthyEndpointMiddleware(logger,
&multiRouter,
))

handler = authDecoderMiddleware(authorizer, handler)
// the middlewares are wrapped and, therefore, executed in the opposite order
handler := healthyEndpointMiddleware(logger, &multiRouter)
handler = logging.HTTPMiddleware(logger, *dumpRequests, handler)
handler = authMiddleware(authorizer, handler)

httpServer := &http.Server{
Addr: address,
Expand Down Expand Up @@ -381,19 +380,33 @@ func healthyEndpointMiddleware(logger *zap.Logger, next http.Handler) http.Handl
})
}

// authDecoderMiddleware decodes the authentication token and adds the Subject claim to the context.
func authDecoderMiddleware(authorizer *auth.Authorizer, handler http.Handler) http.Handler {
// authMiddleware decodes the authentication token and passes the claims to the context.
func authMiddleware(authorizer *auth.Authorizer, handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var ctx context.Context

claims, err := authorizer.ExtractClaims(r)
if err == nil {
if !authorizer.AcceptedAudiences[claims.Audience] {
err = stacktrace.NewErrorWithCode(dsserr.Unauthenticated, "Invalid access token audience: %v", claims.Audience)
}
}

ctx = context.WithValue(r.Context(), auth.CtxAuthKey{}, auth.CtxAuthValue{
Claims: claims,
Copy link
Contributor

@barroco barroco Sep 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of using the context to pass the subject to the logger, we may want to zap it in the logger directly like the httpmiddleware is doing it. Could you please evaluate this approach and propose a new version ?

Copy link
Contributor Author

@MariemBaccari MariemBaccari Sep 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In 9cbfe67 I added the logger as an argument to the auth middleware to write the values there and pass the new (enriched) logger to the httpMiddleware. What do you think ?

Error: err,
})

var errMsg string
if err != nil {
//remove the stacktrace using the formatting specifier "%#s"
ctx = context.WithValue(r.Context(), logging.CtxAuthError{}, fmt.Sprintf("%#s", err))
} else {
ctx = context.WithValue(r.Context(), logging.CtxAuthSubject{}, claims.Subject)
errMsg = fmt.Sprintf("%#s", err)
}

ctx = context.WithValue(ctx, logging.CtxAuthKey{}, logging.CtxAuthValue{
Subject: claims.Subject,
ErrMsg: errMsg,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is propagating the error necessary? Remove if not.
Then the subject may be propagated with a single string

Also, nit, but for context key you may do:

type CtxKey string

ctx = context.WithValue(ctx, CtxKey("sub"), parsedSub)

Copy link
Contributor Author

@MariemBaccari MariemBaccari Oct 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Propagating the error allows us to log it when the sub can't be found:

When provided with a valid token, the log outputs the following:
2025-09-03T06:21:28.402Z info logging/http.go:95 GET / HTTP/1.1 {"address": ":8082", "req_dump": "", "req_sub": "uss1", "resp_dump": "404 page not found\n", "req_headers": {"Accept":["application/json"],"Authorization":["Bearer ...

When the token decoding fails, the log outputs the following:
info logging/http.go:95 GET / HTTP/1.1 {"address": ":8082", "req_dump": "", "resp_sub_err": "Access token validation failed: token is expired by 29h16m20s", "resp_dump": "404 page not found\n", "req_headers": {"Accept":["application/json"],"Authorization":["Bearer

Is this aligned with the expected behavior ? If not, I can remove it.

})

handler.ServeHTTP(w, r.WithContext(ctx))
})
}
Expand Down
45 changes: 24 additions & 21 deletions pkg/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,11 @@ func (r *JWKSResolver) ResolveKeys(ctx context.Context) ([]interface{}, error) {

// Authorizer authorizes incoming requests.
type Authorizer struct {
logger *zap.Logger
keys []interface{}
keyGuard sync.RWMutex
acceptedAudiences map[string]bool
logger *zap.Logger
keys []interface{}
keyGuard sync.RWMutex

AcceptedAudiences map[string]bool
}

// Configuration bundles up creation-time parameters for an Authorizer instance.
Expand All @@ -148,7 +149,7 @@ func NewRSAAuthorizer(ctx context.Context, configuration Configuration) (*Author
}

authorizer := &Authorizer{
acceptedAudiences: auds,
AcceptedAudiences: auds,
logger: logger,
keys: keys,
}
Expand Down Expand Up @@ -182,44 +183,46 @@ func (a *Authorizer) setKeys(keys []interface{}) {
a.keyGuard.Unlock()
}

type CtxAuthKey struct{}
type CtxAuthValue struct {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: you are in package auth, so you may drop the Auth part from the variable names.

Claims Claims
Error error
}

// Authorize extracts and verifies bearer tokens from a http.Request.
func (a *Authorizer) Authorize(_ http.ResponseWriter, r *http.Request, authOptions []api.AuthorizationOption) api.AuthorizationResult {
keyClaims, err := a.ExtractClaims(r)
if err != nil {
return api.AuthorizationResult{Error: stacktrace.PropagateWithCode(err, dsserr.Unauthenticated, "Failed to extract claims from access token")}
}

if !a.acceptedAudiences[keyClaims.Audience] {
return api.AuthorizationResult{Error: stacktrace.NewErrorWithCode(dsserr.Unauthenticated, "Invalid access token audience: %v", keyClaims.Audience)}
authResults := r.Context().Value(CtxAuthKey{}).(CtxAuthValue)
if authResults.Error != nil {
return api.AuthorizationResult{Error: stacktrace.PropagateWithCode(authResults.Error, dsserr.Unauthenticated, "Invalid access token")}
}

if pass, missing := validateScopes(authOptions, keyClaims.Scopes); !pass {
if pass, missing := validateScopes(authOptions, authResults.Claims.Scopes); !pass {
return api.AuthorizationResult{Error: stacktrace.NewErrorWithCode(dsserr.PermissionDenied,
"Access token missing scopes (%v) while expecting %v and got %v",
missing, describeAuthorizationExpectations(authOptions), strings.Join(keyClaims.Scopes.ToStringSlice(), ", "))}
missing, describeAuthorizationExpectations(authOptions), strings.Join(authResults.Claims.Scopes.ToStringSlice(), ", "))}
}

return api.AuthorizationResult{
ClientID: &keyClaims.Subject,
Scopes: keyClaims.Scopes.ToStringSlice(),
ClientID: &authResults.Claims.Subject,
Scopes: authResults.Claims.Scopes.ToStringSlice(),
}
}

func (a *Authorizer) ExtractClaims(r *http.Request) (claims, error) {
func (a *Authorizer) ExtractClaims(r *http.Request) (Claims, error) {
tknStr, ok := getToken(r)
if !ok {
return claims{}, stacktrace.NewErrorWithCode(dsserr.Unauthenticated, "Missing access token")
return Claims{}, stacktrace.NewErrorWithCode(dsserr.Unauthenticated, "Missing access token")
}

a.keyGuard.RLock()
keys := a.keys
a.keyGuard.RUnlock()
validated := false
var err error
var keyClaims claims
var keyClaims Claims

for _, key := range keys {
keyClaims = claims{}
keyClaims = Claims{}
key := key
_, err = jwt.ParseWithClaims(tknStr, &keyClaims, func(token *jwt.Token) (interface{}, error) {
return key, nil
Expand All @@ -234,7 +237,7 @@ func (a *Authorizer) ExtractClaims(r *http.Request) (claims, error) {
if err == nil { // If we have no keys, errs may be nil
err = stacktrace.NewErrorWithCode(dsserr.Unauthenticated, "No keys to validate against")
}
return claims{}, stacktrace.PropagateWithCode(err, dsserr.Unauthenticated, "Access token validation failed")
return Claims{}, stacktrace.PropagateWithCode(err, dsserr.Unauthenticated, "Access token validation failed")
}

return keyClaims, nil
Expand Down
11 changes: 9 additions & 2 deletions pkg/auth/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,14 @@ func TestRSAAuthInterceptor(t *testing.T) {

for i, test := range authTests {
t.Run(strconv.Itoa(i), func(t *testing.T) {
res := a.Authorize(nil, test.req, []api.AuthorizationOption{})
claims, err := a.ExtractClaims(test.req)

ctx := context.WithValue(test.req.Context(), CtxAuthKey{}, CtxAuthValue{
Error: err,
Claims: claims,
})

res := a.Authorize(nil, test.req.WithContext(ctx), []api.AuthorizationOption{})
if test.code != stacktrace.ErrorCode(0) && stacktrace.GetCode(res.Error) != test.code {
t.Logf("%v", res.Error)
t.Errorf("expected: %v, got: %v, with message %s", test.code, stacktrace.GetCode(res.Error), res.Error.Error())
Expand Down Expand Up @@ -203,7 +210,7 @@ func TestClaimsValidation(t *testing.T) {
Now = time.Now
}()

claims := &claims{}
claims := &Claims{}

require.Error(t, claims.Valid())

Expand Down
4 changes: 2 additions & 2 deletions pkg/auth/claims.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,12 @@ func (s *ScopeSet) ToStringSlice() []string {
return scopes
}

type claims struct {
type Claims struct {
jwt.StandardClaims
Scopes ScopeSet `json:"scope"`
}

func (c *claims) Valid() error {
func (c *Claims) Valid() error {
if c.Subject == "" {
return errMissingOrEmptySubject
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/auth/claims_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
)

func TestScopesJSONUnmarshaling(t *testing.T) {
claims := &claims{}
claims := &Claims{}
require.NoError(t, json.Unmarshal([]byte(`{"scope": "one two three"}`), claims))
require.Contains(t, claims.Scopes, "one")
require.Contains(t, claims.Scopes, "two")
Expand Down
16 changes: 9 additions & 7 deletions pkg/logging/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,11 @@ func (w *tracingResponseWriter) WriteHeader(statusCode int) {
w.next.WriteHeader(statusCode)
}

type CtxAuthError struct{}
type CtxAuthSubject struct{}
type CtxAuthKey struct{}
type CtxAuthValue struct {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: rather than a custom struct, I'd suggest to flatten this and just use two different keys with a string value type CtxKey("sub") and CtxKey("sub_err_msg").

Subject string
ErrMsg string
}

// HTTPMiddleware installs a logging http.Handler that logs requests and
// selected aspects of responses to 'logger'.
Expand Down Expand Up @@ -72,12 +75,11 @@ func HTTPMiddleware(logger *zap.Logger, dump bool, handler http.Handler) http.Ha
}
}

subject, ok := r.Context().Value(CtxAuthSubject{}).(string)
if !ok {
authErrorMsg := r.Context().Value(CtxAuthError{}).(string)
logger = logger.With(zap.String("resp_sub_err", authErrorMsg))
authResults := r.Context().Value(CtxAuthKey{}).(CtxAuthValue)
if authResults.ErrMsg != "" {
logger = logger.With(zap.String("resp_sub_err", authResults.ErrMsg))
} else {
logger = logger.With(zap.String("req_sub", subject))
logger = logger.With(zap.String("req_sub", authResults.Subject))
}

handler.ServeHTTP(trw, r)
Expand Down
Loading