diff --git a/.gitignore b/.gitignore
index 77528937246d710da3c140a20ed19ce6c610bae1..0f25e70632f5e5cc8592e87c9ab7a9d3004ba889 100644
--- a/.gitignore
+++ b/.gitignore
@@ -37,3 +37,4 @@ OSX/dsa_priv.pem
 bin/config.json
 bin/release/aws-eb/metabase-aws-eb.zip
 *.sqlite
+/reset-password-artifacts
diff --git a/OSX/Metabase.xcodeproj/project.pbxproj b/OSX/Metabase.xcodeproj/project.pbxproj
index a8d8b8e7704bd269c0071874e4b1cac88d2bd1a6..7c39d5f41b6d2f87bbca706a5546ebf1a817ea97 100644
--- a/OSX/Metabase.xcodeproj/project.pbxproj
+++ b/OSX/Metabase.xcodeproj/project.pbxproj
@@ -41,6 +41,9 @@
 		D18855611BB1C8D700D89803 /* jre in Resources */ = {isa = PBXBuildFile; fileRef = D188555E1BB1C86F00D89803 /* jre */; };
 		D1BDAC681C053E0A0075D3AC /* ResetPasswordWindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = D1BDAC671C053E0A0075D3AC /* ResetPasswordWindowController.m */; };
 		D1BDAC6A1C053E220075D3AC /* ResetPasswordWindowController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D1BDAC691C053E220075D3AC /* ResetPasswordWindowController.xib */; };
+		D1BDAC6D1C0565640075D3AC /* JavaTask.m in Sources */ = {isa = PBXBuildFile; fileRef = D1BDAC6C1C0565640075D3AC /* JavaTask.m */; };
+		D1BDAC721C05695B0075D3AC /* ResetPasswordTask.m in Sources */ = {isa = PBXBuildFile; fileRef = D1BDAC711C05695B0075D3AC /* ResetPasswordTask.m */; };
+		D1BDAC731C056C7C0075D3AC /* reset-password.jar in Resources */ = {isa = PBXBuildFile; fileRef = D1BDAC6E1C0566490075D3AC /* reset-password.jar */; };
 		D1CB9FE81BCEDE9A009A61FB /* LoadingView.m in Sources */ = {isa = PBXBuildFile; fileRef = D1CB9FE71BCEDE9A009A61FB /* LoadingView.m */; };
 		D1CB9FEA1BCEDEA5009A61FB /* LoadingView.xib in Resources */ = {isa = PBXBuildFile; fileRef = D1CB9FE91BCEDEA5009A61FB /* LoadingView.xib */; };
 		D1CB9FED1BCEDF02009A61FB /* Loading_View_Inner.png in Resources */ = {isa = PBXBuildFile; fileRef = D1CB9FEB1BCEDF02009A61FB /* Loading_View_Inner.png */; };
@@ -124,6 +127,11 @@
 		D1BDAC661C053E0A0075D3AC /* ResetPasswordWindowController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ResetPasswordWindowController.h; sourceTree = "<group>"; };
 		D1BDAC671C053E0A0075D3AC /* ResetPasswordWindowController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ResetPasswordWindowController.m; sourceTree = "<group>"; };
 		D1BDAC691C053E220075D3AC /* ResetPasswordWindowController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ResetPasswordWindowController.xib; sourceTree = "<group>"; };
+		D1BDAC6B1C0565640075D3AC /* JavaTask.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JavaTask.h; sourceTree = "<group>"; };
+		D1BDAC6C1C0565640075D3AC /* JavaTask.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JavaTask.m; sourceTree = "<group>"; };
+		D1BDAC6E1C0566490075D3AC /* reset-password.jar */ = {isa = PBXFileReference; lastKnownFileType = archive.jar; path = "reset-password.jar"; sourceTree = "<group>"; };
+		D1BDAC701C05695B0075D3AC /* ResetPasswordTask.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ResetPasswordTask.h; sourceTree = "<group>"; };
+		D1BDAC711C05695B0075D3AC /* ResetPasswordTask.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ResetPasswordTask.m; sourceTree = "<group>"; };
 		D1CB9FE61BCEDE9A009A61FB /* LoadingView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LoadingView.h; sourceTree = "<group>"; };
 		D1CB9FE71BCEDE9A009A61FB /* LoadingView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LoadingView.m; sourceTree = "<group>"; };
 		D1CB9FE91BCEDEA5009A61FB /* LoadingView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = LoadingView.xib; sourceTree = "<group>"; };
@@ -231,12 +239,16 @@
 			children = (
 				D162C4991BC87D2B009F678F /* AppDelegate.h */,
 				D162C49A1BC87D2B009F678F /* AppDelegate.m */,
-				D162C49B1BC87D2B009F678F /* MetabaseTask.h */,
-				D162C49C1BC87D2B009F678F /* MetabaseTask.m */,
 				D162C49D1BC87D2B009F678F /* SettingsManager.h */,
 				D162C49E1BC87D2B009F678F /* SettingsManager.m */,
 				D162C49F1BC87D2B009F678F /* TaskHealthChecker.h */,
 				D162C4A01BC87D2B009F678F /* TaskHealthChecker.m */,
+				D1BDAC6B1C0565640075D3AC /* JavaTask.h */,
+				D1BDAC6C1C0565640075D3AC /* JavaTask.m */,
+				D162C49B1BC87D2B009F678F /* MetabaseTask.h */,
+				D162C49C1BC87D2B009F678F /* MetabaseTask.m */,
+				D1BDAC701C05695B0075D3AC /* ResetPasswordTask.h */,
+				D1BDAC711C05695B0075D3AC /* ResetPasswordTask.m */,
 			);
 			path = Backend;
 			sourceTree = "<group>";
@@ -326,6 +338,7 @@
 			children = (
 				D18853D61BB0CEC600D89803 /* Images.xcassets */,
 				D105B2321BB5BE4A00A5D850 /* Images */,
+				D1BDAC6E1C0566490075D3AC /* reset-password.jar */,
 				D18854021BB0DB6000D89803 /* metabase.jar */,
 				D121FD681BC5B4E7002101B0 /* dsa_pub.pem */,
 			);
