AuthDemoApp.java

/***************************************************************************
   Copyright 2013 Emily Estes

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
***************************************************************************/
package net.metanotion.web.examples;


import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
import javax.sql.DataSource;

import net.metanotion.authident.UserToken;
import net.metanotion.email.MailMergeDbQueue;
import net.metanotion.formsauth.AuthAPI;
import net.metanotion.formsauth.AuthFactory;
import net.metanotion.formsauth.AuthFsm;
import net.metanotion.formsauth.AuthMailer;
import net.metanotion.formsauth.AuthStates;
import net.metanotion.formsauth.FormsAuth;
import net.metanotion.formsauth.FormsAuthAPI;
import net.metanotion.io.ClassLoaderFileSystem;
import net.metanotion.io.File;
import net.metanotion.io.FileSystem;
import net.metanotion.simpletemplate.MountPointResources;
import net.metanotion.simpletemplate.StaticResources;
import net.metanotion.simpletemplate.ResourceDispatcher;
import net.metanotion.simpletemplate.ResourceFactory;
import net.metanotion.simpletemplate.TemplateResources;
import net.metanotion.scripting.ObjectServer;
import net.metanotion.scripting.Scriptable;
import net.metanotion.scripting.ScriptingObjectMagic;
import net.metanotion.sql.DbUtil;
import net.metanotion.sqlauthident.SQLRealm;

import net.metanotion.util.ArgsToStruct;
import net.metanotion.util.Description;
import net.metanotion.util.Extends;
import net.metanotion.util.Service;
import net.metanotion.util.StateMachine;
import net.metanotion.util.Unknown;
import net.metanotion.util.UnknownMagic;

import net.metanotion.web.RequestObject;
import net.metanotion.web.SessionFactory;
import net.metanotion.web.concrete.BasicAuth;
import net.metanotion.web.concrete.JsonUtil;
import net.metanotion.web.concrete.URIPrefixDispatcher;
import net.metanotion.web.servlets.ServerUtil;

/** <p>This app server demonstrates the entire http authentication suite. The application class also contains the main
method to start the web server. AuthDemoApp implements {@link net.metanotion.web.SessionFactory} since it's just going
to go with the default session handling mechanism. However, the authentication state of sessions can be modeled as a
two-state finite state machine with an "anonymous" state and an "authenticated" state. Rather than implement this
boiler plate, you can implement {@link net.metanotion.formsauth.AuthStates} and then create an instance of
{@link net.metanotion.formsauth.AuthFsm} and delegate your session factory
{@link net.metanotion.web.SessionFactory#newSession} calls to the AuthFsm instance which uses the much more descriptive
methods you've implemented: {@link net.metanotion.formsauth.AuthStates#newSession},
{@link net.metanotion.formsauth.AuthStates#login}, and {@link net.metanotion.formsauth.AuthStates#logout}. We then
create two session classes to represent the states: {@link net.metanotion.web.examples.AuthDemoApp.AnonymousSession}
and {@link net.metanotion.web.examples.AuthDemoApp.AuthenticatedSession}.</p>

<p>Authentication credentials and identity services are abstractly described by the {@link net.metanotion.authident}
package. The framework provides a concrete implementation of these interfaces in the
{@link net.metanotion.sqlauthident} package. This app uses that concrete implementation and therefore requires a
relational database configured with the schema for that package. To simplify usage of this application, the command line
options "-createDb" will ask it to create a blank database properly configured. and the "-dropFirst" command line switch
will attempt to drop a pre-existing database with the same name first if you wish.</p>

<p>This app demonstrates the four different authentication "user interfaces" implemented by the
{@link net.metanotion.formsauth} package: {@link net.metanotion.web.concrete.BasicAuth} which implements HTTP Basic
authentication. {@link net.metanotion.formsauth.AuthFactory} which implements a RESTful API appropriate for a
"single-page" JavaScript app. This API is utilized on the client side by the auth.js library (in the source
distribution see <code>/src/main/js/auth.js</code>). Finally, {@link net.metanotion.formsauth.FormsAuth}
implements a "classic" authentication and account management interface that involves forms and page navigation.</p>

<p>This application makes a lot of implicit use of the template engine and scripting language in the
{@link net.metanotion.scripting} and {@link net.metanotion.simpletemplate} packages.</p>

<p>Finally, running this application without any command line options will display a usage message with all the
available command line options.</p>
*/
public final class AuthDemoApp implements SessionFactory<RequestObject>, AuthStates<AuthDemoApp, AuthDemoApp.Session> {
	private static final String AUTH_PREFIX = "/auth/";
	private static final String AUTHUI_PREFIX = "/account/";
	private static final String BASICPROTECTED_PREFIX = "/basicauthzone";
	private static final String HTTP_REALM = "test.realm";
	private static final String APP_PATH = "net/metanotion/web/examples/authdemo/";

