github.com/tickoalcantara12/micro/v3@v3.0.0-20221007104245-9d75b9bcbab9/client/cli/token/token.go (about)

     1  // Package token contains CLI client token related helpers
     2  // tToken files consist of one line per token, each token having
     3  // the structure of `micro://envAddress/namespace[/id]:token`, ie.
     4  // micro://m3o.com/foo-bar-baz/asim@aslam.me:afsafasfasfaceevqcCEWVEWV
     5  // or
     6  // micro://m3o.com/foo-bar-baz:afsafasfasfaceevqcCEWVEWV
     7  package token
     8  
     9  import (
    10  	"bytes"
    11  	"encoding/base64"
    12  	"encoding/json"
    13  	"fmt"
    14  	"io/ioutil"
    15  	"os"
    16  	"path/filepath"
    17  	"strconv"
    18  	"strings"
    19  	"time"
    20  
    21  	"github.com/tickoalcantara12/micro/v3/client/cli/namespace"
    22  	"github.com/tickoalcantara12/micro/v3/client/cli/util"
    23  	"github.com/tickoalcantara12/micro/v3/service/auth"
    24  	"github.com/tickoalcantara12/micro/v3/util/config"
    25  	"github.com/tickoalcantara12/micro/v3/util/user"
    26  	"github.com/urfave/cli/v2"
    27  )
    28  
    29  const tokensFileName = "tokens"
    30  
    31  // Get tries a best effort read of auth token from user config.
    32  // Might have missing `RefreshToken` or `Expiry` fields in case of
    33  // incomplete or corrupted user config.
    34  func Get(ctx *cli.Context) (*auth.AccountToken, error) {
    35  	tok, err := getFromFile(ctx)
    36  	if err == nil {
    37  		return tok, nil
    38  	}
    39  	env, err := util.GetEnv(ctx)
    40  	if err != nil {
    41  		return nil, err
    42  	}
    43  	return getFromUserConfig(env.Name)
    44  }
    45  
    46  type token struct {
    47  	AccessToken  string `json:"access_token"`
    48  	RefreshToken string `json:"refresh_token"`
    49  	// unix timestamp
    50  	Created int64 `json:"created"`
    51  	// unix timestamp
    52  	Expiry int64 `json:"expiry"`
    53  }
    54  
    55  func tokensFilePath() string {
    56  	return filepath.Join(user.Dir, tokensFileName)
    57  }
    58  
    59  func getFromFile(ctx *cli.Context) (*auth.AccountToken, error) {
    60  	tokens, err := getTokens()
    61  	if err != nil {
    62  		return nil, err
    63  	}
    64  
    65  	env, err := util.GetEnv(ctx)
    66  	if err != nil {
    67  		return nil, err
    68  	}
    69  	// We save the current user
    70  	userID, err := config.Get(config.Path(env.Name, "current-user"))
    71  	if err != nil {
    72  		return nil, err
    73  	}
    74  
    75  	// Look up the token
    76  	tk, err := tokenKey(ctx, userID)
    77  	if err != nil {
    78  		return nil, err
    79  	}
    80  	tok, found := tokens[tk]
    81  	if !found {
    82  		ns, err := namespace.Get(env.Name)
    83  		if err != nil {
    84  			return nil, err
    85  		}
    86  		return nil, fmt.Errorf("Can't find token for address %v and namespace %v", env.ProxyAddress, ns)
    87  	}
    88  	return &auth.AccountToken{
    89  		AccessToken:  tok.AccessToken,
    90  		RefreshToken: tok.RefreshToken,
    91  		Created:      time.Unix(tok.Created, 0),
    92  		Expiry:       time.Unix(tok.Expiry, 0),
    93  	}, nil
    94  }
    95  
    96  func getTokens() (map[string]token, error) {
    97  	f, err := os.OpenFile(tokensFilePath(), os.O_RDONLY|os.O_CREATE, 0700)
    98  	if err != nil {
    99  		return nil, err
   100  	}
   101  	dat, err := ioutil.ReadAll(f)
   102  	if err != nil {
   103  		return nil, err
   104  	}
   105  	lines := strings.Split(string(dat), "\n")
   106  	ret := map[string]token{}
   107  	for _, line := range lines {
   108  		parts := strings.Split(line, ":")
   109  		if len(parts) < 3 {
   110  			continue
   111  		}
   112  		key := strings.Join(parts[0:len(parts)-1], ":")
   113  		base64Encoded := parts[len(parts)-1]
   114  		jsonMarshalled, err := base64.StdEncoding.DecodeString(base64Encoded)
   115  		if err != nil {
   116  			return nil, fmt.Errorf("Error base64 decoding token: %v", err)
   117  		}
   118  		tok := token{}
   119  		err = json.Unmarshal(jsonMarshalled, &tok)
   120  		if err != nil {
   121  			return nil, fmt.Errorf("Error unmarshalling token: %v", err)
   122  		}
   123  		ret[key] = tok
   124  	}
   125  	return ret, nil
   126  }
   127  
   128  func getFromUserConfig(envName string) (*auth.AccountToken, error) {
   129  	path := []string{"micro", "auth", envName}
   130  	accessToken, _ := config.Get(config.Path(append(path, "token")...))
   131  
   132  	refreshToken, err := config.Get(config.Path(append(path, "refresh-token")...))
   133  	if err != nil {
   134  		// Gracefully degrading here in case the user only has a temporary access token at hand.
   135  		// The call will fail on the receiving end.
   136  		return &auth.AccountToken{
   137  			AccessToken: accessToken,
   138  		}, nil
   139  	}
   140  
   141  	// See if the access token has expired
   142  	expiry, _ := config.Get(config.Path(append(path, "expiry")...))
   143  	if len(expiry) == 0 {
   144  		return &auth.AccountToken{
   145  			AccessToken:  accessToken,
   146  			RefreshToken: refreshToken,
   147  		}, nil
   148  	}
   149  	expiryInt, err := strconv.ParseInt(expiry, 10, 64)
   150  	if err != nil {
   151  		return nil, err
   152  	}
   153  	return &auth.AccountToken{
   154  		AccessToken:  accessToken,
   155  		RefreshToken: refreshToken,
   156  		Expiry:       time.Unix(expiryInt, 0),
   157  	}, nil
   158  }
   159  
   160  // Save saves the auth token to the user's local config file
   161  // Caution: it overwrites $env.current-user with the accountID
   162  // that the account token represents.
   163  func Save(ctx *cli.Context, token *auth.AccountToken) error {
   164  	return saveToFile(ctx, token)
   165  }
   166  
   167  func tokenKey(ctx *cli.Context, accountID string) (string, error) {
   168  	env, err := util.GetEnv(ctx)
   169  	if err != nil {
   170  		return "", err
   171  	}
   172  	ns, err := namespace.Get(env.Name)
   173  	if err != nil {
   174  		return "", err
   175  	}
   176  	return fmt.Sprintf("micro://%v/%v/%v", env.ProxyAddress, ns, accountID), nil
   177  }
   178  
   179  func saveTokens(tokens map[string]token) error {
   180  	buf := bytes.NewBuffer([]byte{})
   181  	for key, t := range tokens {
   182  		marshalledToken, err := json.Marshal(t)
   183  		if err != nil {
   184  			return err
   185  		}
   186  		base64Token := base64.StdEncoding.EncodeToString(marshalledToken)
   187  		_, err = buf.WriteString(key + ":" + base64Token + "\n")
   188  		if err != nil {
   189  			return err
   190  		}
   191  	}
   192  	return ioutil.WriteFile(tokensFilePath(), buf.Bytes(), 0700)
   193  }
   194  
   195  func saveToFile(ctx *cli.Context, authToken *auth.AccountToken) error {
   196  	tokens, err := getTokens()
   197  	if err != nil {
   198  		return err
   199  	}
   200  	account, err := auth.Inspect(authToken.AccessToken)
   201  	if err != nil {
   202  		return err
   203  	}
   204  
   205  	env, err := util.GetEnv(ctx)
   206  	if err != nil {
   207  		return err
   208  	}
   209  	// We save the current user
   210  	err = config.Set(config.Path(env.Name, "current-user"), account.ID)
   211  	if err != nil {
   212  		return err
   213  	}
   214  
   215  	key, err := tokenKey(ctx, account.ID)
   216  	if err != nil {
   217  		return err
   218  	}
   219  	tokens[key] = token{
   220  		AccessToken:  authToken.AccessToken,
   221  		RefreshToken: authToken.RefreshToken,
   222  		Created:      authToken.Created.Unix(),
   223  		Expiry:       authToken.Expiry.Unix(),
   224  	}
   225  	return saveTokens(tokens)
   226  }
   227  
   228  func saveToUserConfig(envName string, token *auth.AccountToken) error {
   229  	if err := config.Set(config.Path("micro", "auth", envName, "token"), token.AccessToken); err != nil {
   230  		return err
   231  	}
   232  	// Store the refresh token in micro config
   233  	if err := config.Set(config.Path("micro", "auth", envName, "refresh-token"), token.RefreshToken); err != nil {
   234  		return err
   235  	}
   236  	// Store the refresh token in micro config
   237  	return config.Set(config.Path("micro", "auth", envName, "expiry"), fmt.Sprintf("%v", token.Expiry.Unix()))
   238  }
   239  
   240  // Remove deletes a token. Useful when trying to reset test
   241  // for example at testing: not having a token is a different state
   242  // than having an invalid token.
   243  func Remove(ctx *cli.Context) error {
   244  	env, err := util.GetEnv(ctx)
   245  	if err != nil {
   246  		return err
   247  	}
   248  	// intentionally ignoring the errors here
   249  	removeFromUserConfig(env.Name)
   250  	return removeFromFile(ctx)
   251  }
   252  
   253  func removeFromFile(ctx *cli.Context) error {
   254  	tokens, err := getTokens()
   255  	if err != nil {
   256  		return err
   257  	}
   258  	env, err := util.GetEnv(ctx)
   259  	if err != nil {
   260  		return err
   261  	}
   262  	userID, err := config.Get(config.Path(env.Name, "current-user"))
   263  	if err != nil {
   264  		return err
   265  	}
   266  	key, err := tokenKey(ctx, userID)
   267  	if err != nil {
   268  		return err
   269  	}
   270  	delete(tokens, key)
   271  	return saveTokens(tokens)
   272  }
   273  
   274  func removeFromUserConfig(envName string) error {
   275  	if err := config.Set(config.Path("micro", "auth", envName, "token"), ""); err != nil {
   276  		return err
   277  	}
   278  	// Store the refresh token in micro config
   279  	if err := config.Set(config.Path("micro", "auth", envName, "refresh-token"), ""); err != nil {
   280  		return err
   281  	}
   282  	// Store the refresh token in micro config
   283  	return config.Set(config.Path("micro", "auth", envName, "expiry"), "")
   284  }