finnow/finnow-api/source/auth/service.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;
}