github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/kbfs/kbfscrypto/auth_token.go (about)

     1  // Copyright 2016 Keybase Inc. All rights reserved.
     2  // Use of this source code is governed by a BSD
     3  // license that can be found in the LICENSE file.
     4  
     5  package kbfscrypto
     6  
     7  import (
     8  	"fmt"
     9  	"runtime"
    10  	"sync"
    11  	"time"
    12  
    13  	"github.com/keybase/client/go/auth"
    14  	kbname "github.com/keybase/client/go/kbun"
    15  	"github.com/keybase/client/go/libkb"
    16  	"github.com/keybase/client/go/protocol/keybase1"
    17  	"github.com/pkg/errors"
    18  	"golang.org/x/net/context"
    19  )
    20  
    21  // AuthTokenMinRefreshSeconds is the minimum number of seconds between refreshes.
    22  const AuthTokenMinRefreshSeconds = 60
    23  
    24  // AuthTokenRefreshHandler defines a callback to be called when an auth token refresh
    25  // is needed.
    26  type AuthTokenRefreshHandler interface {
    27  	RefreshAuthToken(context.Context)
    28  }
    29  
    30  // AuthToken encapsulates a timed authentication token.
    31  type AuthToken struct {
    32  	signer         Signer
    33  	tokenType      string
    34  	expireIn       int
    35  	clientName     string
    36  	clientVersion  string
    37  	refreshHandler AuthTokenRefreshHandler
    38  	tickerCancel   context.CancelFunc
    39  	tickerMu       sync.Mutex // protects the ticker cancel function
    40  }
    41  
    42  // NewAuthToken creates a new authentication token.
    43  func NewAuthToken(signer Signer, tokenType string, expireIn int,
    44  	submoduleName, version string, rh AuthTokenRefreshHandler) *AuthToken {
    45  	clientName := fmt.Sprintf("go %s %s %s",
    46  		submoduleName, libkb.GetPlatformString(), runtime.GOARCH)
    47  	authToken := &AuthToken{
    48  		signer:         signer,
    49  		tokenType:      tokenType,
    50  		expireIn:       expireIn,
    51  		clientName:     clientName,
    52  		clientVersion:  version,
    53  		refreshHandler: rh,
    54  	}
    55  	return authToken
    56  }
    57  
    58  // Sign is called to create a new signed authentication token.
    59  func (a *AuthToken) signWithUserAndKeyInfo(ctx context.Context,
    60  	challengeInfo keybase1.ChallengeInfo, uid keybase1.UID,
    61  	username kbname.NormalizedUsername, key VerifyingKey) (string, error) {
    62  	// create the token
    63  	token := auth.NewToken(uid, username, key.KID(), a.tokenType,
    64  		challengeInfo.Challenge, challengeInfo.Now, a.expireIn,
    65  		a.clientName, a.clientVersion)
    66  
    67  	// sign the token
    68  	signature, err := a.signer.SignToString(ctx, token.Bytes())
    69  	if err != nil {
    70  		return "", err
    71  	}
    72  
    73  	// reset the ticker
    74  	refreshSeconds := a.expireIn / 2
    75  	if refreshSeconds < AuthTokenMinRefreshSeconds {
    76  		refreshSeconds = AuthTokenMinRefreshSeconds
    77  	}
    78  	a.startTicker(refreshSeconds)
    79  
    80  	return signature, nil
    81  }
    82  
    83  // Sign is called to create a new signed authentication token,
    84  // including a challenge and username/uid/kid identifiers.
    85  func (a *AuthToken) Sign(ctx context.Context,
    86  	currentUsername kbname.NormalizedUsername, currentUID keybase1.UID,
    87  	currentVerifyingKey VerifyingKey,
    88  	challengeInfo keybase1.ChallengeInfo) (string, error) {
    89  	// make sure we're being asked to sign a legit challenge
    90  	if !auth.IsValidChallenge(challengeInfo.Challenge) {
    91  		return "", errors.New("Invalid challenge")
    92  	}
    93  
    94  	return a.signWithUserAndKeyInfo(
    95  		ctx, challengeInfo, currentUID,
    96  		currentUsername, currentVerifyingKey)
    97  }
    98  
    99  // SignUserless signs the token without a username, UID, or challenge.
   100  // This is useful for server-to-server communication where identity is
   101  // established using only the KID.  Assume the client and server
   102  // clocks are roughly synchronized.
   103  func (a *AuthToken) SignUserless(
   104  	ctx context.Context, key VerifyingKey) (
   105  	string, error) {
   106  	// Pass in a reserved, meaningless UID.
   107  	return a.signWithUserAndKeyInfo(ctx,
   108  		keybase1.ChallengeInfo{Now: time.Now().Unix()},
   109  		keybase1.PublicUID, "", key)
   110  }
   111  
   112  // Shutdown is called to stop the refresh ticker.
   113  func (a *AuthToken) Shutdown() {
   114  	a.stopTicker()
   115  }
   116  
   117  // Helper to start the ticker (if not started.)
   118  func (a *AuthToken) startTicker(intervalSeconds int) {
   119  	a.tickerMu.Lock()
   120  	defer a.tickerMu.Unlock()
   121  
   122  	if a.tickerCancel != nil {
   123  		return
   124  	}
   125  
   126  	var ctx context.Context
   127  	ctx, a.tickerCancel = context.WithCancel(context.Background())
   128  	go func() {
   129  		ticker := time.NewTicker(time.Duration(intervalSeconds) * time.Second)
   130  		for {
   131  			select {
   132  			case <-ticker.C:
   133  				a.refreshHandler.RefreshAuthToken(ctx)
   134  			case <-ctx.Done():
   135  				ticker.Stop()
   136  				return
   137  			}
   138  		}
   139  	}()
   140  }
   141  
   142  // Helper to stop the refresh ticker.
   143  func (a *AuthToken) stopTicker() {
   144  	a.tickerMu.Lock()
   145  	defer a.tickerMu.Unlock()
   146  
   147  	if a.tickerCancel != nil {
   148  		a.tickerCancel()
   149  		a.tickerCancel = nil
   150  	}
   151  }