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 }