Sign Up
Hero

Flutter Authentication and Authorization with Auth0, Part 4: Roles and Permissions

In this tutorial, you’ll learn how to enhance your Flutter apps by enabling authentication, supporting federated identity providers, adding authorization by introducing roles and permissions, all leveraging Auth0.

In the previous section, you learned how to add real-time chat to the application and set up the MJ Coffee app to load different chat screens.

In this section, you will learn how to manage roles and permissions in Auth0 and a Flutter app, as well as how to apply a proper authorization flow. You'll also learn how to leverage RBAC and permission-based functionalities in a Flutter application.

Look for the 🛠 emoji if you’d like to skim through the content while focusing on the build and execution steps.

Managing Roles

In a previous section in this tutorial, you learned how to create roles in the Auth0 dashboard. It’s time to see why you need roles and learn how you can leverage them into your app.

Role-based access control (RBAC) is a way to assign permissions to users based on their roles. It offers a simple approach to access management that is less prone to error than assigning permissions to users individually.

For example, suppose you use RBAC to control Customer/Employee access in the MJ Coffee application. In that case, you could give employees a role that allows them to update users’ details or access the community chat screen. In contrast, customers would view the support screen, but wouldn’t be authorized to perform tasks such as deleting messages or uploading attachments.

When planning your access control strategy, it’s generally a good idea to assign users a smalled number of permissions that allow them to get their work done.

You can leverage Auth0 Actions to assign roles automatically to each user after they sign up.

🛠 Once more, create a new custom action. In the left column menu of the Auth0 dashboard, select Actions, then Flows. The Choose Flow page will appear. Select Post User Registration:

🛠 Name the action Assign Role and add the auth0 npm module with version 2.35.1.

🛠 Add the following handler, which will be called during the execution of a PostUserRegistration flow:

// Auth0 Actions

const ManagementClient = require('auth0').ManagementClient;
const AuthenticationClient = require('auth0').AuthenticationClient;

exports.onExecutePostUserRegistration = async (event) => {
  const DOMAIN = '{YOUR_DOMAIN}';
  const auth0 = new AuthenticationClient({
    domain: DOMAIN,
    clientId: event.secrets.M2M_CLIENT_ID,
    clientSecret: event.secrets.M2M_CLIENT_SECRET,
  });
  const response = await auth0.clientCredentialsGrant({
    audience: `https://${DOMAIN}/api/v2/`,
    scope: 'read:users update:users',
  });
  const API_TOKEN = response.access_token;
  const management = new ManagementClient({
    domain: DOMAIN,
    token: API_TOKEN,
  });

  if (event.user.email.endsWith('@mjcoffee.app')) {
    // employee
    await management.assignRolestoUser(
      { id: event.user.user_id },
      { roles: ['rol_CHpJMdZUPCLzo6E2'] }
    );
  } else {
    // customer
    await management.assignRolestoUser(
      { id: event.user.user_id },
      { roles: ['rol_fG50GuNE9S72jNZn'] }
    );
  }
};

Let's analyze this handler’s logic step by step.

The handler imports both ManagementClient and AuthenticationClient from the auth0 module, a Node.js SDK that conveniently provides official Auth0 public APIs.

It then defines your domain name, DOMAIN.

🛠 In the handler, replace {YOUR_DOMAIN} with your application’s domain, which you’ll find in the Auth0 dashboard.

The handler initializes an authentication client, passing its constructor the application’s domain, client ID, and client secret.

Note that the application client is a bit special here! This is not the application you have created and worked with so far. You need to create a Machine-to-Machine (M2M) application or Auth0 non-interactive client, which makes it possible to request a client credentials grant.

🛠 Open the Auth0 dashboard in a new tab in your browser. Go to the Applications page and creating a new application, selecting the Machine to Machine Applications type:

🛠 Once the M2M application is created, take a note of Client ID and Client Secret. You will need to add them to your Actions’ secrets in the next step, then go to the APIs tab and authorize the Auth0 Management API.

