From a52b8a57295b10a5358cb898019b01bd33b942c8 Mon Sep 17 00:00:00 2001 From: Thibault NORMAND Date: Tue, 18 Jan 2022 22:51:53 +0100 Subject: [PATCH] feat(template): JWT parser/verifier. (#95) * feat(template): JWT parser/verifier. * doc(changelog): update changelog * doc(template): functions documentation. --- CHANGELOG.md | 17 ++++++- .../1-template-engine/2-functions.md | 29 ++++++++++++ pkg/sdk/security/crypto/encoder.go | 46 +++++++++++++++++++ pkg/template/engine/funcs.go | 6 +++ pkg/template/engine/funcs_test.go | 4 ++ 5 files changed, 101 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75b0ea71..572bdb2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,22 @@ -## 0.2.4 +## 0.2.5 ## Not released yet +FEATURES: + +* template/engine [#95](https://github.com/elastic/harp/pull/95) + * `parseJwt` to parse JWT without signature validation + * `verifyJwt` to parse a JWT with signature validation + +## 0.2.4 + +### 2022-01-14 + +DIST: + +* Github actions release automation +* go: Build with Golang 1.17.6. + ## 0.2.3 ### 2021-12-10 diff --git a/docs/onboarding/1-template-engine/2-functions.md b/docs/onboarding/1-template-engine/2-functions.md index 0e8065b3..2a16e8ef 100644 --- a/docs/onboarding/1-template-engine/2-functions.md +++ b/docs/onboarding/1-template-engine/2-functions.md @@ -487,6 +487,35 @@ Decrypt input encoded as JWE. {{ decryptJwe $passphrase $encrypted }} ``` +#### parseJwt + +Extract claims _WITHOUT_ signature validation. + +```ruby +{{ $token = "..." }} +# Parse the JWT +{{ $t := parseJwt $token }} +# Access token claims +{{ $t.Claims | toJson }} +# Access token headers +{{ $t.Headers | toJson }} +``` + +#### verifyJwt + +Extract claims _WITH_ signature validation. + +```ruby +{{ $token = "..." }} +{{ $key = "..." }} +# Parse the JWT +{{ $t := verifyJwt $token $key.Public }} +# Access token claims +{{ $t.Claims | toJson }} +# Access token headers +{{ $t.Headers | toJson }} +``` + #### toSSH Encode the given key for OpenSSH usages. diff --git a/pkg/sdk/security/crypto/encoder.go b/pkg/sdk/security/crypto/encoder.go index 87e27b2c..619393a1 100644 --- a/pkg/sdk/security/crypto/encoder.go +++ b/pkg/sdk/security/crypto/encoder.go @@ -36,6 +36,7 @@ import ( _ "golang.org/x/crypto/blake2b" "golang.org/x/crypto/ssh" jose "gopkg.in/square/go-jose.v2" + "gopkg.in/square/go-jose.v2/jwt" "github.com/elastic/harp/build/fips" "github.com/elastic/harp/pkg/sdk/security/crypto/bech32" @@ -346,6 +347,51 @@ func ToJWS(payload, privkey interface{}) (string, error) { return serialize, nil } +// ParseJWT unpack a JWT without signature verification. +func ParseJWT(token string) (interface{}, error) { + // Parse token + t, err := jwt.ParseSigned(token) + if err != nil { + return nil, fmt.Errorf("unable to parse input token: %w", err) + } + + // Extract claims without verification + var claims map[string]interface{} + if err := t.UnsafeClaimsWithoutVerification(&claims); err != nil { + return nil, fmt.Errorf("unable to extract claims from token: %w", err) + } + + return struct { + Headers []jose.Header + Claims map[string]interface{} + }{ + Headers: t.Headers, + Claims: claims, + }, nil +} + +func VerifyJWT(token string, key interface{}) (interface{}, error) { + // Parse token + t, err := jwt.ParseSigned(token) + if err != nil { + return nil, fmt.Errorf("unable to parse input token: %w", err) + } + + // Extract claims without verification + var claims map[string]interface{} + if err := t.Claims(key, &claims); err != nil { + return nil, fmt.Errorf("unable to extract claims from token: %w", err) + } + + return struct { + Headers []jose.Header + Claims map[string]interface{} + }{ + Headers: t.Headers, + Claims: claims, + }, nil +} + // Bech32Decode decodes given bech32 encoded string. func Bech32Decode(in string) (interface{}, error) { hrp, data, err := bech32.Decode(in) diff --git a/pkg/template/engine/funcs.go b/pkg/template/engine/funcs.go index aaae71b9..9808e2b4 100644 --- a/pkg/template/engine/funcs.go +++ b/pkg/template/engine/funcs.go @@ -19,6 +19,7 @@ package engine import ( "encoding/base64" + "encoding/hex" "encoding/json" "net/url" "strconv" @@ -74,6 +75,11 @@ func FuncMap(secretReaders []SecretReaderFunc) template.FuncMap { "encryptJwe": crypto.EncryptJWE, "decryptJwe": crypto.DecryptJWE, "toJws": crypto.ToJWS, + "parseJwt": crypto.ParseJWT, + "verifyJwt": crypto.VerifyJWT, + // Hex + "hexenc": hex.EncodeToString, + "hexdec": hex.DecodeString, // Bech32 "bech32enc": bech32.Encode, "bech32dec": crypto.Bech32Decode, diff --git a/pkg/template/engine/funcs_test.go b/pkg/template/engine/funcs_test.go index 99b3a3b6..9a0f13bb 100644 --- a/pkg/template/engine/funcs_test.go +++ b/pkg/template/engine/funcs_test.go @@ -106,6 +106,10 @@ func TestFuncs(t *testing.T) { tpl: `{{ unquote . }}`, expect: `{"channel":"buu","name":"john", "msg":"doe"}`, vars: `"{\"channel\":\"buu\",\"name\":\"john\", \"msg\":\"doe\"}"`, + }, { + tpl: `{{ parseJwt . }}`, + expect: `{[{ HS256 [] map[typ:JWT]}] map[iat:1.516239022e+09 name:John Doe sub:1234567890]}`, + vars: `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c`, }} for _, tt := range tests {