@@ -413,6 +426,7 @@
 			isa = PBXResourcesBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				D1BDAC731C056C7C0075D3AC /* reset-password.jar in Resources */,
 				D1DFF6C31BCEF9E700ECC7B6 /* Loading_View_Inner@2x.png in Resources */,
 				D1CB9FEE1BCEDF02009A61FB /* Loading_View_Outer.png in Resources */,
 				D105B23D1BB5BE4A00A5D850 /* back_icon@2x.png in Resources */,
@@ -478,6 +492,8 @@
 				D162C4AF1BC87D2B009F678F /* MainViewController.m in Sources */,
 				D105B2281BB378C100A5D850 /* INWindowButton.m in Sources */,
 				D105B2271BB378C100A5D850 /* INAppStoreWindow.m in Sources */,
+				D1BDAC6D1C0565640075D3AC /* JavaTask.m in Sources */,
+				D1BDAC721C05695B0075D3AC /* ResetPasswordTask.m in Sources */,
 				D162C4A91BC87D2B009F678F /* MetabaseTask.m in Sources */,
 				D162C4A81BC87D2B009F678F /* AppDelegate.m in Sources */,
 				D1BDAC681C053E0A0075D3AC /* ResetPasswordWindowController.m in Sources */,
diff --git a/OSX/Metabase/Backend/JavaTask.h b/OSX/Metabase/Backend/JavaTask.h
new file mode 100644
index 0000000000000000000000000000000000000000..c890ecc031b042629f43eac093058fa123163332
--- /dev/null
+++ b/OSX/Metabase/Backend/JavaTask.h
@@ -0,0 +1,25 @@
+//
+//  JavaTask.h
+//  Metabase
+//
+//  Created by Cam Saul on 11/24/15.
+//  Copyright (c) 2015 Metabase. All rights reserved.
+//
+
+NSString *JREPath();
+NSString *UberjarPath();
+NSString *DBPath();			///< Path to the H2 DB file
+
+/// Base class for running JRE-based NSTasks
+@interface JavaTask : NSObject
+
+@property (strong, nonatomic) NSTask *task;
+
+/// Called when a new data is written to the task's stdin / stdout. Default implementation does nothing. This is called on a background thread!
+- (void)readHandleDidRead:(NSString *)message;
+
+/// Terminate the associated NSTask.
+- (void)terminate;
+
+
+@end
diff --git a/OSX/Metabase/Backend/JavaTask.m b/OSX/Metabase/Backend/JavaTask.m
new file mode 100644
index 0000000000000000000000000000000000000000..3ca36118ea998af63f75aa40be68e6a099dacb7f
--- /dev/null
+++ b/OSX/Metabase/Backend/JavaTask.m
@@ -0,0 +1,137 @@
+//
+//  JavaTask.m
+//  Metabase
+//
+//  Created by Cam Saul on 11/24/15.
+//  Copyright (c) 2015 Metabase. All rights reserved.
+//
+
+#import "JavaTask.h"
+
+NSString *JREPath() {
+	return [[NSBundle mainBundle] pathForResource:@"java" ofType:nil inDirectory:@"jre/bin"];
+}
+
+NSString *UberjarPath() {
+	return [[NSBundle mainBundle] pathForResource:@"metabase" ofType:@"jar"];
+}
+
+NSString *DBPath() {
+	NSString *applicationSupportDir = [NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES)[0] stringByAppendingPathComponent:@"Metabase"];
+	if (![[NSFileManager defaultManager] fileExistsAtPath:applicationSupportDir]) {
+		NSError *error = nil;
+		[[NSFileManager defaultManager] createDirectoryAtPath:applicationSupportDir withIntermediateDirectories:YES attributes:nil error:&error];
+		if (error) {
+			NSLog(@"Error creating %@: %@", applicationSupportDir, error.localizedDescription);
+		}
+	}
+	return [applicationSupportDir stringByAppendingPathComponent:@"metabase.db"];
+}
+
+
+@interface JavaTask ()
+@property (strong, nonatomic, readwrite) NSPipe *pipe;
+@property (strong, nonatomic, readwrite) NSFileHandle *readHandle;
+@end
+
+@implementation JavaTask
+
+#pragma mark - Lifecycle
+
+- (instancetype)init {
+	if (self = [super init]) {
+		[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(fileHandleCompletedRead:) name:NSFileHandleReadCompletionNotification object:nil];
+	}
+	return self;
+}
+
+- (void)dealloc {
+	[[NSNotificationCenter defaultCenter] removeObserver:self];
+	[self terminate];
+}
+
+
+#pragma mark - Notifications
+
+- (void)fileHandleCompletedRead:(NSNotification *)notification {
+	if (!self.readHandle || notification.object != self.readHandle) return;
+	
+	__weak JavaTask *weakSelf = self;
+	dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
+		if (!weakSelf) return;
+		
+		NSData *data = notification.userInfo[NSFileHandleNotificationDataItem];
+		if (data.length) [weakSelf readHandleDidReadData:data];
+		
+		dispatch_async(dispatch_get_main_queue(), ^{
+			[weakSelf.readHandle readInBackgroundAndNotify];
+		});
+	});
+}
+
+
+
+#pragma mark - Local Methods
+
+- (void)readHandleDidReadData:(NSData *)data {
+	NSString *message = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
+	if (!message.length) return;
+	
+	[self readHandleDidRead:message];
+}
+
+- (void)readHandleDidRead:(NSString *)message {}
+
+- (void)terminate {
+	if (!self.task) return; // already dead
+	
+	NSLog(@"Killing %@ @ 0x%zx...", [self class], (size_t)self);
+	self.task = nil;
+}
+
+
+#pragma mark - Getters / Setters
+
+- (void)setTask:(NSTask *)task {
+	self.pipe = nil;
+	
+	[_task terminate];
+	_task = task;
+	
+	if (task) {
+		self.pipe					= [NSPipe pipe];
+		self.task.standardOutput	= self.pipe;
+		self.task.standardError		= self.pipe;
+	}
+}
+
+- (void)setPipe:(NSPipe *)pipe {
+	self.readHandle = nil;
+	_pipe = pipe;
+	
+	if (pipe) {
+		self.readHandle = pipe.fileHandleForReading;
+		__weak JavaTask *weakSelf = self;
+		dispatch_async(dispatch_get_main_queue(), ^{
+			if (weakSelf) [weakSelf.readHandle readInBackgroundAndNotify];
+		});
+	}
+}
+
+- (void)setReadHandle:(NSFileHandle *)readHandle {
+	// handle any remaining data in the read handle before closing, if applicable
+	if (_readHandle) {
+		NSData *data;
+		@try {
+			data = [_readHandle availableData];
+		}
+		@catch (NSException *exception) {}
+		
+		if (data.length) [self readHandleDidReadData:data];
+	}
+	
+	[_readHandle closeFile];
+	_readHandle = readHandle;
+}
+
+@end
diff --git a/OSX/Metabase/Backend/MetabaseTask.h b/OSX/Metabase/Backend/MetabaseTask.h
index fd15ae30fe732858b6b6e66def8f09dfe286a56a..f71efc48b2b6d00e8e165cc6b5df887f351cfacc 100644
--- a/OSX/Metabase/Backend/MetabaseTask.h
+++ b/OSX/Metabase/Backend/MetabaseTask.h
@@ -6,13 +6,15 @@
 //  Copyright (c) 2015 Metabase. All rights reserved.
 //
 