As mentioned before, it's always a good idea to limit the permissions. Hence, select only those permissions needed to perform actions with this Client.

🛠 We want to be able to add roles to users, so select the read:users and update:users permissions:

🛠 Add the following to the action:

  • The key M2M_CLIENT_ID, with the client ID as its corresponding value
  • The key M2M_CLIENT_SECRET, with the client secret as the value

You can now successfully authenticate an Auth0 client and request a management API token by specifying audiences and scopes. Scopes are the ones that you have selected in the previous steps in the permission tab under Auth0 management API.

Once access is granted and the token is received, you can create a management client.

The logic is relatively simple for this MJ Coffee app.

if (event.user.email.endsWith('@mjcoffee.app')) {
  // employee
} else {
  // customer
}

Simply put, if the user’s email address ends with @mjcoffee.app they will be assigned the employee role; otherwise, they’ll be assigned the customer role. This, of course, could change based on your implementation.

Finally, the handler calls assignRolesToUser() on the management client and passes:

  • a map containing the user ID, and
  • a list of role IDs.

To get a role’s ID, select User Management in the Auth0 dashboard’s left column menu, then select Roles:

Fantastic! Don’t forget to deploy your function.

🛠 For the last step, go to Flows, then Post User Registration. Add your Assign Role custom action to the flow.

If you are impatient to test what you have created so far, navigate to the Auth0 dashboard’s Users page and create new users with and without email addresses that you have specified in your function logic. Then, go to the Role tab — you should see that for each user, their role was assigned automatically:

Managing Permissions

You’ll often need to create a custom API that defines permissions. Then you can assign those permissions to roles, which can then be assigned to users.

🛠 In the Auth0 dashboard, select Applications in the left column menu, select APIs in the submenu, and then click the Create API button to create a new custom API.

🛠 Give the API a recognizable name, such as StreamChat Management API, then define your identifier. This identifier will become your API audience. Note that the identifier cannot be modified. A good practice for naming identifiers is to use a URL, even if that URL is not publicly available. For example, you could name the identifier https://getStreamChat.mjcoffee.app/v1/.

🛠 Once the API is created, go to the RBAC Settings section and enable RBAC and Add Permissions in the Access Token. The API is supposed to be flagged as First Party so that you can turn on Allow Skipping User Consent. Turn on Allow Offline Access so that Auth0 will allow applications to ask for Refresh Tokens for this API.

🛠 Navigate to the Permissions tab and add your permissions and description. I recommend at adding at least these permissions:

  • delete.user.message
  • edit.user.message
  • upload.attachments

The more permissions you define, and the more explicit they are, the better you can control the resources.

🛠 Next, head over to Roles under User Management. Go to each role and assign the following permissions:

  • Employee
    • edit.user.message
    • upload.attachments
  • Customer
    • edit.user.message
  • Admin
    • delete.user.message
    • edit.user.message
    • upload.attachments

You have managed to create roles and users are automatically assigned roles on registration. Although role assignment is automatic now, you can still alter user roles manually. For example, you can add the Admin role to specific users in the Auth0 dashboard.

The only step remaining is to expose the roles and permissions to idToken and accessToken that the Flutter app can consume.

Exposing Roles and Permission

🛠 This step is pretty similar to what you have done when assigning roles. As a reminder, here are the steps:

  • Create a custom action, triggering Login/Post Login
  • Name the action “Revealing User Roles & Permissions”
  • Add the machine-to-machine application’s client ID and client key to the action’s secrets
  • Add auth0 npm package version 2.35.1

🛠 Once you’ve done that, follow the code below:

// Auth0 Actions

