flutterazureazure-ad-b2c

How to Implement Direct Username and Password Login with Azure AD B2C in Flutter without Using Microsoft's UI?


I'm working on a Flutter app where I need to implement a custom login screen that directly uses a username and password for authentication with Azure AD B2C, bypassing the default Microsoft login UI. The goal is to allow users to input their credentials directly into our app's interface.

Problem However, when trying to acquire tokens using the Resource Owner Password Credential (ROPC) flow, I'm encountering a 404 error. Here is the code:

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'dart:io';

void main() {
  HttpOverrides.global = MyHttpOverrides();
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: LoginPage(),
    );
  }
}

class LoginPage extends StatefulWidget {
  @override
  _LoginPageState createState() => _LoginPageState();
}

class _LoginPageState extends State<LoginPage> {
  final TextEditingController _usernameController = TextEditingController();
  final TextEditingController _passwordController = TextEditingController();

  void _login() async {
    final String b2cTenantName = 'XXXXtest'; // Replace with your B2C tenant name
    final String userFlowName = 'B2C_1_ROPC_Auth'; // Replace with your User Flow name
    final String clientId = '14096713-1c23-XXXX-8284-XXf2b6a5e22e'; // Replace with your App ID
    final String scope = 'openid offline_access $clientId';
    final String username = _usernameController.text;
    final String password = _passwordController.text;

    try {
      var response = await http.post(
        Uri.parse('https://$b2cTenantName.b2clogin.com/$b2cTenantName.onmicrosoft.com/$userFlowName/oauth2/v2.0/token'),
        headers: {'Content-Type': 'application/x-www-form-urlencoded'},
        body: {
          'client_id': clientId,
          'grant_type': 'password',
          'scope': scope,
          'username': username,
          'password': password,
          'response_type': 'token id_token'
        },
      );

      if (response.statusCode == 200) {
        var data = json.decode(response.body);
        print('response.statusCode  ${response.statusCode}');
        print('response.body  ${response.body}');
        _showDialog('Login Successful', 'Access Token: ${data['access_token']}');
      } else {
        print('response.statusCode  ${response.statusCode}');
        _showDialog('Login Failed', 'Failed to log in. Please check your credentials and try again.');
      }
    } catch (e) {
      print('response.e   ${e}');
      _showDialog('Login Error', 'An error occurred: $e');
    }
  }

  void _showDialog(String title, String content) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: Text(title),
        content: Text(content),
        actions: [
          ElevatedButton(
            child: Text('OK'),
            onPressed: () => Navigator.of(context).pop(),
          ),
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Login')),
      body: Padding(
        padding: EdgeInsets.all(20),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            TextField(
              controller: _usernameController,
              decoration: InputDecoration(labelText: 'Username'),
            ),
            SizedBox(height: 10),
            TextField(
              controller: _passwordController,
              decoration: InputDecoration(labelText: 'Password'),
              obscureText: true,
            ),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: _login,
              child: Text('Login'),
            ),
          ],
        ),
      ),
    );
  }
}

class MyHttpOverrides extends HttpOverrides {
  @override
  HttpClient createHttpClient(SecurityContext? context) {
    return super.createHttpClient(context)
      ..badCertificateCallback = (X509Certificate cert, String host, int port) => true;
  }
}

I've tried many approaches and even used code from Stack Overflow, but nothing has worked. Please help me if you know the solution. "

A new attempt: Here are the steps I followed.

  1. I get client id from here
    enter image description here

  2. The user flow name is the same as the one you added.

  3. b2cTenantName enter image description here domain name in here

when run flutter web show this error enter image description here

and when run on android show this error enter image description here

I/flutter (15894): response.statusCode  404

and I created the user here. enter image description here

I tried all these steps. Please let me know if I made any mistakes.

NEW ERROR enter image description here

I tried in postman also enter image description here

App registrations enter image description here

enter image description here

enter image description here

user flow

enter image description here

create user flow enter image description here

enter image description here

