// 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 }