From 61ded008705da4e8e38d697fc0eeb279332623de Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Thu, 19 Jun 2025 16:46:03 -0400 Subject: [PATCH] Working with client side auth --- .vscode/launch.json | 2 +- bin/{matrixgate.dart => matrixoidc.dart} | 4 +- lib/controllers/auth_code_controller.dart | 2 +- lib/controllers/settings_controller.dart | 2 +- lib/helpers/api_helper.dart | 105 ++++++---------------- lib/models/matrix_user.dart | 1 - lib/models/matrix_user.freezed.dart | 27 +++--- lib/models/matrix_user.g.dart | 2 - lib/models/settings.freezed.dart | 14 +-- lib/models/settings.g.dart | 34 +++---- pubspec.yaml | 4 +- 11 files changed, 72 insertions(+), 125 deletions(-) rename bin/{matrixgate.dart => matrixoidc.dart} (94%) diff --git a/.vscode/launch.json b/.vscode/launch.json index 6d69bbd..380f43f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -6,7 +6,7 @@ "configurations": [ { "name": "matrix-oauth2oidc", - "program": "bin/matrixgate.dart", + "program": "bin/matrixoidc.dart", "args": [ "--homeserver", "https://matrix.federated.nexus", diff --git a/bin/matrixgate.dart b/bin/matrixoidc.dart similarity index 94% rename from bin/matrixgate.dart rename to bin/matrixoidc.dart index 3f4fbb9..e405b09 100644 --- a/bin/matrixgate.dart +++ b/bin/matrixoidc.dart @@ -1,7 +1,7 @@ import "dart:io"; import "package:args/args.dart"; -import "package:matrixgate/controllers/settings_controller.dart"; -import "package:matrixgate/helpers/api_helper.dart"; +import "package:matrixoidc/controllers/settings_controller.dart"; +import "package:matrixoidc/helpers/api_helper.dart"; import "package:riverpod/riverpod.dart"; import "package:shelf/shelf.dart"; import "package:shelf/shelf_io.dart"; diff --git a/lib/controllers/auth_code_controller.dart b/lib/controllers/auth_code_controller.dart index d404b3e..5379bac 100644 --- a/lib/controllers/auth_code_controller.dart +++ b/lib/controllers/auth_code_controller.dart @@ -1,5 +1,5 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:matrixgate/models/matrix_user.dart"; +import "package:matrixoidc/models/matrix_user.dart"; import "package:riverpod/riverpod.dart"; class AuthCodeController extends Notifier> { diff --git a/lib/controllers/settings_controller.dart b/lib/controllers/settings_controller.dart index 54ff1d2..8473df5 100644 --- a/lib/controllers/settings_controller.dart +++ b/lib/controllers/settings_controller.dart @@ -1,5 +1,5 @@ import "package:args/args.dart"; -import "package:matrixgate/models/settings.dart"; +import "package:matrixoidc/models/settings.dart"; import "package:riverpod/riverpod.dart"; class SettingsController extends Notifier { diff --git a/lib/helpers/api_helper.dart b/lib/helpers/api_helper.dart index 62ee31b..6b55c18 100644 --- a/lib/helpers/api_helper.dart +++ b/lib/helpers/api_helper.dart @@ -1,11 +1,11 @@ import "dart:io"; import "dart:convert"; import "package:dart_jsonwebtoken/dart_jsonwebtoken.dart"; -import "package:matrixgate/controllers/auth_code_controller.dart"; -import "package:matrixgate/controllers/settings_controller.dart"; +import "package:matrixoidc/controllers/auth_code_controller.dart"; +import "package:matrixoidc/controllers/settings_controller.dart"; import "package:shelf/shelf.dart"; import "package:http/http.dart" as http; -import "package:matrixgate/models/matrix_user.dart"; +import "package:matrixoidc/models/matrix_user.dart"; import "package:riverpod/riverpod.dart"; class ApiHelper { @@ -15,74 +15,33 @@ class ApiHelper { Future handleLogin(Request request) async { final body = await request.readAsString(); final data = Uri.splitQueryString(body); - final settings = ref.read(SettingsController.provider)!; - final username = data["username"]; - final password = data["password"]; + final userId = data["user_id"]; + final accessToken = data["access_token"]; final redirectUri = data["redirect_uri"]; final state = data["state"] ?? ""; - final clientId = data["client_id"]; - final scope = data["scope"]; - final nonce = data["nonce"]; - if ([ - username, - password, - redirectUri, - clientId, - nonce, - scope, - ].any((f) => f == null)) { - return Response(400, body: "Missing required field(s)"); + if (userId == null || accessToken == null || redirectUri == null) { + return Response(400, body: "Missing parameters"); } - if (!Uri.parse(redirectUri!).host.endsWith(settings.serviceDomain)) { - return Response(403, body: "Redirect URI not allowed"); - } - - final loginRes = await http.post( - Uri.parse("${settings.homeserver}/_matrix/client/v3/login"), - headers: {"Content-Type": "application/json"}, - body: json.encode({ - "type": "m.login.password", - "identifier": {"type": "m.id.user", "user": username}, - "password": password, - }), - ); - - if (loginRes.statusCode != 200) { - return Response.forbidden("Login failed"); - } - - final loginData = json.decode(loginRes.body); - final userId = loginData["user_id"]; - final accessToken = loginData["access_token"]; - - final openidRes = await http.post( - Uri.parse( - "${settings.homeserver}/_matrix/client/v3/user/${Uri.encodeComponent(userId)}/openid/request_token", - ), + final settings = ref.read(SettingsController.provider)!; + final whoamiRes = await http.get( + Uri.parse("${settings.homeserver}/_matrix/client/v3/account/whoami"), headers: {"Authorization": "Bearer $accessToken"}, ); - if (openidRes.statusCode != 200) { - return Response.forbidden( - "OpenID request failed, status code ${openidRes.statusCode}", - ); + if (whoamiRes.statusCode != 200) { + return Response.forbidden("Access token validation failed"); } - final openidToken = json.decode(openidRes.body)["access_token"]; - final code = base64Url.encode( List.generate(16, (_) => DateTime.now().millisecond % 256), ); ref .read(AuthCodeController.provider.notifier) - .set( - code, - MatrixUser(userId: userId, matrixToken: openidToken, nonce: nonce!), - ); + .set(code, MatrixUser(userId: userId, matrixToken: accessToken)); return Response.found("$redirectUri?code=$code&state=$state"); } @@ -111,7 +70,6 @@ class ApiHelper { final jwt = JWT( { - "nonce": user.nonce, "exp": DateTime.now().add(Duration(days: 7)).millisecondsSinceEpoch ~/ 1000, @@ -137,31 +95,26 @@ class ApiHelper { Future userinfoHandler(Request request) async { final auth = request.headers["authorization"]; if (auth == null || !auth.startsWith("Bearer ")) { - return Response.forbidden( - json.encode({"error": "missing_token"}), - headers: {"content-type": "application/json"}, - ); + return Response.forbidden("No token"); } - final token = auth.substring(7); - final matrixResp = await http.get( - Uri.parse( - "${ref.read(SettingsController.provider)!.homeserver}/_matrix/federation/v1/openid/userinfo", - ), - headers: {"Authorization": "Bearer $token"}, - ); - - if (matrixResp.statusCode != 200) { - return Response.forbidden( - json.encode({"error": "invalid_token"}), - headers: {"content-type": "application/json"}, + try { + final token = auth.substring(7); + final jwt = JWT.verify( + token, + SecretKey( + await File.fromUri( + Uri.file(ref.read(SettingsController.provider)!.jwtSecretFile), + ).readAsString(), + ), ); + return Response.ok( + jsonEncode({"sub": jwt.subject}), + headers: {"Content-Type": "application/json"}, + ); + } catch (e) { + return Response.forbidden("Invalid token"); } - - return Response.ok( - matrixResp.body, - headers: {"content-type": "application/json"}, - ); } Response jwks(_) => Response.ok( diff --git a/lib/models/matrix_user.dart b/lib/models/matrix_user.dart index e2d88df..805fb3f 100644 --- a/lib/models/matrix_user.dart +++ b/lib/models/matrix_user.dart @@ -8,7 +8,6 @@ abstract class MatrixUser with _$MatrixUser { const factory MatrixUser({ required String userId, required String matrixToken, - required String nonce, }) = _MatrixUser; factory MatrixUser.fromJson(Map json) => diff --git a/lib/models/matrix_user.freezed.dart b/lib/models/matrix_user.freezed.dart index 953ea7e..94504e8 100644 --- a/lib/models/matrix_user.freezed.dart +++ b/lib/models/matrix_user.freezed.dart @@ -16,7 +16,7 @@ T _$identity(T value) => value; /// @nodoc mixin _$MatrixUser { - String get userId; String get matrixToken; String get nonce; + String get userId; String get matrixToken; /// Create a copy of MatrixUser /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -29,16 +29,16 @@ $MatrixUserCopyWith get copyWith => _$MatrixUserCopyWithImpl Object.hash(runtimeType,userId,matrixToken,nonce); +int get hashCode => Object.hash(runtimeType,userId,matrixToken); @override String toString() { - return "MatrixUser(userId: $userId, matrixToken: $matrixToken, nonce: $nonce)"; + return "MatrixUser(userId: $userId, matrixToken: $matrixToken)"; } @@ -49,7 +49,7 @@ abstract mixin class $MatrixUserCopyWith<$Res> { factory $MatrixUserCopyWith(MatrixUser value, $Res Function(MatrixUser) _then) = _$MatrixUserCopyWithImpl; @useResult $Res call({ - String userId, String matrixToken, String nonce + String userId, String matrixToken }); @@ -66,11 +66,10 @@ class _$MatrixUserCopyWithImpl<$Res> /// Create a copy of MatrixUser /// with the given fields replaced by the non-null parameter values. -@pragma("vm:prefer-inline") @override $Res call({Object? userId = null,Object? matrixToken = null,Object? nonce = null,}) { +@pragma("vm:prefer-inline") @override $Res call({Object? userId = null,Object? matrixToken = null,}) { return _then(_self.copyWith( userId: null == userId ? _self.userId : userId // ignore: cast_nullable_to_non_nullable as String,matrixToken: null == matrixToken ? _self.matrixToken : matrixToken // ignore: cast_nullable_to_non_nullable -as String,nonce: null == nonce ? _self.nonce : nonce // ignore: cast_nullable_to_non_nullable as String, )); } @@ -82,12 +81,11 @@ as String, @JsonSerializable() class _MatrixUser implements MatrixUser { - const _MatrixUser({required this.userId, required this.matrixToken, required this.nonce}); + const _MatrixUser({required this.userId, required this.matrixToken}); factory _MatrixUser.fromJson(Map json) => _$MatrixUserFromJson(json); @override final String userId; @override final String matrixToken; -@override final String nonce; /// Create a copy of MatrixUser /// with the given fields replaced by the non-null parameter values. @@ -102,16 +100,16 @@ Map toJson() { @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _MatrixUser&&(identical(other.userId, userId) || other.userId == userId)&&(identical(other.matrixToken, matrixToken) || other.matrixToken == matrixToken)&&(identical(other.nonce, nonce) || other.nonce == nonce)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _MatrixUser&&(identical(other.userId, userId) || other.userId == userId)&&(identical(other.matrixToken, matrixToken) || other.matrixToken == matrixToken)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,userId,matrixToken,nonce); +int get hashCode => Object.hash(runtimeType,userId,matrixToken); @override String toString() { - return "MatrixUser(userId: $userId, matrixToken: $matrixToken, nonce: $nonce)"; + return "MatrixUser(userId: $userId, matrixToken: $matrixToken)"; } @@ -122,7 +120,7 @@ abstract mixin class _$MatrixUserCopyWith<$Res> implements $MatrixUserCopyWith<$ factory _$MatrixUserCopyWith(_MatrixUser value, $Res Function(_MatrixUser) _then) = __$MatrixUserCopyWithImpl; @override @useResult $Res call({ - String userId, String matrixToken, String nonce + String userId, String matrixToken }); @@ -139,11 +137,10 @@ class __$MatrixUserCopyWithImpl<$Res> /// Create a copy of MatrixUser /// with the given fields replaced by the non-null parameter values. -@override @pragma("vm:prefer-inline") $Res call({Object? userId = null,Object? matrixToken = null,Object? nonce = null,}) { +@override @pragma("vm:prefer-inline") $Res call({Object? userId = null,Object? matrixToken = null,}) { return _then(_MatrixUser( userId: null == userId ? _self.userId : userId // ignore: cast_nullable_to_non_nullable as String,matrixToken: null == matrixToken ? _self.matrixToken : matrixToken // ignore: cast_nullable_to_non_nullable -as String,nonce: null == nonce ? _self.nonce : nonce // ignore: cast_nullable_to_non_nullable as String, )); } diff --git a/lib/models/matrix_user.g.dart b/lib/models/matrix_user.g.dart index 5384062..95e855f 100644 --- a/lib/models/matrix_user.g.dart +++ b/lib/models/matrix_user.g.dart @@ -9,12 +9,10 @@ part of "matrix_user.dart"; _MatrixUser _$MatrixUserFromJson(Map json) => _MatrixUser( userId: json["userId"] as String, matrixToken: json["matrixToken"] as String, - nonce: json["nonce"] as String, ); Map _$MatrixUserToJson(_MatrixUser instance) => { "userId": instance.userId, "matrixToken": instance.matrixToken, - "nonce": instance.nonce, }; diff --git a/lib/models/settings.freezed.dart b/lib/models/settings.freezed.dart index 7a858e1..8376cab 100644 --- a/lib/models/settings.freezed.dart +++ b/lib/models/settings.freezed.dart @@ -4,7 +4,7 @@ // ignore_for_file: type=lint // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark -part of 'settings.dart'; +part of "settings.dart"; // ************************************************************************** // FreezedGenerator @@ -20,7 +20,7 @@ mixin _$Settings { /// Create a copy of Settings /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) -@pragma('vm:prefer-inline') +@pragma("vm:prefer-inline") $SettingsCopyWith get copyWith => _$SettingsCopyWithImpl(this as Settings, _$identity); /// Serializes this Settings to a JSON map. @@ -38,7 +38,7 @@ int get hashCode => Object.hash(runtimeType,socket,address,port,homeserver,issue @override String toString() { - return 'Settings(socket: $socket, address: $address, port: $port, homeserver: $homeserver, issuer: $issuer, serviceDomain: $serviceDomain, jwtSecretFile: $jwtSecretFile, authorizeEndpoint: $authorizeEndpoint)'; + return "Settings(socket: $socket, address: $address, port: $port, homeserver: $homeserver, issuer: $issuer, serviceDomain: $serviceDomain, jwtSecretFile: $jwtSecretFile, authorizeEndpoint: $authorizeEndpoint)"; } @@ -66,7 +66,7 @@ class _$SettingsCopyWithImpl<$Res> /// Create a copy of Settings /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? socket = freezed,Object? address = null,Object? port = null,Object? homeserver = null,Object? issuer = null,Object? serviceDomain = null,Object? jwtSecretFile = null,Object? authorizeEndpoint = null,}) { +@pragma("vm:prefer-inline") @override $Res call({Object? socket = freezed,Object? address = null,Object? port = null,Object? homeserver = null,Object? issuer = null,Object? serviceDomain = null,Object? jwtSecretFile = null,Object? authorizeEndpoint = null,}) { return _then(_self.copyWith( socket: freezed == socket ? _self.socket : socket // ignore: cast_nullable_to_non_nullable as String?,address: null == address ? _self.address : address // ignore: cast_nullable_to_non_nullable @@ -102,7 +102,7 @@ class _Settings implements Settings { /// Create a copy of Settings /// with the given fields replaced by the non-null parameter values. @override @JsonKey(includeFromJson: false, includeToJson: false) -@pragma('vm:prefer-inline') +@pragma("vm:prefer-inline") _$SettingsCopyWith<_Settings> get copyWith => __$SettingsCopyWithImpl<_Settings>(this, _$identity); @override @@ -121,7 +121,7 @@ int get hashCode => Object.hash(runtimeType,socket,address,port,homeserver,issue @override String toString() { - return 'Settings(socket: $socket, address: $address, port: $port, homeserver: $homeserver, issuer: $issuer, serviceDomain: $serviceDomain, jwtSecretFile: $jwtSecretFile, authorizeEndpoint: $authorizeEndpoint)'; + return "Settings(socket: $socket, address: $address, port: $port, homeserver: $homeserver, issuer: $issuer, serviceDomain: $serviceDomain, jwtSecretFile: $jwtSecretFile, authorizeEndpoint: $authorizeEndpoint)"; } @@ -149,7 +149,7 @@ class __$SettingsCopyWithImpl<$Res> /// Create a copy of Settings /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? socket = freezed,Object? address = null,Object? port = null,Object? homeserver = null,Object? issuer = null,Object? serviceDomain = null,Object? jwtSecretFile = null,Object? authorizeEndpoint = null,}) { +@override @pragma("vm:prefer-inline") $Res call({Object? socket = freezed,Object? address = null,Object? port = null,Object? homeserver = null,Object? issuer = null,Object? serviceDomain = null,Object? jwtSecretFile = null,Object? authorizeEndpoint = null,}) { return _then(_Settings( socket: freezed == socket ? _self.socket : socket // ignore: cast_nullable_to_non_nullable as String?,address: null == address ? _self.address : address // ignore: cast_nullable_to_non_nullable diff --git a/lib/models/settings.g.dart b/lib/models/settings.g.dart index 2b35689..11a29e1 100644 --- a/lib/models/settings.g.dart +++ b/lib/models/settings.g.dart @@ -1,29 +1,29 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of 'settings.dart'; +part of "settings.dart"; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** _Settings _$SettingsFromJson(Map json) => _Settings( - socket: json['socket'] as String?, - address: json['address'] as String, - port: json['port'] as String, - homeserver: json['homeserver'] as String, - issuer: json['issuer'] as String, - serviceDomain: json['serviceDomain'] as String, - jwtSecretFile: json['jwtSecretFile'] as String, - authorizeEndpoint: json['authorizeEndpoint'] as String, + socket: json["socket"] as String?, + address: json["address"] as String, + port: json["port"] as String, + homeserver: json["homeserver"] as String, + issuer: json["issuer"] as String, + serviceDomain: json["serviceDomain"] as String, + jwtSecretFile: json["jwtSecretFile"] as String, + authorizeEndpoint: json["authorizeEndpoint"] as String, ); Map _$SettingsToJson(_Settings instance) => { - 'socket': instance.socket, - 'address': instance.address, - 'port': instance.port, - 'homeserver': instance.homeserver, - 'issuer': instance.issuer, - 'serviceDomain': instance.serviceDomain, - 'jwtSecretFile': instance.jwtSecretFile, - 'authorizeEndpoint': instance.authorizeEndpoint, + "socket": instance.socket, + "address": instance.address, + "port": instance.port, + "homeserver": instance.homeserver, + "issuer": instance.issuer, + "serviceDomain": instance.serviceDomain, + "jwtSecretFile": instance.jwtSecretFile, + "authorizeEndpoint": instance.authorizeEndpoint, }; diff --git a/pubspec.yaml b/pubspec.yaml index 7db5d1a..1daf022 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ -name: matrixgate -description: A minimal OpenID Connect provider backed by Matrix OpenID tokens. +name: matrixoidc +description: A minimal OpenID Connect provider backed by Matrix. version: 1.0.0 publish_to: none