add auth code flow support

This commit is contained in:
Henry Hiles 2026-07-19 17:09:14 -04:00
commit 790caf1d38
Signed by: Henry-Hiles
SSH key fingerprint: SHA256:VKQUdS31Q90KvX7EkKMHMBpUspcmItAh86a+v7PGiIs
9 changed files with 112 additions and 31 deletions

2
gomuks

@ -1 +1 @@
Subproject commit d7506029a62299515356f72061d36ae35e9c9590 Subproject commit 3c759ba03a5235deb82cb1559ebc42c36ee0e874

View file

@ -0,0 +1,32 @@
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/client.dart";
import "package:nexus/controllers/client_id.dart";
class AuthUrlController extends AsyncNotifier<Uri> {
static const prefString = "auth_url";
final Uri homeserver;
AuthUrlController(this.homeserver);
@override
Future<Uri> build() async {
final fetched = await ref
.watch(ClientController.provider.notifier)
.getAuthUrl(
.new(
homeserverUrl: homeserver,
redirectUri: Uri.https("nexus.federated.nexus"),
responseMode: .query,
scopes: .new([.clientApi]),
clientId: await ref.watch(
ClientIdController.provider(homeserver).future,
),
),
);
return fetched.url;
}
static final provider = AsyncNotifierProvider.autoDispose
.family<AuthUrlController, Uri, Uri>(AuthUrlController.new);
}

View file

