Skip to content

Conversation

@manusa
Copy link
Member

@manusa manusa commented Jul 17, 2025

I tried to use the jose library (required by oidc) but it's not as lenient as golang-jwt.

Encapsulated JWT validation logic (client-side) in (c *JWTClaims) Validate(audience string). Initial step to #179 / #176 (comment)

Tests verify all local JWT parsing and validations. More tests coming in follow-up PR.

/cc @ardaguclu

go.mod Outdated
github.com/BurntSushi/toml v1.5.0
github.com/coreos/go-oidc/v3 v3.14.1
github.com/fsnotify/fsnotify v1.9.0
github.com/golang-jwt/jwt/v4 v4.5.2
Copy link
Member

Choose a reason for hiding this comment

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

I agree that this simplifies the code. On the other hand, we simply require some field based validations after extracting the JWT token. We don't need sophisticated security based processes which is supposed to be handled by OIDC or API Server.

Adding a new dependency just for this simple sanity check brings about some downsides. For example, https://github.com/golang-jwt/jwt/security mentions about the security posture. GHSA-mh63-6h87-95cp looks like an high severity. This was fixed. But who knows what else.

In my opinion, we don't need to rely on external dependency for something that we can handle. Maybe we can move JWT based functions into its dedicated directory. WDYT?

Copy link
Member

Choose a reason for hiding this comment

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

Since we have already an indirect dependency to go-jose due to https://github.com/coreos/go-oidc/blob/a7c457eacb849c163a496b29274242474a8f44ab/go.mod#L8C2-L8C31, maybe it makes sense to use it. But I see that you have tried this as first choice.

Copy link
Member Author

Choose a reason for hiding this comment

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

Adding a new dependency just for this simple sanity check brings about some downsides. For example, https://github.com/golang-jwt/jwt/security mentions about the security posture. GHSA-mh63-6h87-95cp looks like an high severity. This was fixed. But who knows what else.

Oh crap, I added the wrong version 😓

In my opinion, we don't need to rely on external dependency for something that we can handle.

My original plan was to reuse the jose library (brought in by oidc). I'm not happy bringing in dependencies either.

So the additional benefit is that signatures are checked plus we don't need to rely on our own parsing which might be missing something else (e.g. you added padding for base64, but it's also difficult to track what else might be missing).

Let me give it another spin

Copy link
Member Author

Choose a reason for hiding this comment

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

Parsing and validating is now delegated to go-jose.
This should cover both concerns:

  • The dependency is reused from go-oidc, so we're not bringing in a new one
  • Whatever downsides we might face would already be brought in by relying on go-oidc and its side-effects

Copy link
Member

Choose a reason for hiding this comment

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

This sounds reasonable. I'll review in depth tomorrow.

@manusa manusa requested a review from ardaguclu July 17, 2025 13:17
@manusa manusa changed the title fix(auth): delegate JWT parsing to github.com/golang-jwt/jwt fix(auth): delegate JWT parsing to github.com/go-jose/go-jose Jul 17, 2025
Copy link
Member

@ardaguclu ardaguclu left a comment

Choose a reason for hiding this comment

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

Overall looks good to me. Dropped a few comments.

This is totally your preference but maybe #179 PR can be added in this PR as well?.

// Because missing expected audience and expired tokens must be
// rejected already.
claims, err := validateJWTToken(token, audience)
claims, err := ParseJWTClaims(token)
Copy link
Member

Choose a reason for hiding this comment

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

What about we only have one function that manages parsing, validating, extracting. This function returns scopes and error. Like this;

scopes, err := validateJWTAndGetScopes(token, audience, "") // 3rd parameter is authorization url to check the issuer, if it is set

So that we don't need this;

			if err == nil && claims != nil {
				err = claims.Validate(audience)
			}

Copy link
Member Author

Choose a reason for hiding this comment

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

I think that would be the next step.
We need a function or an entity that orchestrates all of the validation