	public static final class Config {
		@Description("The port to listen on. Defaults to 8080.")
		public int port = 8080;
		@Description("If this option is set, create the database first.")
		public boolean createDb;
		@Description("If this option is set along with -create, attempt to drop the database if it already exists "
			+ "before creating the database.")
		public boolean dropFirst;
		@Description("The JDBC connection URL for the database server to use.")
		public String url;
		@Description("The username for the database server.")
		public String user;
		@Description("The password for the database server.")
		public String pass;
		@Description("The name of the database.")
		public String database;
	}

	/** This is main function that starts up the authentication demonstration example app. The command line switches are
	described by the instance variables of the {@link net.metanotion.web.examples.AuthDemoApp.Config} class. On startup
	this application will connect to a database server, and optionally create the required database schema. If the
	application is started with no arguments a help message will be displayed explaining the command line switches and
	the application will terminate.
		@param args The command line arguments for the application.
	*/
	public static void main(final String[] args) {
		try {
			/** Parse the command line options into an instance of the Config class. */
			final Config config = (new ArgsToStruct<>(Config.class)).getInstance(args);

			/** If the createDb switch is set, create the database and populate the schema. */
			if(config.createDb) {
				try (final Connection conn = DriverManager.getConnection(config.url, config.user, config.pass)) {
					DbUtil.createDatabase(conn, config.database, config.user, config.dropFirst);
				}
				try (final Connection conn =
						DriverManager.getConnection(config.url + config.database, config.user, config.pass)) {
					DbUtil.runSchema(conn, MailMergeDbQueue.schemaFactory());
					DbUtil.runSchema(conn, SQLRealm.schemaFactory());
				}
			}
			/** Set up the database connection pool. */
			final DataSource ds = DbUtil.startDBConnectionPool(config.url + config.database, config.user, config.pass);
			/** Create the auth/ident realm instance that will store identities and credentials. */
			final SQLRealm r = new SQLRealm(ds);
			/** Create a mail merge processing queue backed by the default email templates from the forms authentication
			library. The account creation/password reset/email validation messages will be processed by this
			instance. */
			final MailMergeDbQueue mailMerge = new MailMergeDbQueue(ds, AuthFactory.DEFAULT_EMAIL_TEMPLATES);

			/** To demonstrate the <code>auth.js</code> authentication user interface library for "single-page"
			JavaScript style applications we're going to pull in the framework's "standard" JS library. The
			{@link net.metanotion.web.concrete.JsonUtil#getJavaScript} method provides a file system instance to give
			access to these files that are packed in the .jar. We "merge" this with our application specific JavaScript
			through use of the fallback behavior of the {@link net.metanotion.simpletemplate.StaticResources} class and
			we put these files in the <code>/js</code> folder with the
			{@link net.metanotion.simpletemplate.PrefixedResources} class. */
			final FileSystem<File> jsContent = JsonUtil.getJavaScript();

			/** Set up all the other file systems we need for the HTML and JS for the web application. These resources
			are stored in the same classpath that contains the demo application. */
			final ClassLoaderFileSystem staticContent =
				new ClassLoaderFileSystem(AuthDemoApp.class.getClassLoader(), APP_PATH + "files");
			final ClassLoaderFileSystem dynamicContent =
				new ClassLoaderFileSystem(AuthDemoApp.class.getClassLoader(), APP_PATH + "templates");
			final ResourceFactory resources = new TemplateResources(dynamicContent,
				new StaticResources(staticContent,
					new MountPointResources(new StaticResources(jsContent), "/js")));

			/** Create an instance of the AuthMailer interface, with the base URL's for acount validation and password
			resets using a thread pool and the db queue mail merge instance. */
			final AuthMailer.Urls urls = FormsAuth.standardUrls("http://localhost:" + Integer.toString(config.port));
			final ExecutorService es = Executors.newCachedThreadPool();
			final AuthMailer mailer = AuthFactory.mailMerge(es, mailMerge, "authDemoApp@localhost", urls);

			/** Create the authentication API instance, the forms auth instance, and the basic auth instance. */
			final AuthAPI authAPI = AuthFactory.instance(ds, r, mailer, AUTH_PREFIX);
			final FormsAuth formsAuth = new FormsAuth(AUTHUI_PREFIX, authAPI);
			final BasicAuth ba = new BasicAuth(r, HTTP_REALM);

			/** This class is a scriptable object used by the demo app. The sole purpose of this class is display all
			the queued emails queed by the MailMergeDbQueue class. Obviously, this is a horrible thing to do from a
			security standpoint, but this application is a demonstration, and rather than requiring SMTP to be setup,
			the emails will be displayed on the front page, so that you can see how everything hangs together and see
			emails added to the queue as accounts are created, passwords reset, and so on. */
			final AccountEmailLister emails = new AccountEmailLister(ds);

			/** Create the dispatcher for the application. Each prefix in this app is used by a different authentication
			component so you can see each component in action. */
			final URIPrefixDispatcher disp = new URIPrefixDispatcher()
				.addDispatcher(AUTH_PREFIX, AuthFactory.dispatcher())
				.addDispatcher(BASICPROTECTED_PREFIX,
					BasicAuth.dispatcher(new ResourceDispatcher(BASICPROTECTED_PREFIX), true))
				.addDispatcher(AUTHUI_PREFIX, FormsAuth.dispatcher())
				.addDispatcher("", new ResourceDispatcher());

			/** Create an instance of the web application's session factory, basically this is the web application. */
			final SessionFactory<RequestObject> app = new AuthDemoApp(authAPI, ba, formsAuth, resources, emails);

			/** Start the HTTP server listening on the port specified by the configuration(which defaults to 8080) */
			ServerUtil.launchJettyServer(config.port, disp, app);
		} catch (final Exception e) {
			e.printStackTrace();
		}
	}

