You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter中基于授权码模式的OAuth2客户端对接Intuit QuickBooks Online的技术咨询

Hey there! Let’s walk through your QuickBooks OAuth2 implementation in Flutter Web, clear up your questions, and fix up your code to align with the Authorization Code Grant Type properly.

1. Core OAuth2 Authorization Code Flow Clarification

First, let’s correct a key misunderstanding in your current approach: the Authorization Code Grant Type doesn’t return an access token directly in the URL fragment (that’s the Implicit Flow, which is less secure). The correct flow for your use case is:

  • User triggers the auth flow, opening a QuickBooks login popup with response_type=code
  • After user authorization, QuickBooks redirects to your configured redirect_uri with a temporary code parameter
  • Your app uses this code (plus your client ID/secret) to call Intuit’s token endpoint and exchange it for a valid access_token and refresh_token
  • Use the access token to call QuickBooks API, and use the refresh token to get a new access token when the old one expires
2. Answers to Your Key Questions

2.1 Token & Authorization Code Storage

  • Authorization Code: Don’t store this! It’s a one-time use token that expires quickly. You should exchange it for access/refresh tokens immediately after receiving it.
  • Access Token & Refresh Token: Yes, these should be stored, but with security in mind:
    • Use Firestore, but lock it down with Firebase Security Rules so each user can only access their own token document.
    • The refresh token is long-lived, so avoid exposing it to the frontend. It’s safer to let a backend (like Cloud Functions) hold and manage the refresh token, while the frontend only uses the short-lived access token.

2.2 Role of Cloud Functions

Cloud Functions are critical here for two big reasons:

  • Security: You can’t expose your client_secret in frontend Flutter code (it would be visible to anyone inspecting your app’s source). Cloud Functions run server-side, so you can safely store the client secret there and handle the token exchange.
  • Token Management: Use Cloud Functions to handle refresh token logic (when the access token expires, call Intuit’s endpoint to get a new one) and manage Firestore token storage. This keeps your frontend code clean and secure.
  • Optional Firestore Proxy: While frontend can write directly to Firestore, using Cloud Functions adds an extra layer of validation to ensure only authorized users can store/retrieve tokens.

2.3 User Sessions & State Management

  • User Sessions: Pair your QuickBooks auth with Firebase Auth (even if you use email/password login) to create a stable user identity. You can link QuickBooks tokens to a Firebase Auth UID, making it easy to associate tokens with specific users.
  • State Management: Use a package like Riverpod, Provider, or Bloc to manage the access token state across your app. This lets you easily access the token from any screen, trigger refresh logic when it expires, and update the UI when the token state changes.
3. Code Improvements & Fixes

Let’s revise your code to follow the correct Authorization Code Flow and address security gaps.

3.1 Redirect Page (static.html)

First, create a simple HTML page at your redirect_uri to capture the authorization code and send it back to your Flutter app:

<!-- static.html -->
<!DOCTYPE html>
<html>
<head>
  <title>QuickBooks Auth Redirect</title>
</head>
<body>
<script>
  const urlParams = new URLSearchParams(window.location.search);
  const code = urlParams.get('code');
  const state = urlParams.get('state');
  
  // Send code/state to parent Flutter window
  window.opener.postMessage({
    type: 'quickbooks_auth',
    code: code,
    state: state
  }, window.opener.origin);
  
  window.close();
</script>
</body>
</html>

3.2 Updated Flutter Code

Modify your _MyHomePageState to listen for the authorization code and call a Cloud Function to exchange it for tokens:

class _MyHomePageState extends State<MyHomePage> {
  late html.WindowBase _popupWin;
  String? _accessToken;
  final String _expectedState = "security_token=138r5719ru3e1&url=https://qb-payment-app.web.app/"; // Match your auth state

