Simple app with authorization and session management

Introduction

In this extended example you will see how to create a simple authorization app with log in/sign up scenarios and session management.

DataKernel doesn’t include built-in authorization modules or solutions, as this process may significantly vary depending on project’s business logic. This example represents a simple “best practice” which you can extend and modify depending on your needs. You can find full example sources on GitHub.

In the example we will consider only the server which was created using DataKernel HttpServerLauncher and AsyncServlet. This approach allows to create embedded application server in about 100 lines of code with no additional XML configurations or third-party dependencies.

Creating Launcher

Let’s create an AuthLauncher, which is the main part of the application as it manages application lifecycle, routing and authorization processes. We will use DataKernel HttpServerLauncher and extend it:

public final class AuthLauncher extends HttpServerLauncher {
	public static final String SESSION_ID = "SESSION_ID";

	@Provides
	AuthService loginService() {
		return new AuthServiceImpl();
	}

	@Provides
	Executor executor() {
		return newSingleThreadExecutor();
	}

	@Provides
	private StaticLoader staticLoader(Executor executor) {
		return StaticLoader.ofClassPath(executor, "site/");
	}

	@Provides
	SessionStore<String> sessionStore() {
		return new SessionStoreInMemory<>();
	}

	@Provides
	AsyncServlet servlet(SessionStore<String> sessionStore,
			@Named("public") AsyncServlet publicServlet, @Named("private") AsyncServlet privateServlet) {
		return SessionServlet.create(sessionStore, SESSION_ID, publicServlet, privateServlet);
	}

}

We provided the following objects:

  • AuthService - authorization and register logic
  • Executor - needed for StaticLoader
  • StaticLoader - loads static content from /root directory
  • SessionStore - handy storage for information about sessions
  • AsyncServlet servlet - the main servlet which combines public and private servlets (for authorized and unauthorized sessions). As you can see, due to DI, this servlet only requires two servlets without their own dependencies

Now lets provide the public and private servlets.

  • AsyncServlet publicServlet - manages unauthorized sessions:
@Provides
@Named("public")
AsyncServlet publicServlet(AuthService authService, SessionStore<String> store, StaticLoader staticLoader) {
	return RoutingServlet.create()
			//[START REGION_3]
			.map("/", request -> Promise.of(HttpResponse.redirect302("/login")))
			//[END REGION_3]
			.map(GET, "/signup", StaticServlet.create(staticLoader, "signup.html"))
			.map(GET, "/login", StaticServlet.create(staticLoader, "login.html"))
			//[START REGION_4]
			.map(POST, "/login", loadBody()
					.serveFirstSuccessful(
							request -> {
								Map<String, String> params = request.getPostParameters();
								String username = params.get("username");
								String password = params.get("password");
								if (authService.authorize(username, password)) {
									String sessionId = UUID.randomUUID().toString();

									store.save(sessionId, "My object saved in session");
									return Promise.of(HttpResponse.redirect302("/members")
											.withCookie(HttpCookie.of(SESSION_ID, sessionId)));
								}
								return AsyncServlet.NEXT;
							},
							StaticServlet.create(staticLoader, "errorPage.html")))
			//[END REGION_4]
			.map(POST, "/signup", loadBody()
					.serve(request -> {
						Map<String, String> params = request.getPostParameters();
						String username = params.get("username");
						String password = params.get("password");

						if (username != null && password != null) {
							authService.register(username, password);
						}
						return Promise.of(HttpResponse.redirect302("/login"));
					}));
}

Let’s take a closer look at how we set up routing for servlets. DataKernel approach resembles Express. For example, here’s the request to the homepage for unauthorized users:

.map("/", request -> Promise.of(HttpResponse.redirect302("/login")))

Method map(@Nullable HttpMethod method, String path, AsyncServlet servlet) adds the route to the RoutingServlet:

  • method (optional) is one of the HTTP methods (GET, POST etc)
  • path is the path on the server
  • servlet defines the logic of request processing. If you need to get some data from the request while processing you can use:
    • request.getPathParameter(String key)/request.getQueryParameter(String key) (see example of query parameter usage) to provide the key of the needed parameter and receive back a corresponding String
    • request.getPostParameters() to get a Map of all request parameters