	/** This class is a scripting object used by the web application to display all the eamils queued for sending. */
	public static final class AccountEmailLister {
		private final DataSource ds;

		/** Create an instance of the email lister object.
			@param ds The database connection pool where the emails are queued.
		*/
		public AccountEmailLister(final DataSource ds) { this.ds = ds; }

		/** Generate a listing of all the emails in the queue as an HTML table.
			@return an string containing an HTML table of all the queued emails.
			@throws SQLException if there is an error with the database.
		*/
		public String list() throws SQLException {
			final StringBuilder sb = new StringBuilder();
			sb.append("<table>");
			try (final Connection conn = ds.getConnection()) {
				try (final PreparedStatement stmt =
						conn.prepareStatement("SELECT Sender, EmailAddress, Subject, MessageBody"
							+ " FROM MailQueue ORDER BY MailId ASC")) {
					try (final ResultSet rs = stmt.executeQuery()) {
						while(rs.next()) {
							sb.append("<tr><td>");
							sb.append(rs.getString("Sender"));
							sb.append("</td><td>");
							sb.append(rs.getString("EmailAddress"));
							sb.append("</td><td>");
							sb.append(rs.getString("Subject"));
							sb.append("</td><td>");
							sb.append(rs.getString("MessageBody"));
							sb.append("</td></tr>");
						}
					}
				}
			}
			sb.append("</table>");
			return sb.toString();
		}
	}

	/** The session interface for the classes extend the default session interface in the web framework because the
	BasicAuth class needs a {@link net.metanotion.util.StateMachine} instance. If you are NOT using HTTP basic
	authentication in your app, the normal session interface is sufficient. (For that matter, you don't have to use the
	framework's session interface either if it doesn't serve your needs.) */
	public interface Session extends net.metanotion.web.Session<AuthDemoApp> {
		/** Retrieve the state machine associated with this session.
			@return The state machine.
		*/
		public StateMachine sessionMachine();
	}

	@Service public final ResourceFactory resources;
	@Service public final BasicAuth ba;
	@Service public final AuthAPI authAPI;
	@Service public final FormsAuthAPI formsAPI;
	@Scriptable("Emails") public final AccountEmailLister emailLister;

	private final FormsAuth formsAuth;
	private final AuthFsm<AuthDemoApp, Session> factory = new AuthFsm<>(this);

	/** Create the authentication demonstration application.
		@param authAPI The RESTful authentication API object which will be exposed as a component service provided by
			the application.
		@param ba The HTTP Basic Authentication object which will be exposed as a component service provided by the
			application.
		@param formsAuth The server side authentication user interface object which is used to create the component
			service provided by the application and to create scriptable objects for the individual sessions for the
			user interface.
		@param resources The resource factory for the rest of the URI's provided by the application.
		@param emailLister Scripting object for listing the queued emails.
	*/
	public AuthDemoApp(final AuthAPI authAPI,
			final BasicAuth ba,
			final FormsAuth formsAuth,
			final ResourceFactory resources,
			final AccountEmailLister emailLister) {
		this.authAPI = authAPI;
		this.ba = ba;
		this.formsAuth = formsAuth;
		this.formsAPI = formsAuth.instance();
		this.resources = resources;
		this.emailLister = emailLister;
	}