-@interface MetabaseTask : NSObject
+#import "JavaTask.h"
+
+/// Task for running the MetabaseServer
+@interface MetabaseTask : JavaTask
 
 /// Create (and launch) a task to run the Metabase backend server.
 + (MetabaseTask *)task;
 
 - (void)launch;
-- (void)terminate;
 
 - (NSUInteger)port;
 
diff --git a/OSX/Metabase/Backend/MetabaseTask.m b/OSX/Metabase/Backend/MetabaseTask.m
index 7d79c0fff83e60be2e1557d5a8397d7bc33265ee..0668a16240a84abdd747338f6bd99d717dfdee09 100644
--- a/OSX/Metabase/Backend/MetabaseTask.m
+++ b/OSX/Metabase/Backend/MetabaseTask.m
@@ -11,13 +11,6 @@
 #define ENABLE_JAR_UNPACKING 0
 
 @interface MetabaseTask ()
-@property (strong, nonatomic) NSTask *task;
-@property (strong, nonatomic) NSPipe *pipe;
-@property (strong, nonatomic) NSFileHandle *readHandle;
-
-@property (strong, readonly) NSString *javaPath;
-@property (strong, readonly) NSString *jarPath;
-@property (strong, readonly) NSString *dbPath;
 
 #if ENABLE_JAR_UNPACKING
 	@property (strong, readonly) NSString *unpack200Path;
@@ -33,54 +26,23 @@
 }
 
 
-#pragma mark - Lifecycle
-
-- (instancetype)init {
-	if (self = [super init]) {
-		[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(fileHandleCompletedRead:) name:NSFileHandleReadCompletionNotification object:nil];
-	}
-	return self;
-}
-
-- (void)dealloc {
-	[[NSNotificationCenter defaultCenter] removeObserver:self];
-	[self terminate];
-}
-
+#pragma mark - Local Methods
 
-#pragma mark - Notifications
+- (void)readHandleDidRead:(NSString *)message {
+	// skip calls to health endpoint
+	if ([message rangeOfString:@"GET /api/health"].location != NSNotFound) return;
 
-- (void)fileHandleCompletedRead:(NSNotification *)notification {
-	if (!self.readHandle) return;
+	// strip off the timestamp that comes back from the backend so we don't get double-timestamps when NSLog adds its own
+	NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"^[\\d-:,\\s]+(.*$)" options:NSRegularExpressionAnchorsMatchLines|NSRegularExpressionAllowCommentsAndWhitespace error:nil];
+	message = [regex stringByReplacingMatchesInString:message options:0 range:NSMakeRange(0, message.length) withTemplate:@"$1"];
 	
-	__weak MetabaseTask *weakSelf = self;
-	dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
-		if (!weakSelf) return;
-		@try {
-			NSString *message = [[NSString alloc] initWithData:weakSelf.readHandle.availableData encoding:NSUTF8StringEncoding];
-			// skip calls to health endpoint
-			if ([message rangeOfString:@"GET /api/health"].location == NSNotFound) {
-				// strip off the timestamp that comes back from the backend so we don't get double-timestamps when NSLog adds its own
-				NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"^[\\d-:,\\s]+(.*$)" options:NSRegularExpressionAnchorsMatchLines|NSRegularExpressionAllowCommentsAndWhitespace error:nil];
-				message = [regex stringByReplacingMatchesInString:message options:0 range:NSMakeRange(0, message.length) withTemplate:@"$1"];
-				
-				// remove control codes used to color output
-				regex = [NSRegularExpression regularExpressionWithPattern:@"\\[\\d+m" options:0 error:nil];
-				message = [regex stringByReplacingMatchesInString:message options:0 range:NSMakeRange(0, message.length) withTemplate:@""];
-				
-				NSLog(@"%@", message);
-			}
-		} @catch (NSException *) {}
-		
-		dispatch_async(dispatch_get_main_queue(), ^{
-			[weakSelf.readHandle readInBackgroundAndNotify];
-		});
-	});
+	// remove control codes used to color output
+	regex = [NSRegularExpression regularExpressionWithPattern:@"\\[\\d+m" options:0 error:nil];
+	message = [regex stringByReplacingMatchesInString:message options:0 range:NSMakeRange(0, message.length) withTemplate:@""];
+	
+	NSLog(@"%@", message);
 }
 
