matrixoidc/lib/helpers/api_helper.dart
2025-08-18 14:14:30 -04:00

238 lines
6.9 KiB
Dart

import "dart:convert";
import "package:dart_jsonwebtoken/dart_jsonwebtoken.dart";
import "package:matrixoidc/controllers/auth_code_controller.dart";
import "package:matrixoidc/controllers/key_controller.dart";
import "package:matrixoidc/controllers/settings_controller.dart";
import "package:shelf/shelf.dart";
import "package:http/http.dart" as http;
import "package:matrixoidc/models/matrix_user.dart";
import "package:riverpod/riverpod.dart";
class ApiHelper {
final Ref ref;
ApiHelper(this.ref);
Future<Response> loginHandler(Request request) async {
final body = await request.readAsString();
final data = Uri.splitQueryString(body);
final userId = data["user_id"];
final accessToken = data["access_token"];
final redirectUri = data["redirect_uri"];
final state = data["state"] ?? "";
if (userId == null || accessToken == null || redirectUri == null) {
return Response(400, body: json.encode({"error": "Missing parameters"}));
}
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 (whoamiRes.statusCode != 200) {
return Response.forbidden(
json.encode({"error": "Access token validation failed"}),
);
}
final code = base64Url.encode(
List<int>.generate(16, (_) => DateTime.now().millisecond % 256),
);
ref
.read(AuthCodeController.provider.notifier)
.set(
code,
MatrixUser(
userId: userId,
matrixToken: accessToken,
nonce: data["nonce"],
),
);
final uri = Uri.parse(redirectUri);
return Response.found(
uri.replace(
queryParameters: {
...uri.queryParameters,
"code": code,
"state": state,
"nonce": data["nonce"],
},
),
);
}
Future<Response> bridgeHandler(Request request) async {
final query = request.url.queryParameters;
final code = query["code"];
final redirectUri = query["redirect_uri"];
if (code == null || redirectUri == null) {
return Response(
400,
body: json.encode({"error": "Missing code or redirect_uri"}),
);
}
final tokenRes = await tokenHandler(
Request(
"POST",
Uri.base,
body: utf8.encode("code=$code&client_id=proxy"),
),
);
if (tokenRes.statusCode != 200) {
return Response(400, body: json.encode({"error": "Token post failed"}));
}
return Response.found(
redirectUri,
headers: {
"set-cookie":
"id_token=${json.decode(await tokenRes.readAsString())["id_token"]}; Path=/; Secure; HttpOnly; SameSite=Lax; Domain=.${ref.watch(SettingsController.provider)!.serviceDomain}; Max-Age=604800",
},
);
}
Future<Response> tokenHandler(Request request) async {
final settings = ref.read(SettingsController.provider)!;
final body = Uri.splitQueryString(await request.readAsString());
final code = body["code"];
final clientId = body["client_id"];
if (code == null || clientId == null) {
return Response(
400,
body: json.encode({"error": "Missing code or client_id"}),
);
}
final codes = ref.read(AuthCodeController.provider);
if (!codes.containsKey(code)) {
return Response(400, body: json.encode({"error": "Invalid code"}));
}
final user = codes[code]!;
ref.read(AuthCodeController.provider.notifier).remove(code);
final jwt = JWT(
{
"exp":
DateTime.now().add(Duration(days: 7)).millisecondsSinceEpoch ~/
1000,
"nonce": user.nonce,
"iat": DateTime.now().millisecondsSinceEpoch ~/ 1000,
},
subject: user.userId,
issuer: settings.issuer,
audience: Audience([clientId]),
);
final token = jwt.sign(
await ref.read(KeyController.provider.future),
algorithm: JWTAlgorithm.HS256,
);
return Response.ok(
json.encode({
"id_token": token,
"access_token": token,
"token_type": "Bearer",
}),
headers: {"Content-Type": "application/json"},
);
}
Future<Response> userinfoHandler(Request request) async {
final auth = request.headers["authorization"];
if (auth == null || !auth.startsWith("Bearer ")) {
return Response.forbidden("No token");
}
try {
final token = auth.substring(7);
final jwt = JWT.verify(
token,
await ref.read(KeyController.provider.future),
);
final settings = ref.read(SettingsController.provider)!;
final profile = await http.get(
Uri.parse(
"${settings.homeserver}/_matrix/client/v3/profile/${jwt.subject}",
),
);
if (profile.statusCode != 200) {
return Response.forbidden(
json.encode({"error": "Access token validation failed"}),
);
}
final fullName = (json.decode(profile.body)["displayname"] as String);
final name = fullName.length <= 20 ? fullName : fullName.substring(0, 20);
return Response.ok(
jsonEncode({
"sub": jwt.subject,
"name": name,
"first_name": name,
"last_name": "",
"nickname": name,
}),
headers: {"Content-Type": "application/json"},
);
} catch (e) {
return Response.forbidden("Invalid token");
}
}
Future<Response> introspectionHandler(Request request) async {
final token = Uri.splitQueryString(await request.readAsString())["token"];
if (token == null) {
return Response(400, body: json.encode({"error": "Missing token"}));
}
try {
JWT.verify(token, await ref.read(KeyController.provider.future));
return Response.ok(
json.encode({"active": true}),
headers: {"content-type": "application/json"},
);
} catch (_) {
return Response.ok(
json.encode({"active": false}),
headers: {"content-type": "application/json"},
);
}
}
Future<Response> logoutHandler(Request request) async =>
Response.ok(json.encode("Log out is not currently implemented"));
Response openidConfiguration(_) {
final settings = ref.read(SettingsController.provider)!;
return Response.ok(
json.encode({
"issuer": settings.issuer,
"authorization_endpoint": settings.authorizeEndpoint,
"token_endpoint": "${settings.issuer}/token",
"userinfo_endpoint": "${settings.issuer}/userinfo",
"introspection_endpoint": "${settings.issuer}/introspect",
"end_session_endpoint": "#",
"response_types_supported": ["code"],
"subject_types_supported": ["public"],
"id_token_signing_alg_values_supported": ["HS256"],
}),
headers: {"Content-Type": "application/json"},
);
}
static final provider = Provider<ApiHelper>(ApiHelper.new);
}