summaryrefslogtreecommitdiffstats
path: root/fikenauth/source.go
blob: 0a61479f6790c10d8b78e915f3d4cf36d041a13a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
package fikenauth

import (
	"context"
	"errors"
	"fmt"
	"sync"
)

// Source hands out a usable access token, refreshing and re-storing it when the
// stored one has expired. It satisfies fiken.TokenSource.
//
// Refreshing is a side effect of asking for a token rather than a step the
// caller has to remember, which is the only way it can be got right everywhere.
type Source struct {
	cfg   Config
	store *Store

	// LoginCommand is what to tell the operator to run when the grant can no
	// longer be renewed. Every program embedding this spells its own login
	// differently, and an error naming the wrong one sends the reader to a
	// command that does not exist.
	LoginCommand string

	mu     sync.Mutex
	token  Token
	loaded bool
}

// NewSource returns a token source backed by store.
func NewSource(cfg Config, store *Store) *Source {
	return &Source{cfg: cfg, store: store}
}

// loginHint is how to ask for a new grant, in words that are true whatever the
// caller called its command.
func (s *Source) loginHint() string {
	if s.LoginCommand == "" {
		return "log in to Fiken again"
	}
	return "run `" + s.LoginCommand + "` again"
}

// Token returns an access token that is valid now.
func (s *Source) Token(ctx context.Context) (string, error) {
	s.mu.Lock()
	defer s.mu.Unlock()

	if err := s.load(); err != nil {
		return "", err
	}
	if s.token.Valid(s.cfg.now()) {
		return s.token.AccessToken, nil
	}

	// Another program sharing the file may be renewing it right now. Fiken
	// rotates the refresh token on every renewal, so renewing twice from one
	// refresh token logs out whichever finishes second. The lock orders them,
	// and reading again under it picks up what the first one saved.
	unlock, err := s.store.lock()
	if err != nil {
		return "", err
	}
	defer unlock()
	if s.token, err = s.store.Load(); err != nil {
		s.loaded = false
		return "", err
	}
	if s.token.Valid(s.cfg.now()) {
		return s.token.AccessToken, nil
	}
	if !s.token.Renewable() {
		return "", fmt.Errorf("the Fiken access token has expired and cannot be renewed: %w", ErrNoToken)
	}

	if err := s.cfg.Validate(); err != nil {
		return "", err
	}
	refreshed, err := s.cfg.Refresh(ctx, s.token.RefreshToken)
	if err != nil {
		var oauthErr *Error
		if errors.As(err, &oauthErr) {
			return "", fmt.Errorf(
				"could not renew the Fiken login (%s); %s", oauthErr.Code, s.loginHint())
		}
		return "", err
	}

	// Save before returning: a token used but not stored would be refreshed
	// again on the next run, and Fiken invalidates the old refresh token.
	if err := s.store.Save(refreshed); err != nil {
		return "", err
	}
	s.token = refreshed
	return refreshed.AccessToken, nil
}

// Current returns the stored token as it stands, without refreshing it, which
// is what a `fiken status` command reports on.
func (s *Source) Current() (Token, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	if err := s.load(); err != nil {
		return Token{}, err
	}
	return s.token, nil
}

// Logout revokes the grant at Fiken and removes the stored token. The file is
// removed even if the revoke fails, because a token that cannot be revoked is
// exactly the one not to keep lying around.
func (s *Source) Logout(ctx context.Context) error {
	s.mu.Lock()
	defer s.mu.Unlock()

	if err := s.load(); err != nil {
		return err
	}
	revokeErr := s.cfg.Revoke(ctx, s.token.AccessToken)

	s.token, s.loaded = Token{}, false
	if err := s.store.Delete(); err != nil {
		return err
	}
	return revokeErr
}

// Store is where this source keeps its token, for messages.
func (s *Source) Store() *Store { return s.store }

// load reads the token once per process.
func (s *Source) load() error {
	if s.loaded {
		return nil
	}
	token, err := s.store.Load()
	if err != nil {
		return err
	}
	s.token, s.loaded = token, true
	return nil
}