Make basic auth flow

This commit is contained in:
lemoer 2025-07-05 20:08:14 +02:00
parent b104cd584e
commit b4b0291540
2 changed files with 374 additions and 165 deletions

106
app/lib/authentik_api.dart Normal file
View 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());
}
}

View File

@ -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(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => SSHKeyList()),
ChangeNotifierProvider(
create: (context) => SSHKeyList(),
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,7 +284,256 @@ 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 {