const ManagementClient = require('auth0').ManagementClient;
const AuthenticationClient = require('auth0').AuthenticationClient;
exports.onExecutePostLogin = async (event, api) => {
  const DOMAIN = 'mhadaily.eu.auth0.com';
  const auth0 = new AuthenticationClient({
    domain: DOMAIN,
    clientId: event.secrets.M2M_CLIENT_ID,
    clientSecret: event.secrets.M2M_CLIENT_SECRET,
  });
  const response = await auth0.clientCredentialsGrant({
    audience: `https://${DOMAIN}/api/v2/`,
    scope: 'read:users update:users read:roles',
  });
  const API_TOKEN = response.access_token;
  const management = new ManagementClient({
    domain: DOMAIN,
    token: API_TOKEN,
  });

  const params = { id: event.user.user_id };
  const roles = await management.getUserRoles(params);
  const permissions = await management.getUserPermissions(params);

  const namespace = 'https://users.mjcoffee.app';
  if (event.authorization) {
    api.idToken.setCustomClaim(`${namespace}/roles`, roles);
    api.accessToken.setCustomClaim(`${namespace}/roles`, roles);
    api.idToken.setCustomClaim(`${namespace}/permissions`, permissions);
    api.accessToken.setCustomClaim(`${namespace}/permissions`, permissions);
  }
};

The code is pretty similar to what you have written once before. The first change is that it adds read:roles to the scope. Make sure you have enabled this permission under the M2M application as you have done once for the read:users update:users permissions; otherwise, you will face an “unauthorized” error.

It then calls getUserRoles() and getUserPermissions(), passing the user ID. These functions will return user roles and permissions respectively.

After defining a namespace, the code calls setCustomClaim() to add both roles and permission custom claims to the ID and access tokens.

🛠 Make sure you deploy and then navigate to Flows. You want to add the new custom action to the login flow right before the previous action you created earlier:

So far so good, but you still need to go to the Flutter app and add roles and permissions to the models.

Read Roles and Permissions in Flutter

Now that both roles and permissions are available in the ID and access tokens, you can add them to Auth0IdToken and Auth0UserInfo, respectively.

🛠 First, create a file named auth0_roles.dart file in the /lib/models folder:

// /lib/models/auth0_roles.dart

import 'package:json_annotation/json_annotation.dart';

part 'auth0_roles.g.dart';

enum Role {
  Employee,
  Admin,
  Customer,
}

@JsonSerializable()
class Auth0Role {
  Auth0Role({
    required this.id,
    required this.name,
    required this.description,
  });

  final String id;
  final Role name;
  final String description;

  factory Auth0Role.fromJson(Map<String, dynamic> json) =>
      _$Auth0RoleFromJson(json);

  Map<String, dynamic> toJson() => _$Auth0RoleToJson(this);

  @override
  String toString() {
    return '''$name''';
  }
}

🛠 Then create auth0_permissions.dart in the same directory, with the code below:

// /lib/models/auth0_permissions.dart

import 'package:json_annotation/json_annotation.dart';

part 'auth0_permissions.g.dart';

class UserPermissions {
  static const String delete = 'delete.user.message';
  static const String edit = 'edit.user.message';
  static const String upload = 'upload.attachments';
}

@JsonSerializable()
class Auth0Permission {
  Auth0Permission({
    required this.permissionName,
    required this.description,
    required this.resourceServerName,
    required this.resourceServerIdentifier,
    required this.sources,
  });

  @JsonKey(name: 'permission_name')
  final String permissionName;

  final String description;
  @JsonKey(name: 'resource_server_name')
  final String resourceServerName;
  @JsonKey(name: 'resource_server_identifier')
  final String resourceServerIdentifier;

  final List<Auth0PermissionsSource> sources;

  factory Auth0Permission.fromJson(Map<String, dynamic> json) =>
      _$Auth0PermissionFromJson(json);

  Map<String, dynamic> toJson() => _$Auth0PermissionToJson(this);

  @override
  String toString() {
    return '''$permissionName''';
  }
}

@JsonSerializable()
class Auth0PermissionsSource {
  Auth0PermissionsSource({
    required this.sourceId,
    required this.sourceName,
    required this.sourceType,
  });

