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; infoF!"Verifying password for login attempt for user %s."(user.username); auto verificationResult = verifyPassword(password, HashedPassword(user.passwordHash), PASSWORD_HASH_PEPPER); infoF!"Verification result for login: %s"(verificationResult); 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; }