auth.go

v1.0.0
Doc Versions Source
1
package api
2
3
import (
4
	"context"
5
	"encoding/base64"
6
	"encoding/json"
7
	"fmt"
8
	"net/url"
9
	"strconv"
10
11
	"sourcecraft.dev/bigbes/go-bitwarden/crypto"
12
)
13
14
// DeviceInfo describes the client device for token requests.
15
type DeviceInfo struct {
16
	Name       string
17
	Identifier string
18
	Type       int // 14 = CLI/SDK
19
}
20
21
// DefaultDeviceInfo returns sensible defaults for a Go SDK client.
22
func DefaultDeviceInfo() DeviceInfo {
23
	return DeviceInfo{
24
		Name:       "go-bitwarden",
25
		Identifier: "go-bitwarden-sdk",
26
		Type:       14,
27
	}
28
}
29
30
// TwoFactorProvider is called when the server requires 2FA during login.
31
type TwoFactorProvider interface {
32
	// ProvideTwoFactor is called with the available 2FA provider types and their params.
33
	// It should return the chosen provider type, the token/code, and whether to remember.
34
	ProvideTwoFactor(ctx context.Context, providers map[string]map[string]any) (providerType int, token string, remember bool, err error)
35
}
36
37
// AuthClient handles authentication against the Bitwarden/Vaultwarden identity API.
38
type AuthClient struct {
39
	transport *Transport
40
	device    DeviceInfo
41
	twoFA     TwoFactorProvider
42
43
	// identityURL is the base URL for identity endpoints.
44
	// For Vaultwarden/self-hosted, this is the same as the API base URL.
45
	// For Bitwarden cloud, this would be https://identity.bitwarden.com.
46
	identityURL string
47
}
48
49
// NewAuthClient creates a new authentication client.
50
func NewAuthClient(transport *Transport, device DeviceInfo, identityURL string) *AuthClient {
51
	if identityURL == "" {
52
		identityURL = transport.BaseURL()
53
	}
54
	return &AuthClient{
55
		transport:   transport,
56
		device:      device,
57
		identityURL: identityURL,
58
	}
59
}
60
61
// SetTwoFactorProvider sets the 2FA callback.
62
func (a *AuthClient) SetTwoFactorProvider(p TwoFactorProvider) {
63
	a.twoFA = p
64
}
65
66
// Prelogin retrieves KDF parameters for the given email.
67
func (a *AuthClient) Prelogin(ctx context.Context, email string) (*PreloginResponse, error) {
68
	data, err := a.transport.PostNoAuth(ctx, "/identity/accounts/prelogin", PreloginRequest{Email: email})
69
	if err != nil {
70
		return nil, fmt.Errorf("prelogin: %w", err)
71
	}
72
73
	var resp PreloginResponse
74
	if err := json.Unmarshal(data, &resp); err != nil {
75
		return nil, fmt.Errorf("parse prelogin response: %w", err)
76
	}
77
	return &resp, nil
78
}
79
80
// Login authenticates with email and master password, returning the token response.
81
// It also sets up the transport with the received tokens and configures token refresh.
82
func (a *AuthClient) Login(ctx context.Context, email, password string) (*TokenResponse, *crypto.KeyChain, error) {
83
	// 1. Prelogin to get KDF params.
84
	prelogin, err := a.Prelogin(ctx, email)
85
	if err != nil {
86
		return nil, nil, err
87
	}
88
89
	cfg := crypto.DeriveKeyConfig{
90
		KdfType:        crypto.KdfType(prelogin.Kdf),
91
		KdfIterations:  prelogin.KdfIterations,
92
		KdfMemory:      prelogin.KdfMemory,
93
		KdfParallelism: prelogin.KdfParallelism,
94
	}
95
96
	// 2. Derive master key and hash.
97
	masterKey, err := crypto.DeriveMasterKey(password, email, cfg)
98
	if err != nil {
99
		return nil, nil, fmt.Errorf("derive master key: %w", err)
100
	}
101
102
	masterPWHash := crypto.HashMasterPassword(masterKey, password)
103
	masterPWHashB64 := base64.StdEncoding.EncodeToString(masterPWHash)
104
105
	// 3. Request token.
106
	tokenResp, err := a.requestToken(ctx, email, masterPWHashB64, nil)
107
	if err != nil {
108
		return nil, nil, err
109
	}
110
111
	// 4. Handle 2FA if needed.
112
	if tokenResp.TwoFactorProviders2 != nil && len(tokenResp.TwoFactorProviders2) > 0 {
113
		if a.twoFA == nil {
114
			return nil, nil, fmt.Errorf("server requires 2FA but no TwoFactorProvider configured")
115
		}
116
117
		providerType, token, remember, err := a.twoFA.ProvideTwoFactor(ctx, tokenResp.TwoFactorProviders2)
118
		if err != nil {
119
			return nil, nil, fmt.Errorf("2FA provider: %w", err)
120
		}
121
122
		twoFactorParams := &twoFactorParams{
123
			Provider: providerType,
124
			Token:    token,
125
			Remember: remember,
126
		}
127
128
		tokenResp, err = a.requestToken(ctx, email, masterPWHashB64, twoFactorParams)
129
		if err != nil {
130
			return nil, nil, fmt.Errorf("login with 2FA: %w", err)
131
		}
132
	}
133
134
	// 5. Set tokens on transport.
135
	a.transport.SetTokens(tokenResp.AccessToken, tokenResp.RefreshToken, tokenResp.ExpiresIn)
136
	a.transport.SetRefreshFunc(a.doRefreshToken)
137
138
	// 6. Build KeyChain.
139
	keyChain, err := crypto.NewKeyChain(email, password, cfg, tokenResp.Key, tokenResp.PrivateKey)
140
	if err != nil {
141
		return nil, nil, fmt.Errorf("build keychain: %w", err)
142
	}
143
144
	return tokenResp, keyChain, nil
145
}
146
147
// RefreshToken refreshes the access token using the stored refresh token.
148
func (a *AuthClient) RefreshToken(ctx context.Context) error {
149
	_, err := a.transport.AccessToken(ctx) // triggers refresh if needed
150
	return err
151
}
152
153
// Register creates a new account on the server.
154
func (a *AuthClient) Register(ctx context.Context, email, password, name string, cfg crypto.DeriveKeyConfig) error {
155
	masterKey, err := crypto.DeriveMasterKey(password, email, cfg)
156
	if err != nil {
157
		return fmt.Errorf("derive master key: %w", err)
158
	}
159
160
	masterPWHash := crypto.HashMasterPassword(masterKey, password)
161
	masterPWHashB64 := base64.StdEncoding.EncodeToString(masterPWHash)
162
163
	encKey, macKey := crypto.StretchMasterKey(masterKey)
164
	stretchedBytes := make([]byte, 64)
165
	copy(stretchedBytes[:32], encKey)
166
	copy(stretchedBytes[32:], macKey)
167
	stretchedKey, err := crypto.NewSymmetricKey(stretchedBytes)
168
	if err != nil {
169
		return fmt.Errorf("create stretched key: %w", err)
170
	}
171
172
	encSymKey, pubKeyB64, encPrivKey, err := crypto.GenerateUserKeys(stretchedKey)
173
	if err != nil {
174
		return fmt.Errorf("generate user keys: %w", err)
175
	}
176
177
	req := RegisterRequest{
178
		Name:               name,
179
		Email:              email,
180
		MasterPasswordHash: masterPWHashB64,
181
		Key:                encSymKey,
182
		Kdf:                int(cfg.KdfType),
183
		KdfIterations:      cfg.KdfIterations,
184
		KdfMemory:          cfg.KdfMemory,
185
		KdfParallelism:     cfg.KdfParallelism,
186
		Keys: RegisterKeys{
187
			PublicKey:           pubKeyB64,
188
			EncryptedPrivateKey: encPrivKey,
189
		},
190
	}
191
192
	data, err := a.transport.PostNoAuth(ctx, "/identity/accounts/register", req)
193
	if err != nil {
194
		// Check if it's "already exists" (HTTP 400 with specific message).
195
		if se, ok := err.(*ServerError); ok && se.StatusCode == 400 {
196
			return fmt.Errorf("registration failed: %s", se.Msg)
197
		}
198
		return fmt.Errorf("register: %w", err)
199
	}
200
	_ = data
201
	return nil
202
}
203
204
// LoginWithAPIKey authenticates using an API key (client_credentials grant).
205
// API keys bypass 2FA but do NOT return encryption keys — the master password
206
// is still required to derive the KeyChain for vault operations.
207
func (a *AuthClient) LoginWithAPIKey(ctx context.Context, clientID, clientSecret, email, password string) (*TokenResponse, *crypto.KeyChain, error) {
208
	// 1. Request token with client_credentials.
209
	tokenResp, err := a.requestTokenAPIKey(ctx, clientID, clientSecret)
210
	if err != nil {
211
		return nil, nil, err
212
	}
213
214
	// 2. Set tokens on transport.
215
	a.transport.SetTokens(tokenResp.AccessToken, tokenResp.RefreshToken, tokenResp.ExpiresIn)
216
	a.transport.SetRefreshFunc(a.doRefreshToken)
217
218
	// 3. Prelogin to get KDF params (needed for key derivation).
219
	prelogin, err := a.Prelogin(ctx, email)
220
	if err != nil {
221
		return nil, nil, fmt.Errorf("prelogin for key derivation: %w", err)
222
	}
223
224
	cfg := crypto.DeriveKeyConfig{
225
		KdfType:        crypto.KdfType(prelogin.Kdf),
226
		KdfIterations:  prelogin.KdfIterations,
227
		KdfMemory:      prelogin.KdfMemory,
228
		KdfParallelism: prelogin.KdfParallelism,
229
	}
230
231
	// 4. API key response doesn't include Key/PrivateKey.
232
	// We need to fetch them from the sync endpoint.
233
	syncData, err := a.transport.Get(ctx, "/api/sync", nil)
234
	if err != nil {
235
		return nil, nil, fmt.Errorf("sync for keys: %w", err)
236
	}
237
238
	var syncResp struct {
239
		Profile struct {
240
			Key        string `json:"key"`
241
			PrivateKey string `json:"privateKey"`
242
		} `json:"profile"`
243
	}
244
	if err := json.Unmarshal(syncData, &syncResp); err != nil {
245
		return nil, nil, fmt.Errorf("parse sync for keys: %w", err)
246
	}
247
248
	if syncResp.Profile.Key == "" {
249
		return nil, nil, fmt.Errorf("sync response missing encryption key")
250
	}
251
252
	// 5. Build KeyChain from derived keys.
253
	keyChain, err := crypto.NewKeyChain(email, password, cfg, syncResp.Profile.Key, syncResp.Profile.PrivateKey)
254
	if err != nil {
255
		return nil, nil, fmt.Errorf("build keychain: %w", err)
256
	}
257
258
	return tokenResp, keyChain, nil
259
}
260
261
// GetAPIKey retrieves (or creates) an API key for the current user.
262
// Requires an authenticated session (access token set on transport).
263
func (a *AuthClient) GetAPIKey(ctx context.Context, email, password string) (clientID, clientSecret string, err error) {
264
	// Derive master password hash for verification.
265
	prelogin, err := a.Prelogin(ctx, email)
266
	if err != nil {
267
		return "", "", fmt.Errorf("prelogin: %w", err)
268
	}
269
270
	cfg := crypto.DeriveKeyConfig{
271
		KdfType:        crypto.KdfType(prelogin.Kdf),
272
		KdfIterations:  prelogin.KdfIterations,
273
		KdfMemory:      prelogin.KdfMemory,
274
		KdfParallelism: prelogin.KdfParallelism,
275
	}
276
277
	masterKey, err := crypto.DeriveMasterKey(password, email, cfg)
278
	if err != nil {
279
		return "", "", fmt.Errorf("derive master key: %w", err)
280
	}
281
282
	masterPWHash := crypto.HashMasterPassword(masterKey, password)
283
	masterPWHashB64 := base64.StdEncoding.EncodeToString(masterPWHash)
284
285
	body := map[string]string{
286
		"masterPasswordHash": masterPWHashB64,
287
	}
288
289
	data, err := a.transport.Post(ctx, "/api/accounts/api-key", body)
290
	if err != nil {
291
		return "", "", fmt.Errorf("get api key: %w", err)
292
	}
293
294
	var resp struct {
295
		APIKey string `json:"apiKey"`
296
	}
297
	if err := json.Unmarshal(data, &resp); err != nil {
298
		return "", "", fmt.Errorf("parse api key response: %w", err)
299
	}
300
301
	// Get user ID from profile for client_id
302
	syncData, err := a.transport.Get(ctx, "/api/sync", nil)
303
	if err != nil {
304
		return "", "", fmt.Errorf("get profile for user id: %w", err)
305
	}
306
307
	var syncResp struct {
308
		Profile struct {
309
			ID string `json:"id"`
310
		} `json:"profile"`
311
	}
312
	if err := json.Unmarshal(syncData, &syncResp); err != nil {
313
		return "", "", fmt.Errorf("parse profile: %w", err)
314
	}
315
316
	return "user." + syncResp.Profile.ID, resp.APIKey, nil
317
}
318
319
func (a *AuthClient) requestTokenAPIKey(ctx context.Context, clientID, clientSecret string) (*TokenResponse, error) {
320
	form := url.Values{
321
		"grant_type":       {"client_credentials"},
322
		"client_id":        {clientID},
323
		"client_secret":    {clientSecret},
324
		"scope":            {"api"},
325
		"deviceType":       {strconv.Itoa(a.device.Type)},
326
		"deviceIdentifier": {a.device.Identifier},
327
		"deviceName":       {a.device.Name},
328
	}
329
330
	data, err := a.transport.PostFormNoAuth(ctx, a.identityURL+"/identity/connect/token", form)
331
	if err != nil {
332
		return nil, fmt.Errorf("api key token request: %w", err)
333
	}
334
335
	var resp TokenResponse
336
	if err := json.Unmarshal(data, &resp); err != nil {
337
		return nil, fmt.Errorf("parse api key token response: %w", err)
338
	}
339
	return &resp, nil
340
}
341
342
type twoFactorParams struct {
343
	Provider int
344
	Token    string
345
	Remember bool
346
}
347
348
func (a *AuthClient) requestToken(ctx context.Context, email, passwordHash string, twoFA *twoFactorParams) (*TokenResponse, error) {
349
	form := url.Values{
350
		"grant_type":       {"password"},
351
		"username":         {email},
352
		"password":         {passwordHash},
353
		"scope":            {"api offline_access"},
354
		"client_id":        {"cli"},
355
		"deviceType":       {strconv.Itoa(a.device.Type)},
356
		"deviceIdentifier": {a.device.Identifier},
357
		"deviceName":       {a.device.Name},
358
	}
359
360
	if twoFA != nil {
361
		form.Set("twoFactorProvider", strconv.Itoa(twoFA.Provider))
362
		form.Set("twoFactorToken", twoFA.Token)
363
		if twoFA.Remember {
364
			form.Set("twoFactorRemember", "1")
365
		}
366
	}
367
368
	data, err := a.transport.PostFormNoAuth(ctx, a.identityURL+"/identity/connect/token", form)
369
	if err != nil {
370
		// Check if it's a 2FA challenge (HTTP 400 with TwoFactorProviders2).
371
		if se, ok := err.(*ServerError); ok && se.StatusCode == 400 {
372
			var tokenResp TokenResponse
373
			if jsonErr := json.Unmarshal(data, &tokenResp); jsonErr == nil && len(tokenResp.TwoFactorProviders2) > 0 {
374
				return &tokenResp, nil
375
			}
376
		}
377
		return nil, fmt.Errorf("token request: %w", err)
378
	}
379
380
	var resp TokenResponse
381
	if err := json.Unmarshal(data, &resp); err != nil {
382
		return nil, fmt.Errorf("parse token response: %w", err)
383
	}
384
	return &resp, nil
385
}
386
387
func (a *AuthClient) doRefreshToken(ctx context.Context, refreshToken string) (*TokenResponse, error) {
388
	form := url.Values{
389
		"grant_type":    {"refresh_token"},
390
		"refresh_token": {refreshToken},
391
		"client_id":     {"cli"},
392
	}
393
394
	data, err := a.transport.PostFormNoAuth(ctx, a.identityURL+"/identity/connect/token", form)
395
	if err != nil {
396
		return nil, fmt.Errorf("refresh token: %w", err)
397
	}
398
399
	var resp TokenResponse
400
	if err := json.Unmarshal(data, &resp); err != nil {
401
		return nil, fmt.Errorf("parse refresh response: %w", err)
402
	}
403
	return &resp, nil
404
}
405

Source Files