Closed ildoc closed 4 years ago
Hi @ildoc,
Do you have the stack trace?
sure!
Object.noSuchMethod (dart:core-patch/object_patch.dart:53)
_Uri._uriEncode (dart:core-patch/uri_patch.dart:46)
Uri.encodeQueryComponent (dart:core/uri.dart:1105)
mapToQuery.<anonymous closure> (c:\src\flutter\.pub-cache\hosted\pub.dartlang.org\http-0.12.0+4\lib\src\utils.dart:19)
CastMap.forEach.<anonymous closure> (dart:_internal/cast.dart:288)
_LinkedHashMapMixin.forEach (dart:collection-patch/compact_hash.dart:379)
CastMap.forEach (dart:_internal/cast.dart:287)
mapToQuery (c:\src\flutter\.pub-cache\hosted\pub.dartlang.org\http-0.12.0+4\lib\src\utils.dart:17)
Request.bodyFields= (c:\src\flutter\.pub-cache\hosted\pub.dartlang.org\http-0.12.0+4\lib\src\request.dart:137)
BaseClient._sendUnstreamed (c:\src\flutter\.pub-cache\hosted\pub.dartlang.org\http-0.12.0+4\lib\src\base_client.dart:170)
BaseClient.post (c:\src\flutter\.pub-cache\hosted\pub.dartlang.org\http-0.12.0+4\lib\src\base_client.dart:58)
post.<anonymous closure> (c:\src\flutter\.pub-cache\hosted\pub.dartlang.org\openid_client-0.2.5\lib\src\http_util.dart:23)
_withClient (c:\src\flutter\.pub-cache\hosted\pub.dartlang.org\openid_client-0.2.5\lib\src\http_util.dart:36)
post (c:\src\flutter\.pub-cache\hosted\pub.dartlang.org\openid_client-0.2.5\lib\src\http_util.dart:22)
Credential.getTokenResponse (c:\src\flutter\.pub-cache\hosted\pub.dartlang.org\openid_client-0.2.5\lib\src\openid.dart:219)
_MyAppState.authenticate (c:\my_app\lib\my_app.dart:94)
_rootRunUnary (dart:async/zone.dart:1134)
_rootRunUnary (dart:async/zone.dart:0)
_CustomZone.runUnary (dart:async/zone.dart:1031)
_FutureListener.handleValue (dart:async/future_impl.dart:140)
On mobile devices you should use the PKCE flow. This is automatically selected when you omit the redirect uri in the Authenticator constructor.
So, it should be:
var authenticator = new Authenticator(client,
scopes: scopes,
port: port,
urlLancher: urlLauncher,);
Make sure you add the uri http://localhost:4200/
(including the trailing slash) to Valid Redirect URIs
in keycloak.
thank you! it worked :D
thank you! it worked :D
Hi @ildoc can you please share the example code, I have tried many packages but am not done keycloak authentication.
I would also appreciate if someone share the code
this is our implementation with providers
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:openid_client/openid_client_io.dart' as oidc;
import 'package:url_launcher/url_launcher.dart';
import '../core/config/secure_storage.dart';
import '../core/http/http_functions.dart';
import '../models/index.dart' as models;
class Auth with ChangeNotifier {
Auth() {
authenticate();
}
models.UserInfo _userInfo;
bool _isAuth = false;
bool get isAuth {
if (_userInfo == null) {
authenticate();
_isAuth = false;
return false;
}
_isAuth = true;
return true;
}
models.UserInfo get user => _userInfo;
_urlLauncher(String url) async {
if (await canLaunch(url)) {
await launch(url, forceWebView: true);
} else {
throw 'Could not launch $url';
}
}
Future<void> authenticate() async {
if (_connectivity.isInit == false) return;
//get values from securestorage and create a client
var _storage = SecureStorage();
var authorizationEndPoint =
await _storage.getStoredValue('authorizationEndPoint');
var clientId = await _storage.getStoredValue('clientId');
var redirectUriPort = await _storage.getStoredValue('redirectUriPort');
oidc.Credential credential;
bool refreshFail = false;
bool accessTokenSaved = await _storage.getAccessToken() != null;
//if there is an access token saved in the secure storage try to get a new token using the refresh token
if (accessTokenSaved) {
print("login using saved token");
final tt = await _storage.getTokenType();
final rt = await _storage.getRefreshToken();
final it = await _storage.getIdToken();
var issuer = await oidc.Issuer.discover(Uri.parse(authorizationEndPoint));
var client = new oidc.Client(issuer, clientId);
credential = client.createCredential(
accessToken: null, // force use refresh to get new token
tokenType: tt,
refreshToken: rt,
idToken: it,
);
credential.validateToken(validateClaims: true, validateExpiry: true);
try {
_saveToken(await credential.getTokenResponse());
} catch (e) {
print("Error during login (refresh) " + e.toString());
refreshFail = true;
}
}
if (!accessTokenSaved || refreshFail) {
var issuer = await oidc.Issuer.discover(Uri.parse(authorizationEndPoint));
var client = new oidc.Client(issuer, clientId);
//auth from browser
var authenticator = oidc.Authenticator(
client,
scopes: List<String>.of(['openid', 'profile', 'offline_access']),
port: int.parse(redirectUriPort),
urlLancher: _urlLauncher,
);
credential = await authenticator.authorize();
closeWebView();
//save Token
_saveToken(await credential.getTokenResponse());
}
customGetTokenResponse() async {
var token = await credential.getTokenResponse();
print("called getTokenResponse, token expiration:" +
token.expiresAt.toIso8601String());
await _saveToken(token);
return token;
}
//call getTokenResponseFn before each http request to get a new token if old one is expired
var http = HttpFunctions()..getTokenResponseFn = customGetTokenResponse;
final response = await http.get('api/users/UserInfo'); // this call is authenticated
_userInfo = models.UserInfo.fromJson(response);
_userInfo =
_userInfo.copyWith(id: response['userId'], mail: response['email']);
notifyListeners();
}
Future _saveToken(oidc.TokenResponse token) async {
SecureStorage().saveToken(
accessToken: token.accessToken,
refresToken: token.refreshToken,
tokenType: token.tokenType,
idToken: token.idToken.toCompactSerialization());
}
void logout() {
_userInfo = null;
SecureStorage().clearToken().then((_) => notifyListeners());
}
}
Thanks @ildoc for your code and @rbellens for your answer and your work ! But that does not answer to How to make it work with Authorization Flow on Flutter
I have an "organization" issue, I have to work with Keycloak v4. It does not support PKCE Flow. So I have the choice of using Implicit Flow (which I already use on Native mobile app) or Authorization Flow Code.
You said that it's not possible for now ?
I think with Implicit Flow I can make it work without this lib, with a webview, but it's not ideal.
Thanks guys !
this is our implementation with providers
import 'dart:async'; import 'package:flutter/material.dart'; import 'package:openid_client/openid_client_io.dart' as oidc; import 'package:url_launcher/url_launcher.dart'; import '../core/config/secure_storage.dart'; import '../core/http/http_functions.dart'; import '../models/index.dart' as models; class Auth with ChangeNotifier { Auth() { authenticate(); } models.UserInfo _userInfo; bool _isAuth = false; bool get isAuth { if (_userInfo == null) { authenticate(); _isAuth = false; return false; } _isAuth = true; return true; } models.UserInfo get user => _userInfo; _urlLauncher(String url) async { if (await canLaunch(url)) { await launch(url, forceWebView: true); } else { throw 'Could not launch $url'; } } Future<void> authenticate() async { if (_connectivity.isInit == false) return; //get values from securestorage and create a client var _storage = SecureStorage(); var authorizationEndPoint = await _storage.getStoredValue('authorizationEndPoint'); var clientId = await _storage.getStoredValue('clientId'); var redirectUriPort = await _storage.getStoredValue('redirectUriPort'); oidc.Credential credential; bool refreshFail = false; bool accessTokenSaved = await _storage.getAccessToken() != null; //if there is an access token saved in the secure storage try to get a new token using the refresh token if (accessTokenSaved) { print("login using saved token"); final tt = await _storage.getTokenType(); final rt = await _storage.getRefreshToken(); final it = await _storage.getIdToken(); var issuer = await oidc.Issuer.discover(Uri.parse(authorizationEndPoint)); var client = new oidc.Client(issuer, clientId); credential = client.createCredential( accessToken: null, // force use refresh to get new token tokenType: tt, refreshToken: rt, idToken: it, ); credential.validateToken(validateClaims: true, validateExpiry: true); try { _saveToken(await credential.getTokenResponse()); } catch (e) { print("Error during login (refresh) " + e.toString()); refreshFail = true; } } if (!accessTokenSaved || refreshFail) { var issuer = await oidc.Issuer.discover(Uri.parse(authorizationEndPoint)); var client = new oidc.Client(issuer, clientId); //auth from browser var authenticator = oidc.Authenticator( client, scopes: List<String>.of(['openid', 'profile', 'offline_access']), port: int.parse(redirectUriPort), urlLancher: _urlLauncher, ); credential = await authenticator.authorize(); closeWebView(); //save Token _saveToken(await credential.getTokenResponse()); } customGetTokenResponse() async { var token = await credential.getTokenResponse(); print("called getTokenResponse, token expiration:" + token.expiresAt.toIso8601String()); await _saveToken(token); return token; } //call getTokenResponseFn before each http request to get a new token if old one is expired var http = HttpFunctions()..getTokenResponseFn = customGetTokenResponse; final response = await http.get('api/users/UserInfo'); // this call is authenticated _userInfo = models.UserInfo.fromJson(response); _userInfo = _userInfo.copyWith(id: response['userId'], mail: response['email']); notifyListeners(); } Future _saveToken(oidc.TokenResponse token) async { SecureStorage().saveToken( accessToken: token.accessToken, refresToken: token.refreshToken, tokenType: token.tokenType, idToken: token.idToken.toCompactSerialization()); } void logout() { _userInfo = null; SecureStorage().clearToken().then((_) => notifyListeners()); } }
Looks like logout isn't working correctly in this implementation. If try to authenticate two times in a row, I'm not able to change the user.
Hello guys. Frustrated by this so much that I decided to ask you. Can anyone share the below files? Or the full code required to do the connection. Will be highly appreciated. import '../core/config/secure_storage.dart'; import '../core/http/http_functions.dart'; import '../models/index.dart' as models;
will share something tomorrow, hang in there @FrankDupree Hello, Can you shared those files please? import '../core/config/secure_storage.dart'; import '../core/http/http_functions.dart'; import '../models/index.dart' as models;
currently i also try to make this working with flutter but without luck. your help will be very appreciated.
Hi , i followed your example in my app to authenticate with keycloak , however it is not working , it doesn't redirect me to keycloak , this is the function am using (following this example) :
authenticate() async {
var uri = Uri.parse('http://10.0.2.2:8080/auth/realms/clients');
var clientId = 'helium';
var scopes = List<String>.of(['openid', 'profile']);
var port = 4200;
var redirectUri = Uri.parse('http://localhost:4200');
var issuer = await Issuer.discover(uri);
var client = new Client(issuer, clientId);
urlLauncher(String url) async {
if (await canLaunch(url)) {
await launch(url, forceWebView: true);
} else {
throw 'Could not launch $url';
}
}
var authenticator = new Authenticator(client,
scopes: scopes,
port: port,
urlLancher: urlLauncher,);
var c = await authenticator.authorize();
closeWebView();
var token= await c.getTokenResponse();
print(token);
return token;
}
Am calling it inside a login button:
RoundedButton(
text: "Login",
press: (){
authenticate();
}
//Navigator.push(
//context,
//MaterialPageRoute(
// builder: (context) {
//return MainMenu();
//},
// ),
//);
,
),
And this is the error i keep getting whenever i hit the login button :
E/flutter ( 5769): [ERROR:flutter/lib/ui/ui_dart_state.cc(177)] Unhandled Exception: Instance of 'HttpRequestException'
E/flutter ( 5769): #0 _processResponse (package:openid_client/src/http_util.dart:37:5)
E/flutter ( 5769): #1 get (package:openid_client/src/http_util.dart:18:10)
E/flutter ( 5769): <asynchronous suspension>
E/flutter ( 5769): #2 Issuer.discover (package:openid_client/src/openid.dart:124:16)
E/flutter ( 5769): <asynchronous suspension>
E/flutter ( 5769): #3 Body.authenticate (package:helium_app/screens/login/components/body.dart:35:18)
E/flutter ( 5769): <asynchronous suspension>
Am i missing something ? am kind of confused , beacause in the implimentation you shared there is a nother configuration in the code besides the function , where should i add it and what is it used for ? Is there anything else i need to add to the function (keycloak is perfetly configured) ? been stuck for a while because of this issue , so if anyone knows how to fix this i'd be so grateful .
Hi , i followed your example in my app to authenticate with keycloak , however it is not working , it doesn't redirect me to keycloak , this is the function am using (following this example) :
authenticate() async { var uri = Uri.parse('http://10.0.2.2:8080/auth/realms/clients'); var clientId = 'helium'; var scopes = List<String>.of(['openid', 'profile']); var port = 4200; var redirectUri = Uri.parse('http://localhost:4200');
Hello @talbiislam96, I think that you're calling the wrong endpoint for authentication, mine is :
http://localhost:9080/auth/realms/{{realm-name}}/protocol/openid-connect/auth
This is for autentication.
If you need get a new auth token when it expires, by refreshing it, then /auth
must be replaced with /token
.
There's an useful endpoint which you can call to get your realm configuration:
http://localhost:9080/auth/realms/{{realm-name}}/.well-known/openid-configuration
It will give you many useful information, especially regarding which endpoints are available on the IdP for you:
{
"issuer": "http://localhost:9080/auth/realms/jhipster",
"authorization_endpoint": "http://localhost:9080/auth/realms/jhipster/protocol/openid-connect/auth",
"token_endpoint": "http://localhost:9080/auth/realms/jhipster/protocol/openid-connect/token",
"introspection_endpoint": "http://localhost:9080/auth/realms/jhipster/protocol/openid-connect/token/introspect",
"userinfo_endpoint": "http://localhost:9080/auth/realms/jhipster/protocol/openid-connect/userinfo",
"end_session_endpoint": "http://localhost:9080/auth/realms/jhipster/protocol/openid-connect/logout",
"jwks_uri": "http://localhost:9080/auth/realms/jhipster/protocol/openid-connect/certs",
//...
}
(my realm is named 'jhipster')
This endpoint can be used by some plugins to discover the identity provider configuration automatically. It will determine which endpoints to call to get a new auth token, or refresh an existing one. I wasn't able to use this feature, so I'm configuring the endpoints manually... It may be useful when configurations may change, but if you have a single one, then doing the setup manually it's still a valid solution.
A little note: I always had problems without HTTPS
Hope that this may help you, cheers!
Hello guys. Can anyone share the below files? Or the full code required to do the connection. Will be highly appreciated. import '../core/config/secure_storage.dart'; import '../core/http/http_functions.dart'; import '../models/index.dart' as models
Hey, I'm also having difficulties with this issue. Does anybody have any full working example of Keycloak authentication in flutter? I was looking for any example projcets, tutorial etc. but with no luck. Cheers :)
@funder7 thank you so much for ur feedback . This is the function am using , it works perfectly fine it redirects to Keycloak and allows the users to authenticate , I wish to refresh the token am retrieving automatically to get a new one whenever it expires to keep the user session active if anyone knows how to do it I'd be grateful
authenticate() async {
// keyclock url : key-clock-url : example : http://localhost:8080
// my realm : name of your real.m
var uri = Uri.parse('http://169.254.105.22:8080/auth/realms/Clients');
// your client id
var clientId = 'helium';
var scopes = List<String>.of(['openid', 'profile']);
var port = 8080;
var issuer = await Issuer.discover(uri);
var client = new Client(issuer, clientId);
print(issuer.metadata);
urlLauncher(String url) async {
if (await canLaunch(url)) {
await launch(url, forceWebView: true);
} else {
throw 'Could not launch $url';
}
}
authenticator = new Authenticator(
client,
scopes: scopes,
port: port,
urlLancher: urlLauncher,
);
var c = await authenticator.authorize();
closeWebView();
var token = await c.getTokenResponse();
var userInformation = await c.getUserInfo();
setState(() {
userAccessToken = token.accessToken;
userRefreshToken = token.refreshToken;
print (userRefreshToken);
userName = userInformation.preferredUsername;
});
//print(token);
//return token;
parseJwt(userAccessToken);
}
@talbiislam96 in order to refresh:
1) Once you login, and obtain the access token, save it securely with something like secure_storage
, you can find the package on pub.dev, store the token as is. Other than the access token, you get the refresh token once login is done. You can use it later to obtain a fresh access token, when the one you posses is expired (see point #3)
2) When you need to perform any http call, get the access token you saved, from secure_storage (using the same key used for saving it). Once you have the access token, you can refresh it directly without any check, or instead check if it's still valid (not expired). To do that, you can use this function:
bool _isTokenExpired(String accessToken) {
Map<String, dynamic> accessItems = _parseAccessToken(accessToken);
DateTime expiration =
DateTime.fromMillisecondsSinceEpoch(accessItems['exp'] * 1000);
bool isExpired = DateTime.now().isAfter(expiration);
if (isExpired) {
debugPrint('Token expired (at: $expiration)');
}
return isExpired;
}
It checks the token's "exp" param, which contains the token expiration datetime expressed in milliseconds. You compare it with current time, then if exp is before current time, it's expired.
3) In case it's expired, or anyway, to refresh the access token, you must call the refresh
endpoint. Calling the refresh endpoint by itself it's not enough to obtain a new access token: you must call the refresh endpoint with the refresh token in your possess (which you must have saved before in secure_storage, same as you did for access token).
To recap: 1) Once you login, the identity provider returns you: access, identity & refresh tokens 2) Access token is what you can use to access secured resources (eg. an http endpoint which needs authentication) 3) Refresh token is what you must use when the access token is expired. 4) Before doing any http call, the current access_token expiration time must be checked. If it is after current time, token is expired: is no more valid, and must be refreshed by calling the refresh endpoint. 5) Once you call the refresh endpoint, with a valid refresh token, the identity provider will answer with a new access token, and a new refresh token: store them again in secure storage, by overriding the previous ones.
I cannot provide you the code for calling the refresh endpoint as I'm using another library now (flutter_appauth), but the process doesn't change.
good luck!
Hello @ildoc, Can anyone share the below files? Or the full code required to do the connection. Will be highly appreciated. import '../core/config/secure_storage.dart'; import '../core/http/http_functions.dart'; import '../models/index.dart' as models
Hello everyone, im trying to refresh the access token but i cant find the way with this library. please, could someone just share the code of the function that does this? @ildoc your example really helped me, if you have that function please show it to me. thanks
Hello @ildoc, Can anyone share the below files? Or the full code required to do the connection. Will be highly appreciated. import '../core/config/secure_storage.dart'; import '../core/http/http_functions.dart'; import '../models/index.dart' as models
Future<TokenResponse> authenticate(Uri uri, String clientId,
List<String> scopes, BuildContext context) async {
try {
var issuer = await Issuer.discover(uri);
var client = Client(issuer, clientId);
urlLauncher(String url) async {
Uri uri = Uri.parse(url);
if (await launchUrl(uri)) {
await launchUrl(
uri,
);
} else {
throw 'Could not launch $url';
}
}
var authenticator = Authenticator(
client,
scopes: scopes,
urlLancher: urlLauncher,
port: 3000,
);
var c = await authenticator.authorize();
closeInAppWebView();
var res = await c.getTokenResponse();
UserInfo use = await c.getUserInfo();
authUserId(use.subject.toString());
email(use.email.toString());
token(res.accessToken.toString());
logoutUrl = c.generateLogoutUrl(); //This would be needed for logout
return res;
} finally {
context.loaderOverlay.hide();
}
}
Future<void> logout() async {
if (!await canLaunchUrl(logoutUrl) || await canLaunchUrl(logoutUrl)) //I used both because one works on some devices and the other doesn't
{
await launchUrl(logoutUrl);
token('');
Get.offAll(() => const LogOn());
} else {
throw 'Could not launch $logoutUrl';
}
await Future.delayed(const Duration(seconds: 2));
closeInAppWebView();
}
I use openId_client package in a flutter project for keycloak, url_launcher-6.1.6 to launch urls. Also remember to setup keycloak in the admin console before proceeding with this.
Also please note that there are some custom pages or functions. You can try creating them in your file after you paste this code.
You'll also need to parse your issuer(a link) and clientId in a .env file.
I hope this helps
Future<TokenResponse> authenticate(Uri uri, String clientId, List<String> scopes, BuildContext context) async { try { var issuer = await Issuer.discover(uri); var client = Client(issuer, clientId); urlLauncher(String url) async { Uri uri = Uri.parse(url); if (await launchUrl(uri)) { await launchUrl( uri, ); } else { throw 'Could not launch $url'; } } var authenticator = Authenticator( client, scopes: scopes, urlLancher: urlLauncher, port: 3000, ); var c = await authenticator.authorize(); closeInAppWebView(); var res = await c.getTokenResponse(); UserInfo use = await c.getUserInfo(); authUserId(use.subject.toString()); email(use.email.toString()); token(res.accessToken.toString()); logoutUrl = c.generateLogoutUrl(); //This would be needed for logout return res; } finally { context.loaderOverlay.hide(); } } Future<void> logout() async { if (!await canLaunchUrl(logoutUrl) || await canLaunchUrl(logoutUrl)) //I used both because one works on some devices and the other doesn't { await launchUrl(logoutUrl); token(''); Get.offAll(() => const LogOn()); } else { throw 'Could not launch $logoutUrl'; } await Future.delayed(const Duration(seconds: 2)); closeInAppWebView(); }
I use openId_client package in a flutter project for keycloak, url_launcher-6.1.6 to launch urls. Also remember to setup keycloak in the admin console before proceeding with this.
Also please note that there are some custom pages or functions. You can try creating them in your file after you paste this code.
You'll also need to parse your issuer(a link) and clientId in a .env file.
I hope this helps
Hi there, can I ask some?
Do you use it for mobile or web app? Do you import these files only?
import 'dart:async'; import 'package:flutter/material.dart'; import 'package:openid_client/openid_client_io.dart'; import 'package:url_launcher/url_launcher.dart';
Where shall I put the code (unfortunately, I am quite amateur with Flutter : 6) E.g. in Scaffold?, after @override, etc.
Here is my sample:
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:openid_client/openid_client_io.dart';
import 'package:url_launcher/url_launcher.dart';
class Login extends StatefulWidget {
const Login({Key? key}) : super(key: key);
@override
_LoginState createState() => _LoginState();
}
class _LoginState extends State<Login> {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey[300],
body: Padding(
padding: EdgeInsets.only(left:20.0, top:40.0,right:20.0,bottom:10.0),
child:
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
SizedBox(height: 50)
Text(
'Hello',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 24,
),
),
SizedBox(height: 10),
Text('Welcome, you have been missed.',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 20,
),
),
SizedBox(height: 30),
// email tfield
Padding(
padding: const EdgeInsets.symmetric(horizontal: 25.0),
child: Container(
decoration: BoxDecoration(
color: Colors.grey[200],
border: Border.all(color: Colors.white),
borderRadius: BorderRadius.circular(20)
),
child: Padding(
padding: const EdgeInsets.only(left: 20.0),
child: TextField(
decoration: InputDecoration(
border: InputBorder.none,
hintText: 'Email'
),
),
)
),
),
SizedBox(height: 10),
// password textfield
Padding(
padding: const EdgeInsets.symmetric(horizontal: 25.0),
child: Container(
decoration: BoxDecoration(
color: Colors.grey[200],
border: Border.all(color: Colors.white),
borderRadius: BorderRadius.circular(20)
),
child: Padding(
padding: const EdgeInsets.only(left: 20.0),
child: TextField(
obscureText: true,
decoration: InputDecoration(
border: InputBorder.none,
hintText: 'Password'
),
),
)
),
),
// sign in button
]
),
),
),
);
}
}
I use for mobile. We can get on a call. I got your email already
I'm trying to authenticate my flutter app to keycloak
following the repo example, I've wrote an authentication function like this
when I call the function, a webview popup appears and I can login through keycloak, but when the popup closes I get this error at the
c.getTokenResponse()
:inspecting the Credential
c
, I can see that the TokenResponse has only "state", "session_state" and "code" fieldswhat am I missing?