-
-#pragma mark - Local Methods
-
 #if ENABLE_JAR_UNPACKING
 /// unpack the jars in the BG if needed the first time around
 - (void)unpackJars {
@@ -100,8 +62,8 @@
 #endif
 
 - (void)deleteOldDBLockFilesIfNeeded {
-	NSString *lockFile	= [self.dbPath stringByAppendingString:@".lock.db"];
-	NSString *traceFile = [self.dbPath stringByAppendingString:@".trace.db"];
+	NSString *lockFile	= [DBPath() stringByAppendingString:@".lock.db"];
+	NSString *traceFile = [DBPath() stringByAppendingString:@".trace.db"];
 	
 	for (NSString *file in @[lockFile, traceFile]) {
 		if ([[NSFileManager defaultManager] fileExistsAtPath:file]) {
@@ -129,15 +91,11 @@
 		NSLog(@"Starting MetabaseTask @ 0x%zx...", (size_t)self);
 		
 		self.task					= [[NSTask alloc] init];
-		self.task.launchPath		= self.javaPath;
-		self.task.environment		= @{@"MB_DB_FILE": self.dbPath,
+		self.task.launchPath		= JREPath();
+		self.task.environment		= @{@"MB_DB_FILE": DBPath(),
 										@"MB_JETTY_PORT": @(self.port)};
-		self.task.arguments			= @[@"-jar", self.jarPath];
-		
-		self.pipe					= [NSPipe pipe];
-		self.task.standardOutput	= self.pipe;
-		self.task.standardError		= self.pipe;
-		
+		self.task.arguments			= @[@"-jar", UberjarPath()];
+				
 		__weak MetabaseTask *weakSelf = self;
 		self.task.terminationHandler = ^(NSTask *task){
 			NSLog(@"\n\n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! Task terminated with exit code %d !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!", task.terminationStatus);
@@ -149,64 +107,20 @@
 				}
 			});
 		};
-				
-		self.readHandle = self.pipe.fileHandleForReading;
-		dispatch_async(dispatch_get_main_queue(), ^{
-			[self.readHandle readInBackgroundAndNotify];
-		});
-						
-		NSLog(@"%@ -jar %@", self.javaPath, self.jarPath);
+												
+		NSLog(@"Launching MetabaseTask\nMB_DB_FILE='%@' MB_JETTY_PORT=%lu %@ -jar %@", DBPath(), self.port, JREPath(), UberjarPath());
 		[self.task launch];
 	});
 }
 
 - (void)terminate {
-	if (!self.task) return; // already dead
-
-	NSLog(@"Killing MetabaseTask @ 0x%zx...", (size_t)self);
-	self.task = nil;
+	[super terminate];
 	_port = 0;
 }
 
 
 #pragma mark - Getters / Setters
 
-- (void)setTask:(NSTask *)task {
-	self.pipe = nil;
-	[_task terminate];
-	_task = task;
-}
-
-- (void)setPipe:(NSPipe *)pipe {
-	self.readHandle = nil;
-	_pipe = pipe;
-}
-
-- (void)setReadHandle:(NSFileHandle *)readHandle {
-	[_readHandle closeFile];
-	_readHandle = readHandle;
-}
-
-- (NSString *)javaPath {
-	return [[NSBundle mainBundle] pathForResource:@"java" ofType:nil inDirectory:@"jre/bin"];
-}
-
-- (NSString *)jarPath {
-	return [[NSBundle mainBundle] pathForResource:@"metabase" ofType:@"jar"];
-}
-
-- (NSString *)dbPath {
-	NSString *applicationSupportDir = [NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES)[0] stringByAppendingPathComponent:@"Metabase"];
-	if (![[NSFileManager defaultManager] fileExistsAtPath:applicationSupportDir]) {
-		NSError *error = nil;
-		[[NSFileManager defaultManager] createDirectoryAtPath:applicationSupportDir withIntermediateDirectories:YES attributes:nil error:&error];
-		if (error) {
-			NSLog(@"Error creating %@: %@", applicationSupportDir, error.localizedDescription);
-		}
-	}
-	return [applicationSupportDir stringByAppendingPathComponent:@"metabase.db"];
-}
-
 - (NSUInteger)port {
 	if (!_port) {
 		srand((unsigned)time(NULL));
diff --git a/OSX/Metabase/Backend/ResetPasswordTask.h b/OSX/Metabase/Backend/ResetPasswordTask.h
new file mode 100644
index 0000000000000000000000000000000000000000..79027b1c00313542afa9781de4c32df1a74e8383
--- /dev/null
+++ b/OSX/Metabase/Backend/ResetPasswordTask.h
@@ -0,0 +1,16 @@
+//
+//  ResetPasswordTask.h
+//  Metabase
+//
+//  Created by Cam Saul on 11/24/15.
+//  Copyright (c) 2015 Metabase. All rights reserved.
+//
+
+#import "JavaTask.h"
+
+@interface ResetPasswordTask : JavaTask
+
+/// blocks are ran on main thread <3
+- (void)resetPasswordForEmailAddress:(NSString *)emailAddress success:(void(^)(NSString *resetToken))successBlock error:(void(^)(NSString *errorMessage))errorBlock;
+
+@end
diff --git a/OSX/Metabase/Backend/ResetPasswordTask.m b/OSX/Metabase/Backend/ResetPasswordTask.m
new file mode 100644
index 0000000000000000000000000000000000000000..07857a869c13ad230dcb4739516cead174005f83
--- /dev/null
+++ b/OSX/Metabase/Backend/ResetPasswordTask.m
@@ -0,0 +1,75 @@
+//
+//  ResetPasswordTask.m
+//  Metabase
+//
+//  Created by Cam Saul on 11/24/15.
+//  Copyright (c) 2015 Metabase. All rights reserved.
+//
+
+#import "ResetPasswordTask.h"
+
+@interface ResetPasswordTask ()
+@property (nonatomic, readonly) NSString *resetPasswordJarPath;
+@property (copy) NSString *output;
+@end
+
+@implementation ResetPasswordTask
+
+
+#pragma mark - Local Methods
+
+- (void)resetPasswordForEmailAddress:(NSString *)emailAddress success:(void (^)(NSString *))successBlock error:(void (^)(NSString *))errorBlock {
+	dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
+		self.task				= [[NSTask alloc] init];
+		self.task.launchPath	= JREPath();
+		self.task.arguments		= @[@"-classpath", [NSString stringWithFormat:@"%@:%@", UberjarPath(), self.resetPasswordJarPath],
+									@"metabase.reset_password.core",
+									DBPath(), emailAddress];
+		
+		
+		__weak ResetPasswordTask *weakSelf = self;
+		self.task.terminationHandler = ^(NSTask *task) {
+			NSLog(@"ResetPasswordTask terminated with status: %d", task.terminationStatus);
+			dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5f * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
+				[weakSelf terminate];
+				
+				dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5f * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
+					dispatch_async(dispatch_get_main_queue(), ^{
+						if (!task.terminationStatus && weakSelf.output.length) {
+							successBlock(weakSelf.output);
+						} else {
+							errorBlock(weakSelf.output.length ? weakSelf.output : @"An unknown error has occured.");
+						}
+					});
+				});
+			});
+		};
+		
+		NSLog(@"Launching ResetPasswordTask\n%@ -classpath %@:%@ metabase.reset_password.core %@ %@", JREPath(), UberjarPath(), self.resetPasswordJarPath, DBPath(), emailAddress);
+		// delay lauch just a second to make sure pipe is all set up, etc.
+		dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5f * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
+			[self.task launch];
+		});
+	});
+}
+
+- (void)readHandleDidRead:(NSString *)message {
+	/// output comes back like "STATUS [[[message]]]"
+	NSRegularExpression *regex	= [NSRegularExpression regularExpressionWithPattern:@"^(?:(?:OK)||(?:FAIL))\\s+\\[\\[\\[(.+)\\]\\]\\]\\s*$" options:NSRegularExpressionAnchorsMatchLines|NSRegularExpressionAllowCommentsAndWhitespace error:NULL];
+	NSString *result			= [regex stringByReplacingMatchesInString:message options:0 range:NSMakeRange(0, message.length) withTemplate:@"$1"];
+	if (result) {
+		self.output = result;
+		NSLog(@"[PasswordResetTask] %@", self.output);
+	} else {
+		NSLog(@"[PasswordResetTask - Bad Output] %@", message);
+	}
+}
+
+
+#pragma mark - Getters / Setters
+
+- (NSString *)resetPasswordJarPath {
+	return [[NSBundle mainBundle] pathForResource:@"reset-password" ofType:@"jar"];
+}
+
+@end
diff --git a/OSX/Metabase/Backend/TaskHealthChecker.h b/OSX/Metabase/Backend/TaskHealthChecker.h
index 629375d13a45b39fddb90a412a6a35b64709329d..3d6f94e136b653415b24057ce6225dc6e29a2f35 100644
--- a/OSX/Metabase/Backend/TaskHealthChecker.h
+++ b/OSX/Metabase/Backend/TaskHealthChecker.h
@@ -10,6 +10,7 @@ static NSString * const MetabaseTaskBecameHealthyNotification	= @"MetabaseTaskBe
 static NSString * const MetabaseTaskBecameUnhealthyNotification = @"MetabaseTaskBecameUnhealthyNotification";
 static NSString * const MetabaseTaskTimedOutNotification		= @"MetabaseTaskTimedOutNotification";
 