  @JsonKey(name: 'source_id')
  final String sourceId;
  @JsonKey(name: 'source_name')
  final String sourceName;
  @JsonKey(name: 'source_type')
  final String sourceType;

  factory Auth0PermissionsSource.fromJson(Map<String, dynamic> json) =>
      _$Auth0PermissionsSourceFromJson(json);

  Map<String, dynamic> toJson() => _$Auth0PermissionsSourceToJson(this);

  @override
  String toString() {
    return '''
      sourceId: $sourceId,
      sourceName: $sourceName,
      sourceType: $sourceType,
      ''';
  }
}

What you want to achieve here is to serialize and deserialize the roles and permissions for each model.

🛠 The next step is to update the Auth0IdToken model:

// /lib/models/auth0_id_token.dart

@JsonSerializable()
class Auth0IdToken {
  Auth0IdToken({
  
...

    required this.roles,
    required this.permissions,

...

}}

...

  @JsonKey(name: 'https://users.mjcoffee.app/roles')
  final List<Auth0Role> roles;

  @JsonKey(name: 'https://users.mjcoffee.app/permissions')
  final List<Auth0Permission> permissions;
  
...

}

🛠 Do the same for the Auth0User model:

// /lib/models/auth0_user.dart

@JsonSerializable()
class Auth0User {
  Auth0User({
  
...

    required this.permissions,
    required this.roles,
    
...

})

...

  @JsonKey(name: 'https://users.mjcoffee.app/roles')
  final List<Auth0Role> roles;

  @JsonKey(name: 'https://users.mjcoffee.app/permissions')
  final List<Auth0Permission> permissions;
  
...

}

🛠 Finally, run the build_runner command to ensure that the models are generated properly:

flutter pub run build_runner build --delete-conflicting-outputs

You can now restart the MJ Coffee app, and everything should work as expected.

Role-Based Screens in Flutter

It’s finally time to have a loading screen based on the user's role. If you remember, we wanted to add Support and Community screens for customers and employees, respectively. You created both screens earlier.

It would be easier to define getter methods in Auth0User to determine whether a user has a particular role or permission.

🛠 Add code to the Auth0User class as shown below:

// /lib/models/auth0_user.dart

class Auth0User {

...

  bool get hasImage => picture.isNotEmpty;
  bool can(String permission) => permissions
      .where(
        (p) => p.permissionName == permission,
      )
      .isNotEmpty;

  get isAdmin => roles.where((role) => role.name == Role.Admin).isNotEmpty;
  get isEmployee =>
      roles.where((role) => role.name == Role.Employee).isNotEmpty;
  get isCustomer =>
      roles.where((role) => role.name == Role.Customer).isNotEmpty;
      
...

}

These getters are pretty self-explanatory.

🛠 Next, open /lib/screens/menu.dart and locate the tabs list in the _MenuScreenState class:

// /lib/screens/menu.dart

  ...
  
  final List<Widget> tabs = [
      MenuList(coffees: coffees),
      if (AuthService.instance.profile?.isCustomer)
        SupportChatScreen()
      else
        CommunityScreen(),
      ProfileScreen(),
    ];
    
  ...

🛠 In the same file, find BottomNavigationBar and add BottomNavigationBarItem to the second position in the list:

// /lib/screens/menu.dart

...

BottomNavigationBar _bottomNavigationBar(Auth0User? user) {
    return BottomNavigationBar(
      
      ...
      
      items: <BottomNavigationBarItem>[
      
        ...
      
        BottomNavigationBarItem(
          icon: AuthService.instance.profile?.isCustomer
              ? Icon(Icons.support_agent)
              : Icon(Icons.group),
          label: AuthService.instance.profile?.isCustomer
              ? "Support"
              : "Community",
        ),
        
       ...
       
      ],
      
     ...
     
    );
  }
  
...

🛠 To make the UI look better, you can add the user's avatar to the appBar so that the complete implementation is as follows:

