keycloak_client 2.1.1 copy "keycloak_client: ^2.1.1" to clipboard
keycloak_client: ^2.1.1 copied to clipboard

A Flutter package for Keycloak authentication using the Authorization Code flow.

example/lib/main.dart

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:keycloak_client/keycloak_client.dart';

final client = KeycloakClient(
  clientConfig: ClientConfig(
    baseUrl: 'your-keycloak-server',
    realm: 'your-realm',
    clientId: 'your-client-id',
    refreshTimeout: const Duration(seconds: 3),
  ),
);

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Web only: resolve any in-progress OAuth callback before the app renders.
  if (kIsWeb) {
    try {
      final resumed = await client.handleWebCallback(Uri.base);
      if (resumed) {
        debugPrint('OAuth callback completed, session restored.');
      }
    } on Exception catch (e) {
      debugPrint('OAuth callback failed: $e');
    }
  }

  runApp(const _Application());
}

final class _Application extends StatefulWidget {
  const _Application();

  @override
  State<_Application> createState() => _ApplicationState();
}

final class _ApplicationState extends State<_Application> {
  @override
  void initState() {
    super.initState();
    client.initialize();
  }

  @override
  void dispose() {
    client.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Keycloak Example',
      theme: ThemeData(colorSchemeSeed: Colors.teal, useMaterial3: true),
      home: _AuthGate(client: client),
    );
  }
}

final class _AuthGate extends StatelessWidget {
  final KeycloakClient client;
  const _AuthGate({required this.client});

  @override
  Widget build(BuildContext context) {
    return StreamBuilder<AuthState>(
      stream: client.onAuthChange,
      builder: (context, snapshot) {
        final state = snapshot.data ?? AuthState.unknown;

        return switch (state) {
          AuthState.unknown => const _LoadingScreen(),
          AuthState.signedIn => _HomeScreen(client: client),
          AuthState.signedOut => _LoginScreen(client: client),
          AuthState.sessionExpired => _SessionExpiredScreen(client: client),
        };
      },
    );
  }
}

final class _LoadingScreen extends StatelessWidget {
  const _LoadingScreen();

  @override
  Widget build(BuildContext context) {
    return const Scaffold(body: Center(child: CircularProgressIndicator()));
  }
}

final class _LoginScreen extends StatelessWidget {
  final KeycloakClient client;
  const _LoginScreen({required this.client});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          spacing: 16,
          children: [
            const Icon(Icons.lock_outline, size: 64, color: Colors.teal),
            Text(
              'Sign in to continue',
              style: Theme.of(context).textTheme.titleLarge,
            ),
            FilledButton.icon(
              onPressed: client.login,
              icon: const Icon(Icons.login),
              label: const Text('Sign in with Keycloak'),
            ),
          ],
        ),
      ),
    );
  }
}

final class _SessionExpiredScreen extends StatelessWidget {
  final KeycloakClient client;
  const _SessionExpiredScreen({required this.client});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          spacing: 16,
          children: [
            const Icon(
              Icons.timer_off_outlined,
              size: 64,
              color: Colors.orange,
            ),
            Text(
              'Your session has expired',
              style: Theme.of(context).textTheme.titleLarge,
            ),
            const Text('Please sign in again to continue.'),
            FilledButton.icon(
              onPressed: client.login,
              icon: const Icon(Icons.login),
              label: const Text('Sign in again'),
            ),
          ],
        ),
      ),
    );
  }
}

