arblant
firefox-vpn-client / core / internal / proxy / auth.go
auth.go68 lines · 2.0 KB
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
package proxy

import (
	"context"
	"fmt"
	"time"

	"github.com/armanlife/firefox-vpn-client/core/internal/fxa"
	"github.com/armanlife/firefox-vpn-client/core/internal/logx"
)

const oauthRefreshLead = 2 * time.Minute

func refreshRuntimeAuth(ctx context.Context, auth *runtimeAuth) (*runtimeAuth, error) {
	if auth == nil || auth.Token == nil || auth.Token.RefreshToken == "" {
		return nil, fmt.Errorf("no refresh token available")
	}
	ctx, cancel := context.WithTimeout(ctx, APIRequestTimeout)
	token, err := fxa.RefreshToken(ctx, auth.Token.RefreshToken)
	cancel()
	if err != nil {
		return nil, err
	}
	refreshed := &runtimeAuth{Token: token, ObtainedAt: time.Now(), persist: auth.persist, accountID: auth.accountID}
	if err := refreshed.save(); err != nil {
		logx.Warn("saving refreshed tokens failed: %v", err)
	}
	return refreshed, nil
}

// FreshRuntimeAuth returns auth when its access token is still valid and a
// refreshed auth otherwise.
func FreshRuntimeAuth(ctx context.Context, auth *runtimeAuth) (*runtimeAuth, error) {
	if auth.accessTokenValid(time.Now()) {
		return auth, nil
	}
	if auth == nil || auth.Token == nil || auth.Token.RefreshToken == "" {
		return nil, fmt.Errorf("%w: no refresh token stored", fxa.ErrReauthRequired)
	}
	return refreshRuntimeAuth(ctx, auth)
}

type runtimeAuth struct {
	Token      *fxa.TokenResponse
	ObtainedAt time.Time

	// persist stores refreshed credentials in the account store.
	persist func(*runtimeAuth) error
	// accountID names the stored account in multi-account mode.
	accountID string
}

// save persists the credentials where they were loaded from; auths
// without a store (tests) are kept in memory only.
func (a *runtimeAuth) save() error {
	if a.persist != nil {
		return a.persist(a)
	}
	return nil
}

func (a *runtimeAuth) accessTokenValid(now time.Time) bool {
	if a == nil || a.Token == nil || a.Token.AccessToken == "" {
		return false
	}
	expiry := a.ObtainedAt.Add(time.Duration(a.Token.ExpiresIn) * time.Second)
	return now.Before(expiry.Add(-oauthRefreshLead))
}