+/// Manages the MetabaseTask (server) and restarts it if it gets unresponsive
 @interface TaskHealthChecker : NSObject
 
 @property () NSUInteger port;
diff --git a/OSX/Metabase/Backend/TaskHealthChecker.m b/OSX/Metabase/Backend/TaskHealthChecker.m
index e1de91bded3d642987562dc3641e690dc31f09fb..c6a2f65c8445a076abec56c7eb30a4d6b398d6d5 100644
--- a/OSX/Metabase/Backend/TaskHealthChecker.m
+++ b/OSX/Metabase/Backend/TaskHealthChecker.m
@@ -15,7 +15,7 @@ static const CGFloat HealthCheckIntervalSeconds = 1.2f;
 static const CGFloat HealthCheckRequestTimeout = 0.25f;
 
 /// After this many seconds of being unhealthy, consider the task timed out so it can be killed
-static const CFTimeInterval TimeoutIntervalSeconds = 10.0f;
+static const CFTimeInterval TimeoutIntervalSeconds = 15.0f;
 
 @interface TaskHealthChecker ()
 @property (strong, nonatomic) NSOperationQueue *healthCheckOperationQueue;
@@ -54,10 +54,8 @@ static const CFTimeInterval TimeoutIntervalSeconds = 10.0f;
 		self.healthCheckTimer = [NSTimer timerWithTimeInterval:HealthCheckIntervalSeconds target:self selector:@selector(checkHealth) userInfo:nil repeats:YES];
 		self.healthCheckTimer.tolerance = HealthCheckIntervalSeconds / 2.0f;
 		[[NSRunLoop mainRunLoop] addTimer:self.healthCheckTimer forMode:NSRunLoopCommonModes];
-
-//		self.healthCheckTimer = [NSTimer scheduledTimerWithTimeInterval:HealthCheckIntervalSeconds target:self selector:@selector(checkHealth) userInfo:nil repeats:YES];
-//		self.healthCheckTimer.tolerance = HealthCheckIntervalSeconds / 2.0f; // the timer doesn't need to fire exactly on the intervals, so give it a loose tolerance which will improve power savings, etc.
-	});}
+	});
+}
 
 
 - (void)stop {
diff --git a/OSX/Metabase/UI/MainViewController.m b/OSX/Metabase/UI/MainViewController.m
index 57908f4ae55c1e321f9a045169806f298774df97..a96e36e97cf35887d36f969bcfa9316bc578ad75 100644
--- a/OSX/Metabase/UI/MainViewController.m
+++ b/OSX/Metabase/UI/MainViewController.m
@@ -17,7 +17,14 @@
 #import "SettingsManager.h"
 #import "TaskHealthChecker.h"
 
-@interface MainViewController ()
+
+
+NSString *BaseURL() {
+	return SettingsManager.instance.baseURL.length ? SettingsManager.instance.baseURL : LocalHostBaseURL();
+}
+
+
+@interface MainViewController () <ResetPasswordWindowControllerDelegate>
 @property (weak) IBOutlet WebView *webView;
 @property (strong) IBOutlet NSView *titleBarView;
 
@@ -28,8 +35,6 @@
 
 @property (weak) LoadingView *loadingView;
 
-@property (strong, readonly) NSString *baseURL;
-
 @property (nonatomic) BOOL loading;
 
 @end
@@ -102,9 +107,9 @@
 #pragma mark - Local Methods
 
 - (void)loadMainPage {
-	NSLog(@"Connecting to Metabase instance at: %@", self.baseURL);
+	NSLog(@"Connecting to Metabase instance at: %@", BaseURL());
 	
-	NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:self.baseURL]];
+	NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:BaseURL()]];
 	request.cachePolicy = NSURLCacheStorageAllowedInMemoryOnly;
 	[self.webView.mainFrame loadRequest:request];
 }