// /lib/screens/menu.dart

class MenuScreen extends StatefulWidget {
  static String routeName = 'menuScreen';
  static Route<MenuScreen> route() {
    return MaterialPageRoute<MenuScreen>(
      settings: RouteSettings(name: routeName),
      builder: (BuildContext context) => MenuScreen(),
    );
  }

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

class _MenuScreenState extends State<MenuScreen> {
  int _selectedIndex = 0;
  Auth0User? profile = AuthService.instance.profile;

  @override
  void initState() {
    super.initState();
  }

  final List<Widget> tabs = [
    MenuList(coffees: coffees),
    if (AuthService.instance.profile?.isCustomer)
      SupportChatScreen()
    else
      CommunityScreen(),
    ProfileScreen(),
  ];

  void _onItemTapped(int index) {
    setState(() {
      _selectedIndex = index;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      appBar: AppBar(
        automaticallyImplyLeading: false,
        centerTitle: false,
        title: Text("Welcome ${profile?.name}"),
        actions: [
          _avatar(profile),
        ],
      ),
      body: tabs[_selectedIndex],
      bottomNavigationBar: _bottomNavigationBar(profile),
    );
  }

  BottomNavigationBar _bottomNavigationBar(Auth0User? user) {
    return BottomNavigationBar(
      backgroundColor: Colors.white,
      type: BottomNavigationBarType.fixed,
      unselectedItemColor: Colors.brown.shade300,
      items: <BottomNavigationBarItem>[
        BottomNavigationBarItem(
          icon: Icon(Icons.list_alt),
          label: "Menu",
        ),
        BottomNavigationBarItem(
          icon:
              user?.isCustomer ? Icon(Icons.support_agent) : Icon(Icons.group),
          label: user?.isCustomer ? "Support" : "Community",
        ),
        BottomNavigationBarItem(
          icon: Icon(Icons.person),
          label: "Profile",
        ),
      ],
      currentIndex: _selectedIndex,
      selectedItemColor: Colors.brown.shade800,
      onTap: _onItemTapped,
    );
  }

  Padding _avatar(Auth0User? profile) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: FittedBox(
        fit: BoxFit.cover,
        child: ClipRRect(
          clipBehavior: Clip.antiAlias,
          borderRadius: BorderRadius.all(Radius.circular(600)),
          child: Container(
            child: _avatarPhoto(profile),
          ),
        ),
      ),
    );
  }

  Widget _avatarPhoto(Auth0User? profile) {
    return profile != null && profile.hasImage
        ? Image.network(
            profile.picture,
            width: 20,
            height: 20,
          )
        : Container(
            width: 20,
            height: 20,
            color: darkBrown,
            child: Center(
              child: Text('${profile?.name[0].toUpperCase()}'),
            ),
          );
  }
}

You can create a new user and assign it the “employee” role so that you can also test the Employee role screen.

Well done! Restart your app, log out, and log in again, and you’ll see the appropriate screen for your role:

But that's not all! You still need an employee ID to create a private channel between the currently signed-in customer and an employee.

You can define an API that can return available agents to create a channel. However, another strategy that would work for the MJ Coffee app is to retrieve all employees' user IDs via ID tokens’ custom claims and randomly pick one of them.

You can create other custom actions, similar to the previous steps for permissions and roles. I won’t walk you through all steps since you’ve already done it twice.

🛠 Name this action Retrieve Employees User IDs and define its logic as follows:

// Auth0 Action