GET requests with paths “/login” and “/signup” upload the needed HTML pages. POST requests with paths “/login” and “/signup” take care of log in and sign up logic respectively:

.map(POST, "/login", loadBody()
		.serveFirstSuccessful(
				request -> {
					Map<String, String> params = request.getPostParameters();
					String username = params.get("username");
					String password = params.get("password");
					if (authService.authorize(username, password)) {
						String sessionId = UUID.randomUUID().toString();

						store.save(sessionId, "My object saved in session");
						return Promise.of(HttpResponse.redirect302("/members")
								.withCookie(HttpCookie.of(SESSION_ID, sessionId)));
					}
					return AsyncServlet.NEXT;
				},
				StaticServlet.create(staticLoader, "errorPage.html")))

Pay attention at POST “/login” rout. serveFirstSuccessful takes two servlets and waits until one of them finishes processing successfully. So if authorization fails, a Promise of null will be returned (AsyncServlet.NEXT), which means fail. In this case, simple StaticServlet will be created to load the errorPage. Successful log in will generate a session id for user, save string "My saved object in session" to browser cookies and also redirect user to “/members”.

Now let’s get to the next servlet which handles authorized sessions.

  • AsyncServlet privateServlet - manages authorized sessions:
@Provides
@Named("private")
AsyncServlet privateServlet(StaticLoader staticLoader) {
	return RoutingServlet.create()
			//[START REGION_6]
			.map("/", request -> Promise.of(HttpResponse.redirect302("/members")))
			//[END REGION_6]
			//[START REGION_7]
			.map("/members/*", RoutingServlet.create()
					.map(GET, "/", StaticServlet.create(staticLoader, "index.html"))
					//[START REGION_8]
					.map(GET, "/cookie", request -> Promise.of(
							HttpResponse.ok200()
									.withBody(wrapUtf8(request.getAttachment(String.class)))))
					//[END REGION_8]
					.map(POST, "/logout", request -> {
						String id = request.getCookie(SESSION_ID);
						if (id != null) {
							return Promise.of(
									HttpResponse.redirect302("/")
											.withCookie(HttpCookie.of(SESSION_ID, id).withPath("/").withMaxAge(0)));
						}
						return Promise.of(HttpResponse.ofCode(404));
					}));
			//[END REGION_7]
}

First, it redirects requests from homepage to “/members”:

.map("/", request -> Promise.of(HttpResponse.redirect302("/members")))

Next, it takes care of all of the requests that go after “/members” path:

.map("/members/*", RoutingServlet.create()
		.map(GET, "/", StaticServlet.create(staticLoader, "index.html"))
		//[START REGION_8]
		.map(GET, "/cookie", request -> Promise.of(
				HttpResponse.ok200()
						.withBody(wrapUtf8(request.getAttachment(String.class)))))
		//[END REGION_8]
		.map(POST, "/logout", request -> {
			String id = request.getCookie(SESSION_ID);
			if (id != null) {
				return Promise.of(
						HttpResponse.redirect302("/")
								.withCookie(HttpCookie.of(SESSION_ID, id).withPath("/").withMaxAge(0)));
			}
			return Promise.of(HttpResponse.ofCode(404));
		}));

Pay attention to the path “/members/*“. * states, that whichever path until the next / goes after “/members/”, it will be processed by this servlet. So, for example this route:

.map(GET, "/cookie", request -> Promise.of(
		HttpResponse.ok200()
				.withBody(wrapUtf8(request.getAttachment(String.class)))))

is a GET request for “/members/cookie” path. This request shows all cookies stored in the session.

“/members/logout” logs user out, deletes all cookies related to this session and redirects user to homepage.

After public and private servlets are set up, we define main() method, which will start our launcher:

public static void main(String[] args) throws Exception {
	AuthLauncher launcher = new AuthLauncher();
	launcher.launch(args);
}

Running the application

If you want to run the example, clone DataKernel and import it as a Maven project. Before running the example, build the project (Ctrl + F9 for IntelliJ IDEA).

Open AuthLauncher class and run its main() method. Then open your favourite browser and go to localhost:8080. Try to sign up and then log in. When logged in, check out your saved cookies for session. You will see the following content: My saved object in session. Finally, try to log out. You can also try to log in with invalid login or password.