The key idea is authenticate with Keycloak using Flutter on Android and iOS.
Keycloak can be installed in different manners, the example demonstrates the installation with Docker. This is not production ready.
To work properly, the authentication needs a server that makes the redirection. Here, OAuth2 Proxy is used to do it.
OAuth2 Proxy is an open source reverse proxy that provides authentication using providers like Google and Keycloak.
docker network create keycloak-network
docker run -d \
--name mariadb
-e MYSQL_ROOT_PASSWORD=root \
-e MYSQL_DATABASE=keycloak \
-e MYSQL_USER=keycloak \
-e MYSQL_PASSWORD=keycloak \
--network keycloak-network \
mariadb
docker container run \
--name keycloak \
-p 8090:8080 \
-e KEYCLOAK_USER=keycloak \
-e KEYCLOAK_PASSWORD=keycloak \
-e DB_ADDR=mariadb \
-e DB_VENDOR=mariadb \
-e DB_PORT=3306 \
-e DB_USER=keycloak \
-e DB_PASSWORD=keycloak \
-e JDBC_PARAMS='connectTimeout=3600' \
--network keycloak-network \
jboss/keycloak
docker container run \
--name oauth2-proxy \
-p 4180:4180 \
-e OAUTH2_PROXY_PROVIDER=keycloak \
-e OAUTH2_PROXY_CLIENT_ID='oauth2-proxy' \
-e OAUTH2_PROXY_CLIENT_SECRET='534cf91e-7deb-44db-94e4-e987380af802' \
-e OAUTH2_PROXY_SSL-INSECURE-SKIP-VERIFY=true \
-e OAUTH2_PROXY_COOKIE_SECRET='QKgr59Jfgfxw5gTmokq2GQ==' \
-e OAUTH2_PROXY_COOKIE-SECURE=false \
-e OAUTH2_PROXY_EMAIL_DOMAINS='*' \
-e OAUTH2_PROXY_LOGIN-URL='http://keycloak/realms/master/protocol/openid-connect/auth' \
-e OAUTH2_PROXY_REDEEM-URL='http://keycloak/realms/master/protocol/openid-connect/token' \
-e OAUTH2_PROXY_VALIDATE-URL='http://keycloak/realms/master/protocol/openid-connect/userinfo' \
-e OAUTH2_PROXY_KEYCLOAK-GROUP=/admin \
--network keycloak-network \
bitnami/oauth2-proxy:latest
If is used an API server to provide data to the Flutter client, this server needs to connect to Keycloak to validate the token.
The server should to be registered as a Client
. The Access Type
used in Keycloak Administration Console
needs to be confidential
.
For test, Valid Redirect URIs
and Web Origins
can use *
, but can be better wo change it at production.
The server client and secret
can be copied in the section Credentials
after save.
OAuth2 Proxy also is a Client
, the Access Type
also is confidential
.
In the example, the OAuth2 Proxy is http://oauth2-proxy
because Docker is used. In production the URL may change.
To work properly, Root URL
, Valid Redirect URIs
, and Web Origins
needs to be configured.
In the Mappers
section, is needed to create a Mapper Type
"Group Membership" with Token Claim Name
"groups".
The complete configuration of OAuth2 Proxy with Keycloak can be found at OAuth2 Provider Configuration.
Flutter is also a Client
, the Access Type
is confidential
.
For test, Valid Redirect URIs
and Web Origins
can be configured with *
to permit and URL. Can be more secure change at production.
In the section OpenID Connect Compatibility Modes
can be enabled the use of refresh tokens.
The example below shows ASP.NET Core configured to request to Keycloak validate the tokens. Othe languagues and frameworks can also be used with Keycloak.
The NuGet package required are Microsoft.AspNetCore.Authentication.JwtBearer.
The code below needs to be put in the Startup.cs
file:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
namespace KeycloakTestApi
{
public class Startup
{
// ...
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// ...
services.AddAuthorization();
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(jwtBearerOptions =>
{
jwtBearerOptions.Authority = Environment.GetEnvironmentVariable("OIDC_AUTHORITY") ?? "http://localhost:8090/auth/realms/master";
jwtBearerOptions.Audience = Environment.GetEnvironmentVariable("OIDC_CLIENT_ID") ?? "demo-app";
jwtBearerOptions.IncludeErrorDetails = true;
jwtBearerOptions.RequireHttpsMetadata = false;
jwtBearerOptions.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = true,
ValidAudiences = new[] { "master-realm", "account" },
ValidateIssuer = false,
ValidateLifetime = false
};
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// ...
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication(); // Required
app.UseAuthorization(); // Required
app.UseEndpoints(endpoints =>
endpoints.MapControllers());
}
}
}
After configure the authentication, the controllers
can use the Authorize
attribute:
// ...
namespace KeycloakTestApi.Controllers
{
[ApiController]
[Authorize]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
private readonly ILogger<WeatherForecastController> _logger;
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}
[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
var rng = new Random();
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = rng.Next(-20, 55),
Summary = Summaries[rng.Next(Summaries.Length)]
})
.ToArray();
}
}
}
The example focuses on mobile and not support Flutter web. Only the Android version was tested.
To work without HTTPS
is needed to use HttpOverrides
with badCertificateCallback
.
class ApplicationHttpOverrides extends HttpOverrides {
@override
HttpClient createHttpClient(SecurityContext context) {
return super.createHttpClient(context)
..badCertificateCallback = (_, __, ___) => true;
}
}
void main() {
HttpOverrides.global = ApplicationHttpOverrides();
runApp(WeatherForecastApplication());
}
In the AndroidManifest.xml
file, is also needed to put android:usesCleartextTraffic="true"
at application
tag.
The configuration
android:usesCleartextTraffic
is required to used and IP address without HTTPS. To use an domain likesite.com
without HTTPS, is required to use network security configuration.
<uses-permission android:name="android.permission.INTERNET" />
<application
android:icon="@mipmap/ic_launcher"
android:label="oauth2_test"
android:usesCleartextTraffic="true">
The iOS version can need to configure App Transport Security (ATS). The use of IP address without an domain has a similar limitation to that found on Android. To disable ATS, can be used the XML below at Info.plist
file.
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key><true/>
</dict>
The most important part of the code is on the main.dart
file. The Chopper package is not required to the OIDC authentication with Keycloak.
To display the Keycloak's login page can be used Navigator
as show below:
Uri responseUrl;
await Navigator.push(
context,
MaterialPageRoute(
fullscreenDialog: true,
builder: (_) {
return SafeArea(
child: Container(
child: WebView(
javascriptMode: JavascriptMode.unrestricted,
initialUrl: authorizationUrl.toString(),
navigationDelegate: (navigationRequest) {
if (navigationRequest.url
.startsWith(redirectUrl.toString())) {
responseUrl = Uri.parse(navigationRequest.url);
Navigator.pop(context); // closes the WebView
return NavigationDecision.prevent;
}
return NavigationDecision.navigate;
},
),
),
);
}));