Skip to content

Commit

Permalink
🔖 v0.4.1 (#17)
Browse files Browse the repository at this point in the history
  • Loading branch information
fa-fifi authored Dec 5, 2023
2 parents 1e5c30c + ac543fa commit 055fda4
Show file tree
Hide file tree
Showing 11 changed files with 80 additions and 56 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Publish to pub.dev
name: Publish Package

on:
push:
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.4.1
* Edit readme.md.
* Update documentation.

## 0.4.0
* Add package screenshots.
* Rename domain variable into frontendUrl to avoid confusion.
Expand Down
42 changes: 19 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@

<br/><a href="https://www.keycloak.org"><img src="https://www.keycloak.org/resources/images/logo.svg" width="100%" alt="cover image" url="https://www.keycloak.org"/></a><br/>

Integrate **Keycloak Single Sign-On (SSO)** authentication into your Flutter apps seamlessly using this plugin. Tokens are automatically managed under the hood, and if necessary, you can easily access them without writing any extra code. A user authentication state stream is also provided for the app to listen to in order to stay in sync with authentication status changes.
Integrate **Keycloak Single Sign-On (SSO)** authentication into your Flutter apps seamlessly using this plugin. Tokens are automatically managed under the hood and are easily accessible. A user authentication state stream is also provided for the app to listen to in order to stay in sync with authentication status changes.

## 👟 Getting Started

For end-user authentication and authorization, this plugin will integrate with the [**AppAuth**](https://appauth.io) SDKs to establish connections with OAuth 2.0 and OpenID Connect. This integration allows users to securely log in and access protected resources, such as APIs or user data from third-party providers. Additionally, for token security, the [**flutter_secure_storage**](https://pub.dev/packages/flutter_secure_storage) package will be implemented to securely store all the tokens within the Keychain for iOS and Keystore for Android.
For end-user authentication and authorization, this plugin integrates with the [**AppAuth**](https://appauth.io) SDK to establish connections with OAuth 2.0 and OpenID Connect. This integration allows users to securely log in and access protected resources, such as APIs or user data from third-party providers. Meanwhile, for token security, the [**flutter_secure_storage**](https://pub.dev/packages/flutter_secure_storage) package will be implemented to securely store all the tokens within the Keychain for iOS and Keystore for Android.

- [**AndroidX**](https://developer.android.com/jetpack/androidx) is required for this package. Starting from Flutter v1.12.13, newly created projects already enable AndroidX by default. In case your project was created prior to this Flutter version, please migrate it before using this package. You can follow this migration [guide](https://docs.flutter.dev/release/breaking-changes/androidx-migration) provided by the Flutter team.

- Starting with Android API 28 and iOS 9, insecure HTTP connections are disabled by default on iOS and Android. To allow cleartext connections for your builds, you can check out this [guide](https://docs.flutter.dev/release/breaking-changes/network-policy-ios-android) provided by the Flutter team. However, it is not recommended to do this for your release build. Please use secure connections inside your release build whenever possible.

## 🕹️ Platform Configuration
Below are the configurations for each supported platform.

Expand All @@ -40,18 +42,6 @@ Attribute application@name at AndroidManifest.xml:5:9-42 requires a placeholder

If you see this error then update your `build.gradle` to use `+=` instead.

In case your domain is using cleartext network traffic, such as HTTP, don't forget to enable the `android:usesCleartextTraffic` attribute under the `<application>` tag inside your `AndroidManifest.xml` file. This will ensure your app will be able to communicate with the keycloak server without any network issues.

```xml
<manifest ...>
<application
...
android:usesCleartextTraffic="true">
...
</application>
</manifest>
```

### iOS/macOS Setup
Go to the `Info.plist` for your iOS/macOS app to specify the custom scheme so that there should be a section in it that look similar to the following but replace `<bundle_identifier>` with the desired value.

Expand All @@ -69,7 +59,7 @@ Go to the `Info.plist` for your iOS/macOS app to specify the custom scheme so th
</array>
```

## 🚀 Plugin Usage
## 🚀 Usage
Use it directly or create an instance of the plugin somewhere inside your code, like below.

```dart
Expand All @@ -87,7 +77,7 @@ void main() async {
}
```

To listen to the user authentication stream, create a StreamBuilder widget that listens to the `keycloakWrapper.authenticationStream` and navigates the user to the login screen when the stream returns false and redirects the user to the home screen when the login is successful. Set the initial value of the StreamBuilder widget to `false` to make sure the stream will never return null.
To listen to the user authentication state stream, create a StreamBuilder widget that listens to the `keycloakWrapper.authenticationStream` and navigates the user to the login screen when the stream returns false and redirects the user to the home screen when the login is successful. Set the initial value of the StreamBuilder widget to `false` to make sure the stream will never return null.

```dart
class MyApp extends StatelessWidget {
Expand All @@ -104,21 +94,30 @@ class MyApp extends StatelessWidget {
}
```

Afterwards, create a button somewhere inside your login screen and use the following method to initiate the login process. Make sure to replace all the placeholders with your own values.
Afterwards, create a button somewhere inside your login screen and use the following method to initiate the login process. Make sure to replace all the placeholders with your own configuration values.

```dart
Future<void> login() async {
final config = KeycloakConfig(
bundleIdentifier: '<bundle_identifier>',
clientId: '<client_id>',
domain: '<domain>',
frontendUrl: '<frontend_url>',
realm: '<realm>');
await keycloakWrapper.login(config);
}
```

For logout, just this simple method will do. Make sure you pop off all stacked screens, if there are any.
Once logged in, you're able to retrieve the user's information like below.

```
final user = await keycloakWrapper.getUserInfo();
final name = user?['name'];
final email = user?['email'];
```

For logout, just this simple method will do. Make sure to pop off all stacked screens, if there are any.

```dart
Future<void> logout() async {
Expand All @@ -134,7 +133,4 @@ keycloakWrapper.onError = (e, s) {
};
```

You can refer to the [example](https://pub.dev/packages/keycloak_wrapper/example) to see how this plugin works inside a real-life app.

## ✨ Available APIs
To be filled in the next update.
You can refer to the [example](https://pub.dev/packages/keycloak_wrapper/example) to see how this plugin works inside a real-life app.
24 changes: 19 additions & 5 deletions example/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class MyApp extends StatelessWidget {
class LoginScreen extends StatelessWidget {
const LoginScreen({super.key});

// Logs user in using given configuration.
// Login using the given configuration.
Future<bool> login() async {
final config = KeycloakConfig(
bundleIdentifier: '<bundle_identifier>',
Expand Down Expand Up @@ -69,7 +69,7 @@ class LoginScreen extends StatelessWidget {
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});

// Logs user out from the current realm.
// Logout from the current realm.
Future<bool> logout() async {
// Check if user has successfully logged out.
final isLoggedOut = await keycloakWrapper.logout();
Expand All @@ -80,9 +80,23 @@ class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) => Scaffold(
body: Center(
child: TextButton(
onPressed: logout,
child: const Text('Logout'),
child: Column(
children: [
FutureBuilder(
// Retrieve the user information.
future: keycloakWrapper.getUserInfo(),
builder: (context, snapshot) {
final name = snapshot.data?['name'] as String;
final email = snapshot.data?['email'] as String;

return Text('$name\n$email\n\n');
},
),
TextButton(
onPressed: logout,
child: const Text('Logout'),
),
],
),
),
);
Expand Down
3 changes: 2 additions & 1 deletion lib/keycloak_wrapper.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
library keycloak_wrapper;
/// A wrapper library that contains all of the main methods.
library;

import 'dart:async';
import 'dart:convert';
Expand Down
9 changes: 4 additions & 5 deletions lib/src/config.dart
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
part of keycloak_wrapper;
part of '../keycloak_wrapper.dart';

/// Contains all the configurations required for the authentication requests.
class KeycloakConfig {
KeycloakConfig._();

/// The single instance of this class.
static final KeycloakConfig instance = KeycloakConfig._();

/// Initializes the configuration settings such as
/// [bundleIdentifier], [clientId], [frontendUrl], and [realm]. These settings
/// are essential for interacting with the Keycloak server during the
/// authentication process.
/// Initializes the configuration settings, which are essential for interacting with the Keycloak server.
factory KeycloakConfig(
{required String bundleIdentifier,
required String clientId,
Expand Down
2 changes: 1 addition & 1 deletion lib/src/constants.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
part of keycloak_wrapper;
part of '../keycloak_wrapper.dart';

const _appAuth = FlutterAppAuth();
const _secureStorage = FlutterSecureStorage();
Expand Down
4 changes: 3 additions & 1 deletion lib/src/exceptions.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
part of keycloak_wrapper;
part of '../keycloak_wrapper.dart';

// TODO: Implement custom exception

/// Thrown whenever an error occurs during authentication process.
class KeycloakException implements Exception {
final String message;
final Uri? uri;
Expand Down
8 changes: 4 additions & 4 deletions lib/src/helpers.dart
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
part of keycloak_wrapper;
part of '../keycloak_wrapper.dart';

/// Extension on [TokenResponse]
/// Extension of the [TokenResponse] class from flutter_appauth package.
extension TokenResponseHelper on TokenResponse? {
/// Checks the validation of the token response.
/// Checks the validity of the token response.
bool get isValid =>
this == null ? false : this?.accessToken != null && this?.idToken != null;
}

/// Parses the JSON web token and returns its payload.
/// Parses the JSON Web Token and returns its payload.
Map<String, dynamic>? jwtDecode(String? source) => jsonDecode(utf8
.decode(base64Url.decode(base64Url.normalize('$source'.split('.')[1]))));
28 changes: 18 additions & 10 deletions lib/src/wrapper.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
part of keycloak_wrapper;
part of '../keycloak_wrapper.dart';

/// Manages user authentication and token exchange using Keycloak.
///
/// It uses KeycloakConfig for configuration settings and relies on the
/// AppAuth package for OAuth2 authorization.
/// It uses [KeycloakConfig] for configuration settings and relies on flutter_appauth package for OAuth2 authorization.
class KeycloakWrapper {
KeycloakWrapper._();

Expand All @@ -14,25 +13,30 @@ class KeycloakWrapper {
late final _streamController = StreamController<bool>();

/// Stream of the user authentication state.
/// Returns true if login is successful.
///
/// Returns true if the user is currently logged in.
Stream<bool> get authenticationStream => _streamController.stream;

/// Details from making a successful token exchange.
TokenResponse? tokenResponse;

/// Called whenever an error gets caught.
///
/// By default, all errors will be printed into the console.
void Function(Object e, StackTrace s) onError = (e, s) => debugPrint('$e');

/// Returns the id token string.\
/// To get the payload, do ```jwtDecode(KeycloakWrapper().idToken)```.
/// Returns the id token string.
///
/// To get the payload, do `jwtDecode(KeycloakWrapper().idToken)`.
String? get idToken => tokenResponse?.idToken;

/// Returns the access token string.\
/// To get the payload, do ```jwtDecode(KeycloakWrapper().accessToken)```.
/// Returns the access token string.
///
/// To get the payload, do `jwtDecode(KeycloakWrapper().accessToken)`.
String? get accessToken => tokenResponse?.accessToken;

/// Returns the refresh token string.\
/// Returns the refresh token string.
///
/// To get the payload, do `jwtDecode(KeycloakWrapper().refreshToken)`.
String? get refreshToken => tokenResponse?.refreshToken;

Expand Down Expand Up @@ -86,6 +90,7 @@ class KeycloakWrapper {
}

/// Logs the user in.
///
/// Returns true if login is successful.
Future<bool> login(KeycloakConfig config) async {
try {
Expand Down Expand Up @@ -115,6 +120,7 @@ class KeycloakWrapper {
}

/// Logs the user out.
///
/// Returns true if logout is successful.
Future<bool> logout() async {
try {
Expand All @@ -136,7 +142,9 @@ class KeycloakWrapper {
}
}

/// Sends a GET request with access token included inside the headers.
/// Sends a GET request with Bearer Token authorization header.
///
/// To send a request using another HTTP method, just copy this function's code blocks and replace the `getUrl()` method.
Future<dynamic> get(Uri uri) async {
final client = HttpClient();
final request = await client.getUrl(uri)
Expand Down
10 changes: 5 additions & 5 deletions pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
name: keycloak_wrapper
description: Keycloak Single Sign-On (SSO) authentication package for Flutter framework.
version: 0.4.0
version: 0.4.1
repository: https://github.com/fa-fifi/keycloak-wrapper
topics: [authentication, keycloak, oauth2, openidconnect]
topics: [ authentication, keycloak, oauth2, openidconnect ]
screenshots:
- description: 'Login screen with the default Keycloak theme.'
- description: "Login screen with the default Keycloak theme"
path: screenshots/login-screen.png

environment:
sdk: '>=3.1.2 <4.0.0'
sdk: ">=3.1.2 <4.0.0"
flutter: ">=1.17.0"

dependencies:
Expand All @@ -20,6 +20,6 @@ dependencies:
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
flutter_lints: ^3.0.1

flutter:

0 comments on commit 055fda4

Please sign in to comment.