github.com/nats-io/jwt/v2@v2.5.6/creds_utils.go (about)

     1  /*
     2   * Copyright 2019-2020 The NATS Authors
     3   * Licensed under the Apache License, Version 2.0 (the "License");
     4   * you may not use this file except in compliance with the License.
     5   * You may obtain a copy of the License at
     6   *
     7   * http://www.apache.org/licenses/LICENSE-2.0
     8   *
     9   * Unless required by applicable law or agreed to in writing, software
    10   * distributed under the License is distributed on an "AS IS" BASIS,
    11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12   * See the License for the specific language governing permissions and
    13   * limitations under the License.
    14   */
    15  
    16  package jwt
    17  
    18  import (
    19  	"bytes"
    20  	"errors"
    21  	"fmt"
    22  	"regexp"
    23  	"strings"
    24  	"time"
    25  
    26  	"github.com/nats-io/nkeys"
    27  )
    28  
    29  // DecorateJWT returns a decorated JWT that describes the kind of JWT
    30  func DecorateJWT(jwtString string) ([]byte, error) {
    31  	gc, err := Decode(jwtString)
    32  	if err != nil {
    33  		return nil, err
    34  	}
    35  	return formatJwt(string(gc.ClaimType()), jwtString)
    36  }
    37  
    38  func formatJwt(kind string, jwtString string) ([]byte, error) {
    39  	templ := `-----BEGIN NATS %s JWT-----
    40  %s
    41  ------END NATS %s JWT------
    42  
    43  `
    44  	w := bytes.NewBuffer(nil)
    45  	kind = strings.ToUpper(kind)
    46  	_, err := fmt.Fprintf(w, templ, kind, jwtString, kind)
    47  	if err != nil {
    48  		return nil, err
    49  	}
    50  	return w.Bytes(), nil
    51  }
    52  
    53  // DecorateSeed takes a seed and returns a string that wraps
    54  // the seed in the form:
    55  //
    56  //	************************* IMPORTANT *************************
    57  //	NKEY Seed printed below can be used sign and prove identity.
    58  //	NKEYs are sensitive and should be treated as secrets.
    59  //
    60  //	-----BEGIN USER NKEY SEED-----
    61  //	SUAIO3FHUX5PNV2LQIIP7TZ3N4L7TX3W53MQGEIVYFIGA635OZCKEYHFLM
    62  //	------END USER NKEY SEED------
    63  func DecorateSeed(seed []byte) ([]byte, error) {
    64  	w := bytes.NewBuffer(nil)
    65  	ts := bytes.TrimSpace(seed)
    66  	pre := string(ts[0:2])
    67  	kind := ""
    68  	switch pre {
    69  	case "SU":
    70  		kind = "USER"
    71  	case "SA":
    72  		kind = "ACCOUNT"
    73  	case "SO":
    74  		kind = "OPERATOR"
    75  	default:
    76  		return nil, errors.New("seed is not an operator, account or user seed")
    77  	}
    78  	header := `************************* IMPORTANT *************************
    79  NKEY Seed printed below can be used to sign and prove identity.
    80  NKEYs are sensitive and should be treated as secrets.
    81  
    82  -----BEGIN %s NKEY SEED-----
    83  `
    84  	_, err := fmt.Fprintf(w, header, kind)
    85  	if err != nil {
    86  		return nil, err
    87  	}
    88  	w.Write(ts)
    89  
    90  	footer := `
    91  ------END %s NKEY SEED------
    92  
    93  *************************************************************
    94  `
    95  	_, err = fmt.Fprintf(w, footer, kind)
    96  	if err != nil {
    97  		return nil, err
    98  	}
    99  	return w.Bytes(), nil
   100  }
   101  
   102  var userConfigRE = regexp.MustCompile(`\s*(?:(?:[-]{3,}.*[-]{3,}\r?\n)([\w\-.=]+)(?:\r?\n[-]{3,}.*[-]{3,}(\r?\n|\z)))`)
   103  
   104  // An user config file looks like this:
   105  //  -----BEGIN NATS USER JWT-----
   106  //  eyJ0eXAiOiJqd3QiLCJhbGciOiJlZDI1NTE5...
   107  //  ------END NATS USER JWT------
   108  //
   109  //  ************************* IMPORTANT *************************
   110  //  NKEY Seed printed below can be used sign and prove identity.
   111  //  NKEYs are sensitive and should be treated as secrets.
   112  //
   113  //  -----BEGIN USER NKEY SEED-----
   114  //  SUAIO3FHUX5PNV2LQIIP7TZ3N4L7TX3W53MQGEIVYFIGA635OZCKEYHFLM
   115  //  ------END USER NKEY SEED------
   116  
   117  // FormatUserConfig returns a decorated file with a decorated JWT and decorated seed
   118  func FormatUserConfig(jwtString string, seed []byte) ([]byte, error) {
   119  	gc, err := Decode(jwtString)
   120  	if err != nil {
   121  		return nil, err
   122  	}
   123  	if gc.ClaimType() != UserClaim {
   124  		return nil, fmt.Errorf("%q cannot be serialized as a user config", string(gc.ClaimType()))
   125  	}
   126  
   127  	w := bytes.NewBuffer(nil)
   128  
   129  	jd, err := formatJwt(string(gc.ClaimType()), jwtString)
   130  	if err != nil {
   131  		return nil, err
   132  	}
   133  	_, err = w.Write(jd)
   134  	if err != nil {
   135  		return nil, err
   136  	}
   137  	if !bytes.HasPrefix(bytes.TrimSpace(seed), []byte("SU")) {
   138  		return nil, fmt.Errorf("nkey seed is not an user seed")
   139  	}
   140  
   141  	d, err := DecorateSeed(seed)
   142  	if err != nil {
   143  		return nil, err
   144  	}
   145  	_, err = w.Write(d)
   146  	if err != nil {
   147  		return nil, err
   148  	}
   149  
   150  	return w.Bytes(), nil
   151  }
   152  
   153  // ParseDecoratedJWT takes a creds file and returns the JWT portion.
   154  func ParseDecoratedJWT(contents []byte) (string, error) {
   155  	items := userConfigRE.FindAllSubmatch(contents, -1)
   156  	if len(items) == 0 {
   157  		return string(contents), nil
   158  	}
   159  	// First result should be the user JWT.
   160  	// We copy here so that if the file contained a seed file too we wipe appropriately.
   161  	raw := items[0][1]
   162  	tmp := make([]byte, len(raw))
   163  	copy(tmp, raw)
   164  	return string(tmp), nil
   165  }
   166  
   167  // ParseDecoratedNKey takes a creds file, finds the NKey portion and creates a
   168  // key pair from it.
   169  func ParseDecoratedNKey(contents []byte) (nkeys.KeyPair, error) {
   170  	var seed []byte
   171  
   172  	items := userConfigRE.FindAllSubmatch(contents, -1)
   173  	if len(items) > 1 {
   174  		seed = items[1][1]
   175  	} else {
   176  		lines := bytes.Split(contents, []byte("\n"))
   177  		for _, line := range lines {
   178  			if bytes.HasPrefix(bytes.TrimSpace(line), []byte("SO")) ||
   179  				bytes.HasPrefix(bytes.TrimSpace(line), []byte("SA")) ||
   180  				bytes.HasPrefix(bytes.TrimSpace(line), []byte("SU")) {
   181  				seed = line
   182  				break
   183  			}
   184  		}
   185  	}
   186  	if seed == nil {
   187  		return nil, errors.New("no nkey seed found")
   188  	}
   189  	if !bytes.HasPrefix(seed, []byte("SO")) &&
   190  		!bytes.HasPrefix(seed, []byte("SA")) &&
   191  		!bytes.HasPrefix(seed, []byte("SU")) {
   192  		return nil, errors.New("doesn't contain a seed nkey")
   193  	}
   194  	kp, err := nkeys.FromSeed(seed)
   195  	if err != nil {
   196  		return nil, err
   197  	}
   198  	return kp, nil
   199  }
   200  
   201  // ParseDecoratedUserNKey takes a creds file, finds the NKey portion and creates a
   202  // key pair from it. Similar to ParseDecoratedNKey but fails for non-user keys.
   203  func ParseDecoratedUserNKey(contents []byte) (nkeys.KeyPair, error) {
   204  	nk, err := ParseDecoratedNKey(contents)
   205  	if err != nil {
   206  		return nil, err
   207  	}
   208  	seed, err := nk.Seed()
   209  	if err != nil {
   210  		return nil, err
   211  	}
   212  	if !bytes.HasPrefix(seed, []byte("SU")) {
   213  		return nil, errors.New("doesn't contain an user seed nkey")
   214  	}
   215  	kp, err := nkeys.FromSeed(seed)
   216  	if err != nil {
   217  		return nil, err
   218  	}
   219  	return kp, nil
   220  }
   221  
   222  // IssueUserJWT takes an account scoped signing key, account id, and use public key (and optionally a user's name, an expiration duration and tags) and returns a valid signed JWT.
   223  // The scopedSigningKey, is a mandatory account scoped signing nkey pair to sign the generated jwt (note that it _must_ be a signing key attached to the account (and a _scoped_ signing key), not the account's private (seed) key).
   224  // The accountId, is a mandatory public account nkey. Will return error when not set or not account nkey.
   225  // The publicUserKey, is a mandatory public user nkey. Will return error when not set or not user nkey.
   226  // The name, is an optional human-readable name. When absent, default to publicUserKey.
   227  // The expirationDuration, is an optional but recommended duration, when the generated jwt needs to expire. If not set, JWT will not expire.
   228  // The tags, is an optional list of tags to be included in the JWT.
   229  //
   230  // Returns:
   231  // string, resulting jwt.
   232  // error, when issues arose.
   233  func IssueUserJWT(scopedSigningKey nkeys.KeyPair, accountId string, publicUserKey string, name string, expirationDuration time.Duration, tags ...string) (string, error) {
   234  
   235  	if !nkeys.IsValidPublicAccountKey(accountId) {
   236  		return "", errors.New("issueUserJWT requires an account key for the accountId parameter, but got " + nkeys.Prefix(accountId).String())
   237  	}
   238  
   239  	if !nkeys.IsValidPublicUserKey(publicUserKey) {
   240  		return "", errors.New("issueUserJWT requires an account key for the publicUserKey parameter, but got " + nkeys.Prefix(publicUserKey).String())
   241  	}
   242  
   243  	claim := NewUserClaims(publicUserKey)
   244  	claim.SetScoped(true)
   245  
   246  	if expirationDuration != 0 {
   247  		claim.Expires = time.Now().Add(expirationDuration).UTC().Unix()
   248  	}
   249  
   250  	claim.IssuerAccount = accountId
   251  	if name != "" {
   252  		claim.Name = name
   253  	} else {
   254  		claim.Name = publicUserKey
   255  	}
   256  
   257  	claim.Subject = publicUserKey
   258  	claim.Tags = tags
   259  
   260  	encoded, err := claim.Encode(scopedSigningKey)
   261  	if err != nil {
   262  		return "", errors.New("err encoding claim " + err.Error())
   263  	}
   264  
   265  	return encoded, nil
   266  }