fix(auth): delegate JWT parsing to github.com/go-jose/go-jose#189
fix(auth): delegate JWT parsing to github.com/go-jose/go-jose#189
Conversation
Signed-off-by: Marc Nuri <marc@marcnuri.com>
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
This sounds reasonable. I'll review in depth tomorrow.
Signed-off-by: Marc Nuri <marc@marcnuri.com>
| // Because missing expected audience and expired tokens must be | ||
| // rejected already. | ||
| claims, err := validateJWTToken(token, audience) | ||
| claims, err := ParseJWTClaims(token) |
There was a problem hiding this comment.
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 setSo that we don't need this;
if err == nil && claims != nil {
err = claims.Validate(audience)
}There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
| 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 | |
| } |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
We won't need this, as validateJWTAndGetScopes handles it.
There was a problem hiding this comment.
I'd prefer to keep it separate for now
| @@ -1,187 +1,222 @@ | |||
| package http | |||
There was a problem hiding this comment.
Unit tests will also be simplified, because they'll have to exercise only the validateJWTAndGetScopes function.
|
|
||
| var allSignatureAlgorithms = []jose.SignatureAlgorithm{ | ||
| jose.EdDSA, | ||
| jose.HS256, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
OK, I copied the list from them (IIRC)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
I'd prefer to do the refactor in very small steps. This is should be considered as an initial step to simplify the JWTClaims struct. 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) |
Signed-off-by: Marc Nuri <marc@marcnuri.com>
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