final class _HomeScreen extends StatelessWidget {
  final KeycloakClient client;
  const _HomeScreen({required this.client});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Home'),
        actions: [
          IconButton(
            tooltip: 'Manage account',
            icon: const Icon(Icons.manage_accounts),
            onPressed: client.manageAccount,
          ),
          IconButton(
            tooltip: 'Sign out',
            icon: const Icon(Icons.logout),
            onPressed: client.logout,
          ),
        ],
      ),
      body: SingleChildScrollView(
        child: Center(
          child: ConstrainedBox(
            constraints: const BoxConstraints(maxWidth: 520),
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                _UserInfoCard(client: client),
                _CredentialsCard(client: client),
                const SizedBox(height: 24),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

final class _UserInfoCard extends StatelessWidget {
  final KeycloakClient client;
  const _UserInfoCard({required this.client});

  @override
  Widget build(BuildContext context) {
    return StreamBuilder<UserInfo?>(
      stream: client.onUserChange,
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
          return const CircularProgressIndicator();
        }

        if (snapshot.hasError) {
          return _ErrorTile(message: '${snapshot.error}');
        }

        final user = snapshot.data;
        if (user == null) {
          return const Text('No user information available.');
        }

        return Card(
          margin: const EdgeInsets.all(24),
          child: Padding(
            padding: const EdgeInsets.all(24),
            child: Column(
              mainAxisSize: MainAxisSize.min,
              spacing: 8,
              children: [
                CircleAvatar(
                  radius: 32,
                  child: Text(
                    (user.username ?? user.email ?? '?')[0].toUpperCase(),
                    style: const TextStyle(fontSize: 28),
                  ),
                ),
                if (user.username != null)
                  Text(
                    user.username!,
                    style: Theme.of(context).textTheme.titleMedium,
                  ),
                if (user.email != null)
                  Text(
                    user.email!,
                    style: Theme.of(
                      context,
                    ).textTheme.bodyMedium?.copyWith(color: Colors.grey),
                  ),
                const Divider(),
                _InfoRow(label: 'ID', value: user.id),
                if (user.givenName != null)
                  _InfoRow(label: 'First name', value: user.givenName!),
                if (user.familyName != null)
                  _InfoRow(label: 'Last name', value: user.familyName!),
              ],
            ),
          ),
        );
      },
    );
  }
}

final class _CredentialsCard extends StatefulWidget {
  final KeycloakClient client;
  const _CredentialsCard({required this.client});

  @override
  State<_CredentialsCard> createState() => _CredentialsCardState();
}

final class _CredentialsCardState extends State<_CredentialsCard> {
  late Future<List<AccountCredential>> _future =
      widget.client.getAccountCredentials();

  void _refresh() {
    setState(() {
      _future = widget.client.getAccountCredentials();
    });
  }

  @override
  Widget build(BuildContext context) {
    return Card(
      margin: const EdgeInsets.symmetric(horizontal: 24),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Text(
                  'Authentication methods',
                  style: Theme.of(context).textTheme.titleMedium,
                ),
                IconButton(
                  tooltip: 'Refresh',
                  icon: const Icon(Icons.refresh, size: 20),
                  onPressed: _refresh,
                ),
              ],
            ),
            FutureBuilder<List<AccountCredential>>(
              future: _future,
              builder: (context, snapshot) {
                if (snapshot.connectionState == ConnectionState.waiting) {
                  return const Padding(
                    padding: EdgeInsets.all(16),
                    child: Center(child: CircularProgressIndicator()),
                  );
                }
                if (snapshot.hasError) {
                  return _ErrorTile(message: '${snapshot.error}');
                }
                final credentials = snapshot.data ?? const [];
                if (credentials.isEmpty) {
                  return const Padding(
                    padding: EdgeInsets.symmetric(vertical: 8),
                    child: Text('No credentials configured.'),
                  );
                }
                return Column(
                  children: credentials.map(_credentialTile).toList(),
                );
              },
            ),
          ],
        ),
      ),
    );
  }

  Widget _credentialTile(AccountCredential credential) {
    // Pattern match on the sealed family for type-specific rendering.
    final (IconData icon, String subtitle) = switch (credential) {
      PasswordCredential() => (
        Icons.password,
        credential.isConfigured ? 'Configured' : 'Not configured',
      ),
      OtpCredential(:final instances) => (
        Icons.security,
        instances.isEmpty
            ? 'Not configured'
            : instances
                  .map((i) => '${i.userLabel ?? 'OTP'} · ${i.subType.name}')
                  .join(', '),
      ),
      WebAuthnCredential(:final instances) => (
        Icons.fingerprint,
        instances.isEmpty
            ? 'Not configured'
            : instances.map((i) => i.userLabel ?? 'Authenticator').join(', '),
      ),
      UnknownCredential() => (
        Icons.help_outline,
        '${credential.instanceCount} configured',
      ),
    };

    return ListTile(
      dense: true,
      contentPadding: EdgeInsets.zero,
      leading: Icon(icon),
      title: Text(credential.displayName ?? credential.type),
      subtitle: Text(subtitle),
      trailing: credential.isConfigured
          ? const Icon(Icons.check_circle, color: Colors.green, size: 18)
          : const Icon(Icons.radio_button_unchecked, size: 18),
    );
  }
}

final class _InfoRow extends StatelessWidget {
  final String label;
  final String value;
  const _InfoRow({required this.label, required this.value});

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: [
        Text(label, style: const TextStyle(fontWeight: FontWeight.w600)),
        Text(value),
      ],
    );
  }
}

final class _ErrorTile extends StatelessWidget {
  final String message;
  const _ErrorTile({required this.message});

  @override
  Widget build(BuildContext context) {
    return ListTile(
      leading: const Icon(Icons.error_outline, color: Colors.red),
      title: const Text('Something went wrong'),
      subtitle: Text(message),
    );
  }
}
5
likes
150
points
227
downloads

Documentation

API reference

Publisher

verified publisherwinchetechnologies.co.uk

Weekly Downloads

A Flutter package for Keycloak authentication using the Authorization Code flow.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

app_links, crypto, dio, flutter, flutter_secure_storage, logger, oauth2, url_launcher, web

More

Packages that depend on keycloak_client