Make basic auth flow
This commit is contained in:
parent
b104cd584e
commit
b4b0291540
106
app/lib/authentik_api.dart
Normal file
106
app/lib/authentik_api.dart
Normal file
@ -0,0 +1,106 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:oauth2/oauth2.dart' as oauth2;
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
Future<String> getSessionCookie(oauth2.Client client) async {
|
||||
final response0 = await client.get(
|
||||
Uri.parse(
|
||||
'https://auth.leinelab.org/api/v3/flows/instances/default-user-settings-flow/execute/',
|
||||
),
|
||||
);
|
||||
|
||||
var sessionCookieHeader = response0.headers['set-cookie'];
|
||||
if (sessionCookieHeader == null) {
|
||||
throw Exception('No session cookie found in response headers.');
|
||||
}
|
||||
|
||||
String? sessionCookie;
|
||||
int index = sessionCookieHeader.indexOf(';');
|
||||
sessionCookie = (index == -1)
|
||||
? sessionCookieHeader
|
||||
: sessionCookieHeader.substring(0, index);
|
||||
|
||||
print("Session cookie: $sessionCookie");
|
||||
|
||||
return sessionCookie;
|
||||
}
|
||||
|
||||
Future<Object> getUserSettings(
|
||||
oauth2.Client client,
|
||||
String sessionCookie,
|
||||
) async {
|
||||
final response = await client.get(
|
||||
Uri.parse(
|
||||
'https://auth.leinelab.org/api/v3/flows/executor/default-user-settings-flow/?query=',
|
||||
),
|
||||
headers: {'Cookie': sessionCookie},
|
||||
);
|
||||
|
||||
final flowJson = jsonDecode(response.body);
|
||||
|
||||
if (flowJson['fields'] == null) {
|
||||
throw Exception("Expected 'fields' in response, but got: ${response.body}");
|
||||
}
|
||||
|
||||
final fields = flowJson['fields'] as List<dynamic>;
|
||||
var userSettingsObj = {};
|
||||
for (var field in fields) {
|
||||
if (field['field_key'] == null || field['initial_value'] == null) {
|
||||
throw Exception(
|
||||
"Expected 'field_key' and 'initial_value' in field, but got: $field",
|
||||
);
|
||||
}
|
||||
|
||||
userSettingsObj[field['field_key']] = field['initial_value'];
|
||||
}
|
||||
|
||||
return userSettingsObj;
|
||||
}
|
||||
|
||||
Future<void> setUserSettings(
|
||||
oauth2.Client client,
|
||||
String sessionCookie,
|
||||
Object data,
|
||||
) async {
|
||||
final body = jsonEncode(data);
|
||||
|
||||
final response = await client.post(
|
||||
Uri.parse(
|
||||
'https://auth.leinelab.org/api/v3/flows/executor/default-user-settings-flow/?query=',
|
||||
),
|
||||
body: body,
|
||||
headers: {'Content-Type': 'application/json', 'Cookie': sessionCookie},
|
||||
);
|
||||
|
||||
// Authentik expects a redirect after the POST request and only writes
|
||||
// the data to the database after fetching the redirect location.
|
||||
if (response.statusCode != 302) {
|
||||
throw Exception(
|
||||
"Expected a redirect (302) response, but got ${response.statusCode}",
|
||||
);
|
||||
}
|
||||
|
||||
final newLocation = response.headers['location'];
|
||||
if (newLocation == null) {
|
||||
throw Exception("No redirect location found in response headers.");
|
||||
}
|
||||
|
||||
final responseFinal = await client.get(
|
||||
Uri.parse('https://auth.leinelab.org/' + newLocation),
|
||||
headers: {'Cookie': sessionCookie},
|
||||
);
|
||||
|
||||
if (responseFinal.statusCode == 200) {
|
||||
print("User data updated successfully.");
|
||||
print("responseFinal body:");
|
||||
print(responseFinal.body);
|
||||
responseFinal.headers.toString().split('\n').forEach(print);
|
||||
} else {
|
||||
print("Error updating user data: ${responseFinal.statusCode}");
|
||||
print("responseFinal body:");
|
||||
print(responseFinal.body);
|
||||
print(responseFinal.headers.toString());
|
||||
}
|
||||
}
|
@ -16,76 +16,22 @@ import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'authentik_api.dart' as authentik;
|
||||
|
||||
void main() {
|
||||
runApp(
|
||||
ChangeNotifierProvider(
|
||||
create: (context) => SSHKeyList(),
|
||||
MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider(create: (context) => SSHKeyList()),
|
||||
ChangeNotifierProvider(
|
||||
create: (context) => AuthentikUserSettingsChangeDialogState(),
|
||||
),
|
||||
],
|
||||
child: const MyApp(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<oauth2.Client> getOAuth2Client() async {
|
||||
// This is a placeholder for OAuth2 client initialization.
|
||||
// Replace with your actual OAuth2 client setup.
|
||||
|
||||
final authorizationEndpoint = Uri.parse(
|
||||
'https://auth.leinelab.org/application/o/authorize/',
|
||||
);
|
||||
|
||||
final tokenEndpoint = Uri.parse(
|
||||
'https://auth.leinelab.org/application/o/token/',
|
||||
);
|
||||
|
||||
final identifier = 'UwSMm8gTwBTUURSaxp5uPpuwX1OkGO4FRHeO9v3i';
|
||||
final secret = null; // = 'my client secret';
|
||||
|
||||
final redirectUrl = Uri.parse('http://localhost:30165/');
|
||||
|
||||
final credentialsFile = File('~/.myapp/credentials.json');
|
||||
|
||||
//....
|
||||
|
||||
var exists = await credentialsFile.exists();
|
||||
|
||||
if (exists) {
|
||||
var credentials = oauth2.Credentials.fromJson(
|
||||
await credentialsFile.readAsString(),
|
||||
);
|
||||
return oauth2.Client(credentials, identifier: identifier, secret: secret);
|
||||
}
|
||||
|
||||
var grant = oauth2.AuthorizationCodeGrant(
|
||||
identifier,
|
||||
authorizationEndpoint,
|
||||
tokenEndpoint,
|
||||
secret: secret,
|
||||
);
|
||||
|
||||
var authorizationUrl = grant.getAuthorizationUrl(
|
||||
redirectUrl,
|
||||
scopes: ["profile", "email", "goauthentik.io/api", "openid"],
|
||||
);
|
||||
|
||||
// TODO: clicking the button twice might try to bind the server twice
|
||||
var server = await HttpServer.bind("127.0.0.1", 30165);
|
||||
|
||||
await launchUrl(authorizationUrl);
|
||||
|
||||
var queryParameters;
|
||||
|
||||
await server.forEach((HttpRequest request) {
|
||||
request.response.write(
|
||||
'Success! You can close this window now and go back to the app.',
|
||||
);
|
||||
queryParameters = request.uri.queryParameters;
|
||||
request.response.close();
|
||||
server.close();
|
||||
});
|
||||
|
||||
return await grant.handleAuthorizationResponse(queryParameters);
|
||||
}
|
||||
|
||||
void makeAlert(BuildContext context, String title, String message) {
|
||||
showDialog<String>(
|
||||
context: context,
|
||||
@ -155,106 +101,6 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
String _output = '';
|
||||
String key = '';
|
||||
|
||||
Future<void> doOAuth() async {
|
||||
final client = await getOAuth2Client();
|
||||
|
||||
// TODO: Handle errors better
|
||||
try {
|
||||
final jsonMe = await client.read(
|
||||
Uri.parse('https://auth.leinelab.org/api/v3/core/users/me/'),
|
||||
);
|
||||
|
||||
final me = jsonDecode(jsonMe);
|
||||
|
||||
final user = me['user'];
|
||||
final transformed = {
|
||||
"username": user['username'],
|
||||
"name": user['name'],
|
||||
"email": user['email'],
|
||||
"attributes.settings.locale": user['settings']['locale'],
|
||||
"attributes.sshPublicKeys":
|
||||
"foooooooobar :O :O!", // fix oder aus anderer Quelle
|
||||
"component": "ak-stage-prompt",
|
||||
};
|
||||
|
||||
print(user);
|
||||
print(transformed);
|
||||
|
||||
final push = jsonEncode(transformed);
|
||||
|
||||
final response0 = await client.get(
|
||||
Uri.parse(
|
||||
'https://auth.leinelab.org/api/v3/flows/instances/default-user-settings-flow/execute/',
|
||||
),
|
||||
);
|
||||
|
||||
var sessionCookieHeader = response0.headers['set-cookie'];
|
||||
if (sessionCookieHeader == null) {
|
||||
throw Exception('No session cookie found in response headers.');
|
||||
}
|
||||
|
||||
String? sessionCookie;
|
||||
int index = sessionCookieHeader.indexOf(';');
|
||||
sessionCookie = (index == -1)
|
||||
? sessionCookieHeader
|
||||
: sessionCookieHeader.substring(0, index);
|
||||
|
||||
final responsea = await client.get(
|
||||
Uri.parse(
|
||||
'https://auth.leinelab.org/api/v3/flows/executor/default-user-settings-flow/?query=',
|
||||
),
|
||||
headers: {'Cookie': sessionCookie},
|
||||
);
|
||||
|
||||
print("Response A status code: ${responsea.statusCode}");
|
||||
print("Response body:");
|
||||
print(responsea.body);
|
||||
|
||||
print("Session cookie: $sessionCookie");
|
||||
|
||||
final response = await client.post(
|
||||
Uri.parse(
|
||||
'https://auth.leinelab.org/api/v3/flows/executor/default-user-settings-flow/?query=',
|
||||
),
|
||||
body: push,
|
||||
headers: {'Content-Type': 'application/json', 'Cookie': sessionCookie},
|
||||
);
|
||||
|
||||
// Authentik expects a redirect after the POST request and only writes
|
||||
// the data to the database after fetching the redirect location.
|
||||
if (response.statusCode != 302) {
|
||||
throw Exception(
|
||||
"Expected a redirect (302) response, but got ${response.statusCode}",
|
||||
);
|
||||
}
|
||||
|
||||
final newLocation = response.headers['location'];
|
||||
if (newLocation == null) {
|
||||
throw Exception("No redirect location found in response headers.");
|
||||
}
|
||||
|
||||
final responseFinal = await client.get(
|
||||
Uri.parse('https://auth.leinelab.org/' + newLocation),
|
||||
headers: {'Cookie': sessionCookie},
|
||||
);
|
||||
|
||||
if (responseFinal.statusCode == 200) {
|
||||
print("User data updated successfully.");
|
||||
print("responseFinal body:");
|
||||
print(responseFinal.body);
|
||||
responseFinal.headers.toString().split('\n').forEach(print);
|
||||
} else {
|
||||
print("Error updating user data: ${responseFinal.statusCode}");
|
||||
print("responseFinal body:");
|
||||
print(responseFinal.body);
|
||||
print(responseFinal.headers.toString());
|
||||
}
|
||||
} catch (e) {
|
||||
print(e.toString());
|
||||
}
|
||||
;
|
||||
}
|
||||
|
||||
Future<void> doSSH() async {
|
||||
SSHSocket? socket;
|
||||
|
||||
@ -336,11 +182,19 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
? 'Output: $_output'
|
||||
: 'Press the button to run a command.';
|
||||
|
||||
final authentikApiState = context
|
||||
.watch<AuthentikUserSettingsChangeDialogState>();
|
||||
|
||||
//final userSettingsDialog =
|
||||
|
||||
final bodyComponentMain = Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
TextButton(onPressed: doOAuth, child: Text("Oauth2 Login")),
|
||||
TextButton(
|
||||
onPressed: authentikApiState.start,
|
||||
child: Text("Oauth2 Login"),
|
||||
),
|
||||
Text('Current output:'),
|
||||
Consumer(
|
||||
builder: (BuildContext context, SSHKeyList sshKeyList, Widget? child) {
|
||||
@ -393,7 +247,7 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
throw Exception('Unknown navIndex: $navIndex');
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
final mainPage = Scaffold(
|
||||
appBar: AppBar(backgroundColor: Colors.amber, title: Text(widget.title)),
|
||||
body: bodyComponent,
|
||||
floatingActionButton: actionButton,
|
||||
@ -430,9 +284,258 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (authentikApiState.isClosed()) {
|
||||
return mainPage;
|
||||
}
|
||||
|
||||
var title;
|
||||
var children = <Widget>[];
|
||||
var actions = [
|
||||
TextButton(onPressed: authentikApiState.exit, child: Text("Cancel")),
|
||||
];
|
||||
|
||||
switch (authentikApiState.status) {
|
||||
case AuthentikUserSettingsChangeDialogStatus.closed:
|
||||
return mainPage;
|
||||
case AuthentikUserSettingsChangeDialogStatus.waitingForOAuth:
|
||||
title = Text("Waiting for OAuth");
|
||||
children = [Text("Please complete the OAuth flow in your browser.")];
|
||||
case AuthentikUserSettingsChangeDialogStatus.loadingUserSettings:
|
||||
title = Text("Loading User Settings");
|
||||
children = [Text("Loading...")];
|
||||
case AuthentikUserSettingsChangeDialogStatus.userSettingsObtained:
|
||||
title = Text("User Settings Obtained");
|
||||
children = [
|
||||
Text("You can now edit your user settings."),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
await authentikApiState.save();
|
||||
},
|
||||
child: Text("Save User Settings"),
|
||||
),
|
||||
];
|
||||
actions.add(
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
// Close the dialog and return to the main page
|
||||
authentikApiState.save();
|
||||
},
|
||||
child: Text("Save and Close"),
|
||||
),
|
||||
);
|
||||
case AuthentikUserSettingsChangeDialogStatus.savingUserSettings:
|
||||
title = Text("Saving User Settings");
|
||||
children = [Text("Saving...")];
|
||||
}
|
||||
|
||||
final dialog = AlertDialog(
|
||||
title: title,
|
||||
content: Column(children: children),
|
||||
actions: actions,
|
||||
);
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
mainPage,
|
||||
ModalBarrier(dismissible: false, color: Colors.black54),
|
||||
dialog,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum AuthentikUserSettingsChangeDialogStatus {
|
||||
closed,
|
||||
waitingForOAuth,
|
||||
loadingUserSettings,
|
||||
userSettingsObtained,
|
||||
savingUserSettings,
|
||||
}
|
||||
|
||||
class AuthentikUserSettingsChangeDialogState extends ChangeNotifier {
|
||||
AuthentikUserSettingsChangeDialogStatus _status =
|
||||
AuthentikUserSettingsChangeDialogStatus.closed;
|
||||
|
||||
oauth2.Client? oauthClient;
|
||||
String? sessionCookie;
|
||||
Object? userSettings;
|
||||
HttpServer? server;
|
||||
|
||||
AuthentikUserSettingsChangeDialogStatus get status => _status;
|
||||
|
||||
Future<oauth2.Client?> getOAuth2Client() async {
|
||||
// This is a placeholder for OAuth2 client initialization.
|
||||
// Replace with your actual OAuth2 client setup.
|
||||
|
||||
final authorizationEndpoint = Uri.parse(
|
||||
'https://auth.leinelab.org/application/o/authorize/',
|
||||
);
|
||||
|
||||
final tokenEndpoint = Uri.parse(
|
||||
'https://auth.leinelab.org/application/o/token/',
|
||||
);
|
||||
|
||||
final identifier = 'UwSMm8gTwBTUURSaxp5uPpuwX1OkGO4FRHeO9v3i';
|
||||
final secret = null; // = 'my client secret';
|
||||
|
||||
final redirectUrl = Uri.parse('http://localhost:30165/');
|
||||
|
||||
// final credentialsFile = File('~/.myapp/credentials.json');
|
||||
|
||||
// //....
|
||||
|
||||
// var exists = await credentialsFile.exists();
|
||||
|
||||
// if (exists) {
|
||||
// var credentials = oauth2.Credentials.fromJson(
|
||||
// await credentialsFile.readAsString(),
|
||||
// );
|
||||
// return oauth2.Client(credentials, identifier: identifier, secret: secret);
|
||||
// }
|
||||
|
||||
var grant = oauth2.AuthorizationCodeGrant(
|
||||
identifier,
|
||||
authorizationEndpoint,
|
||||
tokenEndpoint,
|
||||
secret: secret,
|
||||
);
|
||||
|
||||
var authorizationUrl = grant.getAuthorizationUrl(
|
||||
redirectUrl,
|
||||
scopes: ["profile", "email", "goauthentik.io/api", "openid"],
|
||||
);
|
||||
|
||||
// TODO: clicking the button twice might try to bind the server twice
|
||||
server = await HttpServer.bind("127.0.0.1", 30165);
|
||||
|
||||
await launchUrl(authorizationUrl);
|
||||
|
||||
var queryParameters;
|
||||
|
||||
if (server == null) {
|
||||
// exit() might have been called before we arrived here.
|
||||
return null;
|
||||
}
|
||||
|
||||
await server!.forEach((HttpRequest request) {
|
||||
request.response.write(
|
||||
'Success! You can close this window now and go back to the app.',
|
||||
);
|
||||
queryParameters = request.uri.queryParameters;
|
||||
request.response.close();
|
||||
server!.close();
|
||||
});
|
||||
|
||||
return await grant.handleAuthorizationResponse(queryParameters);
|
||||
}
|
||||
|
||||
Future<void> start() async {
|
||||
// Reset the state to be sure
|
||||
oauthClient = null;
|
||||
sessionCookie = null;
|
||||
userSettings = null;
|
||||
|
||||
_status = AuthentikUserSettingsChangeDialogStatus.waitingForOAuth;
|
||||
notifyListeners();
|
||||
|
||||
oauthClient = await getOAuth2Client();
|
||||
if (isClosed()) {
|
||||
// If the dialog was closed before we got the client, exit
|
||||
return;
|
||||
}
|
||||
|
||||
sessionCookie = await authentik.getSessionCookie(oauthClient!);
|
||||
if (isClosed()) {
|
||||
// If the dialog was closed before we got the session cookie, exit
|
||||
return;
|
||||
}
|
||||
|
||||
_status = AuthentikUserSettingsChangeDialogStatus.loadingUserSettings;
|
||||
notifyListeners();
|
||||
|
||||
userSettings = await authentik.getUserSettings(
|
||||
oauthClient!,
|
||||
sessionCookie!,
|
||||
);
|
||||
|
||||
if (isClosed()) {
|
||||
// If the dialog was closed before we got the user settings, exit
|
||||
return;
|
||||
}
|
||||
|
||||
_status = AuthentikUserSettingsChangeDialogStatus.userSettingsObtained;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void exit() {
|
||||
_status = AuthentikUserSettingsChangeDialogStatus.closed;
|
||||
oauthClient = null;
|
||||
sessionCookie = null;
|
||||
userSettings = null;
|
||||
if (server != null) {
|
||||
server!.close();
|
||||
server = null;
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool isClosed() {
|
||||
return _status == AuthentikUserSettingsChangeDialogStatus.closed;
|
||||
}
|
||||
|
||||
Future<void> save() async {
|
||||
if (isClosed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_status !=
|
||||
AuthentikUserSettingsChangeDialogStatus.userSettingsObtained) {
|
||||
throw Exception("Cannot save, not started or no settings obtained.");
|
||||
}
|
||||
|
||||
_status = AuthentikUserSettingsChangeDialogStatus.savingUserSettings;
|
||||
notifyListeners();
|
||||
|
||||
await authentik.setUserSettings(
|
||||
oauthClient!,
|
||||
sessionCookie!,
|
||||
userSettings!,
|
||||
);
|
||||
exit();
|
||||
}
|
||||
|
||||
// Future<void> doOAuth1() async {
|
||||
// final client = await getOAuth2Client();
|
||||
|
||||
// // TODO: Handle errors better
|
||||
// try {
|
||||
// final jsonMe = await client.read(
|
||||
// Uri.parse('https://auth.leinelab.org/api/v3/core/users/me/'),
|
||||
// );
|
||||
|
||||
// final me = jsonDecode(jsonMe);
|
||||
|
||||
// final user = me['user'];
|
||||
// final transformed = {
|
||||
// "username": user['username'],
|
||||
// "name": user['name'],
|
||||
// "email": user['email'],
|
||||
// "attributes.settings.locale": user['settings']['locale'],
|
||||
// "attributes.sshPublicKeys":
|
||||
// "foooooooobar :O :O!", // fix oder aus anderer Quelle
|
||||
// "component": "ak-stage-prompt",
|
||||
// };
|
||||
|
||||
// print(user);
|
||||
// print(transformed);
|
||||
// } catch (e) {
|
||||
// print(e.toString());
|
||||
// }
|
||||
// ;
|
||||
// }
|
||||
}
|
||||
|
||||
class SSHKeyList extends ChangeNotifier {
|
||||
final List<String> _allKeys = [];
|
||||
final List<String> _keysToKeep = [];
|
||||
|
Loading…
x
Reference in New Issue
Block a user