diff --git a/.github/workflows/codeChecks.yml b/.github/workflows/codeChecks.yml index 3278408..19b832c 100644 --- a/.github/workflows/codeChecks.yml +++ b/.github/workflows/codeChecks.yml @@ -46,29 +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 }} - - - 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 tests - 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 @@ -82,33 +87,34 @@ jobs: run: go test ./... -coverprofile=./cover.out -covermode=atomic -coverpkg=./... - name: check test coverage + continue-on-error: ${{ github.ref_name != 'main' }} uses: vladopajic/go-test-coverage@v2 with: config: ./.testcoverage.yml 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: . 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/cmd/jwtinfo.go b/cmd/jwtinfo.go new file mode 100644 index 0000000..22bba7f --- /dev/null +++ b/cmd/jwtinfo.go @@ -0,0 +1,112 @@ +/* +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) + return + } + + // 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/devenv.lock b/devenv.lock index cd0bf21..3bca2f5 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" } @@ -35,15 +35,13 @@ "inputs": { "flake-compat": "flake-compat", "gitignore": "gitignore", - "nixpkgs": [ - "nixpkgs" - ] + "nixpkgs": "nixpkgs" }, "locked": { - "lastModified": 1765911976, + "lastModified": 1772024342, "owner": "cachix", "repo": "git-hooks.nix", - "rev": "b68b780b69702a090c8bb1b973bab13756cc7a27", + "rev": "6e34e97ed9788b17796ee43ccdbaf871a5c2b476", "type": "github" }, "original": { @@ -74,25 +72,42 @@ }, "nixpkgs": { "locked": { - "lastModified": 1764580874, - "owner": "cachix", - "repo": "devenv-nixpkgs", - "rev": "dcf61356c3ab25f1362b4a4428a6d871e84f1d1d", + "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" + } + }, + "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": { @@ -102,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.nix b/devenv.nix index 03b37f8..38ea610 100644 --- a/devenv.nix +++ b/devenv.nix @@ -49,6 +49,8 @@ in { ".envrc" "internal/certinfo/common_handlers.go" "internal/certinfo/testdata" + "internal/jwtinfo/testdata" + "internal/jwtinfo/jwtinfo_test.go" "internal/certinfo/testdata/README.md" "completions" ]; @@ -589,6 +591,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 +651,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 +678,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 ''; } 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 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= diff --git a/internal/jwtinfo/jwtinfo.go b/internal/jwtinfo/jwtinfo.go new file mode 100644 index 0000000..4400d36 --- /dev/null +++ b/internal/jwtinfo/jwtinfo.go @@ -0,0 +1,608 @@ +package jwtinfo + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io" + "maps" + "mime" + "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 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 + 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, + ) + } + + mediaType, _, _ := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if mediaType == "application/jwt" { + t.AccessToken = string(bodyBytes) + } + + if mediaType == "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 isValidJSON(data []byte) bool { + var v any + return json.Unmarshal(data, &v) == 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, ".") + if len(tokenB64Elements) != 3 { + return fmt.Errorf("invalid three dotted JWT format in %s", token.name) + } + + tokenHeader, err = base64.RawURLEncoding.DecodeString(tokenB64Elements[0]) + if err != nil { + return fmt.Errorf( + "unable to decode base64 header from %s: %w", + token.name, + err, + ) + } + + 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( + "unable to decode base64 claims from %s: %w", + token.name, + err, + ) + } + + 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 + } + + 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 without 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, cancel := context.WithCancel(context.Background()) + defer cancel() + + 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.Fprintln(w) + 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.Fprint(w, style.CodeSyntaxHighlightWithStyle("json", headerCode, chromaStyle)) + prettyJSON.Reset() + + fmt.Fprintln(w) + 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.Fprint(w, style.CodeSyntaxHighlightWithStyle("json", claimsCode, chromaStyle)) + fmt.Fprintln(w) + } + + 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, fmt.Errorf("unable to unmarshall claims: %w", err) + } + + if _, ok := genericClaims["iat"]; !ok { + 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 + + if vf, ok := vi.(float64); ok { + vInt64 := int64(vf) + t := time.Unix(vInt64, 0) + dateUtc := t.UTC().Format(time.UnixDate) + 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..1a25a88 --- /dev/null +++ b/internal/jwtinfo/jwtinfo_test.go @@ -0,0 +1,790 @@ +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" +) + +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", + ) + }) +} + +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) + } + }) + } +} diff --git a/internal/jwtinfo/main_test.go b/internal/jwtinfo/main_test.go new file mode 100644 index 0000000..a2e040e --- /dev/null +++ b/internal/jwtinfo/main_test.go @@ -0,0 +1,249 @@ +// +// 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)), + IssuedAt: jwt.NewNumericDate(time.Now()), + }, + "level1", + CustomerInfo{user, "human"}, + } + + // Create 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) + 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()) + 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, 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) +} + +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) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + + _, _ = fmt.Fprintln(w, "Sorry, error while Signing Token!") + + log.Printf("Token Signing error: %v\n", err) + + return + } + + tokenJSONString := fmt.Sprintf( + "{\"access_token\":\"%s\"}", + tokenString, + ) + + 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.Fprintln(w, "invalid-jwt-token") + + 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 }