Keycloak Integration For Flutter Web Using openid_client with Authorization Code Flow

Surangi Kanchana
6 min readJul 6, 2023

--

The Authorization Code Flow with Proof Key for Code Exchange (PKCE) provides an enhanced security mechanism for public clients (such as web or mobile applications) when requesting access tokens from an authorization server.

Lets look how PKCE works in a flow chart

Sure! Here’s an explanation of the steps involved in the PKCE flow:

1. The user clicks Login within the application.
The user initiates the authentication process by clicking the login button in the application.

2. The application creates a cryptographically-random code_verifier and generates a code_challenge.
The application generates a random code_verifier and derives a code_challenge from it using a hash function, typically SHA256.

3. The application redirects the user to the Keycloak server along with the code_challenge.
The application constructs a request to the Keycloak server’s authentication endpoint, including the `code_challenge` as a parameter.

4. Keycloak server redirects the user to the login and authorization prompt.
Keycloak server receives the request from the application and redirects the user to its login and authorization prompt page.

5. The user authenticates using one of the configured login options.
The user provides their credentials and goes through the authentication process using one of the available login options provided by Keycloak.

6. Keycloak server stores the code_challenge and redirects the user back to the application with an authorization code, which is valid for one use.
Upon successful authentication, Keycloak server associates the code_challenge with the generated authorization code and redirects the user back to the application’s specified redirect URI, including the authorization code.

7. The application sends this authorization code and the code_verifier (created in step 2) to the Keycloak server.
The application exchanges the received authorization code with the original code_verifier it generated and sends them to the Keycloak server’s token endpoint.

8. Keycloak server verifies the code_challenge and code_verifier.
Keycloak server verifies the code_challenge and code_verifier received from the application to ensure their integrity and match.

9. Keycloak server responds with an ID token and access token (and optionally, a refresh token).
If the code_challenge and code_verifier are successfully verified, Keycloak server responds with an ID token and access token, which can be used to authenticate the user and access protected resources.

10. Your application can use the access token to call an API and access information about the user.
The application can include the access token in API requests to authenticate and authorize the user, enabling access to protected resources or retrieving user-specific data.

11. The API responds with the requested data.
The API server validates the access token sent by the application and responds with the requested data if the token is valid and the user has the necessary permissions.

By following this PKCE flow, applications can ensure secure authentication and authorization, protecting against potential threats such as interception of the authorization code during transmission.

Implementation

Before starting the Flutter implementation, you have to configure your keycloak server. If you are not familiar with keycloack configuration, I recommend visiting the official keycloack document.

To ensure secure authentication, it is recommended to use the Authorization Code flow rather than the Implicit flow when working with OpenID Connect. However, the openid_client library, specifically the browser version, only supports the Implicit flow. If you are using Implicit as the flow type, you can use below code:

openid_client | Dart Package

// import the browser version
import 'package:openid_client/openid_client_browser.dart';

authenticate(Uri uri, String clientId, List<String> scopes) async {

// create the client
var issuer = await Issuer.discover(uri);
var client = new Client(issuer, clientId);

// create an authenticator
var authenticator = new Authenticator(client, scopes: scopes);

// get the credential
var c = await authenticator.credential;

if (c==null) {
// starts the authentication
authenticator.authorize(); // this will redirect the browser
} else {
// return the user info
return await c.getUserInfo();
}
}

Implicit flow type is considered less secure. To overcome this limitation, you can switch to using the openid_client library with slight modifications to the code. With openid_client, you will need to create a code verifier and store it securely in the window.sessionStorage or a similar storage mechanism. This code verifier will be used in the Authorization Code flow for enhanced security. Instead of relying on the openid_client library to handle the code verifier, you need to create and manage it yourself.

Once you have the code verifier stored, you can pass it as a parameter to the authorizationCodeWithPKCE method of openid_client. This method will internally generate the code challenge using the provided code verifier and perform the Authorization Code flow with PKCE.

