238 lines
6.9 KiB
Dart
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);
|
|
}
|