150 lines
5.3 KiB
D
150 lines
5.3 KiB
D
module auth.service;
|
|
|
|
import slf4d;
|
|
import handy_http_primitives;
|
|
import handy_http_handlers.filtered_handler;
|
|
|
|
import auth.model;
|
|
import auth.data;
|
|
import auth.data_impl_fs;
|
|
|
|
const ubyte[] PASSWORD_HASH_PEPPER = []; // Example pepper for password hashing
|
|
|
|
User createNewUser(UserRepository repo, string username, string password) {
|
|
import secured.kdf;
|
|
HashedPassword hash = securePassword(password, PASSWORD_HASH_PEPPER);
|
|
return repo.createUser(username, hash.toString());
|
|
}
|
|
|
|
void deleteUser(User user, UserRepository repo) {
|
|
repo.deleteByUsername(user.username);
|
|
}
|
|
|
|
void changePassword(User user, UserRepository repo, string currentPassword, string newPassword) {
|
|
import secured.kdf;
|
|
auto verificationResult = verifyPassword(currentPassword, HashedPassword(user.passwordHash), PASSWORD_HASH_PEPPER);
|
|
if (verificationResult == VerifyPasswordResult.Failure) {
|
|
throw new HttpStatusException(HttpStatus.FORBIDDEN, "Incorrect password.");
|
|
}
|
|
if (currentPassword == newPassword) {
|
|
throw new HttpStatusException(HttpStatus.BAD_REQUEST, "New password cannot be the same as your current one.");
|
|
}
|
|
HashedPassword newHash = securePassword(newPassword, PASSWORD_HASH_PEPPER);
|
|
repo.updatePasswordHash(User(user.username, newHash.toString()));
|
|
}
|
|
|
|
/**
|
|
* Generates a new token for a user who's logging in with the given credentials.
|
|
* Params:
|
|
* username = The user's username.
|
|
* password = The user's password.
|
|
* Returns: A JWT the user may use to authenticate requests to the API.
|
|
*/
|
|
string generateTokenForLogin(string username, string password) {
|
|
import secured.kdf;
|
|
|
|
UserRepository userRepo = new FileSystemUserRepository();
|
|
Optional!User optionalUser = userRepo.findByUsername(username);
|
|
if (optionalUser.isNull) {
|
|
throw new HttpStatusException(HttpStatus.UNAUTHORIZED, "Invalid credentials.");
|
|
}
|
|
User user = optionalUser.value;
|
|
auto verificationResult = verifyPassword(password, HashedPassword(user.passwordHash), PASSWORD_HASH_PEPPER);
|
|
if (verificationResult == VerifyPasswordResult.Failure) {
|
|
throw new HttpStatusException(HttpStatus.UNAUTHORIZED, "Invalid credentials.");
|
|
}
|
|
return generateTokenForUser(user);
|
|
}
|
|
|
|
/**
|
|
* Generates a new token for a user.
|
|
* Params:
|
|
* user = The user to generate the token for.
|
|
* Returns: A JWT the user may use to authenticate requests to the API.
|
|
*/
|
|
string generateTokenForUser(in User user) {
|
|
import jwt4d;
|
|
import std.datetime;
|
|
JwtClaims claims = JwtClaims()
|
|
.issuer("finnow")
|
|
.subject(user.username)
|
|
.expiresIn(minutes(30))
|
|
.issuedAtNow();
|
|
return writeJwt(claims, "test");
|
|
}
|
|
|
|
/**
|
|
* A request filter that only permits authenticated requests to be processed.
|
|
*/
|
|
class AuthenticationFilter : HttpRequestFilter {
|
|
private static const AUTH_METADATA_KEY = "AuthContext";
|
|
|
|
void doFilter(ref ServerHttpRequest request, ref ServerHttpResponse response, FilterChain fc) {
|
|
Optional!AuthContext optionalAuth = extractAuthContextFromBearerToken(request, response);
|
|
debugF!"Extracted auth context from bearer token: %s"(optionalAuth.value);
|
|
if (!optionalAuth.isNull) {
|
|
debugF!"Request was authenticated for user: %s"(optionalAuth.value.user.username);
|
|
request.contextData[AUTH_METADATA_KEY] = optionalAuth.value;
|
|
fc.doFilter(request, response); // Only continue the filter chain if a valid auth context was obtained.
|
|
debug_("Filter chain was called.");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Information about the current request's authentication status.
|
|
class AuthContext {
|
|
User user;
|
|
this(User user) {
|
|
this.user = user;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper method to get the authentication context from a request context
|
|
* that was previously passed through this filter.
|
|
* Params:
|
|
* request = The request to get.
|
|
* Returns: The auth context that has been set.
|
|
*/
|
|
AuthContext getAuthContext(in ServerHttpRequest request) {
|
|
return cast(AuthContext) request.contextData[AuthenticationFilter.AUTH_METADATA_KEY];
|
|
}
|
|
|
|
private Optional!AuthContext extractAuthContextFromBearerToken(
|
|
ref ServerHttpRequest request,
|
|
ref ServerHttpResponse response
|
|
) {
|
|
import jwt4d;
|
|
|
|
const HEADER_NAME = "Authorization";
|
|
if (!(HEADER_NAME in request.headers)) {
|
|
return setUnauthorized(response, "Missing Authorization header.");
|
|
}
|
|
|
|
string authorizationHeader = request.headers[HEADER_NAME][0];
|
|
if (authorizationHeader.length < 7 || authorizationHeader[0..7] != "Bearer ") {
|
|
return setUnauthorized(response, "Invalid Authorization header format. Expected bearer token.");
|
|
}
|
|
|
|
string rawToken = authorizationHeader[7..$];
|
|
JwtClaims claims;
|
|
try {
|
|
claims = readJwt(rawToken, "test");
|
|
} catch (JwtException e) {
|
|
return setUnauthorized(response, e.message.idup);
|
|
}
|
|
|
|
UserRepository userRepo = new FileSystemUserRepository();
|
|
Optional!User optionalUser = userRepo.findByUsername(claims.subject);
|
|
if (optionalUser.isNull) {
|
|
return setUnauthorized(response, "Invalid user.");
|
|
}
|
|
return Optional!AuthContext.of(new AuthContext(optionalUser.value));
|
|
}
|
|
|
|
private Optional!AuthContext setUnauthorized(ref ServerHttpResponse response, string msg) {
|
|
response.status = HttpStatus.UNAUTHORIZED;
|
|
response.writeBodyString(msg, ContentTypes.TEXT_PLAIN);
|
|
return Optional!AuthContext.empty;
|
|
}
|