@@ -127,7 +132,7 @@
 	if ([savePanel runModal] == NSFileHandlingPanelOKButton) {
 		NSLog(@"Will save CSV at: %@", savePanel.URL);
 		
-		NSURL *url = [NSURL URLWithString:apiURL relativeToURL:[NSURL URLWithString:self.baseURL]];
+		NSURL *url = [NSURL URLWithString:apiURL relativeToURL:[NSURL URLWithString:BaseURL()]];
 		NSURLRequest *csvRequest = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:10.0f];
 		[NSURLConnection sendAsynchronousRequest:csvRequest queue:[[NSOperationQueue alloc] init] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
 			NSError *writeError = nil;
@@ -161,10 +166,6 @@
 
 #pragma mark - Getters / Setters
 
-- (NSString *)baseURL {
-	return SettingsManager.instance.baseURL.length ? SettingsManager.instance.baseURL : LocalHostBaseURL();
-}
-
 - (void)setLoading:(BOOL)loading {
 	_loading = loading;
 
@@ -204,10 +205,22 @@
 
 - (IBAction)resetPassword:(id)sender {
 	ResetPasswordWindowController *resetPasswordWindowController = [[ResetPasswordWindowController alloc] init];
+	resetPasswordWindowController.delegate = self;
 	[[NSApplication sharedApplication] runModalForWindow:resetPasswordWindowController.window];
 }
 
 
+#pragma mark - ResetPasswordWindowControllerDelegate
+
+- (void)resetPasswordWindowController:(ResetPasswordWindowController *)resetPasswordWindowController didFinishWithResetToken:(NSString *)resetToken {
+	NSString *passwordResetURLString = [NSString stringWithFormat:@"%@/auth/reset_password/%@", BaseURL(), resetToken];
+	NSLog(@"Navigating to password reset URL: %@...", passwordResetURLString);
+	
+	NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:passwordResetURLString]];
+	[self.webView.mainFrame loadRequest:request];
+}
+
+
 #pragma mark - WebResourceLoadDelegate
 
 - (void)webView:(WebView *)sender resource:(id)identifier didFinishLoadingFromDataSource:(WebDataSource *)dataSource {
diff --git a/OSX/Metabase/UI/ResetPasswordWindowController.h b/OSX/Metabase/UI/ResetPasswordWindowController.h
index c045c5e4ad8d81f72ac48572083fcf3771d93f52..c6fd88a530d9538d73266a73c05e088ab0cba95b 100644
--- a/OSX/Metabase/UI/ResetPasswordWindowController.h
+++ b/OSX/Metabase/UI/ResetPasswordWindowController.h
@@ -8,6 +8,15 @@
 
 @import Cocoa;
 
-@interface ResetPasswordWindowController : NSWindowController
 
+@class ResetPasswordWindowController;
+
+
+@protocol ResetPasswordWindowControllerDelegate <NSObject>
+- (void)resetPasswordWindowController:(ResetPasswordWindowController *)resetPasswordWindowController didFinishWithResetToken:(NSString *)resetToken;
+@end
+
+
+@interface ResetPasswordWindowController : NSWindowController
+@property (weak) id<ResetPasswordWindowControllerDelegate> delegate;
 @end
diff --git a/OSX/Metabase/UI/ResetPasswordWindowController.m b/OSX/Metabase/UI/ResetPasswordWindowController.m
index 49b70068d75373ab3432a01c00f770f4bc81f3d9..e5e5ec12f796598a4632754350ffade8efaa3856 100644
--- a/OSX/Metabase/UI/ResetPasswordWindowController.m
+++ b/OSX/Metabase/UI/ResetPasswordWindowController.m
@@ -6,12 +6,16 @@
 //  Copyright (c) 2015 Metabase. All rights reserved.
 //
 
+#import <objc/runtime.h>
+
+#import "ResetPasswordTask.h"
 #import "ResetPasswordWindowController.h"
 
 @interface ResetPasswordWindowController () <NSTextFieldDelegate>
 @property (weak) IBOutlet NSButton *resetPasswordButton;
 @property (weak) IBOutlet NSTextField *emailAddressTextField;
 
+@property (nonatomic, strong) ResetPasswordTask *resetPasswordTask;
 @end
 
 
@@ -31,7 +35,29 @@
 #pragma mark - Actions
 
 - (IBAction)resetPasswordButtonPressed:(NSButton *)sender {
-	NSLog(@"RESET PASSWORD!");
+	self.resetPasswordButton.enabled = NO;
+	self.resetPasswordButton.title = @"One moment...";
+	self.emailAddressTextField.enabled = NO;
+	
+	self.resetPasswordTask = [[ResetPasswordTask alloc] init];
+	[self.resetPasswordTask resetPasswordForEmailAddress:self.emailAddressTextField.stringValue success:^(NSString *resetToken) {
+		self.emailAddressTextField.enabled = YES;
+		self.resetPasswordButton.title = @"Success!";
+		
+		NSLog(@"Got reset token: '%@'", resetToken);
+		[self.delegate resetPasswordWindowController:self didFinishWithResetToken:resetToken];
+		
+	} error:^(NSString *errorMessage) {
+		self.emailAddressTextField.enabled = YES;
+		self.resetPasswordButton.enabled = YES;
+		self.resetPasswordButton.title = @"Reset Password";
+		
+		[[NSAlert alertWithMessageText:@"Password Reset Failed" defaultButton:@"Done" alternateButton:nil otherButton:nil informativeTextWithFormat:@"%@", errorMessage] runModal];
+	}];
+}
+
+- (IBAction)emailAddressTextFieldDidReturn:(id)sender {
+	if (self.resetPasswordButton.isEnabled) [self resetPasswordButtonPressed:self.resetPasswordButton];
 }
 
 
diff --git a/OSX/Metabase/UI/ResetPasswordWindowController.xib b/OSX/Metabase/UI/ResetPasswordWindowController.xib
index cbb8e9e39fd9e33f84771505b31123375a2dea8f..c324adae9006a2fe41750ed41957de231885a1ce 100644
--- a/OSX/Metabase/UI/ResetPasswordWindowController.xib
+++ b/OSX/Metabase/UI/ResetPasswordWindowController.xib
@@ -8,30 +8,22 @@
             <connections>
                 <outlet property="emailAddressTextField" destination="SGZ-eW-nZQ" id="aMf-FE-ey1"/>
                 <outlet property="resetPasswordButton" destination="1wF-h5-c8o" id="N1J-e1-a4y"/>
+                <outlet property="window" destination="FU9-gs-izu" id="SSm-mw-LIb"/>
             </connections>
         </customObject>
         <customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
         <customObject id="-3" userLabel="Application"/>
-        <window title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" oneShot="NO" releasedWhenClosed="NO" showsToolbarButton="NO" animationBehavior="default" id="FU9-gs-izu">
+        <window title="Reset Password" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" oneShot="NO" releasedWhenClosed="NO" showsToolbarButton="NO" animationBehavior="default" id="FU9-gs-izu">
             <windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
             <windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
-            <rect key="contentRect" x="283" y="305" width="492" height="242"/>
+            <rect key="contentRect" x="283" y="305" width="492" height="229"/>
             <rect key="screenRect" x="0.0" y="0.0" width="2560" height="1418"/>
             <view key="contentView" id="3rs-Tz-giP">
-                <rect key="frame" x="0.0" y="0.0" width="492" height="242"/>
+                <rect key="frame" x="0.0" y="0.0" width="492" height="229"/>
                 <autoresizingMask key="autoresizingMask"/>
                 <subviews>
-                    <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="QaQ-Qx-Ypt">
-                        <rect key="frame" x="185" y="207" width="122" height="19"/>
-                        <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
-                        <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" enabled="NO" refusesFirstResponder="YES" sendsActionOnEndEditing="YES" title="Reset Password" id="Kcn-M1-bVY">
-                            <font key="font" metaFont="system" size="16"/>
-                            <color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
-                            <color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
-                        </textFieldCell>
-                    </textField>
                     <textField horizontalHuggingPriority="249" verticalHuggingPriority="249" horizontalCompressionResistancePriority="240" verticalCompressionResistancePriority="800" translatesAutoresizingMaskIntoConstraints="NO" id="oEx-8w-YG4">
-                        <rect key="frame" x="14" y="157" width="464" height="34"/>
+                        <rect key="frame" x="14" y="175" width="464" height="34"/>
                         <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
                         <constraints>
                             <constraint firstAttribute="width" relation="greaterThanOrEqual" constant="460" id="qwI-wW-wTd"/>
@@ -43,7 +35,7 @@
                         </textFieldCell>
                     </textField>
                     <button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="1wF-h5-c8o">
-                        <rect key="frame" x="178" y="9" width="136" height="32"/>
+                        <rect key="frame" x="178" y="13" width="136" height="32"/>
                         <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
                         <buttonCell key="cell" type="push" title="Reset Password" bezelStyle="rounded" alignment="center" enabled="NO" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="YII-IA-Kyg">
                             <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
@@ -54,7 +46,7 @@
                         </connections>
                     </button>
                     <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="8ft-6m-5T0">
-                        <rect key="frame" x="200" y="108" width="92" height="17"/>
+                        <rect key="frame" x="200" y="126" width="92" height="17"/>
                         <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
                         <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" enabled="NO" refusesFirstResponder="YES" sendsActionOnEndEditing="YES" title="Email Address" id="ETj-8f-dsZ">
                             <font key="font" metaFont="system"/>
@@ -63,17 +55,18 @@
                         </textFieldCell>
                     </textField>
                     <textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="SGZ-eW-nZQ">
-                        <rect key="frame" x="118" y="78" width="256" height="22"/>
+                        <rect key="frame" x="118" y="96" width="256" height="22"/>
                         <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
                         <constraints>
                             <constraint firstAttribute="width" constant="256" id="8Fr-91-TXf"/>
                         </constraints>
-                        <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" state="on" borderStyle="bezel" placeholderString="cam@toucans4lyfe.org" drawsBackground="YES" id="mP8-DI-PfW">
+                        <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" state="on" borderStyle="bezel" alignment="center" placeholderString="cam@toucans4lyfe.org" drawsBackground="YES" id="mP8-DI-PfW">
                             <font key="font" metaFont="system"/>
                             <color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
                             <color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
                         </textFieldCell>
                         <connections>
+                            <action selector="emailAddressTextFieldDidReturn:" target="-2" id="JLo-r8-R9z"/>
                             <outlet property="delegate" destination="-2" id="h0P-Zb-hJJ"/>
                         </connections>
                     </textField>
@@ -85,13 +78,11 @@
                     <constraint firstItem="1wF-h5-c8o" firstAttribute="top" relation="greaterThanOrEqual" secondItem="SGZ-eW-nZQ" secondAttribute="bottom" constant="32" id="CM1-Nh-mnk"/>
                     <constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="SGZ-eW-nZQ" secondAttribute="trailing" constant="16" id="NHO-1C-XOJ"/>
                     <constraint firstItem="SGZ-eW-nZQ" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="3rs-Tz-giP" secondAttribute="leading" constant="16" id="On6-0w-5PF"/>
-                    <constraint firstAttribute="bottom" secondItem="1wF-h5-c8o" secondAttribute="bottom" constant="16" id="VRO-Qm-4vb"/>
-                    <constraint firstItem="oEx-8w-YG4" firstAttribute="top" secondItem="QaQ-Qx-Ypt" secondAttribute="bottom" constant="16" id="bMB-cF-PJe"/>
+                    <constraint firstAttribute="bottom" secondItem="1wF-h5-c8o" secondAttribute="bottom" constant="20" id="VRO-Qm-4vb"/>
+                    <constraint firstItem="oEx-8w-YG4" firstAttribute="top" secondItem="3rs-Tz-giP" secondAttribute="top" constant="20" id="bzN-6L-86m"/>
                     <constraint firstItem="SGZ-eW-nZQ" firstAttribute="top" secondItem="8ft-6m-5T0" secondAttribute="bottom" constant="8" id="e7T-c5-OYs"/>
-                    <constraint firstAttribute="centerX" secondItem="QaQ-Qx-Ypt" secondAttribute="centerX" id="kFE-8l-poc"/>
-                    <constraint firstAttribute="centerY" secondItem="SGZ-eW-nZQ" secondAttribute="centerY" constant="-32" id="mTu-yO-Smv"/>
+                    <constraint firstAttribute="centerY" secondItem="SGZ-eW-nZQ" secondAttribute="centerY" constant="-8" id="mTu-yO-Smv"/>
                     <constraint firstAttribute="trailing" secondItem="oEx-8w-YG4" secondAttribute="trailing" constant="16" id="shZ-a7-r8y"/>
-                    <constraint firstItem="QaQ-Qx-Ypt" firstAttribute="top" secondItem="3rs-Tz-giP" secondAttribute="top" constant="16" id="uRv-1M-0Si"/>
                     <constraint firstAttribute="centerX" secondItem="1wF-h5-c8o" secondAttribute="centerX" id="vH7-Xk-LcO"/>
                     <constraint firstAttribute="centerX" secondItem="8ft-6m-5T0" secondAttribute="centerX" id="vh8-pN-ohL"/>
                 </constraints>
diff --git a/bin/osx-setup b/bin/osx-setup
index 3b433bca7125ba9ccf039f19c41cbefabb9bc722..66e1f8e6b4c8bcd467627d01c1a16800afefcdb3 100755
--- a/bin/osx-setup
+++ b/bin/osx-setup
@@ -18,6 +18,9 @@ use constant JRE_HOME => trim(`/usr/libexec/java_home`) . '/jre';
 use constant UBERJAR_SRC => getcwd() . '/target/uberjar/metabase.jar';
 use constant UBERJAR_DEST => getcwd() . '/OSX/Resources/metabase.jar';
 
+use constant RESET_PW_SRC => getcwd() . '/reset-password-artifacts/reset-password/reset-password.jar';
+use constant RESET_PW_DEST => getcwd() . '/OSX/Resources/reset-password.jar';
+
 use constant ENABLE_JAR_PACKING => 0;
 use constant ENABLE_JAR_OPTIONAL_FILE_STRIPPING => 0;
 
@@ -76,11 +79,17 @@ remove_jre_optional_files() if ENABLE_JAR_OPTIONAL_FILE_STRIPPING;
 # Pack JARs in JRE if applicable
 pack_jar() if ENABLE_JAR_PACKING;
 
-# Build uberjar if needed
+# Build jars if needed
 (system('./bin/build') or die $!) unless -f UBERJAR_SRC;
+(system('lein', 'with-profile', 'reset-password', 'jar') or die $!) unless -f RESET_PW_SRC;
 
-# Copy uberjar over
-announce 'Copying uberjar ' . UBERJAR_SRC . ' -> ' . UBERJAR_DEST;
-copy(get_file_or_die(UBERJAR_SRC), UBERJAR_DEST) or die $!;
+# Copy jars over
+sub copy_jar {
+  my ($src, $dest) = @_;
+  announce 'Copying jar ' . $src . ' -> ' . $dest;
+  copy(get_file_or_die($src), $dest) or die $!;
+}
+copy_jar(UBERJAR_SRC, UBERJAR_DEST);
+copy_jar(RESET_PW_SRC, RESET_PW_DEST);
 
 print_giant_success_banner();
diff --git a/project.clj b/project.clj
index d296e19aec52706f420d29c0f4cfe08c2b178b3e..6f1f7e4bfc3e448608f1e25094ef6400e346d0b3 100644
--- a/project.clj
+++ b/project.clj
@@ -100,4 +100,14 @@
                                                       [incanter/incanter-core "1.9.0"]] ; Satistical functions like normal distibutions}})
                                        :source-paths ["sample_dataset"]
                                        :global-vars {*warn-on-reflection* false}
