Home > OS >  How do i implement login with FastAPI /token endpoint in Flutter? FastAPI(JWT OAuth) Frontend & Fl
How do i implement login with FastAPI /token endpoint in Flutter? FastAPI(JWT OAuth) Frontend & Fl

Time:06-08

Been trying for days to implement login with the /token endpoint from fastAPI in my flutter app I have checked many tutorials online but none seems to show how to do it correctly when using JWT and OAuth. I decided to go with Dio uisng fullstacks tutorial on youtube i was able to understand Dio and implement it in my app. But i get this error in flutter when i hit the login button:

error: {detail: [{loc: [body, username], msg: field required, type: value_error.missing}, 

And this error on fastAPI:

"POST /token HTTP/1.1" 422 Unprocessable Entity

My flutter login.dart looks like this:

import 'dart:convert';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import '../../dio/dio_client.dart';
import '../../dio/dio_token_manager.dart';
import 'logout.dart';

class LoginScreen extends StatefulWidget {
  const LoginScreen({Key? key}) : super(key: key);

  @override
  _LoginScreenState createState() => _LoginScreenState();
}

class _LoginScreenState extends State<LoginScreen> {
  final _formKey = GlobalKey<FormState>();
  final _controllerUsername = TextEditingController();
  final _controllerPassword = TextEditingController();
  var _username = '', _password = '';

  @override
  void initState() {
    super.initState();
    () async {
      DioClient.dio.interceptors.add(TokenManager.instance);
      await checkLogin();
    }();
  }
  Future<void> checkLogin() async {
    try {
      var response = await DioClient.dio.get('http://10.0.2.2:8000/token');
      if (response.statusCode == 200) {
        Navigator.of(context)
            .push(MaterialPageRoute(builder: (_) => const LogoutScreen()));
      }
    } on DioError catch (e) {
      debugPrint("Status code: ${e.response?.statusCode}");
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Padding(
        padding: const EdgeInsets.all(20),
        child: Form(
          key: _formKey,
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              TextFormField(
                decoration: const InputDecoration(labelText: 'Username'),
                validator: (val) => val!.isEmpty ? 'Username Required' : null,
                onSaved: (val) => _username = val!,
                keyboardType: TextInputType.text,
                controller: _controllerUsername,
                autocorrect: false,
              ),
              TextFormField(
                decoration: const InputDecoration(labelText: 'Password'),
                obscureText: true,
                validator: (val) => val!.isEmpty ? 'Password Required' : null,
                onSaved: (val) => _password = val!,
                controller: _controllerPassword,
                keyboardType: TextInputType.text,
                autocorrect: false,
              ),
              const SizedBox(height: 20),
              ElevatedButton(
                onPressed: () async {
                  final form = _formKey.currentState;
                  if (form!.validate()) {
                    form.save();
                    final snackbar = SnackBar(
                      duration: const Duration(seconds: 30),
                      content: Row( 
                        children: const [
                          CircularProgressIndicator(),
                          Text("  Logging In...")
                        ],
                      ),
                    );
                    ScaffoldMessenger.of(context).showSnackBar(snackbar);
                    await Future.delayed(const Duration(seconds: 2));
                    try {
                      final body = {
                        "username": _username,
                        "password": _password,
                      };
                      var response = await DioClient.dio.post(
                        'http://10.0.2.2:8000/token',
                        //data: jsonEncode(body),
                         data: {
                           "username": _username,
                           "password": _password,
                         },
                      );
                      if (response.statusCode == 200) {
                        Navigator.of(context).push(MaterialPageRoute(
                            builder: (_) => const LogoutScreen()));
                      }
                    } on DioError catch (e) {
                      debugPrint("error: ${e.response?.data}");
                    }
                    ScaffoldMessenger.of(context).hideCurrentSnackBar();
                  }
                },
                child: const Text('Log In'),
              )
            ],
          ),
        ),
      ),
    );
  }
}

From the fastAPI localhost:8000/docs, the /token endpoint returns the 200 OK response if you are testing the API on insomnia or postman but does not work when you try to implement the same logic on a frontend app like flutter. The fastAPI return the json with an access_token:

 {
  "access_token": "string",
  "token_type": "string"
}

The code for the fastAPI endpoints comes from this default FastAPI Docs with username as johndoe

For the Dio_token_manager.dart :

import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';

class TokenManager extends Interceptor {
  static final TokenManager _instance = TokenManager._internal();
  static TokenManager get instance => _instance;
  TokenManager._internal();
  String? _token;
  @override
  void onResponse(
    Response response,
    ResponseInterceptorHandler handler,
  ) {
    if (response.statusCode == 200) {
      var data = Map<String, dynamic>.from(response.data);
      if (data['set-token'] != null) {
        saveToken(data['token']);
      }
    } else if (response.statusCode == 401) {
      clearToken();
    }
    super.onResponse(response, handler);
  }

  @override
  void onRequest(
    RequestOptions options,
    RequestInterceptorHandler handler,
  ) {
    options.headers['Token'] = _token;
    return super.onRequest(options, handler);
  }

  Future<void> initToken() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    _token = prefs.getString('token');
  }

  void saveToken(String newToken) async {
    debugPrint('new token $newToken');
    if (_token != newToken) {
      _token = newToken;
      SharedPreferences prefs = await SharedPreferences.getInstance();
      prefs.setString('token', _token!);
    }
  }

So for the dio_client.dart

class DioClient {
  static Dio dio = Dio();
  static const baseUrl = "http://10.0.2.2:8000";
  static const productsEndpoint = "$baseUrl/products";}

What i have tried so far:

  1. I tried adding the jsonEncode to the data as can be seen from the commented code in the login code.
  2. I tried adding headers to the dioClient.post but it still doesnt work.

The fastAPI login route:

@app.post("/token", response_model=Token)
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
    user = authenticate_user(fake_users_db, form_data.username, form_data.password)
    if not user:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Bearer"},
        )
    access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    access_token = create_access_token(
        data={"sub": user.username}, expires_delta=access_token_expires
    )
    return {"access_token": access_token, "token_type": "bearer"}

The backend works just fine i think. Maybe there is something i am not doing right. Please help. Thank you.

CodePudding user response:

OAuth2PasswordRequestForm expects its data as POST form variables, not as JSON.

You can change the format sent by referencing the example given in the dio documentation for how to "Send form data" instead:

var formData = FormData.fromMap({
  'name': 'wendux',
  'age': 25,
});
var response = await dio.post('/info', data: formData);

In your case this would reference the username/password variables instead:

final body = FormData.fromMap({
  "username": _username,
  "password": _password,
});

var response = await DioClient.dio.post(
  'http://10.0.2.2:8000/token',
  data: body,
);
  • Related