@ -15,12 +15,14 @@ import "package:nexus/controllers/top_level_spaces.dart";
import "package:nexus/helpers/extensions/gomuks_buffer.dart"; import "package:nexus/helpers/extensions/gomuks_buffer.dart";
import "package:nexus/main.dart"; import "package:nexus/main.dart";
import "package:nexus/models/event.dart"; import "package:nexus/models/event.dart";
import "package:nexus/models/oauth_auth_code_response.dart";
import "package:nexus/models/paginate.dart"; import "package:nexus/models/paginate.dart";
import "package:nexus/models/requests/get_event.dart"; import "package:nexus/models/requests/get_event.dart";
import "package:nexus/models/requests/get_related_events.dart"; import "package:nexus/models/requests/get_related_events.dart";
import "package:nexus/models/requests/get_room_state.dart"; import "package:nexus/models/requests/get_room_state.dart";
import "package:nexus/models/requests/join_room.dart"; import "package:nexus/models/requests/join_room.dart";
import "package:nexus/models/profile.dart"; import "package:nexus/models/profile.dart";
import "package:nexus/models/requests/oauth/get_auth_url.dart";
import "package:nexus/models/requests/oauth/register_client.dart"; import "package:nexus/models/requests/oauth/register_client.dart";
import "package:nexus/models/requests/paginate.dart"; import "package:nexus/models/requests/paginate.dart";
import "package:nexus/models/requests/redact_event.dart"; import "package:nexus/models/requests/redact_event.dart";
@ -281,6 +283,11 @@ class ClientController extends AsyncNotifier<int> {
request.toJson(), request.toJson(),
))["client_id"]; ))["client_id"];
Future<OAuthAuthCodeResponse> getAuthUrl(OAuthGetAuthUrl request) async =>
.fromJson(
await _sendCommand("oauth_get_authorization_url", request.toJson()),
);
Future<Uri?> discoverHomeserver(Uri homeserver) async { Future<Uri?> discoverHomeserver(Uri homeserver) async {
try { try {
final response = await _sendCommand("discover_homeserver", { final response = await _sendCommand("discover_homeserver", {

View file

@ -5,6 +5,9 @@ import "package:nexus/controllers/shared_prefs.dart";
class ClientIdController extends AsyncNotifier<String> { class ClientIdController extends AsyncNotifier<String> {
static const prefString = "client_id"; static const prefString = "client_id";
final Uri homeserver;
ClientIdController(this.homeserver);
@override @override
Future<String> build() async { Future<String> build() async {
final prefs = await ref.watch(SharedPrefsController.provider.future); final prefs = await ref.watch(SharedPrefsController.provider.future);
@ -16,8 +19,17 @@ class ClientIdController extends AsyncNotifier<String> {
.watch(ClientController.provider.notifier) .watch(ClientController.provider.notifier)
.registerClient( .registerClient(
.new( .new(
homeserverUrl: Uri.https("matrix.federated.nexus"), clientName: "Nexus",
applicationType: .native,
grantTypes: .new([.authorizationCode, .refreshToken]),
responseTypes: .new([.code]),
logoUri: Uri.https(
"nexus.federated.nexus",
"raw/branch/main/assets/mobile.svg",
),
homeserverUrl: homeserver,
clientUri: Uri.https("nexus.federated.nexus"), clientUri: Uri.https("nexus.federated.nexus"),
redirectUris: .new([.https("nexus.federated.nexus")]),
), ),
); );
@ -26,8 +38,6 @@ class ClientIdController extends AsyncNotifier<String> {
return fetched; return fetched;
} }
static final provider = static final provider = AsyncNotifierProvider.autoDispose
AsyncNotifierProvider.autoDispose<ClientIdController, String>( .family<ClientIdController, String, Uri>(ClientIdController.new);
ClientIdController.new,
);
} }

View file

@ -0,0 +1,15 @@
import "package:freezed_annotation/freezed_annotation.dart";
part "oauth_auth_code_response.freezed.dart";
part "oauth_auth_code_response.g.dart";
@freezed
abstract class OAuthAuthCodeResponse with _$OAuthAuthCodeResponse {
const factory OAuthAuthCodeResponse({
required String state,
required String codeVerifier,
required Uri url,
}) = _OAuthAuthCodeResponse;
factory OAuthAuthCodeResponse.fromJson(Map<String, Object?> json) =>
_$OAuthAuthCodeResponseFromJson(json);
}

View file

@ -0,0 +1,29 @@
import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:freezed_annotation/freezed_annotation.dart";
part "get_auth_url.freezed.dart";
part "get_auth_url.g.dart";
@freezed
abstract class OAuthGetAuthUrl with _$OAuthGetAuthUrl {
const factory OAuthGetAuthUrl({
required ResponseMode responseMode,
required Uri homeserverUrl,
required Uri redirectUri,
required IList<Scope> scopes,
required String clientId,
String? userIdHint,
}) = _OAuthGetAuthUrl;
factory OAuthGetAuthUrl.fromJson(Map<String, Object?> json) =>
_$OAuthGetAuthUrlFromJson(json);
}
enum Scope {
openid,
email,
@JsonValue("urn:matrix:client:api:*")
clientApi,
}
@JsonEnum(fieldRename: .snake)
enum ResponseMode { query, fragment }

View file

@ -7,7 +7,7 @@ part "register_client.g.dart";
abstract class OAuthRegisterClientRequest with _$OAuthRegisterClientRequest { abstract class OAuthRegisterClientRequest with _$OAuthRegisterClientRequest {
const factory OAuthRegisterClientRequest({ const factory OAuthRegisterClientRequest({
required Uri homeserverUrl, required Uri homeserverUrl,
ApplicationType? applicationType, @Default(ApplicationType.web) ApplicationType applicationType,
String? clientName, String? clientName,
required Uri clientUri, required Uri clientUri,
Uri? logoUri, Uri? logoUri,

View file

@ -1,20 +0,0 @@
import "package:flutter/material.dart";
import "package:flutter_hooks/flutter_hooks.dart";
import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:nexus/controllers/client.dart";
import "package:nexus/controllers/client_id_controller.dart";
import "package:nexus/helpers/extensions/better_when.dart";
import "package:nexus/widgets/appbar.dart";
import "package:nexus/helpers/required_validator_helper.dart";
class LoginPage extends HookConsumerWidget {
final Uri homeserver;
const LoginPage({super.key, required this.homeserver});
@override
Widget build(BuildContext context, WidgetRef ref) => Scaffold(
body: ref
.watch(ClientIdController.provider)
.betterWhen(data: (value) => Text(value)),
);
}

View file

@ -2,10 +2,11 @@ import "package:flutter/material.dart";
import "package:flutter_hooks/flutter_hooks.dart"; import "package:flutter_hooks/flutter_hooks.dart";
import "package:flutter_svg/flutter_svg.dart"; import "package:flutter_svg/flutter_svg.dart";
import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:nexus/controllers/auth_url.dart";
import "package:nexus/controllers/client.dart"; import "package:nexus/controllers/client.dart";
import "package:nexus/helpers/launch_helper.dart"; import "package:nexus/helpers/launch_helper.dart";
import "package:nexus/main.dart";
import "package:nexus/models/homeserver.dart"; import "package:nexus/models/homeserver.dart";
import "package:nexus/pages/login.dart";
import "package:nexus/widgets/appbar.dart"; import "package:nexus/widgets/appbar.dart";
import "package:nexus/widgets/divider_text.dart"; import "package:nexus/widgets/divider_text.dart";
@ -32,7 +33,7 @@ class SelectServerPage extends HookConsumerWidget {
final newUrl = newHomeserver == null final newUrl = newHomeserver == null
? null ? null
: await ref : await ref
.watch(ClientController.provider.notifier) .read(ClientController.provider.notifier)
.discoverHomeserver(newHomeserver); .discoverHomeserver(newHomeserver);
if (context.mounted) { if (context.mounted) {
@ -47,11 +48,18 @@ class SelectServerPage extends HookConsumerWidget {
), ),
); );
} else { } else {
await Navigator.of(context).push( final authUrl = await ref.read(
MaterialPageRoute(builder: (_) => LoginPage(homeserver: newUrl)), AuthUrlController.provider(newUrl).future,
); );
print(authUrl);
// await Navigator.of(context).push(
// MaterialPageRoute(builder: (_) => LoginPage(homeserver: newUrl)),
// );
} }
} }
} catch (error, stackTrace) {
showError(error, stackTrace);
} finally { } finally {
isLoading.value = false; isLoading.value = false;
} }