enter image description here

enter image description here


Solution

  • Initially, I too got same error when I ran your code in my environment like this:

    enter image description here

    To resolve the error, follow these steps where I registered one Azure AD B2C application with below account type that supports user flows:

    enter image description here

    While using ROPC flow, make sure to enable public client flow option in Authentication tab of application:

    enter image description here

    In App registration's Manifest, enable enableAccessTokenIssuance attribute to true like this:

    enter image description here

    Now, create one resource owner user flow in your Azure AD B2C tenant like this:

    enter image description here

    enter image description here

    Now, I used below modified code in my environment that asked user to enter credentials and gave access token successfully like this:

    main.dart:

    import 'package:flutter/material.dart';
    import 'package:http/http.dart' as http;
    import 'dart:convert';
    import 'dart:io';
    
    void main() {
      HttpOverrides.global = MyHttpOverrides();
      runApp(MyApp());
    }
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          home: LoginPage(),
        );
      }
    }
    
    class LoginPage extends StatefulWidget {
      @override
      _LoginPageState createState() => _LoginPageState();
    }
    
    class _LoginPageState extends State<LoginPage> {
      final TextEditingController _usernameController = TextEditingController();
      final TextEditingController _passwordController = TextEditingController();
    
      void _login() async {
        final String b2cTenantName = 'infrab2c'; // Replace with your B2C tenant name
        final String userFlowName = 'B2C_1_ROPC_Auth'; // Replace with your User Flow name
        final String clientId = '7983c484-db70-xxx-bc71-xxxxx'; // Replace with your App ID
        final String scope = 'openid offline_access $clientId';
        final String username = _usernameController.text;
        final String password = _passwordController.text;
    
        try {
          var response = await http.post(
            Uri.parse('https://$b2cTenantName.b2clogin.com/$b2cTenantName.onmicrosoft.com/$userFlowName/oauth2/v2.0/token'),
            headers: {'Content-Type': 'application/x-www-form-urlencoded'},
            body: {
              'client_id': clientId,
              'grant_type': 'password',
              'scope': scope,
              'username': username,
              'password': password,
              'response_type': 'token id_token'
            },
          );
    
          if (response.statusCode == 200) {
            var data = json.decode(response.body);
            print('response.statusCode  ${response.statusCode}');
            print('response.body  ${response.body}');
            _showDialog('Login Successful', 'Access Token: ${data['access_token']}');
          } else {
            print('response.statusCode  ${response.statusCode}');
            _showDialog('Login Failed', 'Failed to log in. Please check your credentials and try again.');
          }
        } catch (e) {
          print('response.e   ${e}');
          _showDialog('Login Error', 'An error occurred: $e');
        }
      }
    
      void _showDialog(String title, String content) {
        showDialog(
          context: context,
          builder: (context) => AlertDialog(
            title: Text(title),
            content: Text(content),
            actions: [
              ElevatedButton(
                child: Text('OK'),
                onPressed: () => Navigator.of(context).pop(),
              ),
            ],
          ),
        );
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: Text('Login')),
          body: Padding(
            padding: EdgeInsets.all(20),
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                TextField(
                  controller: _usernameController,
                  decoration: InputDecoration(labelText: 'Username'),
                ),
                SizedBox(height: 10),
                TextField(
                  controller: _passwordController,
                  decoration: InputDecoration(labelText: 'Password'),
                  obscureText: true,
                ),
                SizedBox(height: 20),
                ElevatedButton(
                  onPressed: _login,
                  child: Text('Login'),
                ),
              ],
            ),
          ),
        );
      }
    }
    
    class MyHttpOverrides extends HttpOverrides {
      @override
      HttpClient createHttpClient(SecurityContext? context) {
        return super.createHttpClient(context)
          ..badCertificateCallback = (X509Certificate cert, String host, int port) => true;
      }
    }
    

    Response:

    enter image description here

    Reference:

    Set up a resource owner password credentials flow - Azure AD B2C | Microsoft