Files
rclone/backend/internxt/auth.go
2026-01-30 16:06:32 +00:00

147 lines
4.0 KiB
Go

// Package internxt provides authentication handling
package internxt
import (
"context"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"time"
"github.com/golang-jwt/jwt/v5"
internxtauth "github.com/internxt/rclone-adapter/auth"
internxtconfig "github.com/internxt/rclone-adapter/config"
"github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fs/config/configmap"
"github.com/rclone/rclone/lib/oauthutil"
"golang.org/x/oauth2"
)
type userInfo struct {
RootFolderID string
Bucket string
BridgeUser string
UserID string
}
type userInfoConfig struct {
Token string
}
// getUserInfo fetches user metadata from the refresh endpoint
func getUserInfo(ctx context.Context, cfg *userInfoConfig) (*userInfo, error) {
// Call the refresh endpoint to get all user metadata
refreshCfg := internxtconfig.NewDefaultToken(cfg.Token)
resp, err := internxtauth.RefreshToken(ctx, refreshCfg)
if err != nil {
return nil, fmt.Errorf("failed to fetch user info: %w", err)
}
if resp.User.Bucket == "" {
return nil, errors.New("API response missing user.bucket")
}
if resp.User.RootFolderID == "" {
return nil, errors.New("API response missing user.rootFolderId")
}
if resp.User.BridgeUser == "" {
return nil, errors.New("API response missing user.bridgeUser")
}
if resp.User.UserID == "" {
return nil, errors.New("API response missing user.userId")
}
info := &userInfo{
RootFolderID: resp.User.RootFolderID,
Bucket: resp.User.Bucket,
BridgeUser: resp.User.BridgeUser,
UserID: resp.User.UserID,
}
fs.Debugf(nil, "User info: rootFolderId=%s, bucket=%s",
info.RootFolderID, info.Bucket)
return info, nil
}
// parseJWTExpiry extracts the expiry time from a JWT token string
func parseJWTExpiry(tokenString string) (time.Time, error) {
parser := jwt.NewParser(jwt.WithoutClaimsValidation())
token, _, err := parser.ParseUnverified(tokenString, jwt.MapClaims{})
if err != nil {
return time.Time{}, fmt.Errorf("failed to parse token: %w", err)
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
return time.Time{}, errors.New("invalid token claims")
}
exp, ok := claims["exp"].(float64)
if !ok {
return time.Time{}, errors.New("token missing expiration")
}
return time.Unix(int64(exp), 0), nil
}
// jwtToOAuth2Token converts a JWT string to an oauth2.Token with expiry
func jwtToOAuth2Token(jwtString string) (*oauth2.Token, error) {
expiry, err := parseJWTExpiry(jwtString)
if err != nil {
return nil, err
}
return &oauth2.Token{
AccessToken: jwtString,
TokenType: "Bearer",
Expiry: expiry,
}, nil
}
// computeBasicAuthHeader creates the BasicAuthHeader for bucket operations
// Following the pattern from SDK's auth/access.go:96-102
func computeBasicAuthHeader(bridgeUser, userID string) string {
sum := sha256.Sum256([]byte(userID))
hexPass := hex.EncodeToString(sum[:])
creds := fmt.Sprintf("%s:%s", bridgeUser, hexPass)
return "Basic " + base64.StdEncoding.EncodeToString([]byte(creds))
}
// refreshJWTToken refreshes the token using Internxt's refresh endpoint
func refreshJWTToken(ctx context.Context, name string, m configmap.Mapper) error {
currentToken, err := oauthutil.GetToken(name, m)
if err != nil {
return fmt.Errorf("failed to get current token: %w", err)
}
cfg := internxtconfig.NewDefaultToken(currentToken.AccessToken)
resp, err := internxtauth.RefreshToken(ctx, cfg)
if err != nil {
return fmt.Errorf("refresh request failed: %w", err)
}
if resp.NewToken == "" {
return errors.New("refresh response missing newToken")
}
// Convert JWT to oauth2.Token format
token, err := jwtToOAuth2Token(resp.NewToken)
if err != nil {
return fmt.Errorf("failed to parse refreshed token: %w", err)
}
err = oauthutil.PutToken(name, m, token, false)
if err != nil {
return fmt.Errorf("failed to save token: %w", err)
}
if resp.User.Bucket != "" {
m.Set("bucket", resp.User.Bucket)
}
fs.Debugf(name, "Token refreshed successfully, new expiry: %v", token.Expiry)
return nil
}