  @override
  void initState() {
    super.initState();
    // Listen for auth code from static.html
    html.window.onMessage.listen((event) {
      if (event.data['type'] == 'quickbooks_auth') {
        final receivedCode = event.data['code'];
        final receivedState = event.data['state'];
        
        // Validate state to prevent CSRF attacks
        if (receivedState == _expectedState) {
          _exchangeCodeForToken(receivedCode);
        } else {
          print("Invalid state detected - potential CSRF risk!");
        }
        
        _popupWin.close();
      }
    });

    // Open auth popup on app load
    WidgetsBinding.instance!.addPostFrameCallback((_) {
      final currentUri = Uri.base;
      final redirectUri = Uri(
        host: currentUri.host,
        scheme: currentUri.scheme,
        port: currentUri.port,
        path: '/static.html',
      ).toString();

      final authUrl = Uri.parse('https://appcenter.intuit.com/connect/oauth2')
          .replace(queryParameters: {
        'client_id': clientId,
        'response_type': 'code',
        'scope': 'com.intuit.quickbooks.accounting',
        'redirect_uri': redirectUri,
        'state': _expectedState,
      }).toString();

      _popupWin = html.window.open(
        authUrl, 
        "Intuit QuickBooks Online Auth", 
        "width=800, height=900, scrollbars=yes"
      );
    });
  }

  // Call Cloud Function to exchange code for tokens
  Future<void> _exchangeCodeForToken(String code) async {
    try {
      // Replace with your Cloud Function URL
      final response = await http.post(
        Uri.parse('https://us-central1-your-project.cloudfunctions.net/exchangeQuickBooksCode'),
        body: jsonEncode({
          'code': code,
          'redirect_uri': 'https://qb-payment-app.web.app/static.html'
        }),
        headers: {'Content-Type': 'application/json'},
      );

      if (response.statusCode == 200) {
        final tokenData = jsonDecode(response.body);
        final accessToken = tokenData['access_token'];
        final refreshToken = tokenData['refresh_token'];

        // Store tokens in Firestore (linked to Firebase Auth user)
        await FirebaseFirestore.instance
            .collection('user_tokens')
            .doc(FirebaseAuth.instance.currentUser!.uid)
            .set({
          'access_token': accessToken,
          'refresh_token': refreshToken,
          'expires_at': DateTime.now().add(Duration(seconds: tokenData['expires_in'])),
        });

        setState(() {
          _accessToken = accessToken;
        });
        print("Successfully authenticated with QuickBooks!");
      } else {
        print("Token exchange failed: ${response.body}");
      }
    } catch (e) {
      print("Error during token exchange: $e");
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('QuickBooks Auth Demo')),
      body: Center(
        child: _accessToken == null
            ? const CircularProgressIndicator()
            : Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  const Text('Authenticated Successfully!'),
                  const SizedBox(height: 16),
                  Text('Access Token: ${_accessToken!.substring(0, 20)}...'),
                ],
              ),
      ),
    );
  }
}

3.3 Cloud Function for Token Exchange

Create a Node.js Cloud Function to handle the secure token exchange (this keeps your client secret hidden):

const functions = require("firebase-functions");
const axios = require("axios");

exports.exchangeQuickBooksCode = functions.https.onRequest(async (req, res) => {
  const { code, redirect_uri } = req.body;
  const clientId = "YOUR_QUICKBOOKS_CLIENT_ID";
  const clientSecret = "YOUR_QUICKBOOKS_CLIENT_SECRET";

  try {
    const tokenResponse = await axios.post(
      "https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer",
      new URLSearchParams({
        grant_type: "authorization_code",
        code: code,
        redirect_uri: redirect_uri,
      }),
      {
        auth: { username: clientId, password: clientSecret },
        headers: { "Content-Type": "application/x-www-form-urlencoded" },
      }
    );

    res.status(200).send(tokenResponse.data);
  } catch (error) {
    functions.logger.error("Token exchange error:", error);
    res.status(error.response.status).send(error.response.data);
  }
});
4. Final Recommendations
  • Firebase Security Rules: Lock down your Firestore user_tokens collection to only allow access by the owner:
    rules_version = '2';
    service cloud.firestore {
      match /databases/{database}/documents {
        match /user_tokens/{userId} {
          allow read, write: if request.auth != null && request.auth.uid == userId;
        }
      }
    }
    
  • Token Refresh Logic: Add another Cloud Function to refresh access tokens when they expire, using the stored refresh token.
  • Error Handling: Add user-friendly error messages for cases like failed auth, popup closure without authorization, or token expiration.
  • Firebase Auth Integration: Require users to log into Firebase Auth first, so you have a stable UID to link QuickBooks tokens to.

内容的提问来源于stack exchange,提问作者Stephen Stilwell

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.30 22:57:47