	// SessionFactory
	@Override public Unknown newSession(final RequestObject ro) { return factory.newSession(this); }

	// AuthStates
	@Override public Session login(final UserToken user, final Session currentState) {
		return new AuthenticatedSession(currentState.appInstance(), currentState.sessionMachine(), user);
	}

	@Override public Session logout(final Session currentState) {
		return new AnonymousSession(currentState.appInstance(), currentState.sessionMachine());
	}

	@Override public Session newSession(final AuthDemoApp app, final StateMachine sm) {
		return new AnonymousSession(this, sm);
	}

	/** The non-authenticated session state class. Sessions that are not logged in will be backed by an instance of
	this class. */
	public static final class AnonymousSession implements Session {
		private static final UnknownMagic u = new UnknownMagic(AnonymousSession.class);
		private static final ScriptingObjectMagic som = new ScriptingObjectMagic(AnonymousSession.class);
		/** The services that are identical across session types are annotated in the application object, so this
		instance variable denotes that relationship and links this session to the application so that
		{@link net.metanotion.util.UnknownMagic} and {@link net.metanotion.scripting.ScriptingObjectMagic} extend their
		search to the application object. */
		@Extends public final AuthDemoApp app;
		/** This is the state machine instance associated with this session, code in {@link net.metanotion.formsauth}
		package looks for this service to send the authentication event objects to your session. */
		@Service public final StateMachine sm;
		/** The template engine looks for this service so it can bind scripting expressions to the appropriate
		scriptable objects exposed by your application. */
		@Service public final ObjectServer os;
		/** This scriptable object is provided by the {@link net.metanotion.formsauth.FormsAuth} class so it can
		expose objects to the authentication UI templates. */
		@Scriptable("AuthUI") public Unknown formsAuthUI;

		@Service public final UserToken ut = null;

		/** Create a new "anonymous" session state to handle requests with no authenticated user.
			@param app The application object this session is associated with.
			@param sm The state machine that handles events and state transitions for this session.
		*/
		public AnonymousSession(final AuthDemoApp app, final StateMachine sm) {
			this.app = app;
			this.sm = sm;
			this.os = som.instance(this);
			this.formsAuthUI = app.formsAuth.newSession(this);
		}

		// Session
		@Override public StateMachine sessionMachine() { return this.sm; }
		// net.metanotion.web.Session
		@Override public AuthDemoApp appInstance() { return this.app; }
		// Unknown
		@Override public <I> I lookupInterface(final Class<I> theInterface) {
			return u.lookupInterface(theInterface, this);
		}
	}

	/** This is the session object we use for user sessions. */
	public static final class AuthenticatedSession implements Session {
		private static final UnknownMagic u = new UnknownMagic(AuthenticatedSession.class);
		private static final ScriptingObjectMagic som = new ScriptingObjectMagic(AuthenticatedSession.class);

		/** The services that are identical across session types are annotated in the application object, so this
		instance variable denotes that relationship and links this session to the application so that
		{@link net.metanotion.util.UnknownMagic} and {@link net.metanotion.scripting.ScriptingObjectMagic} extend their
		search to the application object. */
		@Extends public final AuthDemoApp app;
		/** This is the state machine instance associated with this session, code in {@link net.metanotion.formsauth}
		package looks for this service to send the authentication event objects to your session. */
		@Service public final StateMachine sm;
		/** The template engine looks for this service so it can bind scripting expressions to the appropriate
		scriptable objects exposed by your application. */
		@Service public final ObjectServer os;
		/** This scriptable object is provided by the {@link net.metanotion.formsauth.FormsAuth} class so it can
		expose objects to the authentication UI templates. */
		@Scriptable("AuthUI") public Unknown formsAuthUI;

		/** This is the UserToken instance provided by the authentication library during the session state change. This
		object represents the currently authenticated user for this session. */
		@Service public final UserToken ut;

		/** Create a new authenticated session state.
			@param app The application object this session is associated with.
			@param sm The state machine that handles events and state transitions for this session.
			@param ut The authenticated user associated with this session.
		*/
		public AuthenticatedSession(final AuthDemoApp app, final StateMachine sm, final UserToken ut) {
			this.app = app;
			this.sm = sm;
			this.ut = ut;
			this.os = som.instance(this);
			this.formsAuthUI = app.formsAuth.newSession(this);
		}

		// Session
		@Override public StateMachine sessionMachine() { return this.sm; }
		// net.metanotion.web.Session
		@Override public AuthDemoApp appInstance() { return this.app; }
		// Unknown
		@Override public <I> I lookupInterface(final Class<I> theInterface) {
			return u.lookupInterface(theInterface, this);
		}
	}
}