return claims, nil
// Validate Checks if the JWT claims are valid and if the audience matches the expected one.
func (c *JWTClaims) Validate(audience string) error {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
func (c *JWTClaims) Validate(audience string) error {
func validateJWTAndGetScopes(token string, audience string, issuerURL string) ([]string, error) {
tkn, err := jwt.ParseSigned(token, allSignatureAlgorithms)
if err != nil {
return nil, fmt.Errorf("failed to parse JWT token: %w", err)
}
claims := &JWTClaims{}
err = tkn.UnsafeClaimsWithoutVerification(claims)
if err != nil {
return nil, fmt.Errorf("failed to extract claims: %w", err)
}
expected := jwt.Expected{
AnyAudience: jwt.Audience{audience},
Time: time.Now(), // internally it is set to time.Now(), maybe explicitly setting is better
}
if issuerURL != "" { // maybe not the scope of this PR, you can add it or skip it
expected.Issuer = issuerURL
}
err = claims.Claims.Validate(expected)
if err != nil {
return nil, fmt.Errorf("JWT validation failed: %w", err)
}
var scopes []string
if claims.Scope != "" {
scopes = strings.Fields(claims.Scope)
}
return scopes, nil
}

Copy link
Member Author

Choose a reason for hiding this comment

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

I'd prefer to keep JWTToken as a single entity for further refactoring

}

decoded, err := base64.URLEncoding.DecodeString(payload)
func ParseJWTClaims(token string) (*JWTClaims, error) {
Copy link
Member

Choose a reason for hiding this comment

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

We won't need this, as validateJWTAndGetScopes handles it.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'd prefer to keep it separate for now

@@ -1,187 +1,222 @@
package http
Copy link
Member

Choose a reason for hiding this comment

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

Unit tests will also be simplified, because they'll have to exercise only the validateJWTAndGetScopes function.


var allSignatureAlgorithms = []jose.SignatureAlgorithm{
jose.EdDSA,
jose.HS256,
Copy link
Member

Choose a reason for hiding this comment

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

It looks like go-oidc does not easily support verifying symmetric tokens (the ones contain HS prefixes). But it is better to add this in here, in case in the future we want to support this.

Copy link
Member Author

@manusa manusa Jul 18, 2025

Choose a reason for hiding this comment

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

OK, I copied the list from them (IIRC)

Copy link
Member Author

Choose a reason for hiding this comment

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

I copied the list from https://github.com/go-jose/go-jose/blob/3a80e136a96e747bf44049414eadc02828df4d33/jose-util/crypto.go#L48-L62

Since the original implementation was completely lenient on the signature I assumed we wanted to allow anything, hence reusing a list.

Copy link
Member

@ardaguclu ardaguclu Jul 18, 2025

Choose a reason for hiding this comment

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

I should have been clearer. I think, this list is correct and nice. The problem is in oidcProvider side which does not support symmetric algorithms such as HS256. So I think, I need to change oidcProvider side and we should keep this list as is.

@manusa
Copy link
Member Author

manusa commented Jul 18, 2025

Overall looks good to me. Dropped a few comments.

This is totally your preference but maybe #179 PR can be added in this PR as well?.

I'd prefer to do the refactor in very small steps.
There are also areas of the auth "module" that aren't tested that I'd like to test before changing more things.

This is should be considered as an initial step to simplify the JWTClaims struct.
This entity can then evolve to orchestrate all validation or be merged into another one.

e.g. it could be renamed to something else and then be the OIDC provider holder or even the kubernetes holder to perform the verifyToken operation (now in kubernetes)

@manusa manusa added this to the 0.1.0 milestone Jul 18, 2025 — with automated-tasks
@manusa manusa merged commit 775fa21 into main Jul 18, 2025
9 of 10 checks passed
@manusa manusa deleted the refactor/auth branch July 18, 2025 11:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants