From 41ad797709a996ee8a2659bc475c90666d285eb8 Mon Sep 17 00:00:00 2001 From: Zeno Belli Date: Mon, 16 Mar 2026 23:13:20 +0100 Subject: [PATCH 01/23] feat: add jwtinfo command Add JwtInfo command for initial testing --- cmd/jwtinfo.go | 111 ++++ internal/jwtinfo/jwtinfo.go | 583 ++++++++++++++++++ internal/jwtinfo/jwtinfo_test.go | 479 ++++++++++++++ internal/jwtinfo/main_test.go | 236 +++++++ internal/jwtinfo/testdata/README.md | 72 +++ .../testdata/ecdsa-plaintext-private-key.pem | 5 + .../ed25519-plaintext-private-key.pem | 3 + ...jwkset-from-rsa-private-key-corrupted.json | 16 + .../jwkset-from-rsa-private-key-valid.json | 16 + internal/jwtinfo/testdata/rsa-pkcs8-crt.pem | 32 + internal/jwtinfo/testdata/rsa-pkcs8-csr.pem | 28 + .../rsa-pkcs8-encrypted-private-key.pem | 54 ++ .../rsa-pkcs8-plaintext-private-key.pem | 52 ++ .../jwtinfo/testdata/rsa-pkcs8-public-key.pem | 14 + internal/style/style_handlers.go | 7 +- 15 files changed, 1707 insertions(+), 1 deletion(-) create mode 100644 cmd/jwtinfo.go create mode 100644 internal/jwtinfo/jwtinfo.go create mode 100644 internal/jwtinfo/jwtinfo_test.go create mode 100644 internal/jwtinfo/main_test.go create mode 100644 internal/jwtinfo/testdata/README.md create mode 100644 internal/jwtinfo/testdata/ecdsa-plaintext-private-key.pem create mode 100644 internal/jwtinfo/testdata/ed25519-plaintext-private-key.pem create mode 100644 internal/jwtinfo/testdata/jwkset-from-rsa-private-key-corrupted.json create mode 100644 internal/jwtinfo/testdata/jwkset-from-rsa-private-key-valid.json create mode 100644 internal/jwtinfo/testdata/rsa-pkcs8-crt.pem create mode 100644 internal/jwtinfo/testdata/rsa-pkcs8-csr.pem create mode 100644 internal/jwtinfo/testdata/rsa-pkcs8-encrypted-private-key.pem create mode 100644 internal/jwtinfo/testdata/rsa-pkcs8-plaintext-private-key.pem create mode 100644 internal/jwtinfo/testdata/rsa-pkcs8-public-key.pem diff --git a/cmd/jwtinfo.go b/cmd/jwtinfo.go new file mode 100644 index 0000000..d317225 --- /dev/null +++ b/cmd/jwtinfo.go @@ -0,0 +1,111 @@ +/* +Copyright © 2026 Zeno Belli +*/ + +package cmd + +import ( + "fmt" + "io" + "net/http" + "os" + + "github.com/MicahParks/keyfunc/v3" + "github.com/spf13/cobra" + "github.com/xenos76/https-wrench/internal/jwtinfo" +) + +var ( + flagNameRequestJSONValues = "request-values-json" + flagNameRequestURL = "request-url" + flagNameJwksURL = "validation-url" + requestJSONValues string + requestURL string + jwksURL string + keyfuncDefOverride keyfunc.Override +) + +var jwtinfoCmd = &cobra.Command{ + Use: "jwtinfo", + Short: "Request and display JWT token data", + Long: `Request and display JWT token data.`, + Run: func(cmd *cobra.Command, args []string) { + var err error + client := &http.Client{} + requestValuesMap := make(map[string]string) + + if requestJSONValues != "" { + requestValuesMap, err = jwtinfo.ParseRequestJSONValues( + requestJSONValues, + requestValuesMap, + ) + if err != nil { + fmt.Printf( + "error while parsing request's values JSON string: %s", + err, + ) + return + } + } + + tokenData, err := jwtinfo.RequestToken( + requestURL, + requestValuesMap, + client, + io.ReadAll, + ) + if err != nil { + fmt.Printf("error while requesting token data: %s\n", err) + return + } + + err = tokenData.DecodeBase64() + if err != nil { + fmt.Printf("DecodeBase64 error: %s\n", err) + } + + // TODO: turn into method + token, err := jwtinfo.ParseTokenData(tokenData, jwksURL, keyfuncDefOverride) + if err != nil { + fmt.Printf("error while parsing token data: %s\n", err) + return + } + + fmt.Printf("Token valid: %v\n", token.Valid) + + err = jwtinfo.PrintTokenInfo(tokenData, os.Stdout) + if err != nil { + fmt.Printf("error while printing token data: %s\n", err) + return + } + }, +} + +func init() { + rootCmd.AddCommand(jwtinfoCmd) + + jwtinfoCmd.Flags().StringVar( + &requestURL, + flagNameRequestURL, + "", + "HTTP address to use for the JWT token request", + ) + + jwtinfoCmd.Flags().StringVar( + &requestJSONValues, + flagNameRequestJSONValues, + "", + "JSON encoded values to use for the JWT token request", + ) + + jwtinfoCmd.Flags().StringVar( + &jwksURL, + flagNameJwksURL, + "", + "Url of the JSON Web Key Set (JWKS) to use for validating the JWT token", + ) + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + // jwtinfoCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} diff --git a/internal/jwtinfo/jwtinfo.go b/internal/jwtinfo/jwtinfo.go new file mode 100644 index 0000000..f5cca4c --- /dev/null +++ b/internal/jwtinfo/jwtinfo.go @@ -0,0 +1,583 @@ +package jwtinfo + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io" + "maps" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + "github.com/MicahParks/keyfunc/v3" + "github.com/charmbracelet/lipgloss/table" + "github.com/golang-jwt/jwt/v5" + "github.com/xenos76/https-wrench/internal/style" +) + +var ( + chromaStyle = "catppuccin-frappe" + emptyString string + userAgent = "HTTPS-Wrench/JwtInfo" +) + +type CustomClaims struct { + Issuer string `json:"iss"` + Subject string `json:"sub"` + ExpiresAt int64 `json:"exp"` + NotBefore int64 `json:"nbf"` + IssuedAt int64 `json:"iat"` + jwt.RegisteredClaims +} + +type JwtTokenData struct { + AccessToken string `json:"access_token"` + AccessTokenHeader []byte + AccessTokenClaims []byte + RefreshToken string `json:"refresh_token"` + RefreshTokenHeader []byte + RefreshTokenClaims []byte +} + +type allReader func(io.Reader) ([]byte, error) + +func RequestToken(reqURL string, reqValues map[string]string, client *http.Client, readAll allReader) (JwtTokenData, error) { + if reqURL == emptyString { + return JwtTokenData{}, errors.New("empty string provided as request URL") + } + + if len(reqValues) == 0 { + return JwtTokenData{}, errors.New("empty map provided as request values") + } + + var t JwtTokenData + + urlReqValues := url.Values{} + for k, v := range reqValues { + urlReqValues.Add(k, v) + } + + req, err := http.NewRequest( + "POST", + reqURL, + strings.NewReader(urlReqValues.Encode()), + ) + if err != nil { + return JwtTokenData{}, fmt.Errorf( + "HTTP error while defining token data request: %w", + err, + ) + } + + req.Header.Add("User-Agent", userAgent) + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + req.Header.Add("Content-Length", strconv.Itoa(len(urlReqValues.Encode()))) + + resp, err := client.Do(req) + if err != nil { + return JwtTokenData{}, err + } + + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return JwtTokenData{}, fmt.Errorf( + "token request returned the following status code: %d", + resp.StatusCode, + ) + } + + bodyBytes, errBodyRead := readAll(resp.Body) + resp.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) + + if errBodyRead != nil { + return JwtTokenData{}, fmt.Errorf( + "unable to read body: %w", + errBodyRead, + ) + } + + respContentType := resp.Header["Content-Type"][0] + if respContentType == "application/jwt" { + t.AccessToken = string(bodyBytes) + } + + if respContentType == "application/json" { + if err = json.NewDecoder(resp.Body).Decode(&t); err != nil { + return JwtTokenData{}, fmt.Errorf( + "error validating token request data: %w", + err, + ) + } + } + + _, _, err = jwt.NewParser().ParseUnverified( + t.AccessToken, + &jwt.RegisteredClaims{}, + ) + if err != nil { + return JwtTokenData{}, fmt.Errorf( + "unable to parse JWT token from HTTP response: %w", + err, + ) + } + + return t, nil +} + +func ParseRequestJSONValues( + reqValues string, + reqValuesMap map[string]string, +) ( + map[string]string, + error, +) { + if reqValues == "" { + return nil, errors.New("empty string provided as JSON encoded request values") + } + + var objmap map[string]string + + err := json.Unmarshal([]byte(reqValues), &objmap) + if err != nil { + return nil, fmt.Errorf("unable to parse Json request values: %w", err) + } + + maps.Copy(reqValuesMap, objmap) + + return reqValuesMap, nil +} + +func (jtd *JwtTokenData) DecodeBase64() error { + tokens := []struct { + name string + raw string + }{ + { + name: "AccessToken", + raw: jtd.AccessToken, + }, + { + name: "RefreshToken", + raw: jtd.RefreshToken, + }, + } + + for _, token := range tokens { + var tokenHeader []byte + + var tokenClaims []byte + + var err error + + if token.raw == emptyString { + continue + } + + tokenB64Elements := strings.Split(token.raw, ".") + + tokenHeader, err = base64.RawStdEncoding.DecodeString(tokenB64Elements[0]) + if err != nil { + return fmt.Errorf( + "unable to decode base64 header from %s: %w", + token.name, + err, + ) + } + + tokenClaims, err = base64.RawStdEncoding.DecodeString(tokenB64Elements[1]) + if err != nil { + return fmt.Errorf( + "unable to decode base64 claims from %s: %w", + token.name, + err, + ) + } + + if token.name == "AccessToken" { + jtd.AccessTokenHeader = tokenHeader + jtd.AccessTokenClaims = tokenClaims + } + + if token.name == "RefreshToken" { + jtd.RefreshTokenHeader = tokenHeader + jtd.RefreshTokenClaims = tokenClaims + } + } + + return nil +} + +func ParseTokenData(jtd JwtTokenData, jwksURL string, keyfuncOverride keyfunc.Override) (*jwt.Token, error) { + // Parsing the access token whitout validation + if jwksURL == "" { + token, _, err := jwt.NewParser().ParseUnverified( + jtd.AccessToken, + &jwt.RegisteredClaims{}, + ) + if err != nil { + return nil, fmt.Errorf( + "unable to parse unverified access token: %w", + err, + ) + } + + return token, nil + } + + // Parsing and validating the access token + ctx := context.Background() + + jwks, err := keyfunc.NewDefaultOverrideCtx( + ctx, + []string{jwksURL}, + keyfuncOverride, + ) + if err != nil { + return nil, fmt.Errorf( + "failed to create JWK Set from resource at URL %s: %w", + jwksURL, + err, + ) + } + + token, err := jwt.Parse( + jtd.AccessToken, + jwks.Keyfunc, + ) + if err != nil { + return nil, fmt.Errorf("failed to parse the JWT: %w", err) + } + + return token, nil +} + +// func DisplayTokenInfo(t *jwt.Token, w io.Writer) error { +// sl := style.CertKeyP4.Render +// sv := style.CertValue.Render +// sTrue := style.BoolTrue.Render +// sFalse := style.BoolFalse.Render +// +// fmt.Fprintln(w) +// fmt.Fprintln(w, style.LgSprintf(style.Cmd, "JwtInfo")) +// fmt.Fprintln(w) +// +// validString := sFalse("false") +// if t.Valid { +// validString = sTrue("true") +// } +// +// fmt.Fprintln(w, style.LgSprintf(style.CertKeyP3, "Valid %s", validString)) +// +// tokenHeaders := getTokenHeadersMap(t) +// hTable := table.New().Border(style.LGDefBorder).Headers("Header") +// +// for hKey, hVal := range tokenHeaders { +// hTable.Row(sl(hKey), sv(hVal)) +// } +// +// fmt.Fprintln(w, hTable.Render()) +// +// hTable.ClearRows() +// +// tokenClaims, err := getTokenClaimsMap(t) +// if err != nil { +// return fmt.Errorf("unable to get token Claims: %w", err) +// } +// +// cTable := table.New().Border(style.LGDefBorder).Headers("Claims") +// +// for cKey, cVal := range tokenClaims { +// cTable.Row(sl(cKey), sv(cVal)) +// } +// +// unregisteredClaims := getUnregisteredClaimsMap(t, tokenClaims) +// for ucKey, ucVal := range unregisteredClaims { +// cTable.Row(sl(ucKey), sv(ucVal)) +// } +// +// fmt.Fprintln(w, cTable.Render()) +// +// cTable.ClearRows() +// +// return nil +// } +func PrintTokenInfo(jtd JwtTokenData, w io.Writer) error { + sl := style.CertKeyP4.Render + sv := style.CertValue.Render + // sTrue := style.BoolTrue.Render + // sFalse := style.BoolFalse.Render + + fmt.Fprintln(w) + fmt.Fprintln(w, style.LgSprintf(style.Cmd, "JwtInfo")) + fmt.Fprintln(w) + + // validString := sFalse("false") + // if t.Valid { + // validString = sTrue("true") + // } + + // fmt.Fprintln(w, style.LgSprintf(style.CertKeyP3, "Valid %s", validString)) + + tokens := []struct { + name string + header []byte + claims []byte + }{ + { + name: "AccessToken", + header: jtd.AccessTokenHeader, + claims: jtd.AccessTokenClaims, + }, + { + name: "RefreshToken", + header: jtd.RefreshTokenHeader, + claims: jtd.RefreshTokenClaims, + }, + } + + for _, token := range tokens { + if len(token.header) == 0 { + continue + } + + fmt.Fprintln(w, style.LgSprintf(style.Title, "%s", token.name)) + + fmt.Println() + fmt.Fprintln(w, style.LgSprintf(style.ItemKey, "Header")) + + var prettyJSON bytes.Buffer + + err := json.Indent(&prettyJSON, token.header, "", " ") + if err != nil { + prettyJSON.Write(token.header) + } + + headerCode := prettyJSON.String() + + fmt.Print(style.CodeSyntaxHighlightWithStyle("json", headerCode, chromaStyle)) + prettyJSON.Reset() + + fmt.Println() + fmt.Fprintln(w, style.LgSprintf(style.ItemKey, "Claims")) + + tokenTimeClaims, err := unmarshallTokenTimeClaims(token.claims) + if err != nil { + return fmt.Errorf("unable to unmashall time claims from %s: %w", token.name, err) + } + + cTable := table.New().Border(style.LGDefBorder) + cTable.Row(sl("Issued At"), sv(tokenTimeClaims["iat"])) + cTable.Row(sl("Expiration Time"), sv(tokenTimeClaims["exp"])) + fmt.Fprintln(w, cTable.Render()) + cTable.ClearRows() + + err = json.Indent(&prettyJSON, token.claims, "", " ") + if err != nil { + prettyJSON.Write(token.claims) + } + + claimsCode := prettyJSON.String() + + fmt.Print(style.CodeSyntaxHighlightWithStyle("json", claimsCode, chromaStyle)) + fmt.Println() + } + + return nil +} + +func unmarshallTokenTimeClaims(claims []byte) (map[string]string, error) { + tokenClaims := make(map[string]string) + + genericClaims := make(map[string]any) + + if err := json.Unmarshal(claims, &genericClaims); err != nil { + return nil, err + } + + if _, ok := genericClaims["iat"]; !ok { + return nil, errors.New("unable to find Issued At (iat) in token Claims") + } + + if _, ok := genericClaims["exp"]; !ok { + return nil, errors.New("unable to find Expiration Time (exp) in token Claims") + } + + for k, v := range genericClaims { + var vi any = v + + if vf, ok := vi.(float64); ok { + vInt64 := int64(vf) + t := time.Unix(vInt64, 0) + dateUtc := t.UTC().String() + tokenClaims[k] = fmt.Sprintf("%v", dateUtc) + + continue + } + } + + return tokenClaims, nil +} + +// func unmarshallTokenClaims(claims []byte) (map[string]string, error) { +// tokenClaims := make(map[string]string) +// +// genericClaims := make(map[string]any) +// +// if err := json.Unmarshal(claims, &genericClaims); err != nil { +// return nil, err +// } +// +// for k, v := range genericClaims { +// var vi any = v +// +// if vs, ok := vi.(map[string]any); ok { +// tokenClaims[k] = fmt.Sprintf("%s", vs) +// continue +// } +// +// if vf, ok := vi.(float64); ok { +// vInt64 := int64(vf) +// t := time.Unix(vInt64, 0) +// dateUtc := t.UTC().String() +// +// outString := fmt.Sprintf("%v (%s)", int64(vf), dateUtc) +// +// tokenClaims[k] = fmt.Sprintf("%v", outString) +// +// continue +// } +// +// if vls, ok := vi.([]string); ok { +// tokenClaims[k] = strings.Join(vls, ",") +// continue +// } +// +// if vla, ok := vi.([]any); ok { +// tokenClaims[k] = fmt.Sprintf("%v", vla) +// continue +// } +// +// if vb, ok := vi.(bool); ok { +// tokenClaims[k] = fmt.Sprintf("%v", vb) +// continue +// } +// +// if vs, ok := vi.(string); ok { +// tokenClaims[k] = vs +// } else { +// fmt.Printf("not asserted: %v\n", v) +// } +// } +// +// return tokenClaims, nil +// } +// +// func unmarshallTokenHeader(header []byte) (map[string]string, error) { +// tokenHeader := make(map[string]string) +// +// if err := json.Unmarshal(header, &tokenHeader); err != nil { +// return nil, err +// } +// +// return tokenHeader, nil +// } +// +// func getTokenClaimsMap(t *jwt.Token) (map[string]string, error) { +// m := make(map[string]string) +// +// // Mandatory Registered Claims +// issuer, err := t.Claims.GetIssuer() +// if err != nil || issuer == emptyString { +// return nil, fmt.Errorf("unable to get issuer: %w", err) +// } +// +// subject, err := t.Claims.GetSubject() +// if err != nil || subject == emptyString { +// return nil, fmt.Errorf("unable to get subject: %w", err) +// } +// +// issuedAt, err := t.Claims.GetIssuedAt() +// if err != nil || issuedAt == nil { +// return nil, fmt.Errorf("unable to get issuedAt: %w", err) +// } +// +// expiresAt, err := t.Claims.GetExpirationTime() +// if err != nil || expiresAt == nil { +// return nil, fmt.Errorf("unable to get expiration time: %w", err) +// } +// +// audienceElems, err := t.Claims.GetAudience() +// if err != nil { +// return nil, fmt.Errorf("unable to get audience: %w", err) +// } +// +// audience := strings.Join(audienceElems, ",") +// +// m["iss"] = issuer +// m["sub"] = subject +// m["iat"] = issuedAt.UTC().String() +// m["exp"] = expiresAt.UTC().String() +// m["aud"] = audience +// +// // Optional Registered Claims +// notBefore, err := t.Claims.GetNotBefore() +// if err != nil { +// return nil, fmt.Errorf("unable to get notBefore time: %w", err) +// } +// +// if notBefore != nil { +// m["nbf"] = notBefore.UTC().String() +// } +// +// return m, nil +// } +// +// func getUnregisteredClaimsMap(t *jwt.Token, existingClaims map[string]string) map[string]string { +// unregistreredClaims := make(map[string]string) +// +// var claimsInt any = t.Claims +// +// if claimsMap, ok := claimsInt.(jwt.MapClaims); ok { +// for ck := range claimsMap { +// if _, alreadyPresent := existingClaims[ck]; alreadyPresent { +// continue +// } +// +// cki := claimsMap[ck] +// +// if cStringValue, ok := cki.(string); ok { +// unregistreredClaims[ck] = cStringValue +// } +// +// if cIntList, ok := cki.([]any); ok { +// unregistreredClaims[ck] = fmt.Sprintf("%s", cIntList) +// } +// } +// } +// +// return unregistreredClaims +// } +// +// func getTokenHeadersMap(t *jwt.Token) map[string]string { +// m := make(map[string]string) +// +// for k, v := range t.Header { +// headerValue := "undefined" +// i := v +// +// if v, ok := i.(string); ok { +// headerValue = v +// } +// +// m[k] = headerValue +// } +// +// return m +// } diff --git a/internal/jwtinfo/jwtinfo_test.go b/internal/jwtinfo/jwtinfo_test.go new file mode 100644 index 0000000..8a34abb --- /dev/null +++ b/internal/jwtinfo/jwtinfo_test.go @@ -0,0 +1,479 @@ +package jwtinfo + +import ( + "io" + "maps" + "testing" + + "github.com/MicahParks/keyfunc/v3" + "github.com/stretchr/testify/require" +) + +func TestParseRequestJSONValues(t *testing.T) { + inputMap := make(map[string]string) + inputMap["testKey"] = "testValue" + + mapToValidJSON := make(map[string]string) + mapToValidJSON["testKey2"] = "testValue2" + mapToValidJSON["testKey3"] = "testValue3" + + tests := []struct { + name string + jsonStr string + jsonRefMap map[string]string + requireError bool + errorMsg string + }{ + { + name: "validJson", + jsonStr: "{\"testKey2\":\"testValue2\", \"testKey3\":\"testValue3\"}", + jsonRefMap: mapToValidJSON, + requireError: false, + }, + { + name: "invalidJson", + jsonStr: "{\"testKey2 :\"testValue2\", \"testKey3\":\"testValue3\"}", + jsonRefMap: mapToValidJSON, + requireError: true, + errorMsg: "unable to parse Json request values: invalid character 't' after object key", + }, + { + name: "emptyJsonString", + jsonStr: "", + jsonRefMap: mapToValidJSON, + requireError: true, + errorMsg: "empty string provided as JSON encoded request values", + }, + } + for _, tc := range tests { + tt := tc + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + outputMap, err := ParseRequestJSONValues( + tt.jsonStr, + inputMap, + ) + + if tt.requireError { + require.Error(t, err) + require.ErrorContains(t, err, tt.errorMsg) + + return + } + + require.NoError(t, err) + + sourceMaps := make(map[string]string) + maps.Copy(sourceMaps, inputMap) + maps.Copy(sourceMaps, tt.jsonRefMap) + + for k := range outputMap { + _, ok := sourceMaps[k] + require.True( + t, + ok, + "outputMap must contain all keys from inputMap and tt.jsonRefMap", + ) + require.Equal( + t, + sourceMaps[k], + outputMap[k], + "outputMap must contain all values from inputMap and tt.jsonRefMap", + ) + } + }) + } +} + +func TestRequestToken(t *testing.T) { + tests := []struct { + name string + user string + pass string + scope string + expError bool + }{ + { + name: "applicationJwt", + user: "test", + pass: "known", + scope: "default", + }, + + { + name: "appJwtInvalid", + user: "test", + pass: "known", + scope: "appJwtInvalid", + expError: true, + }, + + { + name: "emptyReqValues", + scope: "emptyValuesMap", + expError: true, + }, + + { + name: "emptyReqUrl", + user: "test", + pass: "known", + scope: "emptyReqUrl", + expError: true, + }, + + { + name: "wrongReqUrl", + user: "test", + pass: "known", + scope: "wrongReqUrl", + expError: true, + }, + + { + name: "wrongReqParam", + user: "test", + pass: "known", + scope: "wrongReqParam", + expError: true, + }, + + { + name: "applicationJson", + user: "test", + pass: "known", + scope: "applicationJson", + }, + + { + name: "appJsonInvalid", + user: "test", + pass: "known", + scope: "appJsonInvalid", + expError: true, + }, + + { + name: "wrongPass", + user: "test", + pass: "wrong", + scope: "applicationJson", + expError: true, + }, + } + for _, tc := range tests { + tt := tc + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + server, err := NewJwtTestServer() + + require.NoError(t, err) + + defer server.Close() + + client := server.Client() + serverRoot := server.URL + + serverJwtEndpoint := serverRoot + "/jwt" + + if tt.scope == "emptyReqUrl" { + serverJwtEndpoint = "" + } + + if tt.scope == "wrongReqUrl" { + serverJwtEndpoint = "https://does.not.exist/wrong" + } + + if tt.scope == "wrongReqParam" { + serverJwtEndpoint = "https://local$%#@@&host/wrongUrl" + } + + reqValues := make(map[string]string) + if tt.scope != "emptyValuesMap" { + reqValues["user"] = tt.user + reqValues["pass"] = tt.pass + reqValues["scope"] = tt.scope + } + + _, err = RequestToken( + serverJwtEndpoint, + reqValues, + client, + io.ReadAll, + ) + + if tt.expError { + require.Error( + t, + err, + "RequestToken - expected error: %s", + err, + ) + + return + } + + require.NoError( + t, + err, + "RequestToken error: %s", + err, + ) + + // godump.Dump(td) + }) + } +} + +func TestParseTokenData(t *testing.T) { + tests := []struct { + name string + user string + pass string + scope string + bodyReader allReader + expError bool + expReqError bool + }{ + { + name: "applicationJwt", + user: "test", + pass: "known", + bodyReader: io.ReadAll, + scope: "default", + }, + + { + name: "applicationJson", + user: "test", + pass: "known", + bodyReader: io.ReadAll, + scope: "applicationJson", + }, + { + name: "readError", + user: "test", + pass: "known", + bodyReader: mockErrReader.ReadAll, + scope: "applicationJson", + expReqError: true, + }, + + { + name: "jwksEmpty", + user: "test", + pass: "known", + bodyReader: io.ReadAll, + scope: "jwksEmpty", + }, + { + name: "jwksFaulty", + user: "test", + pass: "known", + bodyReader: io.ReadAll, + scope: "jwksFaulty", + }, + } + + for _, tc := range tests { + tt := tc + + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + server, err := NewJwtTestServer() + + require.NoError(t, err) + + defer server.Close() + + client := server.Client() + serverRoot := server.URL + serverJwtEndpoint := serverRoot + "/jwt" + serverJwksEndpoint := serverRoot + "/jwks.json" + serverJwksEmptyEndpoint := serverRoot + "/jwksEmpty.json" + serverJwksFaultyEndpoint := serverRoot + "/jwksFaulty.json" + + rootResp, err := client.Get(serverRoot) + require.NoError(t, err) + + rootBody, err := io.ReadAll(rootResp.Body) + require.NoError(t, err) + require.Contains(t, string(rootBody), "root handler") + + defer rootResp.Body.Close() + + reqValues := make(map[string]string) + reqValues["user"] = tt.user + reqValues["pass"] = tt.pass + reqValues["scope"] = tt.scope + + td, err := RequestToken( + serverJwtEndpoint, + reqValues, + client, + tt.bodyReader, + ) + + if !tt.expReqError { + require.NoError( + t, + err, + "RequestToken error: %s", err, + ) + } + + if tt.expReqError { + require.ErrorContains( + t, + err, + "unable to read body: mock Reader error", + ) + + return + } + + keyfuncOverrideTesting := keyfunc.Override{ + Client: server.Client(), + } + + _, err = ParseTokenData( + td, + "", + keyfuncOverrideTesting, + ) + require.NoError(t, err) + + if tt.scope == "jwksEmpty" { + respJwksEmpty, errEmpty := server.Client().Get(serverJwksEmptyEndpoint) + require.NoError(t, errEmpty) + + defer respJwksEmpty.Body.Close() + + respJwksEmptyBody, errEmptyBody := io.ReadAll(respJwksEmpty.Body) + require.NoError(t, errEmptyBody) + + require.Contains( + t, + string(respJwksEmptyBody), + "{}", + ) + + _, err = ParseTokenData( + td, + serverJwksEmptyEndpoint, + keyfuncOverrideTesting, + ) + require.ErrorContains( + t, + err, + "keyfunc returned empty verification key set", + ) + + return + } + + if tt.scope == "jwksFaulty" { + respJwksFaulty, errFaulty := server.Client().Get(serverJwksFaultyEndpoint) + require.NoError(t, errFaulty) + + defer respJwksFaulty.Body.Close() + + respJwksFaultyBody, errFaultyBody := io.ReadAll(respJwksFaulty.Body) + require.NoError(t, errFaultyBody) + + require.Contains( + t, + string(respJwksFaultyBody), + "UniqueKeyID1", + ) + + _, err = ParseTokenData( + td, + serverJwksFaultyEndpoint, + keyfuncOverrideTesting, + ) + require.ErrorContains( + t, + err, + "keyfunc returned empty verification key set", + ) + + return + } + + tokenVerified, err := ParseTokenData( + td, + serverJwksEndpoint, + keyfuncOverrideTesting, + ) + require.NoError(t, err) + require.True( + t, + tokenVerified.Valid, + "JWT token must be valid", + ) + }) + } +} + +func TestParseTokenData_Errors(t *testing.T) { + t.Run("ParseUnverifiedError", func(t *testing.T) { + t.Parallel() + + td := JwtTokenData{AccessToken: "notValidString"} + + _, err := ParseTokenData( + td, + "", + keyfunc.Override{}, + ) + require.ErrorContains( + t, + err, + "token is malformed: token contains an invalid number of segments", + ) + }) + + t.Run("WrongJwksURL", func(t *testing.T) { + t.Parallel() + + token, err := createToken("demo") + require.NoError(t, err) + + td := JwtTokenData{AccessToken: token} + + _, err = ParseTokenData( + td, + "https://localhost:54321/jkws.wrong.json", + keyfunc.Override{}, + ) + require.ErrorContains( + t, + err, + "keyfunc returned empty verification key set", + ) + }) + + t.Run("JwksURIParseError", func(t *testing.T) { + t.Parallel() + + token, err := createToken("demo") + require.NoError(t, err) + + td := JwtTokenData{AccessToken: token} + + _, err = ParseTokenData( + td, + "https://loca#$%^/jkws.json", + keyfunc.Override{}, + ) + require.ErrorContains( + t, + err, + "failed to create JWK Set from resource at URL", + ) + }) +} diff --git a/internal/jwtinfo/main_test.go b/internal/jwtinfo/main_test.go new file mode 100644 index 0000000..124d5ab --- /dev/null +++ b/internal/jwtinfo/main_test.go @@ -0,0 +1,236 @@ +// +// JwtInfo testing resource +// +// Refs: +// * https://github.com/golang-jwt/jwt/blob/main/http_example_test.go +// + +package jwtinfo + +import ( + "context" + "crypto/rsa" + "errors" + "fmt" + "io" + "log" + "net/http" + "net/http/httptest" + "os" + "testing" + "time" + + "github.com/MicahParks/jwkset" + "github.com/golang-jwt/jwt/v5" +) + +type ( + CustomerInfo struct { + Name string + Kind string + } + + CustomClaimsExample struct { + jwt.RegisteredClaims + TokenType string + CustomerInfo + } + + MockErrReader struct{} +) + +const ( + rsaPrivKeyPath = "./testdata/rsa-pkcs8-plaintext-private-key.pem" +) + +var ( + // rsaSignKeyPriv *rsa.PrivateKey + signKey *rsa.PrivateKey + mockErrReader MockErrReader +) + +func (MockErrReader) ReadAll(r io.Reader) ([]byte, error) { + return nil, errors.New("mock Reader error") +} + +func TestMain(m *testing.M) { + signBytes, err := os.ReadFile(rsaPrivKeyPath) + fatal(err) + + signKey, err = jwt.ParseRSAPrivateKeyFromPEM(signBytes) + fatal(err) + + m.Run() +} + +func fatal(err error) { + if err != nil { + log.Fatal(err) + } +} + +func createToken(user string) (string, error) { + // create a signer for rsa 256 + t := jwt.New(jwt.GetSigningMethod("RS256")) + + // set our claims + t.Claims = &CustomClaimsExample{ + jwt.RegisteredClaims{ + // set the expire time + // see https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4 + ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Minute * 1)), + }, + "level1", + CustomerInfo{user, "human"}, + } + + // Creat token string + return t.SignedString(signKey) +} + +func rootHandler(w http.ResponseWriter, _ *http.Request) { + fmt.Fprintln(w, "JWT testing server: root handler") +} + +func testHandler(w http.ResponseWriter, _ *http.Request) { + fmt.Fprintln(w, "JWT testing server: test handler") +} + +func jwksHandler(writer http.ResponseWriter, request *http.Request) { + ctx := context.Background() + jwkSet := jwkset.NewMemoryStorage() + + // Create the JWK options. + metadata := jwkset.JWKMetadataOptions{ + KID: "rsa-key-id", // Not technically required, but is required for JWK Set operations using this package. + } + options := jwkset.JWKOptions{ + Metadata: metadata, + } + + // Create the JWK from the key and options. + jwk, err := jwkset.NewJWKFromKey(signKey, options) + if err != nil { + fmt.Printf("failed to create JWK from key: %s", err) + } + + // Write the key to the JWK Set storage. + err = jwkSet.KeyWrite(ctx, jwk) + if err != nil { + fmt.Printf("failed to store RSA key: %s", err) + } + + response, err := jwkSet.JSONPublic(request.Context()) + if err != nil { + fmt.Printf("failed to get JWK Set JSON: %s", err) + writer.WriteHeader(http.StatusInternalServerError) + + return + } + + writer.Header().Set("Content-Type", "application/json") + _, _ = writer.Write(response) +} + +func jwksFaultyHandler(writer http.ResponseWriter, _ *http.Request) { + // Jwks file created with: + // jwkset testdata/rsa-pkcs8-plaintext-private-key.pem + + // validJwksFile := "testdata/jwkset-from-rsa-private-key-valid.json" + corruptedJwksFile := "testdata/jwkset-from-rsa-private-key-corrupted.json" + jwksContent, _ := os.ReadFile(corruptedJwksFile) + + writer.Header().Set("Content-Type", "application/json") + _, _ = writer.Write(jwksContent) +} + +func jwksEmptyHandler(writer http.ResponseWriter, _ *http.Request) { + respString := "{}" + + writer.Header().Set("Content-Type", "application/json") + _, _ = writer.Write([]byte(respString)) +} + +func authHandler(w http.ResponseWriter, r *http.Request) { + // make sure its post + if r.Method != "POST" { + w.WriteHeader(http.StatusBadRequest) + _, _ = fmt.Fprintln(w, "No POST", r.Method) + + return + } + + user := r.FormValue("user") + pass := r.FormValue("pass") + scope := r.FormValue("scope") + + // log.Printf("Authenticate: user[%s] pass[%s]\n", user, pass) + + if user != "test" || pass != "known" { + w.WriteHeader(http.StatusForbidden) + + _, _ = fmt.Fprintln(w, "Wrong info") + + return + } + + tokenString, err := createToken(user) + tokenJSONString := fmt.Sprintf( + "{\"access_token\":\"%s\"}", + tokenString, + ) + + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + + _, _ = fmt.Fprintln(w, "Sorry, error while Signing Token!") + + log.Printf("Token Signing error: %v\n", err) + + return + } + + if scope == "applicationJson" { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = fmt.Fprintln(w, tokenJSONString) + + return + } + + if scope == "appJsonInvalid" { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = fmt.Fprintln(w, "{\"test\":\"invalid}") + + return + } + + if scope == "appJwtInvalid" { + w.Header().Set("Content-Type", "application/jwt") + // w.WriteHeader(http.StatusOK) + fmt.Println("JWT invalid") + // _, _ = fmt.Fprintln(w, "") + + return + } + + w.Header().Set("Content-Type", "application/jwt") + w.WriteHeader(http.StatusOK) + _, _ = fmt.Fprintln(w, tokenString) +} + +func NewJwtTestServer() (*httptest.Server, error) { + mux := http.NewServeMux() + mux.HandleFunc("/", rootHandler) + mux.HandleFunc("/jwt", authHandler) + mux.HandleFunc("/jwks.json", jwksHandler) + mux.HandleFunc("/jwksFaulty.json", jwksFaultyHandler) + mux.HandleFunc("/jwksEmpty.json", jwksEmptyHandler) + + ts := httptest.NewUnstartedServer(mux) + ts.EnableHTTP2 = true + ts.StartTLS() + + return ts, nil +} diff --git a/internal/jwtinfo/testdata/README.md b/internal/jwtinfo/testdata/README.md new file mode 100644 index 0000000..c25f171 --- /dev/null +++ b/internal/jwtinfo/testdata/README.md @@ -0,0 +1,72 @@ +# JwtInfo testdata + +Sample private keys generated with Openssl. Plaintext and encrypted versions. +_Warning_: the keys stored in this folder are meant to be used for testing only. + +## Create sample RSA private keys + +Create an encrypted key: + +```shell +> openssl genrsa -aes256 -out rsa-pkcs8-encrypted-private-key.pem 4096 +Enter PEM pass phrase: +Verifying - Enter PEM pass phrase: + +> head -n 3 rsa-pkcs8-encrypted-private-key.pem +-----BEGIN ENCRYPTED PRIVATE KEY----- +(REDACTED PEM Block) +(REDACTED PEM Block) +``` + +Decrypt the key: + +```shell +> openssl rsa -in rsa-pkcs8-encrypted-private-key.pem -out rsa-pkcs8-plaintext-private-key.pem +Enter pass phrase for rsa-pkcs8-encrypted-private-key.pem: +writing RSA key + +> head -n 3 rsa-pkcs8-plaintext-private-key.pem +-----BEGIN PRIVATE KEY----- +(REDACTED PEM Block) +``` + +Extract the public key: + +```shell +❯ openssl rsa -in rsa-pkcs8-plaintext-private-key.pem -pubout > rsa-pkcs8-public-key.pem +writing RSA key +``` + +## Create sample RSA certificates + +```shell +> openssl req -new -key rsa-pkcs8-plaintext-private-key.pem -out rsa-pkcs8-csr.pem +[...] +> openssl x509 -req -days 3650 -in rsa-pkcs8-csr.pem -signkey rsa-pkcs8-plaintext-private-key.pem -out rsa-pkcs8-crt.pem +Certificate request self-signature ok +subject=C=DE, ST=Some-State, L=Berlin, O=example Ltd, CN=example.com +``` + +## Create sample ECDSA private keys + +Create the plaintext key: + +```shell + > openssl ecparam -name prime256v1 -genkey -noout -out ecdsa-plaintext-private-key.pem + +> head -n 2 ecdsa-plaintext-private-key.pem +-----BEGIN EC PRIVATE KEY----- +(REDACTED PEM Block) +``` + +## Create sample ED25519 private keys + +Create the plaintext key: + +```shell +> openssl genpkey -algorithm Ed25519 -out ed25519-plaintext-private-key.pem + +> head -n2 ed25519-plaintext-private-key.pem +-----BEGIN PRIVATE KEY----- +(REDACTED PEM Block) +``` diff --git a/internal/jwtinfo/testdata/ecdsa-plaintext-private-key.pem b/internal/jwtinfo/testdata/ecdsa-plaintext-private-key.pem new file mode 100644 index 0000000..113c175 --- /dev/null +++ b/internal/jwtinfo/testdata/ecdsa-plaintext-private-key.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIP0jLEHvoM/YMzzeQzkHCkR8XHiE6TvHAKrEwsLHna3aoAoGCCqGSM49 +AwEHoUQDQgAEyuZkxG6jDCk5byQPbK/pbUe9SsfMPYAB1iW58a4MBk4eGOV1whtc +sd1HhJhLgVrHa95hSojd15L1vH+HN+fNeA== +-----END EC PRIVATE KEY----- diff --git a/internal/jwtinfo/testdata/ed25519-plaintext-private-key.pem b/internal/jwtinfo/testdata/ed25519-plaintext-private-key.pem new file mode 100644 index 0000000..ef9b060 --- /dev/null +++ b/internal/jwtinfo/testdata/ed25519-plaintext-private-key.pem @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEINloKqfWZiwop3M9kFRzNq16ZoEvdBLOrb93qQ6x0vhR +-----END PRIVATE KEY----- diff --git a/internal/jwtinfo/testdata/jwkset-from-rsa-private-key-corrupted.json b/internal/jwtinfo/testdata/jwkset-from-rsa-private-key-corrupted.json new file mode 100644 index 0000000..1ffada9 --- /dev/null +++ b/internal/jwtinfo/testdata/jwkset-from-rsa-private-key-corrupted.json @@ -0,0 +1,16 @@ +{ + "keys": [ + { + "kty": "RSA", + "kid": "UniqueKeyID1", + "d": "AYwvXuwM70m3gf8aCjkMwTXQWqovbFVIi70fO1Ngi4hL_0AkARAeCdzl8RwsokG2TpsG6OVIa-1cejm-kZbky98eqiynV5PobRV6O-ko6accRR3XFJKZPiv-YmEK4tzOPnOXOly6sKI1o6YfyAsEHQozsAScYhqCMfXdE0yL76xupjSbHPBE0ByphlWHAuk9kNflp0k-PWXWoSVVF-CLYJA2gm9Td6mTtxPSa8Y3nlpeesl2ux6nQxDNvQYz2TRsE-iqtbgISqZqXbYBF1RO-QqPpCg6cVW2FrBucgIndXXrKcpClE4Y3fAYvI5CDXiH8IVx9xID4oxffZ-V4hRiRHax1MKuSlnY86P0ldzt3HhNoUsyBIK6cB1pQg26zx6wYJHHavg5LdsLG9fvNvMIlhdFhLkljSeXct8N9rOQW4qHaNEBcyVi4xAOK8-JMoXXXf1fiJJUZNwCW8YRTm4IPl7UpZ1iLOpeVIbeTB34CnrzG2VvX6BUOWRC1a68ILO0ti3DpelH7E3EUIF5wJLRUYaOOGv0BU0OVdjp7KgjdMGJVS6_nTQ6KgdFUBe_BSWN8DFZTIKeyXnUt28A9KslkttagUy3CdjWs0qpXDUS-0e87Vy_dC4YjrNckaceAEEeDDsrV_g9ixITFm4XSgnf9E5Piwv3ZRkiA0J9bp2PNAE", + "n": "AAAAziXw91-qUozm4A6Wivk5Xv67kO5azmqajFCikwxrqQX_w7bERL8DHJXAhk3qXuES5aMhQrOtYKrgkMQOOPTA6ySzqk_g5qJX7kI_KU-Eld2eTAZYq4pPfYV-HNcIRpqXjGbbjYGWkBPqEpvNGFmTzSwV99SAXynIDISzmMdoLfr290mIFpKaNuGh0bfaw8X4wlJ4SlpKODxfNhgPMs2pKWmp9oxv3SlNWUFMzEyxNREVah3tkmlN5w78ydr9nD0GaoilqgMgnYGRwcDwCyJ_qWItHybQ8YLSa3UkptDkvh12e4mO3PhI3R6Hn7ysm1xPow_c4tn-Nj9__4TyTBs8zkPJL-iw6GKL8Z--dXmAOMtzCyfJENtz2NGrHwjznsQ6uISBNGziugasAT2e9P0v5YCV5LEFzFpPqPUTomxqU7XHunYflwSZ2l-xbiztZnY9_Q3RZAEUJ88DKn2o5b8b_3WlQ4Q0l6g0QRHTWvA1zrMXLdLb6HY-pikyeKvb5bucn-O91nXique_hYERlgohcHbXTP7BaxpVFzmj0kb3onFqhucXxwyr98zMITaQ6Vaw3ANh3TQFoXeybL2nQv0navpoPfiNZ87xEQ-R4q6wk_ORJ3adA1Bp7FFiN7hTeOlHcwmqqa6u3JF1LWS09-GQXbDYH7CS97A4EEGXzv8r8zk", + "e": "ZZZAQAB", + "p": "ZZZ8MPJZ20LQkkDfhmhkG3NUAKOPCb4L_FMKgaEpPpGJpylQA8UtNbNW6k188dEsj_xAqPhbqkJtx-iDiGoDv-0dgGolTgfOEK5SrKEq3_wqaje7Wj1j8AJVBHlhHxYTvuRavpMyRH0IQTw4YNRNJno5lST3qOIO3_mHhve_lTC6NC3LFN_rmqdDLFe8tJWIOfHZS2N0LgRQknAJaXoxzBcx3OzNYq19P9zgH-BlF1XYMcdQ5m0ZAcoxTHcCjIRyom5-FVRi8Xbr51AzjjQPT02oOEAZCh_yvLoqdrapbHTr9fH2yiREXkeeh0ef0bKO6Y70WDN5WRSTfdk64ZCU7FWmQ", + "q": "2zFkKENTsDvpfy9UKbxbyW-oMsF4BY2O0I38vQVkUvPLYZcyoyf7Hw_cb8vIGTpL-wbUFXizeT9lVuaoZjs3w3ETvmyXn5NA2hPLeqi_JRhT2u60PMss1u04Q4nzxBGmt-1VahLfmk8biFbPUWFjM__R3fdNpwzxvfZelbEOGNNA7JQiQ2R_cqsln83LD8kXcYj6aBUvqF0U3f8tG9E2lBB4EvUoEcI8yYUmXfrpE4cDzZMo2sQlH6udVZi-rD79iKZbI8INOhl4IcoNA5BRgF5ozVj5WV-0m2vHYeanw698woB2fTQFDeVS90R8IcFz8ka-RsowjmwsHYNyIz-FoQ", + "dp": "PMzUgZ_R5g1WKnQCjtyLqpbaoGoe5syCtWIsARWKvZl1SbYFPp24Alu62jMKMBFhpY8JEM7zOyFr5TGupQsB4YMNtZEUoTqIXEq0ojKO7elNUprc2E5gjLBwS4KzMk7pLCDFX0l_Yw9CXVCyIxxy6ieVp_WYI55q2FOlfoidJ_n9J952Me-OdXQdTCi9w1WEzxIv6_NvuFOziE5beGHILD4VlYHP7WzNGDga2wKvXCssQrQAD_qW2yV6McDvKTyakBHLpAZ1MeX-RZWnUK-yqfHoDWME0pWBF9PCv3EmyhMtzzRxXf4-KgxMdUvFKkcSeA-AL1ukRTgibi_bdBMmkQ", + "dq": "SGSmrLmX0VPoSW5LQMGKGxx6k9DcIBFhwrWybId0XAVS_bdfLQ3OXbLyXiYSv2pGn_DgaPsFY50xjiL-KU2TnEQjfjgFV9ndiGkTQj6rasf_IgbGlnGQLKgKdhwA25fs1UBYfoEfQqqv8DajoEAm8IykNsgv6GVZDiFpmczxV_elsL04F8QAZ9Hoyj_AukTzLjdMZMXiiJu9gZh-wHo3qW1LCw_XHQ5m3zPPuShehGmKMwJQcvhnPm-CtjuNdfwT5mbzIPs9PRweViKSa8Pldx03ReMF76OxVceiAU6ZyAKUlPSyraVZqf48iZgf21I2RiVhQKYUpWVKqLC6KLQZIQ", + "qi": "gX7XGIVNxoyRlFEhCImATOIBwK_DksbxymBftLZ6A69pkc3bYUOr00pSvSO5HGsqHzhKJbbczDWovYvpUYmzO3jKsKp6M7Hpd6tnU1F1J_oLwPMeFm2EOrW689k0g0DT6sIZ-_PYEOjWW9MCp55r6U7XaBYSKGhajspIFNEeSVFSpjW-cBjTqPiA3rwnutKGz7kfUsDv5DkwTXVyKjRd7QnX-4zyQZas0wjJzMU_cuBdVZQrc0CcR7yoe8sf9TJ7lH6MTi4ehyT4L1AuAyUZzqoMZ73iBZ9202n40Vm6Fkb8B9CiEQHvdTKv01kH6Pj9quFboHlUxnBNa2tA3obwTQ" + } + ] +} diff --git a/internal/jwtinfo/testdata/jwkset-from-rsa-private-key-valid.json b/internal/jwtinfo/testdata/jwkset-from-rsa-private-key-valid.json new file mode 100644 index 0000000..774a249 --- /dev/null +++ b/internal/jwtinfo/testdata/jwkset-from-rsa-private-key-valid.json @@ -0,0 +1,16 @@ +{ + "keys": [ + { + "kty": "RSA", + "kid": "UniqueKeyID1", + "d": "AYwvXuwM70m3gf8aCjkMwTXQWqovbFVIi70fO1Ngi4hL_0AkARAeCdzl8RwsokG2TpsG6OVIa-1cejm-kZbky98eqiynV5PobRV6O-ko6accRR3XFJKZPiv-YmEK4tzOPnOXOly6sKI1o6YfyAsEHQozsAScYhqCMfXdE0yL76xupjSbHPBE0ByphlWHAuk9kNflp0k-PWXWoSVVF-CLYJA2gm9Td6mTtxPSa8Y3nlpeesl2ux6nQxDNvQYz2TRsE-iqtbgISqZqXbYBF1RO-QqPpCg6cVW2FrBucgIndXXrKcpClE4Y3fAYvI5CDXiH8IVx9xID4oxffZ-V4hRiRHax1MKuSlnY86P0ldzt3HhNoUsyBIK6cB1pQg26zx6wYJHHavg5LdsLG9fvNvMIlhdFhLkljSeXct8N9rOQW4qHaNEBcyVi4xAOK8-JMoXXXf1fiJJUZNwCW8YRTm4IPl7UpZ1iLOpeVIbeTB34CnrzG2VvX6BUOWRC1a68ILO0ti3DpelH7E3EUIF5wJLRUYaOOGv0BU0OVdjp7KgjdMGJVS6_nTQ6KgdFUBe_BSWN8DFZTIKeyXnUt28A9KslkttagUy3CdjWs0qpXDUS-0e87Vy_dC4YjrNckaceAEEeDDsrV_g9ixITFm4XSgnf9E5Piwv3ZRkiA0J9bp2PNAE", + "n": "ziXw91-qUozm4A6Wivk5Xv67kO5azmqajFCikwxrqQX_w7bERL8DHJXAhk3qXuES5aMhQrOtYKrgkMQOOPTA6ySzqk_g5qJX7kI_KU-Eld2eTAZYq4pPfYV-HNcIRpqXjGbbjYGWkBPqEpvNGFmTzSwV99SAXynIDISzmMdoLfr290mIFpKaNuGh0bfaw8X4wlJ4SlpKODxfNhgPMs2pKWmp9oxv3SlNWUFMzEyxNREVah3tkmlN5w78ydr9nD0GaoilqgMgnYGRwcDwCyJ_qWItHybQ8YLSa3UkptDkvh12e4mO3PhI3R6Hn7ysm1xPow_c4tn-Nj9__4TyTBs8zkPJL-iw6GKL8Z--dXmAOMtzCyfJENtz2NGrHwjznsQ6uISBNGziugasAT2e9P0v5YCV5LEFzFpPqPUTomxqU7XHunYflwSZ2l-xbiztZnY9_Q3RZAEUJ88DKn2o5b8b_3WlQ4Q0l6g0QRHTWvA1zrMXLdLb6HY-pikyeKvb5bucn-O91nXique_hYERlgohcHbXTP7BaxpVFzmj0kb3onFqhucXxwyr98zMITaQ6Vaw3ANh3TQFoXeybL2nQv0navpoPfiNZ87xEQ-R4q6wk_ORJ3adA1Bp7FFiN7hTeOlHcwmqqa6u3JF1LWS09-GQXbDYH7CS97A4EEGXzv8r8zk", + "e": "AQAB", + "p": "8MPJZ20LQkkDfhmhkG3NUAKOPCb4L_FMKgaEpPpGJpylQA8UtNbNW6k188dEsj_xAqPhbqkJtx-iDiGoDv-0dgGolTgfOEK5SrKEq3_wqaje7Wj1j8AJVBHlhHxYTvuRavpMyRH0IQTw4YNRNJno5lST3qOIO3_mHhve_lTC6NC3LFN_rmqdDLFe8tJWIOfHZS2N0LgRQknAJaXoxzBcx3OzNYq19P9zgH-BlF1XYMcdQ5m0ZAcoxTHcCjIRyom5-FVRi8Xbr51AzjjQPT02oOEAZCh_yvLoqdrapbHTr9fH2yiREXkeeh0ef0bKO6Y70WDN5WRSTfdk64ZCU7FWmQ", + "q": "2zFkKENTsDvpfy9UKbxbyW-oMsF4BY2O0I38vQVkUvPLYZcyoyf7Hw_cb8vIGTpL-wbUFXizeT9lVuaoZjs3w3ETvmyXn5NA2hPLeqi_JRhT2u60PMss1u04Q4nzxBGmt-1VahLfmk8biFbPUWFjM__R3fdNpwzxvfZelbEOGNNA7JQiQ2R_cqsln83LD8kXcYj6aBUvqF0U3f8tG9E2lBB4EvUoEcI8yYUmXfrpE4cDzZMo2sQlH6udVZi-rD79iKZbI8INOhl4IcoNA5BRgF5ozVj5WV-0m2vHYeanw698woB2fTQFDeVS90R8IcFz8ka-RsowjmwsHYNyIz-FoQ", + "dp": "PMzUgZ_R5g1WKnQCjtyLqpbaoGoe5syCtWIsARWKvZl1SbYFPp24Alu62jMKMBFhpY8JEM7zOyFr5TGupQsB4YMNtZEUoTqIXEq0ojKO7elNUprc2E5gjLBwS4KzMk7pLCDFX0l_Yw9CXVCyIxxy6ieVp_WYI55q2FOlfoidJ_n9J952Me-OdXQdTCi9w1WEzxIv6_NvuFOziE5beGHILD4VlYHP7WzNGDga2wKvXCssQrQAD_qW2yV6McDvKTyakBHLpAZ1MeX-RZWnUK-yqfHoDWME0pWBF9PCv3EmyhMtzzRxXf4-KgxMdUvFKkcSeA-AL1ukRTgibi_bdBMmkQ", + "dq": "SGSmrLmX0VPoSW5LQMGKGxx6k9DcIBFhwrWybId0XAVS_bdfLQ3OXbLyXiYSv2pGn_DgaPsFY50xjiL-KU2TnEQjfjgFV9ndiGkTQj6rasf_IgbGlnGQLKgKdhwA25fs1UBYfoEfQqqv8DajoEAm8IykNsgv6GVZDiFpmczxV_elsL04F8QAZ9Hoyj_AukTzLjdMZMXiiJu9gZh-wHo3qW1LCw_XHQ5m3zPPuShehGmKMwJQcvhnPm-CtjuNdfwT5mbzIPs9PRweViKSa8Pldx03ReMF76OxVceiAU6ZyAKUlPSyraVZqf48iZgf21I2RiVhQKYUpWVKqLC6KLQZIQ", + "qi": "gX7XGIVNxoyRlFEhCImATOIBwK_DksbxymBftLZ6A69pkc3bYUOr00pSvSO5HGsqHzhKJbbczDWovYvpUYmzO3jKsKp6M7Hpd6tnU1F1J_oLwPMeFm2EOrW689k0g0DT6sIZ-_PYEOjWW9MCp55r6U7XaBYSKGhajspIFNEeSVFSpjW-cBjTqPiA3rwnutKGz7kfUsDv5DkwTXVyKjRd7QnX-4zyQZas0wjJzMU_cuBdVZQrc0CcR7yoe8sf9TJ7lH6MTi4ehyT4L1AuAyUZzqoMZ73iBZ9202n40Vm6Fkb8B9CiEQHvdTKv01kH6Pj9quFboHlUxnBNa2tA3obwTQ" + } + ] +} \ No newline at end of file diff --git a/internal/jwtinfo/testdata/rsa-pkcs8-crt.pem b/internal/jwtinfo/testdata/rsa-pkcs8-crt.pem new file mode 100644 index 0000000..2b2c4b3 --- /dev/null +++ b/internal/jwtinfo/testdata/rsa-pkcs8-crt.pem @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIIFfzCCA2egAwIBAgIUNxsgkJsVYys9J7SCCgNF/YTvI50wDQYJKoZIhvcNAQEL +BQAwaDELMAkGA1UEBhMCREUxDzANBgNVBAgMBkJlcmxpbjEPMA0GA1UEBwwGQmVy +bGluMREwDwYDVQQKDAhPczc2Lnh5ejEQMA4GA1UECwwHdGVzdGluZzESMBAGA1UE +AwwJbG9jYWxob3N0MB4XDTI2MDMwMTIwMzEwN1oXDTM2MDIyNzIwMzEwN1owaDEL +MAkGA1UEBhMCREUxDzANBgNVBAgMBkJlcmxpbjEPMA0GA1UEBwwGQmVybGluMREw +DwYDVQQKDAhPczc2Lnh5ejEQMA4GA1UECwwHdGVzdGluZzESMBAGA1UEAwwJbG9j +YWxob3N0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAziXw91+qUozm +4A6Wivk5Xv67kO5azmqajFCikwxrqQX/w7bERL8DHJXAhk3qXuES5aMhQrOtYKrg +kMQOOPTA6ySzqk/g5qJX7kI/KU+Eld2eTAZYq4pPfYV+HNcIRpqXjGbbjYGWkBPq +EpvNGFmTzSwV99SAXynIDISzmMdoLfr290mIFpKaNuGh0bfaw8X4wlJ4SlpKODxf +NhgPMs2pKWmp9oxv3SlNWUFMzEyxNREVah3tkmlN5w78ydr9nD0GaoilqgMgnYGR +wcDwCyJ/qWItHybQ8YLSa3UkptDkvh12e4mO3PhI3R6Hn7ysm1xPow/c4tn+Nj9/ +/4TyTBs8zkPJL+iw6GKL8Z++dXmAOMtzCyfJENtz2NGrHwjznsQ6uISBNGziugas +AT2e9P0v5YCV5LEFzFpPqPUTomxqU7XHunYflwSZ2l+xbiztZnY9/Q3RZAEUJ88D +Kn2o5b8b/3WlQ4Q0l6g0QRHTWvA1zrMXLdLb6HY+pikyeKvb5bucn+O91nXique/ +hYERlgohcHbXTP7BaxpVFzmj0kb3onFqhucXxwyr98zMITaQ6Vaw3ANh3TQFoXey +bL2nQv0navpoPfiNZ87xEQ+R4q6wk/ORJ3adA1Bp7FFiN7hTeOlHcwmqqa6u3JF1 +LWS09+GQXbDYH7CS97A4EEGXzv8r8zkCAwEAAaMhMB8wHQYDVR0OBBYEFLnUwUUo +UTqZkKZZASM/CkDE7LYiMA0GCSqGSIb3DQEBCwUAA4ICAQC31YOOwgycjlaRhY6F +b864c5nw8XDCx16nQ8QkUBPh0UFU3OOS680fyymiPX+pB+eckZcI9GRJNstdiuJC +e719ny22y+9KOoQBLDrSqjP1hc4XYok3jxkZM1+kD+YydNl8NXqE4l2qRdXJeAaf +PJfbLZVPV70OD1aTafZKxablTaaskWiNiKe+nZsxsGYN1Xe2SK4/vR7ekYxHKbsd +JkRhQFitxM+L/qC7KdVoY31yY8Z02i3AUmxwIp826y0DBxy9je1t3mxXA45Uv8ZN +ybSUhOLzEUQlwrc+y8lOrtD3VmYng/QKh484/cX09FypmY+8TyeSl0xDMe50eBcG +d9Iije7QI+Qtr18tEbtI5uGBNaTUmoPf4DdACqMW6uEhQNWFizgHY7ZXBE162gUD +/opbdfbAby6Xognvv7XHjTT/XtJNDs+J4YO3ZUbfFQKdWrqlIEWBuuNxD+Bv8y4Z +SKRvNZd5wUY/2Ws7KdebWJW3SuuSGDEs7T/CUkArVZTtG1DyOThsj/irX4mdlN/g +FPagamSPRRiYZh5ku00C/tUDB1e/cHVf8iDGE/9WHNIADbf7yfcSnpCwrJRvmrOs +GG131ZEDBD3N2p/8bdOIseORrYOI4qqtPXyzbr204PX4Pcu9ttpdUWZaVwTUa8Fm +WRtdvAkwEadTfskpvyXWu0aAUQ== +-----END CERTIFICATE----- diff --git a/internal/jwtinfo/testdata/rsa-pkcs8-csr.pem b/internal/jwtinfo/testdata/rsa-pkcs8-csr.pem new file mode 100644 index 0000000..6aaad77 --- /dev/null +++ b/internal/jwtinfo/testdata/rsa-pkcs8-csr.pem @@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIErTCCApUCAQAwaDELMAkGA1UEBhMCREUxDzANBgNVBAgMBkJlcmxpbjEPMA0G +A1UEBwwGQmVybGluMREwDwYDVQQKDAhPczc2Lnh5ejEQMA4GA1UECwwHdGVzdGlu +ZzESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC +CgKCAgEAziXw91+qUozm4A6Wivk5Xv67kO5azmqajFCikwxrqQX/w7bERL8DHJXA +hk3qXuES5aMhQrOtYKrgkMQOOPTA6ySzqk/g5qJX7kI/KU+Eld2eTAZYq4pPfYV+ +HNcIRpqXjGbbjYGWkBPqEpvNGFmTzSwV99SAXynIDISzmMdoLfr290mIFpKaNuGh +0bfaw8X4wlJ4SlpKODxfNhgPMs2pKWmp9oxv3SlNWUFMzEyxNREVah3tkmlN5w78 +ydr9nD0GaoilqgMgnYGRwcDwCyJ/qWItHybQ8YLSa3UkptDkvh12e4mO3PhI3R6H +n7ysm1xPow/c4tn+Nj9//4TyTBs8zkPJL+iw6GKL8Z++dXmAOMtzCyfJENtz2NGr +HwjznsQ6uISBNGziugasAT2e9P0v5YCV5LEFzFpPqPUTomxqU7XHunYflwSZ2l+x +biztZnY9/Q3RZAEUJ88DKn2o5b8b/3WlQ4Q0l6g0QRHTWvA1zrMXLdLb6HY+piky +eKvb5bucn+O91nXique/hYERlgohcHbXTP7BaxpVFzmj0kb3onFqhucXxwyr98zM +ITaQ6Vaw3ANh3TQFoXeybL2nQv0navpoPfiNZ87xEQ+R4q6wk/ORJ3adA1Bp7FFi +N7hTeOlHcwmqqa6u3JF1LWS09+GQXbDYH7CS97A4EEGXzv8r8zkCAwEAAaAAMA0G +CSqGSIb3DQEBCwUAA4ICAQA5+289xMcyLGiSWB7d6WXiCEF9Jn6cD9VD9sZ3v4vO +Ihis3ApEukn1GKNqtI4o5aJf8HY8kT2C7eZEbeLHXno7q4/iuIJU6OKlEfpBczlq +UDCQj3hy4z+1baY+WqUuqp2lePzQ7fbk21tORUO58UxQd5+tLLgs98uAGadkrHM3 +F5I0k/GInwz+Xvq9cnJiH+VFVtYJIfdeXdr7GHxWxq4sV1VwBg5jQuhPWRFCU57Z +7SCpu3OjlD4eTxS/pppboND6r6EEcoeTrm9Vnq3RpuS1AVxY32tS9mAEZj1rX8Iy +llalAMdpk+W6JkAI3wO6RNjQl7k3DsKMAf49n9ecpSgyhoNyQFv81fzyqQeTdBUZ +m7pw9IivU6AxYkw3w2rb1PLGfOKaO5D6eDEAIMauf0FFhJ26ljGLfpQEYsCt2iJu +3QPPIqz/mCKobBwCcabLMVa+IjxPmxMr2gqmN/kujVmjdfmdkQUIrqhLl7fKj//b +r1XqZVlLMzuzED7B/qnA+ztKcZOyermDquLs+zPDrduSa31oM8ddLDh67gJyV2ij +XX7hs9fbiD3ngXr/wT6aXUi5tKNmghffWiEu8zkbdO/hKi4KGW9XJXop2PG8urMD +iRQuKKZ+uJq89s4goZw/vVAKupVCmUtFpzxppL/23wKhYJ+rcFFJq9E+1ApDv07+ +9Q== +-----END CERTIFICATE REQUEST----- diff --git a/internal/jwtinfo/testdata/rsa-pkcs8-encrypted-private-key.pem b/internal/jwtinfo/testdata/rsa-pkcs8-encrypted-private-key.pem new file mode 100644 index 0000000..07bc1b2 --- /dev/null +++ b/internal/jwtinfo/testdata/rsa-pkcs8-encrypted-private-key.pem @@ -0,0 +1,54 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +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 +-----END ENCRYPTED PRIVATE KEY----- diff --git a/internal/jwtinfo/testdata/rsa-pkcs8-plaintext-private-key.pem b/internal/jwtinfo/testdata/rsa-pkcs8-plaintext-private-key.pem new file mode 100644 index 0000000..db988d1 --- /dev/null +++ b/internal/jwtinfo/testdata/rsa-pkcs8-plaintext-private-key.pem @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +01kH6Pj9quFboHlUxnBNa2tA3obwTQ== +-----END PRIVATE KEY----- diff --git a/internal/jwtinfo/testdata/rsa-pkcs8-public-key.pem b/internal/jwtinfo/testdata/rsa-pkcs8-public-key.pem new file mode 100644 index 0000000..d46e38b --- /dev/null +++ b/internal/jwtinfo/testdata/rsa-pkcs8-public-key.pem @@ -0,0 +1,14 @@ +-----BEGIN PUBLIC KEY----- +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAziXw91+qUozm4A6Wivk5 +Xv67kO5azmqajFCikwxrqQX/w7bERL8DHJXAhk3qXuES5aMhQrOtYKrgkMQOOPTA +6ySzqk/g5qJX7kI/KU+Eld2eTAZYq4pPfYV+HNcIRpqXjGbbjYGWkBPqEpvNGFmT +zSwV99SAXynIDISzmMdoLfr290mIFpKaNuGh0bfaw8X4wlJ4SlpKODxfNhgPMs2p +KWmp9oxv3SlNWUFMzEyxNREVah3tkmlN5w78ydr9nD0GaoilqgMgnYGRwcDwCyJ/ +qWItHybQ8YLSa3UkptDkvh12e4mO3PhI3R6Hn7ysm1xPow/c4tn+Nj9//4TyTBs8 +zkPJL+iw6GKL8Z++dXmAOMtzCyfJENtz2NGrHwjznsQ6uISBNGziugasAT2e9P0v +5YCV5LEFzFpPqPUTomxqU7XHunYflwSZ2l+xbiztZnY9/Q3RZAEUJ88DKn2o5b8b +/3WlQ4Q0l6g0QRHTWvA1zrMXLdLb6HY+pikyeKvb5bucn+O91nXique/hYERlgoh +cHbXTP7BaxpVFzmj0kb3onFqhucXxwyr98zMITaQ6Vaw3ANh3TQFoXeybL2nQv0n +avpoPfiNZ87xEQ+R4q6wk/ORJ3adA1Bp7FFiN7hTeOlHcwmqqa6u3JF1LWS09+GQ +XbDYH7CS97A4EEGXzv8r8zkCAwEAAQ== +-----END PUBLIC KEY----- diff --git a/internal/style/style_handlers.go b/internal/style/style_handlers.go index 3ca05d2..2e5de6f 100644 --- a/internal/style/style_handlers.go +++ b/internal/style/style_handlers.go @@ -89,7 +89,12 @@ func PrintKeyInfoStyle(w io.Writer, privKey crypto.PrivateKey) { } func CodeSyntaxHighlight(lang, code string) string { - st := styles.Get(chromaDefStyle) + out := CodeSyntaxHighlightWithStyle(lang, code, chromaDefStyle) + return out +} + +func CodeSyntaxHighlightWithStyle(lang, code string, chromaStyle string) string { + st := styles.Get(chromaStyle) if st == nil { st = styles.Fallback } From dc24db77119d1390699efded80ff4032d91263a1 Mon Sep 17 00:00:00 2001 From: Zeno Belli Date: Mon, 16 Mar 2026 23:13:43 +0100 Subject: [PATCH 02/23] chore: deps update --- devenv.lock | 44 +++++++++++++++++++++++--------- go.mod | 36 +++++++++++++++------------ go.sum | 72 +++++++++++++++++++++++++++++------------------------ 3 files changed, 92 insertions(+), 60 deletions(-) diff --git a/devenv.lock b/devenv.lock index cd0bf21..823b8f2 100644 --- a/devenv.lock +++ b/devenv.lock @@ -3,10 +3,10 @@ "devenv": { "locked": { "dir": "src/modules", - "lastModified": 1766843567, + "lastModified": 1772483048, "owner": "cachix", "repo": "devenv", - "rev": "d0f2c8545f09e5aba9d321079a284b550371879d", + "rev": "40f410e3a5e0f9198cf67bfa8673c9a17d8c605c", "type": "github" }, "original": { @@ -19,14 +19,14 @@ "flake-compat": { "flake": false, "locked": { - "lastModified": 1766661267, - "owner": "edolstra", + "lastModified": 1767039857, + "owner": "NixOS", "repo": "flake-compat", - "rev": "f275e157c50c3a9a682b4c9b4aa4db7a4cd3b5f2", + "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab", "type": "github" }, "original": { - "owner": "edolstra", + "owner": "NixOS", "repo": "flake-compat", "type": "github" } @@ -40,10 +40,10 @@ ] }, "locked": { - "lastModified": 1765911976, + "lastModified": 1772024342, "owner": "cachix", "repo": "git-hooks.nix", - "rev": "b68b780b69702a090c8bb1b973bab13756cc7a27", + "rev": "6e34e97ed9788b17796ee43ccdbaf871a5c2b476", "type": "github" }, "original": { @@ -73,11 +73,14 @@ } }, "nixpkgs": { + "inputs": { + "nixpkgs-src": "nixpkgs-src" + }, "locked": { - "lastModified": 1764580874, + "lastModified": 1770434727, "owner": "cachix", "repo": "devenv-nixpkgs", - "rev": "dcf61356c3ab25f1362b4a4428a6d871e84f1d1d", + "rev": "8430f16a39c27bdeef236f1eeb56f0b51b33d348", "type": "github" }, "original": { @@ -87,12 +90,29 @@ "type": "github" } }, + "nixpkgs-src": { + "flake": false, + "locked": { + "lastModified": 1769922788, + "narHash": "sha256-H3AfG4ObMDTkTJYkd8cz1/RbY9LatN5Mk4UF48VuSXc=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "207d15f1a6603226e1e223dc79ac29c7846da32e", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, "nixpkgs-stable": { "locked": { - "lastModified": 1766736597, + "lastModified": 1772047000, "owner": "NixOS", "repo": "nixpkgs", - "rev": "f560ccec6b1116b22e6ed15f4c510997d99d5852", + "rev": "1267bb4920d0fc06ea916734c11b0bf004bbe17e", "type": "github" }, "original": { diff --git a/go.mod b/go.mod index 3ffd6ad..b5f7a67 100644 --- a/go.mod +++ b/go.mod @@ -3,42 +3,45 @@ module github.com/xenos76/https-wrench go 1.25.4 require ( + github.com/MicahParks/jwkset v0.11.0 + github.com/MicahParks/keyfunc/v3 v3.8.0 github.com/alecthomas/assert/v2 v2.11.0 - github.com/alecthomas/chroma/v2 v2.21.1 - github.com/breml/rootcerts v0.3.3 + github.com/alecthomas/chroma/v2 v2.23.1 + github.com/breml/rootcerts v0.3.4 github.com/catppuccin/go v0.3.0 github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 github.com/dustin/go-humanize v1.0.1 + github.com/goforj/godump v1.9.1 + github.com/golang-jwt/jwt/v5 v5.3.1 github.com/google/go-cmp v0.7.0 - github.com/gookit/goutil v0.7.2 - github.com/pires/go-proxyproto v0.8.1 + github.com/gookit/goutil v0.7.3 + github.com/pires/go-proxyproto v0.11.0 github.com/spf13/cobra v1.10.2 github.com/spf13/viper v1.21.0 github.com/stretchr/testify v1.11.1 github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 - golang.org/x/term v0.38.0 + golang.org/x/term v0.40.0 ) require ( github.com/alecthomas/repr v0.5.2 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/charmbracelet/colorprofile v0.4.1 // indirect - github.com/charmbracelet/x/ansi v0.11.3 // indirect - github.com/charmbracelet/x/cellbuf v0.0.14 // indirect + github.com/charmbracelet/colorprofile v0.4.2 // indirect + github.com/charmbracelet/x/ansi v0.11.6 // indirect + github.com/charmbracelet/x/cellbuf v0.0.15 // indirect github.com/charmbracelet/x/term v0.2.2 // indirect - github.com/clipperhouse/displaywidth v0.6.2 // indirect - github.com/clipperhouse/stringish v0.1.1 // indirect - github.com/clipperhouse/uax29/v2 v2.3.0 // indirect + github.com/clipperhouse/displaywidth v0.11.0 // indirect + github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dlclark/regexp2 v1.11.5 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect - github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/hexops/gotextdiff v1.0.3 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.19 // indirect + github.com/mattn/go-runewidth v0.0.20 // indirect github.com/muesli/termenv v0.16.0 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect @@ -51,9 +54,10 @@ require ( github.com/subosito/gotenv v1.6.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.46.0 // indirect - golang.org/x/sys v0.39.0 // indirect - golang.org/x/text v0.32.0 // indirect + golang.org/x/crypto v0.48.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/text v0.34.0 // indirect + golang.org/x/time v0.14.0 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 3c7081a..a9c19c7 100644 --- a/go.sum +++ b/go.sum @@ -1,35 +1,37 @@ +github.com/MicahParks/jwkset v0.11.0 h1:yc0zG+jCvZpWgFDFmvs8/8jqqVBG9oyIbmBtmjOhoyQ= +github.com/MicahParks/jwkset v0.11.0/go.mod h1:U2oRhRaLgDCLjtpGL2GseNKGmZtLs/3O7p+OZaL5vo0= +github.com/MicahParks/keyfunc/v3 v3.8.0 h1:Hx2dgIjAXGk9slakM6rV9BOeaWDPEXXZ4Us8guNBfds= +github.com/MicahParks/keyfunc/v3 v3.8.0/go.mod h1:z66bkCviwqfg2YUp+Jcc/xRE9IXLcMq6DrgV/+Htru0= github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= -github.com/alecthomas/chroma/v2 v2.21.1 h1:FaSDrp6N+3pphkNKU6HPCiYLgm8dbe5UXIXcoBhZSWA= -github.com/alecthomas/chroma/v2 v2.21.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o= +github.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY= +github.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o= github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs= github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= -github.com/breml/rootcerts v0.3.3 h1://GnaRtQ/9BY2+GtMk2wtWxVdCRysiaPr5/xBwl7NKw= -github.com/breml/rootcerts v0.3.3/go.mod h1:S/PKh+4d1HUn4HQovEB8hPJZO6pUZYrIhmXBhsegfXw= +github.com/breml/rootcerts v0.3.4 h1:9i7WNl/ctd9OEAOaTfLy//Wrlfxq/tRQ7v4okYFN9Ys= +github.com/breml/rootcerts v0.3.4/go.mod h1:S/PKh+4d1HUn4HQovEB8hPJZO6pUZYrIhmXBhsegfXw= github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY= github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= -github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk= -github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk= +github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY= +github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8= github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE= github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA= -github.com/charmbracelet/x/ansi v0.11.3 h1:6DcVaqWI82BBVM/atTyq6yBoRLZFBsnoDoX9GCu2YOI= -github.com/charmbracelet/x/ansi v0.11.3/go.mod h1:yI7Zslym9tCJcedxz5+WBq+eUGMJT0bM06Fqy1/Y4dI= -github.com/charmbracelet/x/cellbuf v0.0.14 h1:iUEMryGyFTelKW3THW4+FfPgi4fkmKnnaLOXuc+/Kj4= -github.com/charmbracelet/x/cellbuf v0.0.14/go.mod h1:P447lJl49ywBbil/KjCk2HexGh4tEY9LH0/1QrZZ9rA= +github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8= +github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ= +github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI= +github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q= github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30= github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= -github.com/clipperhouse/displaywidth v0.6.2 h1:ZDpTkFfpHOKte4RG5O/BOyf3ysnvFswpyYrV7z2uAKo= -github.com/clipperhouse/displaywidth v0.6.2/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o= -github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= -github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= -github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4= -github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= +github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= +github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= +github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= +github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= @@ -43,12 +45,16 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= -github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/goforj/godump v1.9.1 h1:9OGpb978Ytz3B59d5Yi2PzRYYLid6UkmhYDIDNiF15Y= +github.com/goforj/godump v1.9.1/go.mod h1:JsuL6AEZfKIU+iR5ewL6iQ2fIuhvLtPmJDH47M9Ptrc= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/gookit/goutil v0.7.2 h1:NSiqWWY+BT0MwIlKDeSVPfQmr9xTkkAqwDjhplobdgo= -github.com/gookit/goutil v0.7.2/go.mod h1:vJS9HXctYTCLtCsZot5L5xF+O1oR17cDYO9R0HxBmnU= +github.com/gookit/goutil v0.7.3 h1:nXDd/AB17nEjqVCNDGioDhVL/gVqdlqRMfFergKDjHE= +github.com/gookit/goutil v0.7.3/go.mod h1:vJS9HXctYTCLtCsZot5L5xF+O1oR17cDYO9R0HxBmnU= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -61,14 +67,14 @@ github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQ github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= -github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ= +github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= -github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0= -github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU= +github.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4= +github.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= @@ -100,17 +106,19 @@ github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zU github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= -golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= -golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= -golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= -golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= +golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From a4d0cf92dc2e94bad861ebe17ba13d6aeb5363a5 Mon Sep 17 00:00:00 2001 From: Zeno Belli Date: Mon, 16 Mar 2026 23:14:14 +0100 Subject: [PATCH 03/23] ci: add devenv tests for jwtinfo --- .golangci.yml | 3 +-- devenv.nix | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 8d56f8a..2899fcb 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -32,7 +32,6 @@ linters: enable-all-rules: true severity: warning rules: - # https://github.com/mgechev/revive/blob/HEAD/RULES_DESCRIPTIONS.md#add-constant - name: add-constant severity: warning @@ -45,7 +44,7 @@ linters: severity: warning disabled: false exclude: [""] - arguments: [100] + arguments: [120] # https://github.com/mgechev/revive/blob/HEAD/RULES_DESCRIPTIONS.md#cognitive-complexity - name: cognitive-complexity diff --git a/devenv.nix b/devenv.nix index 03b37f8..546a6fb 100644 --- a/devenv.nix +++ b/devenv.nix @@ -49,6 +49,7 @@ in { ".envrc" "internal/certinfo/common_handlers.go" "internal/certinfo/testdata" + "internal/jwtinfo/testdata" "internal/certinfo/testdata/README.md" "completions" ]; @@ -589,6 +590,32 @@ in { test-certinfo-ecdsa-cert ''; + scripts.run-jwtinfo-test-auth0.exec = '' + gum format "### JwtInfo request against Auth0" + + REQ_URL="https://dev-x3cci6dykofnlj5z.eu.auth0.com/oauth/token" + VALIDATION_URL="https://dev-x3cci6dykofnlj5z.eu.auth0.com/.well-known/jwks.json" + + ./dist/https-wrench jwtinfo --request-url "$REQ_URL" --request-values-json "$JWTINFO_TEST_AUTH0" --validation-url "$VALIDATION_URL" + ''; + + scripts.run-jwtinfo-test-auth0-no-validation.exec = '' + gum format "### JwtInfo request against Auth0: no validation" + + REQ_URL="https://dev-x3cci6dykofnlj5z.eu.auth0.com/oauth/token" + + ./dist/https-wrench jwtinfo --request-url "$REQ_URL" --request-values-json "$JWTINFO_TEST_AUTH0" + ''; + + scripts.run-jwtinfo-test-keycloak.exec = '' + gum format "### JwtInfo request against priv Keycloak" + + REQ_URL="https://keycloak.k3s.os76.xyz/realms/os76/protocol/openid-connect/token" + VALIDATION_URL="https://keycloak.k3s.os76.xyz/realms/os76/protocol/openid-connect/certs" + + ./dist/https-wrench jwtinfo --request-url "$REQ_URL" --request-values-json "$JWTINFO_TEST_KEYCLOAK" --validation-url "$VALIDATION_URL" + ''; + scripts.run-go-tests.exec = '' gum format "## Run GO tests" @@ -623,6 +650,11 @@ in { gum format "# Devenv shell" export GITEA_TOKEN=$(cat ~/.config/goreleaser/gitea_token) export GITHUB_TOKEN=$(cat ~/.config/goreleaser/github_token) + + # JwtInfo tests against authentication providers when not on CI + test -f ~/.config/https-wrench/jwtinfo_test_auth0_req_values.json && export JWTINFO_TEST_AUTH0=$(cat ~/.config/https-wrench/jwtinfo_test_auth0_req_values.json) + test -f ~/.config/https-wrench/jwtinfo_test_keycloak_req_values.json && export JWTINFO_TEST_KEYCLOAK=$(cat ~/.config/https-wrench/jwtinfo_test_keycloak_req_values.json) + go version create-certs ''; @@ -645,5 +677,8 @@ in { run-certinfo-priv-key-tests run-certinfo-cert-tests run-certinfo-tlsendpoint-tests + + run-jwtinfo-test-auth0 + run-jwtinfo-test-auth0-no-validation ''; } From 4d63db48ce6c34d6736e17e2e4f5e4c04b1b3200 Mon Sep 17 00:00:00 2001 From: Zeno Belli Date: Tue, 17 Mar 2026 09:30:55 +0100 Subject: [PATCH 04/23] ci: jwtinfo test creds via env --- .github/workflows/codeChecks.yml | 2 ++ devenv.nix | 8 ++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeChecks.yml b/.github/workflows/codeChecks.yml index bed7ea8..332ccb3 100644 --- a/.github/workflows/codeChecks.yml +++ b/.github/workflows/codeChecks.yml @@ -66,6 +66,8 @@ jobs: run: nix profile add nixpkgs#devenv - name: Build the devenv shell and run any pre-commit hooks + env: + JWTINFO_TEST_AUTH0: ${{ secrets.JWTINFO_TEST_AUTH0 }} run: devenv test timeout-minutes: 15 diff --git a/devenv.nix b/devenv.nix index 546a6fb..6962103 100644 --- a/devenv.nix +++ b/devenv.nix @@ -678,7 +678,11 @@ in { run-certinfo-cert-tests run-certinfo-tlsendpoint-tests - run-jwtinfo-test-auth0 - run-jwtinfo-test-auth0-no-validation + if [ -n "$JWTINFO_TEST_AUTH0" ]; then + run-jwtinfo-test-auth0 + run-jwtinfo-test-auth0-no-validation + else + gum format "## Skipping JwtInfo Auth0 tests (JWTINFO_TEST_AUTH0 not set)" + fi ''; } From 52ca5d8fb4e9137b09c21aeba151dd934d2afb92 Mon Sep 17 00:00:00 2001 From: Zeno Belli Date: Tue, 17 Mar 2026 09:42:37 +0100 Subject: [PATCH 05/23] ci: comment out file checks in devenv shell --- devenv.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/devenv.nix b/devenv.nix index 6962103..fb40616 100644 --- a/devenv.nix +++ b/devenv.nix @@ -652,8 +652,8 @@ in { export GITHUB_TOKEN=$(cat ~/.config/goreleaser/github_token) # JwtInfo tests against authentication providers when not on CI - test -f ~/.config/https-wrench/jwtinfo_test_auth0_req_values.json && export JWTINFO_TEST_AUTH0=$(cat ~/.config/https-wrench/jwtinfo_test_auth0_req_values.json) - test -f ~/.config/https-wrench/jwtinfo_test_keycloak_req_values.json && export JWTINFO_TEST_KEYCLOAK=$(cat ~/.config/https-wrench/jwtinfo_test_keycloak_req_values.json) + # test -f ~/.config/https-wrench/jwtinfo_test_auth0_req_values.json && export JWTINFO_TEST_AUTH0=$(cat ~/.config/https-wrench/jwtinfo_test_auth0_req_values.json) + # test -f ~/.config/https-wrench/jwtinfo_test_keycloak_req_values.json && export JWTINFO_TEST_KEYCLOAK=$(cat ~/.config/https-wrench/jwtinfo_test_keycloak_req_values.json) go version create-certs From bdff950c951938c91e4a578f8e3c3e4a8eecf00c Mon Sep 17 00:00:00 2001 From: Zeno Belli Date: Tue, 17 Mar 2026 09:57:32 +0100 Subject: [PATCH 06/23] ci: add git-hook input to devenv --- devenv.lock | 41 +++++++++++++++++++++++++++-------------- devenv.yaml | 15 ++------------- 2 files changed, 29 insertions(+), 27 deletions(-) diff --git a/devenv.lock b/devenv.lock index 823b8f2..3bca2f5 100644 --- a/devenv.lock +++ b/devenv.lock @@ -35,9 +35,7 @@ "inputs": { "flake-compat": "flake-compat", "gitignore": "gitignore", - "nixpkgs": [ - "nixpkgs" - ] + "nixpkgs": "nixpkgs" }, "locked": { "lastModified": 1772024342, @@ -73,20 +71,17 @@ } }, "nixpkgs": { - "inputs": { - "nixpkgs-src": "nixpkgs-src" - }, "locked": { - "lastModified": 1770434727, - "owner": "cachix", - "repo": "devenv-nixpkgs", - "rev": "8430f16a39c27bdeef236f1eeb56f0b51b33d348", + "lastModified": 1773597492, + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "a07d4ce6bee67d7c838a8a5796e75dff9caa21ef", "type": "github" }, "original": { - "owner": "cachix", - "ref": "rolling", - "repo": "devenv-nixpkgs", + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", "type": "github" } }, @@ -122,11 +117,29 @@ "type": "github" } }, + "nixpkgs_2": { + "inputs": { + "nixpkgs-src": "nixpkgs-src" + }, + "locked": { + "lastModified": 1770434727, + "owner": "cachix", + "repo": "devenv-nixpkgs", + "rev": "8430f16a39c27bdeef236f1eeb56f0b51b33d348", + "type": "github" + }, + "original": { + "owner": "cachix", + "ref": "rolling", + "repo": "devenv-nixpkgs", + "type": "github" + } + }, "root": { "inputs": { "devenv": "devenv", "git-hooks": "git-hooks", - "nixpkgs": "nixpkgs", + "nixpkgs": "nixpkgs_2", "nixpkgs-stable": "nixpkgs-stable", "pre-commit-hooks": [ "git-hooks" diff --git a/devenv.yaml b/devenv.yaml index 6a83107..13c4dd2 100644 --- a/devenv.yaml +++ b/devenv.yaml @@ -1,18 +1,7 @@ ---- -# yaml-language-server: $schema=https://devenv.sh/devenv.schema.json inputs: + git-hooks: + url: github:cachix/git-hooks.nix nixpkgs: url: github:cachix/devenv-nixpkgs/rolling nixpkgs-stable: url: github:NixOS/nixpkgs/nixos-25.11 - -# If you're using non-OSS software, you can set allowUnfree to true. -# allowUnfree: true - -# If you're willing to use a package that's vulnerable -# permittedInsecurePackages: -# - "openssl-1.1.1w" - -# If you have more than one devenv you can merge them -#imports: -# - ./backend From bd62c08084f5a56172f12be2dbb285eb3099d847 Mon Sep 17 00:00:00 2001 From: Zeno Belli Date: Tue, 17 Mar 2026 10:10:58 +0100 Subject: [PATCH 07/23] ci: codeChecks workflow continue on test coverage fail --- .github/workflows/codeChecks.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/codeChecks.yml b/.github/workflows/codeChecks.yml index 332ccb3..b568f79 100644 --- a/.github/workflows/codeChecks.yml +++ b/.github/workflows/codeChecks.yml @@ -84,6 +84,7 @@ jobs: run: go test ./... -coverprofile=./cover.out -covermode=atomic -coverpkg=./... - name: check test coverage + continue-on-error: true uses: vladopajic/go-test-coverage@v2 with: config: ./.testcoverage.yml From 811914b789d0429d428071c3aab9b21db9e63d57 Mon Sep 17 00:00:00 2001 From: Zeno Belli Date: Tue, 17 Mar 2026 10:23:40 +0100 Subject: [PATCH 08/23] ci: comment jwtinfo tests in devenv --- devenv.nix | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/devenv.nix b/devenv.nix index fb40616..f62b1f0 100644 --- a/devenv.nix +++ b/devenv.nix @@ -678,11 +678,7 @@ in { run-certinfo-cert-tests run-certinfo-tlsendpoint-tests - if [ -n "$JWTINFO_TEST_AUTH0" ]; then - run-jwtinfo-test-auth0 - run-jwtinfo-test-auth0-no-validation - else - gum format "## Skipping JwtInfo Auth0 tests (JWTINFO_TEST_AUTH0 not set)" - fi + # run-jwtinfo-test-auth0 + # run-jwtinfo-test-auth0-no-validation ''; } From 111761edf332411204da55e5dc2f4bacd4fa47b6 Mon Sep 17 00:00:00 2001 From: Zeno Belli Date: Tue, 17 Mar 2026 21:54:17 +0100 Subject: [PATCH 09/23] fix: base64 decoding, linting --- cmd/jwtinfo.go | 1 + internal/jwtinfo/jwtinfo.go | 33 +++++++++++++++++++-------------- internal/jwtinfo/main_test.go | 16 +++++++++++----- 3 files changed, 31 insertions(+), 19 deletions(-) diff --git a/cmd/jwtinfo.go b/cmd/jwtinfo.go index d317225..22bba7f 100644 --- a/cmd/jwtinfo.go +++ b/cmd/jwtinfo.go @@ -62,6 +62,7 @@ var jwtinfoCmd = &cobra.Command{ err = tokenData.DecodeBase64() if err != nil { fmt.Printf("DecodeBase64 error: %s\n", err) + return } // TODO: turn into method diff --git a/internal/jwtinfo/jwtinfo.go b/internal/jwtinfo/jwtinfo.go index f5cca4c..f2b93aa 100644 --- a/internal/jwtinfo/jwtinfo.go +++ b/internal/jwtinfo/jwtinfo.go @@ -9,6 +9,7 @@ import ( "fmt" "io" "maps" + "mime" "net/http" "net/url" "strconv" @@ -37,10 +38,11 @@ type CustomClaims struct { } type JwtTokenData struct { - AccessToken string `json:"access_token"` - AccessTokenHeader []byte - AccessTokenClaims []byte - RefreshToken string `json:"refresh_token"` + AccessToken string `json:"access_token"` //nolint:tagliatelle // OAuth token field name + AccessTokenHeader []byte + AccessTokenClaims []byte + RefreshToken string `json:"refresh_token"` //nolint:tagliatelle // OAuth token field name + RefreshTokenHeader []byte RefreshTokenClaims []byte } @@ -103,12 +105,12 @@ func RequestToken(reqURL string, reqValues map[string]string, client *http.Clien ) } - respContentType := resp.Header["Content-Type"][0] - if respContentType == "application/jwt" { + mediaType, _, _ := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if mediaType == "application/jwt" { t.AccessToken = string(bodyBytes) } - if respContentType == "application/json" { + if mediaType == "application/json" { if err = json.NewDecoder(resp.Body).Decode(&t); err != nil { return JwtTokenData{}, fmt.Errorf( "error validating token request data: %w", @@ -181,8 +183,11 @@ func (jtd *JwtTokenData) DecodeBase64() error { } tokenB64Elements := strings.Split(token.raw, ".") + if len(tokenB64Elements) < 2 { + return fmt.Errorf("invalid JWT format in %s", token.name) + } - tokenHeader, err = base64.RawStdEncoding.DecodeString(tokenB64Elements[0]) + tokenHeader, err = base64.RawURLEncoding.DecodeString(tokenB64Elements[0]) if err != nil { return fmt.Errorf( "unable to decode base64 header from %s: %w", @@ -191,7 +196,7 @@ func (jtd *JwtTokenData) DecodeBase64() error { ) } - tokenClaims, err = base64.RawStdEncoding.DecodeString(tokenB64Elements[1]) + tokenClaims, err = base64.RawURLEncoding.DecodeString(tokenB64Elements[1]) if err != nil { return fmt.Errorf( "unable to decode base64 claims from %s: %w", @@ -349,7 +354,7 @@ func PrintTokenInfo(jtd JwtTokenData, w io.Writer) error { fmt.Fprintln(w, style.LgSprintf(style.Title, "%s", token.name)) - fmt.Println() + fmt.Fprintln(w) fmt.Fprintln(w, style.LgSprintf(style.ItemKey, "Header")) var prettyJSON bytes.Buffer @@ -361,10 +366,10 @@ func PrintTokenInfo(jtd JwtTokenData, w io.Writer) error { headerCode := prettyJSON.String() - fmt.Print(style.CodeSyntaxHighlightWithStyle("json", headerCode, chromaStyle)) + fmt.Fprint(w, style.CodeSyntaxHighlightWithStyle("json", headerCode, chromaStyle)) prettyJSON.Reset() - fmt.Println() + fmt.Fprintln(w) fmt.Fprintln(w, style.LgSprintf(style.ItemKey, "Claims")) tokenTimeClaims, err := unmarshallTokenTimeClaims(token.claims) @@ -385,8 +390,8 @@ func PrintTokenInfo(jtd JwtTokenData, w io.Writer) error { claimsCode := prettyJSON.String() - fmt.Print(style.CodeSyntaxHighlightWithStyle("json", claimsCode, chromaStyle)) - fmt.Println() + fmt.Fprint(w, style.CodeSyntaxHighlightWithStyle("json", claimsCode, chromaStyle)) + fmt.Fprintln(w) } return nil diff --git a/internal/jwtinfo/main_test.go b/internal/jwtinfo/main_test.go index 124d5ab..94e79c0 100644 --- a/internal/jwtinfo/main_test.go +++ b/internal/jwtinfo/main_test.go @@ -112,12 +112,18 @@ func jwksHandler(writer http.ResponseWriter, request *http.Request) { jwk, err := jwkset.NewJWKFromKey(signKey, options) if err != nil { fmt.Printf("failed to create JWK from key: %s", err) + writer.WriteHeader(http.StatusInternalServerError) + + return } // Write the key to the JWK Set storage. err = jwkSet.KeyWrite(ctx, jwk) if err != nil { fmt.Printf("failed to store RSA key: %s", err) + writer.WriteHeader(http.StatusInternalServerError) + + return } response, err := jwkSet.JSONPublic(request.Context()) @@ -175,11 +181,6 @@ func authHandler(w http.ResponseWriter, r *http.Request) { } tokenString, err := createToken(user) - tokenJSONString := fmt.Sprintf( - "{\"access_token\":\"%s\"}", - tokenString, - ) - if err != nil { w.WriteHeader(http.StatusInternalServerError) @@ -190,6 +191,11 @@ func authHandler(w http.ResponseWriter, r *http.Request) { return } + tokenJSONString := fmt.Sprintf( + "{\"access_token\":\"%s\"}", + tokenString, + ) + if scope == "applicationJson" { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) From 841ed6494fbcd5d11a535caf2bd03b61149a261b Mon Sep 17 00:00:00 2001 From: Zeno Belli Date: Tue, 17 Mar 2026 21:54:54 +0100 Subject: [PATCH 10/23] ci: report fails from test coverage on main only --- .github/workflows/codeChecks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codeChecks.yml b/.github/workflows/codeChecks.yml index b568f79..1107c57 100644 --- a/.github/workflows/codeChecks.yml +++ b/.github/workflows/codeChecks.yml @@ -84,7 +84,7 @@ jobs: run: go test ./... -coverprofile=./cover.out -covermode=atomic -coverpkg=./... - name: check test coverage - continue-on-error: true + continue-on-error: ${{ github.ref_name != 'main' }} uses: vladopajic/go-test-coverage@v2 with: config: ./.testcoverage.yml From 5a56ffabf0547c0b3a7017f6bd0b890d8b7c1cf9 Mon Sep 17 00:00:00 2001 From: Zeno Belli Date: Tue, 17 Mar 2026 22:28:16 +0100 Subject: [PATCH 11/23] fix: check type when extracting time claims --- internal/jwtinfo/jwtinfo.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/internal/jwtinfo/jwtinfo.go b/internal/jwtinfo/jwtinfo.go index f2b93aa..a01de9a 100644 --- a/internal/jwtinfo/jwtinfo.go +++ b/internal/jwtinfo/jwtinfo.go @@ -220,7 +220,7 @@ func (jtd *JwtTokenData) DecodeBase64() error { } func ParseTokenData(jtd JwtTokenData, jwksURL string, keyfuncOverride keyfunc.Override) (*jwt.Token, error) { - // Parsing the access token whitout validation + // Parsing the access token without validation if jwksURL == "" { token, _, err := jwt.NewParser().ParseUnverified( jtd.AccessToken, @@ -410,10 +410,18 @@ func unmarshallTokenTimeClaims(claims []byte) (map[string]string, error) { return nil, errors.New("unable to find Issued At (iat) in token Claims") } + if _, ok := genericClaims["iat"].(float64); !ok { + return nil, errors.New("Issued At (iat) claim is not a numeric timestamp") + } + if _, ok := genericClaims["exp"]; !ok { return nil, errors.New("unable to find Expiration Time (exp) in token Claims") } + if _, ok := genericClaims["exp"].(float64); !ok { + return nil, errors.New("Expiration Time (exp) claim is not a numeric timestamp") + } + for k, v := range genericClaims { var vi any = v From bcd3f656d932ad953a17eceefb1274aaa757fb05 Mon Sep 17 00:00:00 2001 From: Zeno Belli Date: Tue, 17 Mar 2026 22:29:09 +0100 Subject: [PATCH 12/23] ci: refine file input checks and webserver reponses --- internal/jwtinfo/main_test.go | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/internal/jwtinfo/main_test.go b/internal/jwtinfo/main_test.go index 94e79c0..d9fb1de 100644 --- a/internal/jwtinfo/main_test.go +++ b/internal/jwtinfo/main_test.go @@ -144,7 +144,14 @@ func jwksFaultyHandler(writer http.ResponseWriter, _ *http.Request) { // validJwksFile := "testdata/jwkset-from-rsa-private-key-valid.json" corruptedJwksFile := "testdata/jwkset-from-rsa-private-key-corrupted.json" - jwksContent, _ := os.ReadFile(corruptedJwksFile) + + jwksContent, err := os.ReadFile(corruptedJwksFile) + if err != nil { + log.Printf("failed to read corrupted JWKS file: %s", err) + writer.WriteHeader(http.StatusInternalServerError) + + return + } writer.Header().Set("Content-Type", "application/json") _, _ = writer.Write(jwksContent) @@ -214,9 +221,8 @@ func authHandler(w http.ResponseWriter, r *http.Request) { if scope == "appJwtInvalid" { w.Header().Set("Content-Type", "application/jwt") - // w.WriteHeader(http.StatusOK) - fmt.Println("JWT invalid") - // _, _ = fmt.Fprintln(w, "") + w.WriteHeader(http.StatusOK) + _, _ = fmt.Fprintln(w, "invalid-jwt-token") return } From 1c59677350e8099158baa610b4baa34e54063236 Mon Sep 17 00:00:00 2001 From: Zeno Belli Date: Tue, 17 Mar 2026 22:47:11 +0100 Subject: [PATCH 13/23] fix: remove unused struct --- internal/jwtinfo/jwtinfo.go | 18 ++++-------------- internal/jwtinfo/main_test.go | 3 ++- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/internal/jwtinfo/jwtinfo.go b/internal/jwtinfo/jwtinfo.go index a01de9a..0b44acb 100644 --- a/internal/jwtinfo/jwtinfo.go +++ b/internal/jwtinfo/jwtinfo.go @@ -28,21 +28,11 @@ var ( userAgent = "HTTPS-Wrench/JwtInfo" ) -type CustomClaims struct { - Issuer string `json:"iss"` - Subject string `json:"sub"` - ExpiresAt int64 `json:"exp"` - NotBefore int64 `json:"nbf"` - IssuedAt int64 `json:"iat"` - jwt.RegisteredClaims -} - type JwtTokenData struct { - AccessToken string `json:"access_token"` //nolint:tagliatelle // OAuth token field name - AccessTokenHeader []byte - AccessTokenClaims []byte - RefreshToken string `json:"refresh_token"` //nolint:tagliatelle // OAuth token field name - + AccessToken string `json:"access_token"` //nolint:tagliatelle // OAuth token field name + AccessTokenHeader []byte + AccessTokenClaims []byte + RefreshToken string `json:"refresh_token"` //nolint:tagliatelle // OAuth token field name RefreshTokenHeader []byte RefreshTokenClaims []byte } diff --git a/internal/jwtinfo/main_test.go b/internal/jwtinfo/main_test.go index d9fb1de..a2e040e 100644 --- a/internal/jwtinfo/main_test.go +++ b/internal/jwtinfo/main_test.go @@ -79,12 +79,13 @@ func createToken(user string) (string, error) { // set the expire time // see https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4 ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Minute * 1)), + IssuedAt: jwt.NewNumericDate(time.Now()), }, "level1", CustomerInfo{user, "human"}, } - // Creat token string + // Create token string return t.SignedString(signKey) } From d2c3c6b5e7772d421294ce870e0897071c963091 Mon Sep 17 00:00:00 2001 From: Zeno Belli Date: Tue, 17 Mar 2026 23:17:17 +0100 Subject: [PATCH 14/23] ci: devenv test --- devenv.nix | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/devenv.nix b/devenv.nix index f62b1f0..b6340d7 100644 --- a/devenv.nix +++ b/devenv.nix @@ -648,8 +648,8 @@ in { enterShell = '' gum format "# Devenv shell" - export GITEA_TOKEN=$(cat ~/.config/goreleaser/gitea_token) - export GITHUB_TOKEN=$(cat ~/.config/goreleaser/github_token) + # export GITEA_TOKEN=$(cat ~/.config/goreleaser/gitea_token) + # export GITHUB_TOKEN=$(cat ~/.config/goreleaser/github_token) # JwtInfo tests against authentication providers when not on CI # test -f ~/.config/https-wrench/jwtinfo_test_auth0_req_values.json && export JWTINFO_TEST_AUTH0=$(cat ~/.config/https-wrench/jwtinfo_test_auth0_req_values.json) @@ -661,22 +661,23 @@ in { enterTest = '' gum format "# Running tests" - # update-go-deps + echo "About to run tests" + build #run-go-tests - test-cmd-root-version - test-cmd-requests-version - test-cmd-certinfo-version - test-cmd-root-help-when-no-flags - test-cmd-requests-help-when-no-flags - test-cmd-certinfo-help-when-no-flags - + # test-cmd-root-version + # test-cmd-requests-version + # test-cmd-certinfo-version + # test-cmd-root-help-when-no-flags + # test-cmd-requests-help-when-no-flags + # test-cmd-certinfo-help-when-no-flags + # run-requests-tests - run-certinfo-priv-key-tests - run-certinfo-cert-tests - run-certinfo-tlsendpoint-tests + # run-certinfo-priv-key-tests + # run-certinfo-cert-tests + # run-certinfo-tlsendpoint-tests # run-jwtinfo-test-auth0 # run-jwtinfo-test-auth0-no-validation From b9fbf2fa5950cfcc1fbac3d0c7ef8d636d7b4ecb Mon Sep 17 00:00:00 2001 From: Zeno Belli Date: Tue, 17 Mar 2026 23:23:48 +0100 Subject: [PATCH 15/23] test: ci pipeline break --- devenv.nix | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/devenv.nix b/devenv.nix index b6340d7..21180f1 100644 --- a/devenv.nix +++ b/devenv.nix @@ -616,6 +616,12 @@ in { ./dist/https-wrench jwtinfo --request-url "$REQ_URL" --request-values-json "$JWTINFO_TEST_KEYCLOAK" --validation-url "$VALIDATION_URL" ''; + scripts.test-pipeline-break.exec = '' + echo "Testing pipeline break" + + exit 1 + ''; + scripts.run-go-tests.exec = '' gum format "## Run GO tests" @@ -663,8 +669,11 @@ in { gum format "# Running tests" echo "About to run tests" + test-pipeline-break + build + #run-go-tests # test-cmd-root-version From 075fba98f19f2e2c85dd0120465ab32733764043 Mon Sep 17 00:00:00 2001 From: Zeno Belli Date: Tue, 17 Mar 2026 23:34:43 +0100 Subject: [PATCH 16/23] test: split devenv shell commands --- .github/workflows/codeChecks.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/codeChecks.yml b/.github/workflows/codeChecks.yml index 1107c57..e3fc927 100644 --- a/.github/workflows/codeChecks.yml +++ b/.github/workflows/codeChecks.yml @@ -71,6 +71,9 @@ jobs: run: devenv test timeout-minutes: 15 + - name: Run a single command in the devenv shell + run: devenv shell run-certinfo-cert-tests + go_test_coverage_check: needs: go_tests runs-on: ubuntu-latest From 72e4ef035217f67cabf8bccecc2d686cb208de8b Mon Sep 17 00:00:00 2001 From: Zeno Belli Date: Tue, 17 Mar 2026 23:36:52 +0100 Subject: [PATCH 17/23] test: remove pipeline break --- devenv.nix | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/devenv.nix b/devenv.nix index 21180f1..23a97b7 100644 --- a/devenv.nix +++ b/devenv.nix @@ -667,26 +667,20 @@ in { enterTest = '' gum format "# Running tests" - echo "About to run tests" - - test-pipeline-break build + test-cmd-root-version + test-cmd-requests-version + test-cmd-certinfo-version + test-cmd-root-help-when-no-flags + test-cmd-requests-help-when-no-flags + test-cmd-certinfo-help-when-no-flags - #run-go-tests - - # test-cmd-root-version - # test-cmd-requests-version - # test-cmd-certinfo-version - # test-cmd-root-help-when-no-flags - # test-cmd-requests-help-when-no-flags - # test-cmd-certinfo-help-when-no-flags - # run-requests-tests - # run-certinfo-priv-key-tests - # run-certinfo-cert-tests - # run-certinfo-tlsendpoint-tests + run-certinfo-priv-key-tests + run-certinfo-cert-tests + run-certinfo-tlsendpoint-tests # run-jwtinfo-test-auth0 # run-jwtinfo-test-auth0-no-validation From 784ade4139ea95137622644e123c873de0fdefbb Mon Sep 17 00:00:00 2001 From: Zeno Belli Date: Tue, 17 Mar 2026 23:44:30 +0100 Subject: [PATCH 18/23] test: run build in a separate ci step --- .github/workflows/codeChecks.yml | 6 +++--- devenv.nix | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeChecks.yml b/.github/workflows/codeChecks.yml index e3fc927..7c7eac1 100644 --- a/.github/workflows/codeChecks.yml +++ b/.github/workflows/codeChecks.yml @@ -65,15 +65,15 @@ jobs: - name: Install devenv.sh run: nix profile add nixpkgs#devenv + - name: Run devenv build + run: devenv shell build + - name: Build the devenv shell and run any pre-commit hooks env: JWTINFO_TEST_AUTH0: ${{ secrets.JWTINFO_TEST_AUTH0 }} run: devenv test timeout-minutes: 15 - - name: Run a single command in the devenv shell - run: devenv shell run-certinfo-cert-tests - go_test_coverage_check: needs: go_tests runs-on: ubuntu-latest diff --git a/devenv.nix b/devenv.nix index 23a97b7..daebd4e 100644 --- a/devenv.nix +++ b/devenv.nix @@ -668,7 +668,7 @@ in { enterTest = '' gum format "# Running tests" - build + #build test-cmd-root-version test-cmd-requests-version From 48a947f364697a067f95fe3b5c38a52e02ccb8b8 Mon Sep 17 00:00:00 2001 From: Zeno Belli Date: Wed, 18 Mar 2026 00:40:59 +0100 Subject: [PATCH 19/23] test: install-nix-action use nixpkgs stable --- .github/workflows/codeChecks.yml | 4 +--- devenv.nix | 14 +++++--------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/.github/workflows/codeChecks.yml b/.github/workflows/codeChecks.yml index 7c7eac1..3b7bd76 100644 --- a/.github/workflows/codeChecks.yml +++ b/.github/workflows/codeChecks.yml @@ -57,6 +57,7 @@ jobs: - uses: cachix/install-nix-action@v31 with: github_access_token: ${{ secrets.GITHUB_TOKEN }} + nix_path: nixpkgs=channel:nixos-25.11 - uses: cachix/cachix-action@v16 with: @@ -65,9 +66,6 @@ jobs: - name: Install devenv.sh run: nix profile add nixpkgs#devenv - - name: Run devenv build - run: devenv shell build - - name: Build the devenv shell and run any pre-commit hooks env: JWTINFO_TEST_AUTH0: ${{ secrets.JWTINFO_TEST_AUTH0 }} diff --git a/devenv.nix b/devenv.nix index daebd4e..f62b1f0 100644 --- a/devenv.nix +++ b/devenv.nix @@ -616,12 +616,6 @@ in { ./dist/https-wrench jwtinfo --request-url "$REQ_URL" --request-values-json "$JWTINFO_TEST_KEYCLOAK" --validation-url "$VALIDATION_URL" ''; - scripts.test-pipeline-break.exec = '' - echo "Testing pipeline break" - - exit 1 - ''; - scripts.run-go-tests.exec = '' gum format "## Run GO tests" @@ -654,8 +648,8 @@ in { enterShell = '' gum format "# Devenv shell" - # export GITEA_TOKEN=$(cat ~/.config/goreleaser/gitea_token) - # export GITHUB_TOKEN=$(cat ~/.config/goreleaser/github_token) + export GITEA_TOKEN=$(cat ~/.config/goreleaser/gitea_token) + export GITHUB_TOKEN=$(cat ~/.config/goreleaser/github_token) # JwtInfo tests against authentication providers when not on CI # test -f ~/.config/https-wrench/jwtinfo_test_auth0_req_values.json && export JWTINFO_TEST_AUTH0=$(cat ~/.config/https-wrench/jwtinfo_test_auth0_req_values.json) @@ -667,8 +661,10 @@ in { enterTest = '' gum format "# Running tests" + # update-go-deps + build - #build + #run-go-tests test-cmd-root-version test-cmd-requests-version From 414cee35351f8470842ee07585b485f8fe18e081 Mon Sep 17 00:00:00 2001 From: Zeno Belli Date: Wed, 18 Mar 2026 00:48:20 +0100 Subject: [PATCH 20/23] test: add stable nixpath to devenv install step in ci --- .github/workflows/codeChecks.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/codeChecks.yml b/.github/workflows/codeChecks.yml index 3b7bd76..81a0e7b 100644 --- a/.github/workflows/codeChecks.yml +++ b/.github/workflows/codeChecks.yml @@ -65,6 +65,8 @@ jobs: - name: Install devenv.sh run: nix profile add nixpkgs#devenv + with: + nix_path: nixpkgs=channel:nixos-25.11 - name: Build the devenv shell and run any pre-commit hooks env: From 46ce1555f6bb2e205762a576996476d3b1a89d81 Mon Sep 17 00:00:00 2001 From: Zeno Belli Date: Thu, 19 Mar 2026 08:13:03 +0100 Subject: [PATCH 21/23] ci: disble devenv related tests for Github actions --- .github/workflows/codeChecks.yml | 104 +++++++++++++++---------------- 1 file changed, 52 insertions(+), 52 deletions(-) diff --git a/.github/workflows/codeChecks.yml b/.github/workflows/codeChecks.yml index 81a0e7b..19b832c 100644 --- a/.github/workflows/codeChecks.yml +++ b/.github/workflows/codeChecks.yml @@ -46,34 +46,34 @@ jobs: go-package: ./... work-dir: . - devenv_test: - needs: go_tests - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v5 - - - uses: cachix/install-nix-action@v31 - with: - github_access_token: ${{ secrets.GITHUB_TOKEN }} - nix_path: nixpkgs=channel:nixos-25.11 - - - uses: cachix/cachix-action@v16 - with: - name: devenv - - - name: Install devenv.sh - run: nix profile add nixpkgs#devenv - with: - nix_path: nixpkgs=channel:nixos-25.11 - - - name: Build the devenv shell and run any pre-commit hooks - env: - JWTINFO_TEST_AUTH0: ${{ secrets.JWTINFO_TEST_AUTH0 }} - run: devenv test - timeout-minutes: 15 - + # # WARN this action will install devenv 2.x.x while + # # the repo still uses 1.11.1. Disabling it until devenv is upgraded + # devenv_test: + # needs: go_tests + # runs-on: ubuntu-latest + # + # steps: + # - name: Checkout + # uses: actions/checkout@v5 + # + # - uses: cachix/install-nix-action@v31 + # with: + # github_access_token: ${{ secrets.GITHUB_TOKEN }} + # nix_path: nixpkgs=channel:nixos-25.11 + # + # - uses: cachix/cachix-action@v16 + # with: + # name: devenv + # + # - name: Install devenv.sh + # run: nix profile add nixpkgs#devenv + # + # - name: Build the devenv shell and run any pre-commit hooks + # env: + # JWTINFO_TEST_AUTH0: ${{ secrets.JWTINFO_TEST_AUTH0 }} + # run: devenv test + # timeout-minutes: 15 + # go_test_coverage_check: needs: go_tests runs-on: ubuntu-latest @@ -94,27 +94,27 @@ jobs: git-token: ${{ github.ref_name == 'main' && secrets.GITHUB_TOKEN || '' }} git-branch: badges - goreleaser_test: - needs: devenv_test - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v5 - with: - fetch-depth: 0 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Go - uses: actions/setup-go@v6 - with: - go-version: "1.25" - - - name: Run GoReleaser test - uses: goreleaser/goreleaser-action@v6 - with: - version: "~> 2" - args: release --snapshot --clean - workdir: . + # goreleaser_test: + # needs: devenv_test + # runs-on: ubuntu-latest + # + # steps: + # - name: Checkout + # uses: actions/checkout@v5 + # with: + # fetch-depth: 0 + # + # - name: Set up QEMU + # uses: docker/setup-qemu-action@v3 + # + # - name: Set up Go + # uses: actions/setup-go@v6 + # with: + # go-version: "1.25" + # + # - name: Run GoReleaser test + # uses: goreleaser/goreleaser-action@v6 + # with: + # version: "~> 2" + # args: release --snapshot --clean + # workdir: . From 14a1662c43ac303c0024603a5aad41b84f9d6fdd Mon Sep 17 00:00:00 2001 From: Zeno Belli Date: Sat, 21 Mar 2026 20:17:17 +0100 Subject: [PATCH 22/23] ci: add tests for DecodeBase64, PrintTokenInfo and unmarshallTokenTimeClaims --- devenv.nix | 5 +- internal/jwtinfo/jwtinfo.go | 29 ++- internal/jwtinfo/jwtinfo_test.go | 311 +++++++++++++++++++++++++++++++ 3 files changed, 339 insertions(+), 6 deletions(-) diff --git a/devenv.nix b/devenv.nix index f62b1f0..38ea610 100644 --- a/devenv.nix +++ b/devenv.nix @@ -50,6 +50,7 @@ in { "internal/certinfo/common_handlers.go" "internal/certinfo/testdata" "internal/jwtinfo/testdata" + "internal/jwtinfo/jwtinfo_test.go" "internal/certinfo/testdata/README.md" "completions" ]; @@ -678,7 +679,7 @@ in { run-certinfo-cert-tests run-certinfo-tlsendpoint-tests - # run-jwtinfo-test-auth0 - # run-jwtinfo-test-auth0-no-validation + run-jwtinfo-test-auth0 + run-jwtinfo-test-auth0-no-validation ''; } diff --git a/internal/jwtinfo/jwtinfo.go b/internal/jwtinfo/jwtinfo.go index 0b44acb..69b7de0 100644 --- a/internal/jwtinfo/jwtinfo.go +++ b/internal/jwtinfo/jwtinfo.go @@ -146,6 +146,11 @@ func ParseRequestJSONValues( return reqValuesMap, nil } +func isValidJSON(data []byte) bool { + var v any + return json.Unmarshal(data, &v) == nil +} + func (jtd *JwtTokenData) DecodeBase64() error { tokens := []struct { name string @@ -173,8 +178,8 @@ func (jtd *JwtTokenData) DecodeBase64() error { } tokenB64Elements := strings.Split(token.raw, ".") - if len(tokenB64Elements) < 2 { - return fmt.Errorf("invalid JWT format in %s", token.name) + if len(tokenB64Elements) < 3 { + return fmt.Errorf("invalid three dotted JWT format in %s", token.name) } tokenHeader, err = base64.RawURLEncoding.DecodeString(tokenB64Elements[0]) @@ -186,6 +191,14 @@ func (jtd *JwtTokenData) DecodeBase64() error { ) } + if !isValidJSON(tokenHeader) { + return fmt.Errorf( + "invalid JSON found in header from %s: %w", + token.name, + err, + ) + } + tokenClaims, err = base64.RawURLEncoding.DecodeString(tokenB64Elements[1]) if err != nil { return fmt.Errorf( @@ -195,6 +208,14 @@ func (jtd *JwtTokenData) DecodeBase64() error { ) } + if !isValidJSON(tokenClaims) { + return fmt.Errorf( + "invalid JSON found in claims from %s: %w", + token.name, + err, + ) + } + if token.name == "AccessToken" { jtd.AccessTokenHeader = tokenHeader jtd.AccessTokenClaims = tokenClaims @@ -393,7 +414,7 @@ func unmarshallTokenTimeClaims(claims []byte) (map[string]string, error) { genericClaims := make(map[string]any) if err := json.Unmarshal(claims, &genericClaims); err != nil { - return nil, err + return nil, fmt.Errorf("unable to unmarshall claims: %w", err) } if _, ok := genericClaims["iat"]; !ok { @@ -418,7 +439,7 @@ func unmarshallTokenTimeClaims(claims []byte) (map[string]string, error) { if vf, ok := vi.(float64); ok { vInt64 := int64(vf) t := time.Unix(vInt64, 0) - dateUtc := t.UTC().String() + dateUtc := t.UTC().Format(time.UnixDate) tokenClaims[k] = fmt.Sprintf("%v", dateUtc) continue diff --git a/internal/jwtinfo/jwtinfo_test.go b/internal/jwtinfo/jwtinfo_test.go index 8a34abb..1a25a88 100644 --- a/internal/jwtinfo/jwtinfo_test.go +++ b/internal/jwtinfo/jwtinfo_test.go @@ -1,11 +1,15 @@ package jwtinfo import ( + "bytes" + "encoding/base64" "io" "maps" "testing" + "time" "github.com/MicahParks/keyfunc/v3" + "github.com/golang-jwt/jwt/v5" "github.com/stretchr/testify/require" ) @@ -477,3 +481,310 @@ func TestParseTokenData_Errors(t *testing.T) { ) }) } + +func TestDecodeBase64(t *testing.T) { + notThreeDotted := "notThreeDottedBase64CompliantString" + + validJSONHeader := "{\"header\":\"validHeader\"}" + validJSONClaims := "{\"claims\":\"validClaim\"}" + + invalidJSONHeader := "{\"headerNoQuote:\"invalidHeader\"}" + invalidJSONClaims := "{\"claimsNoQuote:\"validClaim\"}" + + b64notThreeDotted := base64.RawURLEncoding.EncodeToString([]byte(notThreeDotted)) + + b64ValidJSONHeader := base64.RawURLEncoding.EncodeToString([]byte(validJSONHeader)) + b64ValidJSONClaims := base64.RawURLEncoding.EncodeToString([]byte(validJSONClaims)) + + b64InvalidJSONHeader := base64.RawURLEncoding.EncodeToString([]byte(invalidJSONHeader)) + b64InvalidJSONClaims := base64.RawURLEncoding.EncodeToString([]byte(invalidJSONClaims)) + + notB64JSONEncodedHeader := "{\"header\":\"invalidBase64$%^&^&\"}" + notB64JSONEncodedClaims := "{\"claims\":\"invalidBase64$%^&^&\"}" + + signaturePlaceholder := "signature placeholder" + + invalidB64HeaderTokenString := notB64JSONEncodedHeader + "." + b64ValidJSONClaims + "." + signaturePlaceholder + invalidB64ClaimsTokenString := b64ValidJSONHeader + "." + notB64JSONEncodedClaims + "." + signaturePlaceholder + + invalidJSONHeaderTokenString := b64InvalidJSONHeader + "." + b64ValidJSONClaims + "." + signaturePlaceholder + invalidJSONClaimsTokenString := b64ValidJSONHeader + "." + b64InvalidJSONClaims + "." + signaturePlaceholder + + tests := []struct { + name string + tokenString string + errMsg string + }{ + { + name: "not three dotted string", + tokenString: b64notThreeDotted, + errMsg: "invalid three dotted JWT format in", + }, + { + name: "invalid base64 header", + tokenString: invalidB64HeaderTokenString, + errMsg: "unable to decode base64 header from", + }, + + { + name: "invalid base64 claims", + tokenString: invalidB64ClaimsTokenString, + errMsg: "unable to decode base64 claims from", + }, + + { + name: "invalid JSON header", + tokenString: invalidJSONHeaderTokenString, + errMsg: "invalid JSON found in header from", + }, + { + name: "invalid JSON claims", + tokenString: invalidJSONClaimsTokenString, + errMsg: "invalid JSON found in claims from", + }, + } + + for _, tc := range tests { + tt := tc + + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + accessTokenRaw, err := createToken("demo") + require.NoError(t, err) + + td := JwtTokenData{AccessToken: accessTokenRaw} + err = td.DecodeBase64() + require.NoError(t, err) + + require.Contains(t, string(td.AccessTokenClaims), "demo") + require.NoError(t, err) + + tdAccessTokenTest := td + tdAccessTokenTest.AccessToken = tt.tokenString + err = tdAccessTokenTest.DecodeBase64() + require.ErrorContains(t, err, tt.errMsg) + + refreshTokenRaw, err := createToken("demo") + require.NoError(t, err) + + tdR := JwtTokenData{RefreshToken: refreshTokenRaw} + err = tdR.DecodeBase64() + require.NoError(t, err) + + tdRefreshTokenTest := tdR + tdRefreshTokenTest.RefreshToken = tt.tokenString + err = tdRefreshTokenTest.DecodeBase64() + require.ErrorContains(t, err, tt.errMsg) + }) + } +} + +func TestUnmarshallTokenTimeClaims(t *testing.T) { + t.Run("unmarshallTokenTimeClaims", func(t *testing.T) { + t.Parallel() + + var jtd JwtTokenData + + var err error + + now := time.Now() + inOneMinute := time.Now().Add(time.Minute * 1) + expiresAt := jwt.NewNumericDate(inOneMinute) + issuedAt := jwt.NewNumericDate(now) + + testTimeClaims := make(map[string]time.Time) + testTimeClaims["iat"] = now + testTimeClaims["exp"] = inOneMinute + + token := jwt.New(jwt.GetSigningMethod("RS256")) + + token.Claims = &CustomClaimsExample{ + jwt.RegisteredClaims{ + ExpiresAt: expiresAt, + IssuedAt: issuedAt, + }, + "level1", + CustomerInfo{"demo", "human"}, + } + + jtd.AccessToken, err = token.SignedString(signKey) + require.NoError(t, err) + + err = jtd.DecodeBase64() + require.NoError(t, err) + + claimsMap, err := unmarshallTokenTimeClaims( + jtd.AccessTokenClaims, + ) + require.NoError(t, err) + + _, ok := claimsMap["iat"] + require.True(t, ok, "key iat (Issued At) must exist") + + _, ok = claimsMap["exp"] + require.True(t, ok, "key exp (Expiration Time) must exist") + + for k, testTimeClaim := range testTimeClaims { + dateUtcString := testTimeClaim.UTC().Format(time.UnixDate) + require.Equal(t, dateUtcString, claimsMap[k]) + } + }) +} + +func TestUnmarshallTokenTimeClaims_MapErrors(t *testing.T) { + invalidJSONClaims := "can not unmarshal" + + noIatClaims := "{\"exp\":1}" + iatStringClaims := "{\"iat\":\"now\"}" + + noExpClaims := "{\"iat\":1}" + expStringClaims := "{\"exp\":\"now\", \"iat\":1}" + + tests := []struct { + name string + claims []byte + errMsg string + }{ + { + name: "invalid JSON", + claims: []byte(invalidJSONClaims), + errMsg: "unable to unmarshall claims", + }, + { + name: "missing Issued At", + claims: []byte(noIatClaims), + errMsg: "unable to find Issued At (iat) in token Claims", + }, + { + name: "not numeric Issued At", + claims: []byte(iatStringClaims), + errMsg: "Issued At (iat) claim is not a numeric timestamp", + }, + + { + name: "claims no Expiration Time", + claims: []byte(noExpClaims), + errMsg: "unable to find Expiration Time (exp) in token Claims", + }, + { + name: "not numeric Expiration Time", + claims: []byte(expStringClaims), + errMsg: "Expiration Time (exp) claim is not a numeric timestamp", + }, + } + + for _, test := range tests { + tt := test + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + _, err := unmarshallTokenTimeClaims(tt.claims) + require.ErrorContains(t, err, tt.errMsg) + }) + } +} + +func TestPrintTokenInfo(t *testing.T) { + tests := []struct { + name string + user string + pass string + scope string + bodyReader allReader + expError bool + expReqError bool + }{ + { + name: "default case", + user: "test", + pass: "known", + bodyReader: io.ReadAll, + scope: "default", + }, + } + + for _, tc := range tests { + tt := tc + + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + server, err := NewJwtTestServer() + + require.NoError(t, err) + + defer server.Close() + + client := server.Client() + serverRoot := server.URL + serverJwtEndpoint := serverRoot + "/jwt" + serverJwksEndpoint := serverRoot + "/jwks.json" + + rootResp, err := client.Get(serverRoot) + require.NoError(t, err) + + rootBody, err := io.ReadAll(rootResp.Body) + require.NoError(t, err) + require.Contains(t, string(rootBody), "root handler") + + defer rootResp.Body.Close() + + reqValues := make(map[string]string) + reqValues["user"] = tt.user + reqValues["pass"] = tt.pass + reqValues["scope"] = tt.scope + + td, err := RequestToken( + serverJwtEndpoint, + reqValues, + client, + tt.bodyReader, + ) + require.NoError(t, err) + + keyfuncOverrideTesting := keyfunc.Override{ + Client: server.Client(), + } + + tokenVerified, err := ParseTokenData( + td, + serverJwksEndpoint, + keyfuncOverrideTesting, + ) + require.NoError(t, err) + require.True( + t, + tokenVerified.Valid, + "JWT token must be valid", + ) + + err = td.DecodeBase64() + require.NoError(t, err) + + buffer := bytes.Buffer{} + err = PrintTokenInfo(td, &buffer) + require.NoError(t, err) + + got := buffer.String() + + stringsToCheck := []string{ + "JwtInfo", + "Header", + "Claims", + "alg", + "RS256", + "typ", + "JWT", + "Issued At", + "Expiration Time", + "exp", + "iat", + } + + for _, outStr := range stringsToCheck { + require.Contains(t, got, outStr) + } + }) + } +} From efc39b2d79c07fac821ae08ec461e74ea33d4a11 Mon Sep 17 00:00:00 2001 From: Zeno Belli Date: Sat, 21 Mar 2026 21:09:46 +0100 Subject: [PATCH 23/23] refactor: check on token elements lenght and context in ParseTokenData --- internal/jwtinfo/jwtinfo.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/internal/jwtinfo/jwtinfo.go b/internal/jwtinfo/jwtinfo.go index 69b7de0..4400d36 100644 --- a/internal/jwtinfo/jwtinfo.go +++ b/internal/jwtinfo/jwtinfo.go @@ -178,7 +178,7 @@ func (jtd *JwtTokenData) DecodeBase64() error { } tokenB64Elements := strings.Split(token.raw, ".") - if len(tokenB64Elements) < 3 { + if len(tokenB64Elements) != 3 { return fmt.Errorf("invalid three dotted JWT format in %s", token.name) } @@ -248,7 +248,8 @@ func ParseTokenData(jtd JwtTokenData, jwksURL string, keyfuncOverride keyfunc.Ov } // Parsing and validating the access token - ctx := context.Background() + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() jwks, err := keyfunc.NewDefaultOverrideCtx( ctx,