-                                       :main ^:skip-aot metabase.sample-dataset.generate}})
+                                       :main ^:skip-aot metabase.sample-dataset.generate}
+             ;; Run reset password from source: lein with-profile reset-password run /path/to/metabase.db email@address.com
+             ;; Create the reset password JAR:  lein with-profile reset-password jar
+             ;;                                   -> ./reset-password-artifacts/reset-password/reset-password.jar
+             ;; Run the reset password JAR:     java -classpath /path/to/metabase-uberjar.jar:/path/to/reset-password.jar \
+             ;;                                   metabase.reset_password.core /path/to/metabase.db email@address.com
+             :reset-password {:source-paths ["reset_password"]
+                              :global-vars {*warn-on-reflection* false}
+                              :main metabase.reset-password.core
+                              :jar-name "reset-password.jar"
+                              :target-path "reset-password-artifacts/%s"}}) ; different than ./target because otherwise lein uberjar will delete our artifacts and vice versa
diff --git a/reset_password/metabase/reset_password/core.clj b/reset_password/metabase/reset_password/core.clj
new file mode 100644
index 0000000000000000000000000000000000000000..d931151ce205557c7b1c366e60fb99b6dbdee022
--- /dev/null
+++ b/reset_password/metabase/reset_password/core.clj
@@ -0,0 +1,23 @@
+(ns metabase.reset-password.core
+  (:gen-class)
+  (:require [clojure.java.jdbc :as jdbc]))
+
+(defn- db-filepath->connection-details [filepath]
+  {:classname   "org.h2.Driver"
+   :subprotocol "h2"
+   :subname     (str "file:" filepath ";MV_STORE=FALSE;AUTO_SERVER=TRUE;DB_CLOSE_DELAY=-1")})
+
+(defn- set-reset-token! [dbpath email-address reset-token]
+  (let [rows-affected (jdbc/execute! (db-filepath->connection-details dbpath) ["UPDATE CORE_USER SET RESET_TOKEN = ?, RESET_TRIGGERED = ? WHERE EMAIL = ?;" reset-token (System/currentTimeMillis) email-address])]
+    (when (not= rows-affected [1])
+      (throw (Exception. (format "No user found with email address '%s'. Please check the spelling and try again." email-address))))))
+
+(defn -main
+  [dbpath email-address]
+  (try
+    (let [reset-token (str (java.util.UUID/randomUUID))]
+      (set-reset-token! dbpath email-address reset-token)
+      (println (format "OK [[[%s]]]" reset-token)))
+    (catch Throwable e
+      (println (format "FAIL [[[%s]]]" (.getMessage e)))
+      (System/exit -1))))
diff --git a/src/metabase/db.clj b/src/metabase/db.clj
index 1b322ad8c53ab9fa51ab2e262a6c872e7f9ae8b4..0beed2168090bf7a1218f52cbe15bc774938647f 100644
--- a/src/metabase/db.clj
+++ b/src/metabase/db.clj
@@ -81,9 +81,9 @@
   {:pre [(map? db-details)]}
   ;; TODO: it's probably a good idea to put some more validation here and be really strict about what's in `db-details`
   (case (:type db-details)
-    :h2       (kdb/h2 (assoc db-details :naming {:keys   s/lower-case
-                                                 :fields s/upper-case}))
-    :mysql    (kdb/mysql (assoc db-details :db (:dbname db-details)))
+    :h2       (kdb/h2       (assoc db-details :naming {:keys   s/lower-case
+                                                       :fields s/upper-case}))
+    :mysql    (kdb/mysql    (assoc db-details :db (:dbname db-details)))
     :postgres (kdb/postgres (assoc db-details :db (:dbname db-details)))))