wise_zitadel_login
A Zitadel login package to be used with Wisemen backends. Provides a ready-made login screen with configurable identity provider buttons, built on top of oidc, auto_route and hooks_riverpod.
Features
- Pre-built
WiseLoginScreenwith bottom aligned login buttons - Multiple login types (IDPs) on one screen (e.g. Google, Apple, Microsoft, internal)
- Configurable button styling (color, text style, border radius, border)
- Loading state per login button while authenticating
auto_routeroute (WiseLoginScreenRoute) ready to drop into your router- Riverpod provider for flavored configuration
- Returns an
OAuthTokenfromwiseclienton successful login
Installation
Add this to your package's pubspec.yaml file:
dependencies:
wise_zitadel_login: ^1.2.0
Platform setup
The login flow runs in the platform's browser, which needs a little setup per platform. Skip the platforms your app does not ship on.
Android
Requires minSdk 23 (the Flutter default is higher, so usually nothing to do)
and the redirect scheme of your app. The scheme has to match the bundleId you
pass to WiseZitadelOptions, since that is what the redirect URL is built from.
Add it to android/app/build.gradle:
android {
defaultConfig {
minSdk = 23
manifestPlaceholders += [oidcRedirectScheme: "com.example.app"]
}
}
Nothing has to be added to AndroidManifest.xml: the activity that catches the
redirect ships with the oidc plugin and picks up the placeholder above.
iOS
Requires iOS 13.0 or higher, which is below the Flutter minimum, so there is
usually nothing to do. The flow uses ASWebAuthenticationSession, so the
redirect scheme does not have to be registered in Info.plist.
Set the deployment target in Xcode and in ios/Podfile if your app is still on
an older one:
platform :ios, '13.0'
macOS
Requires macOS 10.15 or higher, and the network client entitlement, since the
token exchange is an outgoing request. Add it to both
macos/Runner/DebugProfile.entitlements and macos/Runner/Release.entitlements:
<key>com.apple.security.network.client</key>
<true/>
Web
The browser comes back to a redirect.html page that hands the result to your
app over a BroadcastChannel. Copy
redirect.html
from the oidc example into your app's web/ folder, so it is served at
https://your-app.com/redirect.html.
That page is versioned together with the oidc package: re-copy it whenever the
oidc dependency of this package moves to a new major version.
The login runs in the app's own tab: the tap navigates the page to Zitadel, and
redirect.html navigates it back when the user returns. No popup and no second
tab, so there is nothing for a popup blocker to block, and nothing that has to
reach back into an opener window — an app that is cross-origin isolated
(COOP/COEP, e.g. for SharedArrayBuffer) logs in like any other.
What it costs is a page load in the middle of the login: the app is torn down while the user is at Zitadel and started again on the way back. Two things follow from that.
-
The flow state has to survive the reload, so a web app has to pass a persistent store. The default is in-memory and the login throws on web rather than fail quietly. Add
oidc_web_coreto your app and handWiseZitadelOptions.storeanOidcWebStore, which writes the same browser storage keysredirect.htmlreads:dependencies: oidc_web_core: ^1.2.0import 'package:oidc_web_core/oidc_web_core.dart'; WiseZitadelOptions( // ... store: const OidcWebStore(), );oidc_web_coreonly compiles for the web, so an app that also ships natively imports it behind a conditional import. The package clears the store again as soon as the token is handed to your app, so no session is left behind in the browser. -
The token arrives on the next page load, not out of the tap that started the login.
WiseLoginScreenhandles this for you — it finishes the pending login when it is shown and callsonLoginSuccesswith the token. If you driveWiseZitadelAuthenticatoryourself, callprepare()when your login UI appears and treat the token it returns exactly like one fromlogin().
Make sure the login screen is what the app shows on a fresh load of the URL the login started from, since that is where the browser comes back to.
Zitadel
Whitelist the redirect URLs on the application in the Zitadel console:
com.example.app:/for Android, iOS and macOS, with your own bundle idhttps://your-app.com/redirect.htmlfor web
Usage
1. Override the options provider
Override wiseZitadelOptionsProvider in your ProviderScope with your (usually flavored) Zitadel configuration:
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:wise_zitadel_login/wise_zitadel_login.dart';
void main() {
runApp(
ProviderScope(
overrides: [
wiseZitadelOptionsProvider.overrideWithValue(
WiseZitadelOptions(
zitadelBaseUrl: F.zitadelBaseUrl,
bundleId: F.bundleId,
applicationId: F.zitadelAppId,
organizationId: F.zitadelOrganizationId,
buttonOptions: WiseZitadelButtonOptions(
color: (context) => context.backgroundColors.primary,
buttonTextStyle: (context) => context.body.copyWith(
color: context.foregroundColors.primary,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
onLoginSuccess: (router, ref, token) async {
if (token == null) {
return;
}
await ref.read(appRepositoryServiceProvider).setToken(token);
router.replace(const HomeScreenRoute());
},
supportedTypes: [
const ZitadelLoginType(
buttonText: 'Internal',
iconSvgString: 'assets/icons/logo.svg',
idp: '',
),
],
),
),
],
child: const App(),
),
);
}
2. Register the route
Add WiseLoginScreenRoute to your auto_route router:
import 'package:auto_route/auto_route.dart';
import 'package:wise_zitadel_login/wise_zitadel_login.dart';
@AutoRouterConfig(replaceInRouteName: '')
class AppRouter extends RootStackRouter {
late final List<AutoRoute> routes = [
CustomRoute(
path: '/',
page: SplashScreenRoute.page,
guards: [AuthGuard(ref: ref)],
),
CustomRoute(
page: WiseLoginScreenRoute.page,
transitionsBuilder: TransitionsBuilders.noTransition,
),
];
}
3. Navigate to the login screen
Redirect unauthenticated users to the login screen, for example from an AutoRouteGuard:
import 'package:auto_route/auto_route.dart';
import 'package:wise_zitadel_login/wise_zitadel_login.dart';
import 'package:wiseclient/wiseclient.dart' show AuthenticationStatus;
class AuthGuard extends AutoRouteGuard {
@override
Future<void> onNavigation(NavigationResolver resolver, StackRouter router) async {
final status = await ref.read(appRepositoryServiceProvider).authenticationStatus.first;
switch (status) {
case AuthenticationStatus.initial:
resolver.next();
case AuthenticationStatus.unauthenticated:
resolver.redirectUntil(WiseLoginScreenRoute());
case AuthenticationStatus.authenticated:
resolver.redirectUntil(const HomeScreenRoute());
}
}
}
Optionally pass a builder to WiseLoginScreenRoute to render content behind the buttons, usually a brand's logo:
WiseLoginScreenRoute(
builder: (context) => Center(
child: Image.asset('assets/images/logo.png'),
),
)
Tokens and refreshing
A successful login returns an OAuthToken from wiseclient and nothing else:
this package keeps no session of its own. It does not persist the login and it
never refreshes the token, that stays the job of the app (through wiseclient's
fresh interceptor).
This is deliberate. Zitadel rotates refresh tokens, so a second component refreshing in the background would invalidate the token the app is holding, and whichever one refreshes last logs the user out.
Testing
The login screen reads its login flow from wiseZitadelAuthenticatorProvider.
Override it with your own WiseZitadelAuthenticator to test screens behind the
login without opening a browser:
class FakeAuthenticator implements WiseZitadelAuthenticator {
// No login is ever pending in a test, so there is nothing to resume.
@override
Future<OAuthToken?> prepare() async => null;
@override
Future<OAuthToken?> login([ZitadelLoginType? type]) async =>
OAuthToken(accessToken: 'access_token', refreshToken: 'refresh_token');
@override
Future<void> dispose() async {}
}
ProviderScope(
overrides: [
wiseZitadelAuthenticatorProvider.overrideWithValue(FakeAuthenticator()),
],
child: const App(),
);
Parameters
WiseZitadelOptions
zitadelBaseUrl(String, required): The base URL of your Zitadel instancebundleId(String, required): The app's bundle id, used for the redirect URLapplicationId(String, required): The Zitadel application idorganizationId(String, required): The Zitadel organization idsupportedTypes(List<ZitadelLoginType>, required): The login types shown as buttons on the login screenonLoginSuccess(Function, required): Callback called after a login attempt, receives theStackRouter,WidgetRefand the (nullable)OAuthTokenbuttonOptions(WiseZitadelButtonOptions, required): Styling options for the login buttonsstore(OidcStore, optional): The store the login flow keeps its state in, in-memory when left out. Web apps have to pass a persistent one, see Web
WiseZitadelButtonOptions
color(Color Function(BuildContext), required): The background color of the buttonbuttonTextStyle(TextStyle Function(BuildContext), required): The text style of the button's textborderRadius(BorderRadius, default: circular 10): The border radius of the buttonborderSide(BorderSide?, optional): The border side of the button
ZitadelLoginType
buttonText(String, required): The text displayed in the buttoniconSvgString(String, required): The SVG asset used for the button icon, usually Google, Apple, Microsoft, etc.'s logoidp(String, required): The identity provider id used for the login, an empty id logs in through Zitadel itself
Requirements
- Flutter SDK: >=3.19.5
- Dart SDK: >=3.10.0 <4.0.0
- Android:
minSdk23 - iOS: 13.0, macOS: 10.15
Dependencies
oidc: For the OAuth/OIDC authentication flowauto_route: For the login screen routehooks_riverpod: For state management and configurationwiseclient: For theOAuthTokentypewise_nav_bar&wisewidgetslibrary: For platform aware UI components
License
See LICENSE file for details.
Libraries
- wise_zitadel_login
- Wise Zitadel Login package