From ab990e3c7c589b44b1b2025bbb2bcb6c9effa153 Mon Sep 17 00:00:00 2001 From: ehlxr Date: Wed, 24 Jan 2018 16:37:57 +0800 Subject: [PATCH] add jwt tools --- .gitignore | 3 +- glide.lock | 25 ++- glide.yaml | 4 + utils/date/formater_test.go | 2 +- utils/jwt/README.md | 25 +++ utils/jwt/key/jwtRS256.key | 27 +++ utils/jwt/key/jwtRS256.key.pub | 9 + utils/jwt/main.go | 292 +++++++++++++++++++++++++++++++++ utils/log/README.md | 2 +- 9 files changed, 378 insertions(+), 11 deletions(-) create mode 100644 utils/jwt/README.md create mode 100644 utils/jwt/key/jwtRS256.key create mode 100644 utils/jwt/key/jwtRS256.key.pub create mode 100644 utils/jwt/main.go diff --git a/.gitignore b/.gitignore index 3b7bb0f..4769662 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,5 @@ vendor JetBrainsLicenseServer* -jbls \ No newline at end of file +utils/jbls/jbls +utils/jwt/jwt \ No newline at end of file diff --git a/glide.lock b/glide.lock index 88ca173..cac6d94 100644 --- a/glide.lock +++ b/glide.lock @@ -1,16 +1,25 @@ -hash: b4a7296ce90ac2076dfad3f71f09367dd2ab02fddd602a6520055363cd94f0d2 -updated: 2017-06-25T18:28:49.820023852+08:00 +hash: c0f781010f60ae6afa49239d110b06ba51e08cf5d3691d4fe99b6c8990c3f78f +updated: 2018-01-24T16:19:54.509676+08:00 imports: +- name: github.com/dgrijalva/jwt-go + version: dbeaa9332f19a944acb5736b4456cfcc02140e29 - name: github.com/mattn/go-colorable - version: 941b50ebc6efddf4c41c8e4537a5f68a4e686b24 + version: 6cc8b475d4682021d75d2cbe2bc481bec4ce98e5 - name: github.com/mattn/go-isatty - version: fc9e8d8ef48496124e79ae0df75490096eccf6fe + version: 6ca4dbf54d38eea1a992b3c722a76a5d1c4cb25c - name: github.com/mgutz/ansi version: 9520e82c474b0a04dd04f8a40959027271bab992 - name: github.com/sirupsen/logrus - version: 3d4380f53a34dcdc95f0c1db702615992b38d9a4 + version: d682213848ed68c0a260ca37d6dd5ace8423f5ba - name: github.com/x-cray/logrus-prefixed-formatter - version: 31b341217ca8d2c6958611c89032550fcd015df8 -- name: golang.org/x/sys/unix - version: c23410a886927bab8ca5e80b08af6a56faeb330d + version: bb2702d423886830dee131692131d35648c382e2 +- name: golang.org/x/crypto + version: 3d37316aaa6bd9929127ac9a527abf408178ea7b + subpackages: + - ssh/terminal +- name: golang.org/x/sys + version: af50095a40f9041b3b38960738837185c26e9419 + subpackages: + - unix + - windows testImports: [] diff --git a/glide.yaml b/glide.yaml index 22b0494..eff8cc7 100644 --- a/glide.yaml +++ b/glide.yaml @@ -1,4 +1,8 @@ package: github.com/ehlxr/go-utils import: +- package: github.com/dgrijalva/jwt-go + version: ^3.1.0 - package: github.com/sirupsen/logrus + version: ^1.0.4 - package: github.com/x-cray/logrus-prefixed-formatter + version: ^0.5.2 diff --git a/utils/date/formater_test.go b/utils/date/formater_test.go index da5c7a9..906997b 100644 --- a/utils/date/formater_test.go +++ b/utils/date/formater_test.go @@ -3,7 +3,7 @@ package date import ( "testing" - "github.com/ehlxr/go-utils/log" + "github.com/ehlxr/go-utils/utils/log" ) func TestDateFormater(t *testing.T) { diff --git a/utils/jwt/README.md b/utils/jwt/README.md new file mode 100644 index 0000000..f9daec4 --- /dev/null +++ b/utils/jwt/README.md @@ -0,0 +1,25 @@ +`jwt` command-line tool +======================= + +This is a simple tool to sign, verify and show JSON Web Tokens from +the command line. + +```shell +# The following will create and sign a token +$ echo {\"foo\":\"bar\"} | ./jwt -key key/jwtRS256.key -alg RS256 -sign - + +# then verify it and output the original claims: +$ echo {\"foo\":\"bar\"} | ./jwt -key key/jwtRS256.key -alg RS256 -sign - | ./jwt -key key/jwtRS256.key.pub -alg RS256 -verify - + +# To simply display a token, use: +$ echo {\"foo\":\"bar\"} | ./jwt -key key/jwtRS256.key -alg RS256 -sign - | ./jwt -show - +``` + +> generate rsa256 key + +```shell +# generate key +$ ssh-keygen -t rsa -b 2048 -f jwtRS256.key + +$ openssl rsa -in jwtRS256.key -pubout -outform PEM -out jwtRS256.key.pub +``` \ No newline at end of file diff --git a/utils/jwt/key/jwtRS256.key b/utils/jwt/key/jwtRS256.key new file mode 100644 index 0000000..2b1bb50 --- /dev/null +++ b/utils/jwt/key/jwtRS256.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA43puSCCvTFvjWmWb90B1K9BcNKv6pVc+kqneBuM0lcWVvENc +Q80Kk+3bKLF7N+fmNU/ShuxYiWd4v7Jz5vZGFbbZrHFsbsZ8QOAOF4R9l7FhQXgv +9Rnc4QR1jjqyKmttofRfJ4JTe8JLB2PX3cirKQrXaeUCfnaQFkE/VM+7h9zcXrvk +0xxAoG/mhmW6KkxyRiF87odTysK36ByjFIHlPUPakfa84VY6MrDhJZsWvKawBunH +6H99vCrPdrSEDDnjaexcaH2/4OkBaQbp1VOo0H2fVRTKp0nlhdu5IyYysBh8QeH7 +0JOXOcsUfvRUOZ8plz4krijL5b5hYmm4BFEeBQIDAQABAoIBAEImbj+HUbLQ3dKB +zdMe6XPuSYH/qQm/zzBzOV+jXr1XEe4HYKNO0w6lkp/IzN47D5TdO4mE3VJHxrNo +P5Agec0CuVYOPcwQ6D0taquoGaWtLW0OeFO8S6Eb3NuZcgon51+7Yphoq8JHg+4I +ONQD8NIklvPyZsFD1S7DlpkhN5WQNgpA2FVV6p+zIgwSHiRMj7kOBQ2AGLZx2GZ5 +TN+hApV/Af2Y15NQ6E2G7w8P5Kqpphx2of9GZ/vNovfUogleWadsacYpTj9NyNs/ +tOiJbiKyIaaPoNGyWmZff2a0vSYmR+Xa3ML2mCW/mmKWRD3Frz814AkbD5pIWGE2 +TlUrukECgYEA+n+0b7SrFkJaYFvVtQo60+RsXwaUTiqjPKM++cyrn4YB5unSbpPn +BN1+ccFeLOxYGLyx/+o/zHy6Ar5mrL46AsP4zAxKgdjDBVeUBoXwZBn1R78fMjFH +x0t5yUaWoZS0iu1dKHQVIz8P1rzaQfSjBPsZpy/LdexN57OTKXa9MdkCgYEA6HlO +FCHuYq611UTBC+PGBO6Q5p+joclaku7ftjecpK2XcbpRiAi00lP/kjWIfT5KHvVd ++kJtexx+b9qCT5S0NgCl4Uw6Roo64jyWRHqVLNrZ2NNiHPFwWGiehXjVr/aej/D/ +1XWKhwyZqGJfL092oqZxmpo8cXCIwLGtN5jOhg0CgYEA9xvHWXK2W2z1Tp0JQmBn +C+QH6+3HmxyAjy7SzwVSQDmn4qzCg4avnKMLOxhv2I0FktGCHlxst0JLFK3TB5FY +FKZR5qgxT8oPCFQOmCjErVrWFgK5uX/XuQgBicZyjc7uEyZ4gZGR0IVDEKiX/fxg +XGeANOb3JMsJRwpkn8CcfnECgYAjuG/s3AHbG+lIqdXX09nbbCTLqv/jniLSrO2m +/AiPrTS1/uEEPAI8xzdf6eXdCLMu6pjUGVzlK82ptjOLste17IeQhLv3lsnRdWnJ +f+RoDgCnNmO++sI+c2TYMWb0MLrQd0F4NSVh9uetXeAuTtF10IloLgbXj2kT4rkw +78PcCQKBgFUgSqYsStT8K3QmjsamLwMz4XI8dRAdeZaI4QDTBg06D6DEBk+6xNre +yopcVdLfRhT/G+VUax243Tdb3Znoo+LN1r3SkubXlp7Ikm+Xr31JifU4fONUom63 +Y8CQ97c7/GzubqmwLIoqKseYIDn1uSYrFvaIxrl8EWTY6id4OdhH +-----END RSA PRIVATE KEY----- diff --git a/utils/jwt/key/jwtRS256.key.pub b/utils/jwt/key/jwtRS256.key.pub new file mode 100644 index 0000000..0f92b57 --- /dev/null +++ b/utils/jwt/key/jwtRS256.key.pub @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA43puSCCvTFvjWmWb90B1 +K9BcNKv6pVc+kqneBuM0lcWVvENcQ80Kk+3bKLF7N+fmNU/ShuxYiWd4v7Jz5vZG +FbbZrHFsbsZ8QOAOF4R9l7FhQXgv9Rnc4QR1jjqyKmttofRfJ4JTe8JLB2PX3cir +KQrXaeUCfnaQFkE/VM+7h9zcXrvk0xxAoG/mhmW6KkxyRiF87odTysK36ByjFIHl +PUPakfa84VY6MrDhJZsWvKawBunH6H99vCrPdrSEDDnjaexcaH2/4OkBaQbp1VOo +0H2fVRTKp0nlhdu5IyYysBh8QeH70JOXOcsUfvRUOZ8plz4krijL5b5hYmm4BFEe +BQIDAQAB +-----END PUBLIC KEY----- diff --git a/utils/jwt/main.go b/utils/jwt/main.go new file mode 100644 index 0000000..b64bba7 --- /dev/null +++ b/utils/jwt/main.go @@ -0,0 +1,292 @@ +package main + +import ( + "encoding/json" + "flag" + "fmt" + "io" + "io/ioutil" + "os" + "regexp" + "strings" + + jwt "github.com/dgrijalva/jwt-go" +) + +var ( + // Options + flagAlg = flag.String("alg", "", "signing algorithm identifier") + flagKey = flag.String("key", "", "path to key file or '-' to read from stdin") + flagCompact = flag.Bool("compact", false, "output compact JSON") + flagDebug = flag.Bool("debug", false, "print out all kinds of debug data") + flagClaims = make(ArgList) + flagHead = make(ArgList) + + // Modes - exactly one of these is required + flagSign = flag.String("sign", "", "path to claims object to sign, '-' to read from stdin, or '+' to use only -claim args") + flagVerify = flag.String("verify", "", "path to JWT token to verify or '-' to read from stdin") + flagShow = flag.String("show", "", "path to JWT file or '-' to read from stdin") +) + +func main() { + // Plug in Var flags + flag.Var(flagClaims, "claim", "add additional claims. may be used more than once") + flag.Var(flagHead, "header", "add additional header params. may be used more than once") + + // Usage message if you ask for -help or if you mess up inputs. + flag.Usage = func() { + fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) + fmt.Fprintf(os.Stderr, " One of the following flags is required: sign, verify\n") + flag.PrintDefaults() + } + + // Parse command line options + flag.Parse() + + // Do the thing. If something goes wrong, print error to stderr + // and exit with a non-zero status code + if err := start(); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } +} + +// Figure out which thing to do and then do that +func start() error { + if *flagSign != "" { + return signToken() + } else if *flagVerify != "" { + return verifyToken() + } else if *flagShow != "" { + return showToken() + } else { + flag.Usage() + return fmt.Errorf("None of the required flags are present. What do you want me to do?") + } +} + +// Helper func: Read input from specified file or stdin +func loadData(p string) ([]byte, error) { + if p == "" { + return nil, fmt.Errorf("No path specified") + } + + var rdr io.Reader + if p == "-" { + rdr = os.Stdin + } else if p == "+" { + return []byte("{}"), nil + } else { + if f, err := os.Open(p); err == nil { + rdr = f + defer f.Close() + } else { + return nil, err + } + } + return ioutil.ReadAll(rdr) +} + +// Print a json object in accordance with the prophecy (or the command line options) +func printJSON(j interface{}) error { + var out []byte + var err error + + if *flagCompact == false { + out, err = json.MarshalIndent(j, "", " ") + } else { + out, err = json.Marshal(j) + } + + if err == nil { + fmt.Println(string(out)) + } + + return err +} + +// Verify a token and output the claims. This is a great example +// of how to verify and view a token. +func verifyToken() error { + // get the token + tokData, err := loadData(*flagVerify) + if err != nil { + return fmt.Errorf("Couldn't read token: %v", err) + } + + // trim possible whitespace from token + tokData = regexp.MustCompile(`\s*$`).ReplaceAll(tokData, []byte{}) + if *flagDebug { + fmt.Fprintf(os.Stderr, "Token len: %v bytes\n", len(tokData)) + } + + // Parse the token. Load the key from command line option + token, err := jwt.Parse(string(tokData), func(t *jwt.Token) (interface{}, error) { + data, err := loadData(*flagKey) + if err != nil { + return nil, err + } + if isEs() { + return jwt.ParseECPublicKeyFromPEM(data) + } else if isRs() { + return jwt.ParseRSAPublicKeyFromPEM(data) + } + return data, nil + }) + + // Print some debug data + if *flagDebug && token != nil { + fmt.Fprintf(os.Stderr, "Header:\n%v\n", token.Header) + fmt.Fprintf(os.Stderr, "Claims:\n%v\n", token.Claims) + } + + // Print an error if we can't parse for some reason + if err != nil { + return fmt.Errorf("Couldn't parse token: %v", err) + } + + // Is token invalid? + if !token.Valid { + return fmt.Errorf("Token is invalid") + } + + // Print the token details + if err := printJSON(token.Claims); err != nil { + return fmt.Errorf("Failed to output claims: %v", err) + } + + return nil +} + +// Create, sign, and output a token. This is a great, simple example of +// how to use this library to create and sign a token. +func signToken() error { + // get the token data from command line arguments + tokData, err := loadData(*flagSign) + if err != nil { + return fmt.Errorf("Couldn't read token: %v", err) + } else if *flagDebug { + fmt.Fprintf(os.Stderr, "Token: %v bytes", len(tokData)) + } + + // parse the JSON of the claims + var claims jwt.MapClaims + if err := json.Unmarshal(tokData, &claims); err != nil { + return fmt.Errorf("Couldn't parse claims JSON: %v", err) + } + + // add command line claims + if len(flagClaims) > 0 { + for k, v := range flagClaims { + claims[k] = v + } + } + + // get the key + var key interface{} + key, err = loadData(*flagKey) + if err != nil { + return fmt.Errorf("Couldn't read key: %v", err) + } + + // get the signing alg + alg := jwt.GetSigningMethod(*flagAlg) + if alg == nil { + return fmt.Errorf("Couldn't find signing method: %v", *flagAlg) + } + + // create a new token + token := jwt.NewWithClaims(alg, claims) + + // add command line headers + if len(flagHead) > 0 { + for k, v := range flagHead { + token.Header[k] = v + } + } + + if isEs() { + if k, ok := key.([]byte); !ok { + return fmt.Errorf("Couldn't convert key data to key") + } else { + key, err = jwt.ParseECPrivateKeyFromPEM(k) + if err != nil { + return err + } + } + } else if isRs() { + if k, ok := key.([]byte); !ok { + return fmt.Errorf("Couldn't convert key data to key") + } else { + key, err = jwt.ParseRSAPrivateKeyFromPEM(k) + if err != nil { + return err + } + } + } + + if out, err := token.SignedString(key); err == nil { + fmt.Println(out) + } else { + return fmt.Errorf("Error signing token: %v", err) + } + + return nil +} + +// showToken pretty-prints the token on the command line. +func showToken() error { + // get the token + tokData, err := loadData(*flagShow) + if err != nil { + return fmt.Errorf("Couldn't read token: %v", err) + } + + // trim possible whitespace from token + tokData = regexp.MustCompile(`\s*$`).ReplaceAll(tokData, []byte{}) + if *flagDebug { + fmt.Fprintf(os.Stderr, "Token len: %v bytes\n", len(tokData)) + } + + token, err := jwt.Parse(string(tokData), nil) + if token == nil { + return fmt.Errorf("malformed token: %v", err) + } + + // Print the token details + fmt.Println("Header:") + if err := printJSON(token.Header); err != nil { + return fmt.Errorf("Failed to output header: %v", err) + } + + fmt.Println("Claims:") + if err := printJSON(token.Claims); err != nil { + return fmt.Errorf("Failed to output claims: %v", err) + } + + return nil +} + +func isEs() bool { + return strings.HasPrefix(*flagAlg, "ES") +} + +func isRs() bool { + return strings.HasPrefix(*flagAlg, "RS") +} + +type ArgList map[string]string + +func (l ArgList) String() string { + data, _ := json.Marshal(l) + return string(data) +} + +func (l ArgList) Set(arg string) error { + parts := strings.SplitN(arg, "=", 2) + if len(parts) != 2 { + return fmt.Errorf("Invalid argument '%v'. Must use format 'key=value'. %v", arg, parts) + } + l[parts[0]] = parts[1] + return nil +} diff --git a/utils/log/README.md b/utils/log/README.md index dfa6063..dac9b5a 100644 --- a/utils/log/README.md +++ b/utils/log/README.md @@ -31,7 +31,7 @@ $ curl https://glide.sh/get | sh ```bash $ go get -u github.com/ehlxr/go-utils -$ cd $GOPATH/src/github.com/ehlxr/go-utils/log +$ cd $GOPATH/src/github.com/ehlxr/go-utils/utils/log $ glide install ```