By making these modifications, you can ensure that the more secure Authorization Code flow is used, with the code verifier being generated and managed by your application rather than relying on the openid_client library to handle it.

static String _randomString(int length) {
var r = math.Random.secure();
var chars =
'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
return Iterable.generate(length, (_) => chars[r.nextInt(chars.length)])
.join();
}

static Future<Credential> authenticateWeb() async {
var Uri discoveryUri = Uri.https("your discover uri");
var clientId = "your client id";
var scopes = ['openid', 'profile', 'email', 'offline_access'];
var issuer = await Issuer.discover(discoveryUri);
var client = Client(issuer, clientId);

var codeVerifier =
window.sessionStorage["auth_code_verifier"] ?? _randomString(50);
var state = window.sessionStorage["auth_state"] ?? _randomString(20);
var responseUrl = window.sessionStorage["auth_callback_response_url"];
var flow = authorizationCodeWithPKCE(
client,
scopes: scopes,
codeVerifier: codeVerifier,
state: state,
);

flow.redirectUri =
Uri.parse(
'${window.location.protocol}//${window.location.host}${window.location.pathname}');

if (responseUrl != null) {
// handle callback
try {
var responseUri = Uri.parse(responseUrl);
var credentials = await flow.callback(responseUri.queryParameters);
return credentials;
} finally {
window.sessionStorage.remove("auth_code_verifier");
window.sessionStorage.remove("auth_callback_response_url");
window.sessionStorage.remove("auth_state");
}
} else {
// redirect to auth server
window.sessionStorage["auth_code_verifier"] = codeVerifier;
window.sessionStorage["auth_state"] = state;
var authorizationUrl = flow.authenticationUri;
window.location.href = authorizationUrl.toString();
throw "Authenticating...";
}
}

Let’s go through the code step by step:

1. The _randomString function generates a random string of the specified length using alphanumeric characters.

2. The authenticateWeb function is an asynchronous function that performs web-based authentication. It starts by discovering the OpenID Connect issuer (authorization server) using the discoveryUri.

3. A client is created using the discovered issuer and clientId.

4. The codeVerifier, state, and responseUrl are obtained from window.sessionStorage or randomly generated if not present. The codeVerifier is a random string used in the PKCE (Proof Key for Code Exchange) flow for enhanced security.

5. An authorization flow is initialized using the OpenID Connect Flow.authorizationCodeWithPKCE method. The client, scopes, code verifier, and state are provided as parameters.

6. The redirectUri is set for the flow. It is derived from the current window’s location, preserving the protocol, host, and path.

7. If a responseUrl is present, it means a callback has occurred after authentication. The response URL is parsed, and the flow’s callback method is invoked to obtain the credentials. The credentials are returned.

8. If there is no responseUrl, it means authentication needs to be initiated. The codeVerifier and state are stored in window.sessionStorage, and the authentication URL is obtained from the flow’s authenticationUri. The browser is then redirected to the authentication URL for the user to authenticate.

9. In the event of an exception, the window.sessionStorage is cleared of temporary data related to authentication.

Overall, this code implements the authentication process using OpenID Connect, including the PKCE flow for enhanced security, handling callbacks, and redirecting the user for authentication.

and change your index.html file

<script>
window.addEventListener('load', function(ev) {
// include this part to your index.html page
if(window.location.href.includes('?code=') || window.location.href.includes('&code=')) {
sessionStorage.setItem("auth_callback_response_url", window.location.href);
location.assign("/");
}
// -----------------------------------
_flutter.loader.loadEntrypoint({
serviceWorker: {
serviceWorkerVersion: serviceWorkerVersion,
},
onEntrypointLoaded: function(engineInitializer) {
engineInitializer.initializeEngine().then(function(appRunner) {
appRunner.runApp();
});
}
});
});
</script>

I hope you learned how to integrate keycloack with Flutter Web using openid_client with Authorization code.

Thanks for reading!

--

--

Surangi Kanchana

A highly motivated and enthusiastic individual with a passion for technology. | BSc (Hons) in Information Technology | University of Moratuwa