const ManagementClient = require('auth0').ManagementClient;
const AuthenticationClient = require('auth0').AuthenticationClient;
exports.onExecutePostLogin = async (event, api) => {
  const DOMAIN = 'mhadaily.eu.auth0.com';
  const auth0 = new AuthenticationClient({
    domain: DOMAIN,
    clientId: event.secrets.M2M_CLIENT_ID,
    clientSecret: event.secrets.M2M_CLIENT_SECRET,
  });
  const response = await auth0.clientCredentialsGrant({
    audience: `https://${DOMAIN}/api/v2/`,
    scope: 'read:users read:roles',
  });
  const API_TOKEN = response.access_token;
  const management = new ManagementClient({
    domain: DOMAIN,
    token: API_TOKEN,
  });
  const params = { id: event.secrets.EMPLOYEE_ROLE_ID, per_page: 10, page: 0 };
  const employees = await management.getUsersInRole(params);
  const employee_ids = employees.map((employee) => employee.user_id);
  const namespace = 'https://employees.mjcoffee.app';
  if (event.authorization) {
    api.idToken.setCustomClaim(`${namespace}/id`, employee_ids);
  }
};

Again, you will use ManagementClient to get the first ten users based on their role by calling getUsersInRole() and passing the role ID that EMPLOYEE_ROLE_ID identifies from secrets, then define the namespace and set a custom claim on idToken.

🛠 Lastly, deploy this action and add it to the Login flow right after the GetStream User Token action and apply:

Locate the Auth0IdToken and Auth0User classes in the Flutter app and add a new property, availableAgents, to both of them.

🛠 In Auth0IdToken you should have:

// /lib/models/auth0_id_token.dart

@JsonSerializable()
class Auth0IdToken {
  Auth0IdToken({
  
  ...
  
    required this.availableAgents,
    
    ...
    
  });
  
...

  @JsonKey(name: 'https://employees.mjcoffee.app/id', defaultValue: [])
  final List<String> availableAgents;
  
...

}

🛠 ...and in Auth0User you can do the same:

// /lib/models/auth0_user.dart

@JsonSerializable()
class Auth0User {
  Auth0User({
  
  ...
  
    required this.availableAgents,
  
    ...
    
  });
  
...

  @JsonKey(name: 'https://employees.mjcoffee.app/id', defaultValue: [])
  final List<String> availableAgents;

...

}

🛠 Don’t forget to run this:

flutter pub run build_runner build --delete-conflicting-outputs

🛠 It's perfectly fine if you decide to make this change only to the user class. Locate createSupportChat in ChatService class. You left the employee ID blank, so now you can refactor this to pick an employee ID randomly:

// /lib/services/chat_service.dart

 String? _currentEmployeeId;

Future<Channel> createSupportChat(List<String> availableAgents) async {
    // skip if the chat is still open with current employeeId
    if (_currentEmployeeId == null) {
      final _random = new Random();
      final randomNumber = 0 + _random.nextInt(availableAgents.length - 0);
      final String employeeId = availableAgents[randomNumber].split('|').join('');
      _currentEmployeeId = employeeId;
    }

    final channel = client.channel(
      'support',
      id: _currentChannelId,
      extraData: {
        'name': 'MJCoffee Support',
        'members': [
          _currentEmployeeId,
          client.state.user!.id,
        ]
      },
    );
    await channel.watch();
    _currentChannelId = channel.id;
    return channel;
  }

Let's examine this code. First, it passes a list of employee IDs to createSupportChat. Then, it makes sure it is storing the current employee ID that has an open support chat in order to avoid recreating a new channel.

Finally, it randomly picks an ID from the list and creates a channel with the current customer.

This solution might not be the best possible one. However, it would work for our small coffee store. Ideally, you would define an API that can return an available employee to a customer on-demand. I may write another article to show you how you can better with other solutions.

🛠 Lastly, locate the createChannel() method in the _SupportChatScreenState class (it’s in the /lib/screens/support.dart file) and refactor it to pass the availableAgents.

// /lib/screens/support.dart

...

createChannel() async {
    if (profile != null) {
      final _channel = await ChatService.instance.createSupportChat(
        profile!.availableAgents,
      );
      setState(() {
        channel = _channel;
      });
    }
  }
  
  ...

It's very important that you have registered all of your employees’ IDs in Stream chat. Typically, users can log in as employees should log in and everything will work. However, if you still have not registered all of your employees, you might get an error with the message The following users are specified in channel.members but don't exist. This usually happens if you have created any users before the Login flow and Custom token generation action have been created.

