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 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.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 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 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 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 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 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.new); }