2020-05-07 22:12:39 +02:00
|
|
|
import 'dart:async';
|
|
|
|
|
import 'dart:io';
|
|
|
|
|
|
|
|
|
|
import 'package:aqueduct/aqueduct.dart';
|
|
|
|
|
import 'package:corsac_jwt/corsac_jwt.dart';
|
|
|
|
|
|
2020-05-11 18:08:09 +02:00
|
|
|
import 'compute.dart';
|
|
|
|
|
import 'crypt.dart';
|
2020-05-07 22:12:39 +02:00
|
|
|
import 'database.dart';
|
|
|
|
|
|
2020-05-13 13:42:07 +02:00
|
|
|
/// Information on the user making the request.
|
2020-05-07 22:12:39 +02:00
|
|
|
extension AuthorizationInfo on Request {
|
2020-05-13 13:42:07 +02:00
|
|
|
/// The username of the logged in user.
|
|
|
|
|
///
|
|
|
|
|
/// If this is a non null value, the user was authenticated.
|
|
|
|
|
String get username => this.attachments['username'];
|
|
|
|
|
set username(String value) => this.attachments['username'] = value;
|
|
|
|
|
|
2020-05-07 22:12:39 +02:00
|
|
|
/// Whether the user may create new resources.
|
2020-05-13 13:42:07 +02:00
|
|
|
///
|
|
|
|
|
/// This can only be true if the user was authenticated.
|
2020-05-07 22:12:39 +02:00
|
|
|
bool get mayUpload => this.attachments['mayUpload'] ?? false;
|
2020-05-13 13:42:07 +02:00
|
|
|
set mayUpload(bool value) => this.attachments['mayUpload'] = value;
|
2020-05-07 22:12:39 +02:00
|
|
|
|
|
|
|
|
/// Whether the user may edit existing resources.
|
2020-05-13 13:42:07 +02:00
|
|
|
///
|
|
|
|
|
/// This can only be true if the user was authenticated.
|
2020-05-07 22:12:39 +02:00
|
|
|
bool get mayEdit => this.attachments['mayEdit'] ?? false;
|
2020-05-13 13:42:07 +02:00
|
|
|
set mayEdit(bool value) => this.attachments['mayEdit'] = value;
|
2020-05-07 22:12:39 +02:00
|
|
|
|
|
|
|
|
/// Whether the user may delete resources.
|
2020-05-13 13:42:07 +02:00
|
|
|
///
|
|
|
|
|
/// This can only be true if the user was authenticated.
|
2020-05-07 22:12:39 +02:00
|
|
|
bool get mayDelete => this.attachments['mayDelete'] ?? false;
|
2020-05-13 13:42:07 +02:00
|
|
|
set mayDelete(bool value) => this.attachments['mayDelete'] = value;
|
2020-05-07 22:12:39 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Endpoint controller for user registration.
|
|
|
|
|
///
|
|
|
|
|
/// This expects a POST request with a JSON body representing a [RequestUser].
|
|
|
|
|
class RegisterController extends Controller {
|
|
|
|
|
final ServerDatabase db;
|
|
|
|
|
|
|
|
|
|
RegisterController(this.db);
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Future<Response> handle(Request request) async {
|
2020-05-08 17:55:44 +02:00
|
|
|
if (request.method == 'POST') {
|
|
|
|
|
final json = await request.body.decode<Map<String, dynamic>>();
|
2020-05-13 13:42:07 +02:00
|
|
|
|
|
|
|
|
final String username = json['username'];
|
|
|
|
|
final String email = json['email'];
|
|
|
|
|
final String password = json['password'];
|
2020-05-08 17:55:44 +02:00
|
|
|
|
|
|
|
|
// Check if we already have a user with that name.
|
2020-05-13 13:42:07 +02:00
|
|
|
final existingUser = await db.getUser(username);
|
2020-05-08 17:55:44 +02:00
|
|
|
if (existingUser != null) {
|
|
|
|
|
// Returning something different than 200 here has the security
|
2020-05-11 18:08:09 +02:00
|
|
|
// 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
|
2020-05-08 17:55:44 +02:00
|
|
|
// addresses for identification. The client needs to know, whether the
|
|
|
|
|
// user name is already given.
|
|
|
|
|
return Response.conflict();
|
|
|
|
|
} else {
|
2020-05-11 18:08:09 +02:00
|
|
|
// This will take a long time, so we run it in a new isolate.
|
2020-05-13 13:42:07 +02:00
|
|
|
final result = await compute(Crypt.hashPassword, password);
|
2020-05-08 17:55:44 +02:00
|
|
|
|
|
|
|
|
db.updateUser(User(
|
2020-05-13 13:42:07 +02:00
|
|
|
name: username,
|
|
|
|
|
email: email,
|
2020-05-11 18:08:09 +02:00
|
|
|
salt: result.salt,
|
|
|
|
|
hash: result.hash,
|
2020-05-08 17:55:44 +02:00
|
|
|
mayUpload: true,
|
|
|
|
|
mayEdit: false,
|
|
|
|
|
mayDelete: false,
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
return Response.ok(null);
|
|
|
|
|
}
|
2020-05-07 22:12:39 +02:00
|
|
|
} else {
|
2020-05-08 17:55:44 +02:00
|
|
|
return Response(HttpStatus.methodNotAllowed, null, null);
|
2020-05-07 22:12:39 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 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 JWTHmacSha256Signer _signer;
|
|
|
|
|
|
|
|
|
|
LoginController(this.db, this.secret) : _signer = JWTHmacSha256Signer(secret);
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Future<Response> handle(Request request) async {
|
|
|
|
|
if (request.method == 'POST') {
|
|
|
|
|
final json = await request.body.decode<Map<String, dynamic>>();
|
|
|
|
|
|
2020-05-13 13:42:07 +02:00
|
|
|
final String username = json['username'];
|
|
|
|
|
final String password = json['password'];
|
|
|
|
|
|
|
|
|
|
final user = await db.getUser(username);
|
|
|
|
|
if (user != null) {
|
2020-05-11 18:08:09 +02:00
|
|
|
// We check the password in a new isolate, because this can take a long
|
|
|
|
|
// time.
|
|
|
|
|
if (await compute(
|
|
|
|
|
Crypt.checkPassword,
|
|
|
|
|
CheckPasswordRequest(
|
2020-05-13 13:42:07 +02:00
|
|
|
password: password,
|
|
|
|
|
salt: user.salt,
|
|
|
|
|
hash: user.hash,
|
2020-05-11 18:08:09 +02:00
|
|
|
),
|
|
|
|
|
)) {
|
2020-05-07 22:12:39 +02:00
|
|
|
final builder = JWTBuilder()
|
|
|
|
|
..expiresAt = DateTime.now().add(Duration(minutes: 30))
|
2020-05-13 13:42:07 +02:00
|
|
|
..setClaim('user', username);
|
2020-05-07 22:12:39 +02:00
|
|
|
|
|
|
|
|
final token = builder.getSignedToken(_signer).toString();
|
|
|
|
|
|
2020-05-08 18:39:28 +02:00
|
|
|
return Response.ok(token, headers: {'Content-Type': 'text/plain'});
|
2020-05-07 22:12:39 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Response.unauthorized();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Response(HttpStatus.methodNotAllowed, null, null);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-13 13:42:07 +02:00
|
|
|
/// An endpoint controller for retrieving and changing account details.
|
|
|
|
|
class AccountDetailsController extends Controller {
|
|
|
|
|
final ServerDatabase db;
|
|
|
|
|
|
|
|
|
|
AccountDetailsController(this.db);
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Future<Response> handle(Request request) async {
|
|
|
|
|
if (request.method == 'GET') {
|
|
|
|
|
if (request.username != null) {
|
|
|
|
|
final user = await db.getUser(request.username);
|
|
|
|
|
return Response.ok({
|
|
|
|
|
'email': user.email,
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
return Response.forbidden();
|
|
|
|
|
}
|
|
|
|
|
} else if (request.method == 'POST') {
|
|
|
|
|
final json = await request.body.decode<Map<String, dynamic>>();
|
|
|
|
|
|
|
|
|
|
final String username = json['username'];
|
|
|
|
|
final String password = json['password'];
|
|
|
|
|
final String newEmail = json['newEmail'];
|
|
|
|
|
final String newPassword = json['newPassword'];
|
|
|
|
|
|
|
|
|
|
final user = await db.getUser(username);
|
|
|
|
|
|
|
|
|
|
// Check whether the user exists and the password was right.
|
|
|
|
|
if (user != null &&
|
|
|
|
|
await compute(
|
|
|
|
|
Crypt.checkPassword,
|
|
|
|
|
CheckPasswordRequest(
|
|
|
|
|
password: password,
|
|
|
|
|
salt: user.salt,
|
|
|
|
|
hash: user.hash,
|
|
|
|
|
),
|
|
|
|
|
)) {
|
2020-05-13 15:59:06 +02:00
|
|
|
HashPasswordResult hashResult;
|
|
|
|
|
|
|
|
|
|
if (newPassword != null) {
|
|
|
|
|
hashResult = await compute(Crypt.hashPassword, newPassword);
|
|
|
|
|
} else {
|
|
|
|
|
hashResult = HashPasswordResult(
|
|
|
|
|
hash: user.hash,
|
|
|
|
|
salt: user.salt,
|
|
|
|
|
);
|
|
|
|
|
}
|
2020-05-13 13:42:07 +02:00
|
|
|
|
|
|
|
|
db.updateUser(User(
|
|
|
|
|
name: username,
|
2020-05-13 15:59:06 +02:00
|
|
|
email: newEmail ?? user.email,
|
2020-05-13 13:42:07 +02:00
|
|
|
salt: hashResult.salt,
|
|
|
|
|
hash: hashResult.hash,
|
|
|
|
|
mayUpload: user.mayUpload,
|
|
|
|
|
mayEdit: user.mayEdit,
|
|
|
|
|
mayDelete: user.mayDelete,
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
return Response.ok(null);
|
|
|
|
|
} else {
|
|
|
|
|
return Response.forbidden();
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
return Response(HttpStatus.methodNotAllowed, null, null);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// An endpoint controller for deleting an account.
|
|
|
|
|
class AccountDeleteController extends Controller {
|
|
|
|
|
final ServerDatabase db;
|
|
|
|
|
|
|
|
|
|
AccountDeleteController(this.db);
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Future<Response> handle(Request request) async {
|
|
|
|
|
if (request.method == 'POST') {
|
|
|
|
|
final json = await request.body.decode<Map<String, dynamic>>();
|
|
|
|
|
|
|
|
|
|
final String username = json['username'];
|
|
|
|
|
final String password = json['password'];
|
|
|
|
|
|
|
|
|
|
final user = await db.getUser(username);
|
|
|
|
|
|
|
|
|
|
// Check whether the user exists and the password was right.
|
|
|
|
|
if (user != null &&
|
|
|
|
|
await compute(
|
|
|
|
|
Crypt.checkPassword,
|
|
|
|
|
CheckPasswordRequest(
|
|
|
|
|
password: password,
|
|
|
|
|
salt: user.salt,
|
|
|
|
|
hash: user.hash,
|
|
|
|
|
),
|
|
|
|
|
)) {
|
|
|
|
|
await db.deleteUser(username);
|
|
|
|
|
|
|
|
|
|
return Response.ok(null);
|
|
|
|
|
} else {
|
|
|
|
|
return Response.forbidden();
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
return Response(HttpStatus.methodNotAllowed, null, null);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-07 22:12:39 +02:00
|
|
|
/// 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<RequestOrResponse> 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]);
|
|
|
|
|
|
2020-05-11 18:12:58 +02:00
|
|
|
/// The JWTValidator will automatically use the current time. An empty
|
|
|
|
|
/// result will mean that the token is valid and its signature was
|
|
|
|
|
/// verified.
|
|
|
|
|
if (JWTValidator().validate(jwt, signer: _signer).isEmpty) {
|
2020-05-07 22:12:39 +02:00
|
|
|
final user = await db.getUser(jwt.claims['user']);
|
|
|
|
|
if (user != null) {
|
2020-05-13 13:42:07 +02:00
|
|
|
request.username = user.name;
|
2020-05-07 22:12:39 +02:00
|
|
|
request.mayUpload = user.mayUpload;
|
|
|
|
|
request.mayEdit = user.mayEdit;
|
|
|
|
|
request.mayDelete = user.mayDelete;
|
2020-05-08 17:55:44 +02:00
|
|
|
|
|
|
|
|
return request;
|
|
|
|
|
} else {
|
|
|
|
|
return Response.unauthorized();
|
2020-05-07 22:12:39 +02:00
|
|
|
}
|
2020-05-08 17:55:44 +02:00
|
|
|
} else {
|
|
|
|
|
return Response.unauthorized();
|
2020-05-07 22:12:39 +02:00
|
|
|
}
|
2020-05-08 17:55:44 +02:00
|
|
|
} else {
|
|
|
|
|
return Response.badRequest();
|
2020-05-07 22:12:39 +02:00
|
|
|
}
|
2020-05-08 17:55:44 +02:00
|
|
|
} else {
|
|
|
|
|
return request;
|
2020-05-07 22:12:39 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|