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

     1  /*
     2   * Copyright 2018 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  	"crypto/sha256"
    20  	"encoding/base32"
    21  	"errors"
    22  	"fmt"
    23  	"strings"
    24  
    25  	"github.com/nats-io/nkeys"
    26  )
    27  
    28  // Activation defines the custom parts of an activation claim
    29  type Activation struct {
    30  	ImportSubject Subject    `json:"subject,omitempty"`
    31  	ImportType    ExportType `json:"kind,omitempty"`
    32  	// IssuerAccount stores the public key for the account the issuer represents.
    33  	// When set, the claim was issued by a signing key.
    34  	IssuerAccount string `json:"issuer_account,omitempty"`
    35  	GenericFields
    36  }
    37  
    38  // IsService returns true if an Activation is for a service
    39  func (a *Activation) IsService() bool {
    40  	return a.ImportType == Service
    41  }
    42  
    43  // IsStream returns true if an Activation is for a stream
    44  func (a *Activation) IsStream() bool {
    45  	return a.ImportType == Stream
    46  }
    47  
    48  // Validate checks the exports and limits in an activation JWT
    49  func (a *Activation) Validate(vr *ValidationResults) {
    50  	if !a.IsService() && !a.IsStream() {
    51  		vr.AddError("invalid import type: %q", a.ImportType)
    52  	}
    53  
    54  	a.ImportSubject.Validate(vr)
    55  }
    56  
    57  // ActivationClaims holds the data specific to an activation JWT
    58  type ActivationClaims struct {
    59  	ClaimsData
    60  	Activation `json:"nats,omitempty"`
    61  }
    62  
    63  // NewActivationClaims creates a new activation claim with the provided sub
    64  func NewActivationClaims(subject string) *ActivationClaims {
    65  	if subject == "" {
    66  		return nil
    67  	}
    68  	ac := &ActivationClaims{}
    69  	ac.Subject = subject
    70  	return ac
    71  }
    72  
    73  // Encode turns an activation claim into a JWT strimg
    74  func (a *ActivationClaims) Encode(pair nkeys.KeyPair) (string, error) {
    75  	if !nkeys.IsValidPublicAccountKey(a.ClaimsData.Subject) {
    76  		return "", errors.New("expected subject to be an account")
    77  	}
    78  	a.Type = ActivationClaim
    79  	return a.ClaimsData.encode(pair, a)
    80  }
    81  
    82  // DecodeActivationClaims tries to create an activation claim from a JWT string
    83  func DecodeActivationClaims(token string) (*ActivationClaims, error) {
    84  	claims, err := Decode(token)
    85  	if err != nil {
    86  		return nil, err
    87  	}
    88  	ac, ok := claims.(*ActivationClaims)
    89  	if !ok {
    90  		return nil, errors.New("not activation claim")
    91  	}
    92  	return ac, nil
    93  }
    94  
    95  // Payload returns the activation specific part of the JWT
    96  func (a *ActivationClaims) Payload() interface{} {
    97  	return a.Activation
    98  }
    99  
   100  // Validate checks the claims
   101  func (a *ActivationClaims) Validate(vr *ValidationResults) {
   102  	a.validateWithTimeChecks(vr, true)
   103  }
   104  
   105  // Validate checks the claims
   106  func (a *ActivationClaims) validateWithTimeChecks(vr *ValidationResults, timeChecks bool) {
   107  	if timeChecks {
   108  		a.ClaimsData.Validate(vr)
   109  	}
   110  	a.Activation.Validate(vr)
   111  	if a.IssuerAccount != "" && !nkeys.IsValidPublicAccountKey(a.IssuerAccount) {
   112  		vr.AddError("account_id is not an account public key")
   113  	}
   114  }
   115  
   116  func (a *ActivationClaims) ClaimType() ClaimType {
   117  	return a.Type
   118  }
   119  
   120  func (a *ActivationClaims) updateVersion() {
   121  	a.GenericFields.Version = libVersion
   122  }
   123  
   124  // ExpectedPrefixes defines the types that can sign an activation jwt, account and oeprator
   125  func (a *ActivationClaims) ExpectedPrefixes() []nkeys.PrefixByte {
   126  	return []nkeys.PrefixByte{nkeys.PrefixByteAccount, nkeys.PrefixByteOperator}
   127  }
   128  
   129  // Claims returns the generic part of the JWT
   130  func (a *ActivationClaims) Claims() *ClaimsData {
   131  	return &a.ClaimsData
   132  }
   133  
   134  func (a *ActivationClaims) String() string {
   135  	return a.ClaimsData.String(a)
   136  }
   137  
   138  // HashID returns a hash of the claims that can be used to identify it.
   139  // The hash is calculated by creating a string with
   140  // issuerPubKey.subjectPubKey.<subject> and constructing the sha-256 hash and base32 encoding that.
   141  // <subject> is the exported subject, minus any wildcards, so foo.* becomes foo.
   142  // the one special case is that if the export start with "*" or is ">" the <subject> "_"
   143  func (a *ActivationClaims) HashID() (string, error) {
   144  
   145  	if a.Issuer == "" || a.Subject == "" || a.ImportSubject == "" {
   146  		return "", fmt.Errorf("not enough data in the activaion claims to create a hash")
   147  	}
   148  
   149  	subject := cleanSubject(string(a.ImportSubject))
   150  	base := fmt.Sprintf("%s.%s.%s", a.Issuer, a.Subject, subject)
   151  	h := sha256.New()
   152  	h.Write([]byte(base))
   153  	sha := h.Sum(nil)
   154  	hash := base32.StdEncoding.EncodeToString(sha)
   155  
   156  	return hash, nil
   157  }
   158  
   159  func cleanSubject(subject string) string {
   160  	split := strings.Split(subject, ".")
   161  	cleaned := ""
   162  
   163  	for i, tok := range split {
   164  		if tok == "*" || tok == ">" {
   165  			if i == 0 {
   166  				cleaned = "_"
   167  				break
   168  			}
   169  
   170  			cleaned = strings.Join(split[:i], ".")
   171  			break
   172  		}
   173  	}
   174  	if cleaned == "" {
   175  		cleaned = subject
   176  	}
   177  	return cleaned
   178  }