Nicely done! You can restart your app, and this time you can see the support channel screen.

Permission-Based Functionalities

After applying roles to have specific kinds of access in the app, you can go one step deeper and use functionalities based on the user's permission inherited from the role.

You have already defined the can method on Auth0User in the previous section. The purpose of this method is to check if the user has given permission. Let's use it.

🛠 Locate MessageInput in the support.dart file, and you can replace it with

// /lib/screens/support.dart

...

MessageInput(
  disableAttachments: !profile!.can(UserPermissions.upload),
  sendButtonLocation: SendButtonLocation.inside,
  actionsLocation: ActionsLocation.leftInside,
  showCommandsButton: !profile?.isCustomer,
),

...

In the implementation above, disableAttachments is enabled based on the user's permission, or showCommandsButton is active only for the Customer role.

Another approach you can take is to limit the delete message functionality and apply UserPermissions.delete to remove the applicable UI.

Moreover, you may want to apply for these permissions on your back-end or API to perform. I will leave this part as homework.

Closing a Support Chat Channel

For the last section of this tutorial, I'd like to show you how to close a support channel chat.

🛠 First, you need to create a method for the ChatService class to send the command to close a channel.

// /lib/services/chat_service.dart

...

Future<void> archiveSupportChat() async {
   await client.hideChannel(
      _currentChannelId!,
      'support',
      clearHistory: true,
    );
  client.channel('support', id: _currentChannelId).dispose();
  _currentChannelId = null;
  _currentEmployeeId = null;
}

...

In this implementation, you can hide a chat with an existing ID and the type support and finally, set both _currentChannelId and _currentEmployeeId to null so that next time users come to a support screen, they’ll see a new channel created, and it will connect them to another employee.

Hiding a channel makes it invisible to the query channels. It can be retrieved if the user adds a new message to it or calls the show() method to remove the hidden status.

However, there are other possibilities. For example, you can archive or delete a channel. At the moment, archive is not exposed to the Stream Dart SDK. Therefore, for now, you can hide a channel.

🛠 Next, locate MessageInput in the support.dart file, and add actions.

// /lib/screens/support.dart
{

...

MessageInput(
  actions: [_closeChat()],
  disableAttachments: !profile!.can(UserPermissions.upload),
  sendButtonLocation: SendButtonLocation.inside,
  actionsLocation: ActionsLocation.leftInside,
  showCommandsButton: !profile?.isCustomer,
),

...

  /// method in the class
  CommonButton _closeChat() {
    return CommonButton(
      onPressed: () {
        ChatService.instance.archiveSupportChat();
        CoffeeRouter.instance.push(MenuScreen.route());
      },
      child: Icon(
        Icons.close,
        color: Colors.white,
      ),
    );
  }
}

The actions parameter adds a list of additional actions to the GetStream chat input UI. You can call the archiveSupportChat method OnPressed() and hide the chat, and redirect the user to the menu screen to show a proper message that the discussion is closed. They can reopen by returning to the support screen.

Okta FGA LogoWant to take your Authorization to the next level? → fga.dev

Conclusion

Authentication and authorization are complex but necessary features of most applications, and they can be tricky to implement and manage. Auth0 provides a reliable service that takes on these tasks. You can this service in Flutter applications without having to set up a server or maintain infrastructure. You can also make use of serverless tools such as Auth0 Actions, which you can use to add sophistication to your authentication and authorization process.

You have seen how you can speed up your development by adding a support chat using GetStreamChat to a Flutter application. You’ve also seen how you can limit the functionality available to users by leveraging roles and permissions received from Auth0 via tokens.

Congratulations! You’ve come a long way over this tutorial’s four parts, and I hope that you have learned a lot. This is just the beginning — you can still implement and configure a lot, using both Stream and Auth0, and take your apps to the next level.