From 7aecbbba693bcb40793b34c7e53c9cd51272afe8 Mon Sep 17 00:00:00 2001 From: Elias Projahn Date: Thu, 7 May 2020 22:12:39 +0200 Subject: [PATCH] server: Protect routes with user authorization --- server/bin/main.dart | 1 + server/config.src.yaml | 5 +- server/config.yaml | 4 +- server/lib/src/auth.dart | 176 ++++++++++++++++++++++++++++++ server/lib/src/configuration.dart | 4 + server/lib/src/database.dart | 19 ++++ server/lib/src/database.moor | 9 ++ server/lib/src/ensembles.dart | 16 +++ server/lib/src/instruments.dart | 16 +++ server/lib/src/persons.dart | 16 +++ server/lib/src/recordings.dart | 16 +++ server/lib/src/server.dart | 36 +++++- server/lib/src/works.dart | 16 +++ server/pubspec.yaml | 11 +- 14 files changed, 334 insertions(+), 11 deletions(-) create mode 100644 server/lib/src/auth.dart create mode 100644 server/lib/src/database.dart create mode 100644 server/lib/src/database.moor diff --git a/server/bin/main.dart b/server/bin/main.dart index 3fe0109..176c9e6 100644 --- a/server/bin/main.dart +++ b/server/bin/main.dart @@ -15,5 +15,6 @@ Future main() async { ); print('Database: ${config.dbPath ?? 'memory'}'); + print('Server database: ${config.serverDbPath ?? 'memory'}'); print('Listening on ${config.host}:${config.port}'); } diff --git a/server/config.src.yaml b/server/config.src.yaml index ed01e99..18e5f6b 100644 --- a/server/config.src.yaml +++ b/server/config.src.yaml @@ -1,3 +1,4 @@ -# A dbPath of null means that we want an in-memory database. +# A dbPath and serverDbPath of null means that we want in-memory databases. host: localhost -port: 1833 \ No newline at end of file +port: 1833 +secret: vulnerable \ No newline at end of file diff --git a/server/config.yaml b/server/config.yaml index 203c392..5fbf0a6 100644 --- a/server/config.yaml +++ b/server/config.yaml @@ -1,3 +1,5 @@ host: localhost port: 1833 -dbPath: db.sqlite \ No newline at end of file +secret: vulnerable +dbPath: db.sqlite +serverDbPath: server.sqlite \ No newline at end of file diff --git a/server/lib/src/auth.dart b/server/lib/src/auth.dart new file mode 100644 index 0000000..40521d3 --- /dev/null +++ b/server/lib/src/auth.dart @@ -0,0 +1,176 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:math'; + +import 'package:aqueduct/aqueduct.dart'; +import 'package:corsac_jwt/corsac_jwt.dart'; +import 'package:steel_crypt/steel_crypt.dart'; + +import 'database.dart'; + +/// Information on the rights of the user making the request. +extension AuthorizationInfo on Request { + /// Whether the user may create new resources. + set mayUpload(bool value) => this.attachments['mayUpload'] = value; + bool get mayUpload => this.attachments['mayUpload'] ?? false; + + /// Whether the user may edit existing resources. + set mayEdit(bool value) => this.attachments['mayEdit'] = value; + bool get mayEdit => this.attachments['mayEdit'] ?? false; + + /// Whether the user may delete resources. + set mayDelete(bool value) => this.attachments['mayDelete'] = value; + bool get mayDelete => this.attachments['mayDelete'] ?? false; +} + +/// A user as presented within a request. +class RequestUser { + /// The unique user name. + final String name; + + /// An optional email address. + final String email; + + /// The password in clear text. + final String password; + + RequestUser({ + this.name, + this.email, + this.password, + }); + + factory RequestUser.fromJson(Map json) => RequestUser( + name: json['name'], + email: json['email'], + password: json['password'], + ); +} + +/// Endpoint controller for user registration. +/// +/// This expects a POST request with a JSON body representing a [RequestUser]. +class RegisterController extends Controller { + final ServerDatabase db; + + final _crypt = PassCrypt(); + final _rand = Random.secure(); + + RegisterController(this.db); + + @override + Future handle(Request request) async { + final json = await request.body.decode>(); + final requestUser = RequestUser.fromJson(json); + + // Check if we already have a user with that name. + final existingUser = await db.getUser(requestUser.name); + if (existingUser != null) { + // Returning something different than 200 here has the security + // implication that an attacker can check for existing user names. At the + // moment, I don't see any alternatives, because we don't use email + // addresses for identification. The client needs to know, whether the + // user name is already given. + return Response.conflict(); + } else { + final bytes = List.generate(32, (i) => _rand.nextInt(256)); + final salt = base64UrlEncode(bytes); + final hash = _crypt.hashPass(salt, requestUser.password); + + db.updateUser(User( + name: requestUser.name, + email: requestUser.email, + salt: salt, + hash: hash, + mayUpload: true, + mayEdit: false, + mayDelete: false, + )); + + return Response.ok(null); + } + } +} + +/// Endpoint controller for user login. +/// +/// This expects a POST request with a JSON body representing a [RequestUser]. +class LoginController extends Controller { + final ServerDatabase db; + + /// The secret that will be used for signing the token. + final String secret; + + final _crypt = PassCrypt(); + final JWTHmacSha256Signer _signer; + + LoginController(this.db, this.secret) : _signer = JWTHmacSha256Signer(secret); + + @override + Future handle(Request request) async { + if (request.method == 'POST') { + final json = await request.body.decode>(); + final requestUser = RequestUser.fromJson(json); + + final realUser = await db.getUser(requestUser.name); + if (realUser != null) { + if (_crypt.checkPassKey( + realUser.salt, requestUser.password, realUser.hash)) { + final builder = JWTBuilder() + ..expiresAt = DateTime.now().add(Duration(minutes: 30)) + ..setClaim('user', requestUser.name); + + final token = builder.getSignedToken(_signer).toString(); + + return Response.ok(token); + } + } + + return Response.unauthorized(); + } + + return Response(HttpStatus.methodNotAllowed, null, null); + } +} + +/// Middleware for checking authorization. +/// +/// This will set the fields defined in [AuthorizationInfo] on this request +/// according to the provided access token. +class AuthorizationController extends Controller { + final ServerDatabase db; + + /// The secret that was used to sign the token. + final String secret; + + final JWTHmacSha256Signer _signer; + + AuthorizationController(this.db, this.secret) + : _signer = JWTHmacSha256Signer(secret); + + @override + FutureOr handle(Request request) async { + final authHeaderValue = + request.raw.headers.value(HttpHeaders.authorizationHeader); + + if (authHeaderValue != null) { + final authHeaderParts = authHeaderValue.split(' '); + + if (authHeaderParts.length == 2 && authHeaderParts[0] == 'Bearer') { + final jwt = JWT.parse(authHeaderParts[1]); + + if (jwt.verify(_signer)) { + final user = await db.getUser(jwt.claims['user']); + if (user != null) { + request.mayUpload = user.mayUpload; + request.mayEdit = user.mayEdit; + request.mayDelete = user.mayDelete; + } + } + } + } + + return request; + } +} diff --git a/server/lib/src/configuration.dart b/server/lib/src/configuration.dart index 3d3af7c..9cd842a 100644 --- a/server/lib/src/configuration.dart +++ b/server/lib/src/configuration.dart @@ -7,7 +7,11 @@ class MusicusServerConfiguration extends Configuration { String host; int port; + String secret; @optionalConfiguration String dbPath; + + @optionalConfiguration + String serverDbPath; } \ No newline at end of file diff --git a/server/lib/src/database.dart b/server/lib/src/database.dart new file mode 100644 index 0000000..8490430 --- /dev/null +++ b/server/lib/src/database.dart @@ -0,0 +1,19 @@ +import 'package:moor/moor.dart'; + +part 'database.g.dart'; + +@UseMoor(include: {'database.moor'}) +class ServerDatabase extends _$ServerDatabase { + @override + final schemaVersion = 0; + + ServerDatabase(QueryExecutor e) : super(e); + + Future getUser(String name) async { + return await (select(users)..where((u) => u.name.equals(name))).getSingle(); + } + + Future updateUser(User user) async { + await into(users).insert(user, mode: InsertMode.insertOrReplace); + } +} diff --git a/server/lib/src/database.moor b/server/lib/src/database.moor new file mode 100644 index 0000000..0c556df --- /dev/null +++ b/server/lib/src/database.moor @@ -0,0 +1,9 @@ +CREATE TABLE users ( + name TEXT PRIMARY KEY, + email TEXT, + salt TEXT NOT NULL, + hash TEXT NOT NULL, + may_upload BOOLEAN NOT NULL DEFAULT TRUE, + may_edit BOOLEAN NOT NULL DEFAULT FALSE, + may_delete BOOLEAN NOT NULL DEFAULT FALSE +); \ No newline at end of file diff --git a/server/lib/src/ensembles.dart b/server/lib/src/ensembles.dart index d827c0a..46babb1 100644 --- a/server/lib/src/ensembles.dart +++ b/server/lib/src/ensembles.dart @@ -1,6 +1,8 @@ import 'package:aqueduct/aqueduct.dart'; import 'package:musicus_database/musicus_database.dart'; +import 'auth.dart'; + class EnsemblesController extends ResourceController { final Database db; @@ -26,6 +28,16 @@ class EnsemblesController extends ResourceController { @Operation.put('id') Future putEnsemble( @Bind.path('id') int id, @Bind.body() Map json) async { + if (await db.ensembleById(id).getSingle() != null) { + if (!request.mayEdit) { + return Response.forbidden(); + } + } else { + if (!request.mayUpload) { + return Response.forbidden(); + } + } + final ensemble = Ensemble.fromJson(json).copyWith( id: id, ); @@ -37,6 +49,10 @@ class EnsemblesController extends ResourceController { @Operation.delete('id') Future deleteEnsemble(@Bind.path('id') int id) async { + if (!request.mayDelete) { + return Response.forbidden(); + } + await db.deleteEnsemble(id); return Response.ok(null); } diff --git a/server/lib/src/instruments.dart b/server/lib/src/instruments.dart index 946df37..f93d158 100644 --- a/server/lib/src/instruments.dart +++ b/server/lib/src/instruments.dart @@ -1,6 +1,8 @@ import 'package:aqueduct/aqueduct.dart'; import 'package:musicus_database/musicus_database.dart'; +import 'auth.dart'; + class InstrumentsController extends ResourceController { final Database db; @@ -26,6 +28,16 @@ class InstrumentsController extends ResourceController { @Operation.put('id') Future putInstrument( @Bind.path('id') int id, @Bind.body() Map json) async { + if (await db.instrumentById(id).getSingle() != null) { + if (!request.mayEdit) { + return Response.forbidden(); + } + } else { + if (!request.mayUpload) { + return Response.forbidden(); + } + } + final instrument = Instrument.fromJson(json).copyWith( id: id, ); @@ -37,6 +49,10 @@ class InstrumentsController extends ResourceController { @Operation.delete('id') Future deleteInstrument(@Bind.path('id') int id) async { + if (!request.mayDelete) { + return Response.forbidden(); + } + await db.deleteInstrument(id); return Response.ok(null); } diff --git a/server/lib/src/persons.dart b/server/lib/src/persons.dart index e4f6261..9e3542f 100644 --- a/server/lib/src/persons.dart +++ b/server/lib/src/persons.dart @@ -1,6 +1,8 @@ import 'package:aqueduct/aqueduct.dart'; import 'package:musicus_database/musicus_database.dart'; +import 'auth.dart'; + class PersonsController extends ResourceController { final Database db; @@ -26,6 +28,16 @@ class PersonsController extends ResourceController { @Operation.put('id') Future putPerson( @Bind.path('id') int id, @Bind.body() Map json) async { + if (await db.personById(id).getSingle() != null) { + if (!request.mayEdit) { + return Response.forbidden(); + } + } else { + if (!request.mayUpload) { + return Response.forbidden(); + } + } + final person = Person.fromJson(json).copyWith( id: id, ); @@ -37,6 +49,10 @@ class PersonsController extends ResourceController { @Operation.delete('id') Future deletePerson(@Bind.path('id') int id) async { + if (!request.mayDelete) { + return Response.forbidden(); + } + await db.deletePerson(id); return Response.ok(null); } diff --git a/server/lib/src/recordings.dart b/server/lib/src/recordings.dart index b1f5836..57bd202 100644 --- a/server/lib/src/recordings.dart +++ b/server/lib/src/recordings.dart @@ -1,6 +1,8 @@ import 'package:aqueduct/aqueduct.dart'; import 'package:musicus_database/musicus_database.dart'; +import 'auth.dart'; + class RecordingsController extends ResourceController { final Database db; @@ -19,6 +21,16 @@ class RecordingsController extends ResourceController { @Operation.put('id') Future putRecording( @Bind.path('id') int id, @Bind.body() Map json) async { + if (await db.recordingById(id).getSingle() != null) { + if (!request.mayEdit) { + return Response.forbidden(); + } + } else { + if (!request.mayUpload) { + return Response.forbidden(); + } + } + final recordingInfo = RecordingInfo.fromJson(json); await db.updateRecording(recordingInfo); @@ -27,6 +39,10 @@ class RecordingsController extends ResourceController { @Operation.delete('id') Future deleteRecording(@Bind.path('id') int id) async { + if (!request.mayDelete) { + return Response.forbidden(); + } + await db.deleteRecording(id); return Response.ok(null); } diff --git a/server/lib/src/server.dart b/server/lib/src/server.dart index c032bcb..b07ce22 100644 --- a/server/lib/src/server.dart +++ b/server/lib/src/server.dart @@ -3,18 +3,22 @@ import 'dart:io'; import 'package:aqueduct/aqueduct.dart'; import 'package:moor_ffi/moor_ffi.dart'; import 'package:musicus_database/musicus_database.dart'; -import 'package:musicus_server/src/work_recordings.dart'; +import 'auth.dart'; import 'compositions.dart'; import 'configuration.dart'; +import 'database.dart'; import 'ensembles.dart'; import 'instruments.dart'; import 'persons.dart'; import 'recordings.dart'; import 'works.dart'; +import 'work_recordings.dart'; class MusicusServer extends ApplicationChannel { Database db; + ServerDatabase serverDb; + String secret; @override Future prepare() async { @@ -25,15 +29,35 @@ class MusicusServer extends ApplicationChannel { } else { db = Database(VmDatabase.memory()); } + + if (config.serverDbPath != null) { + serverDb = ServerDatabase(VmDatabase(File(config.serverDbPath))); + } else { + serverDb = ServerDatabase(VmDatabase.memory()); + } + + secret = config.secret; } @override Controller get entryPoint => Router() - ..route('/persons/[:id]').link(() => PersonsController(db)) + ..route('/login').link(() => LoginController(serverDb, secret)) + ..route('/register').link(() => RegisterController(serverDb)) + ..route('/persons/[:id]') + .link(() => AuthorizationController(serverDb, secret)) + .link(() => PersonsController(db)) ..route('/persons/:id/works').link(() => CompositionsController(db)) - ..route('/instruments/[:id]').link(() => InstrumentsController(db)) - ..route('/works/:id').link(() => WorksController(db)) + ..route('/instruments/[:id]') + .link(() => AuthorizationController(serverDb, secret)) + .link(() => InstrumentsController(db)) + ..route('/works/:id') + .link(() => AuthorizationController(serverDb, secret)) + .link(() => WorksController(db)) ..route('/works/:id/recordings').link(() => WorkRecordingsController(db)) - ..route('/ensembles/[:id]').link(() => EnsemblesController(db)) - ..route('/recordings/:id').link(() => RecordingsController(db)); + ..route('/ensembles/[:id]') + .link(() => AuthorizationController(serverDb, secret)) + .link(() => EnsemblesController(db)) + ..route('/recordings/:id') + .link(() => AuthorizationController(serverDb, secret)) + .link(() => RecordingsController(db)); } diff --git a/server/lib/src/works.dart b/server/lib/src/works.dart index 236f8de..b0777c7 100644 --- a/server/lib/src/works.dart +++ b/server/lib/src/works.dart @@ -1,6 +1,8 @@ import 'package:aqueduct/aqueduct.dart'; import 'package:musicus_database/musicus_database.dart'; +import 'auth.dart'; + class WorksController extends ResourceController { final Database db; @@ -19,6 +21,16 @@ class WorksController extends ResourceController { @Operation.put('id') Future putWork( @Bind.path('id') int id, @Bind.body() Map json) async { + if (await db.workById(id).getSingle() != null) { + if (!request.mayEdit) { + return Response.forbidden(); + } + } else { + if (!request.mayUpload) { + return Response.forbidden(); + } + } + final workInfo = WorkInfo.fromJson(json); await db.updateWork(workInfo); @@ -27,6 +39,10 @@ class WorksController extends ResourceController { @Operation.delete('id') Future deleteWork(@Bind.path('id') int id) async { + if (!request.mayDelete) { + return Response.forbidden(); + } + await db.deleteWork(id); return Response.ok(null); } diff --git a/server/pubspec.yaml b/server/pubspec.yaml index 869aeb2..9fdbdbd 100644 --- a/server/pubspec.yaml +++ b/server/pubspec.yaml @@ -3,10 +3,17 @@ description: A server hosting a Musicus database. version: 0.0.1 environment: - sdk: ">=2.3.0 <3.0.0" + sdk: ">=2.6.0 <3.0.0" dependencies: aqueduct: - moor_ffi: + corsac_jwt: + moor: ^3.0.2 + moor_ffi: ^0.5.0 musicus_database: path: ../database + steel_crypt: + +dev_dependencies: + build_runner: + moor_generator: ^3.0.0