/** * Copyright (C) 2013-2015 all@code-story.net * * 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.codestory.http.filters.auth; import net.codestory.http.Context; import net.codestory.http.Cookie; import net.codestory.http.NewCookie; import net.codestory.http.convert.TypeConvert; import net.codestory.http.filters.Filter; import net.codestory.http.filters.PayloadSupplier; import net.codestory.http.payload.Payload; import net.codestory.http.security.SessionIdStore; import net.codestory.http.security.User; import net.codestory.http.security.Users; import java.util.Random; import java.util.concurrent.TimeUnit; import static java.lang.Long.toHexString; import static java.util.stream.Stream.concat; import static java.util.stream.Stream.of; import static net.codestory.http.constants.Headers.CACHE_CONTROL; import static net.codestory.http.constants.HttpStatus.UNAUTHORIZED; import static net.codestory.http.constants.Methods.GET; import static net.codestory.http.constants.Methods.POST; import static net.codestory.http.payload.Payload.seeOther; public class CookieAuthFilter implements Filter { protected static final String[] DEFAULT_EXCLUDE = {".less", ".css", ".map", ".js", ".coffee", ".ico", ".jpeg", ".jpg", ".gif", ".png", ".svg", ".eot", ".ttf", ".woff", "woff2", "robots.txt"}; protected static final Random RANDOM = new Random(); protected static final int ONE_DAY = (int) TimeUnit.DAYS.toSeconds(1L); protected final String uriPrefix; protected final Users users; protected final SessionIdStore sessionIdStore; protected final String[] ignoreExtensions; public CookieAuthFilter(String uriPrefix, Users users) { this(uriPrefix, users, SessionIdStore.inMemory(), DEFAULT_EXCLUDE); } public CookieAuthFilter(String uriPrefix, Users users, SessionIdStore sessionIdStore) { this(uriPrefix, users, sessionIdStore, DEFAULT_EXCLUDE); } public CookieAuthFilter(String uriPrefix, Users users, SessionIdStore sessionIdStore, String ignoreExtension, String... moreIgnoreExtensions) { this(uriPrefix, users, sessionIdStore, concat(of(ignoreExtension), of(moreIgnoreExtensions)).toArray(String[]::new)); } private CookieAuthFilter(String uriPrefix, Users users, SessionIdStore sessionIdStore, String[] ignoreExtensions) { this.uriPrefix = uriPrefix; this.users = users; this.sessionIdStore = sessionIdStore; this.ignoreExtensions = ignoreExtensions; } @Override public boolean matches(String uri, Context context) { return uri.startsWith("/auth/") || (uri.startsWith(uriPrefix) && of(ignoreExtensions).noneMatch(uri::endsWith)); } @Override public Payload apply(String uri, Context context, PayloadSupplier nextFilter) throws Exception { return uri.startsWith("/auth/") ? authenticationUri(uri, context, nextFilter) : otherUri(uri, context, nextFilter); } protected Payload authenticationUri(String uri, Context context, PayloadSupplier nextFilter) throws Exception { String method = context.method(); if (uri.startsWith("/auth/signin") && POST.equals(method)) { return signin(context); } if (uri.startsWith("/auth/signout") && GET.equals(method)) { return signout(context); } // Don't protect other /auth/ pages. Login and lost password pages for eg. return nextFilter.get(); } protected Payload otherUri(String uri, Context context, PayloadSupplier nextFilter) throws Exception { String sessionId = readSessionIdInCookie(context); if (sessionId != null) { String login = sessionIdStore.getLogin(sessionId); if (login != null) { User user = users.find(login); context.setCurrentUser(user); return nextFilter.get().withHeader(CACHE_CONTROL, "must-revalidate"); } } if (redirectToLogin(uri)) { return seeOther("/auth/login").withCookie(authCookie(buildCookie(null, uri))); } // Don't set a realm so that the browser doesn't open an Authentication dialog return new Payload(UNAUTHORIZED); } protected boolean redirectToLogin(String uri) { return true; } protected Payload signin(Context context) { String login = context.get("login"); String password = context.get("password"); User user = users.find(login, password); if (user == null) { // Unknown user. Go back to login return seeOther("/auth/login"); } return seeOther(validRedirectUrl(readRedirectUrlInCookie(context))) .withCookie(authCookie(buildCookie(user, "/"))); } protected Payload signout(Context context) { String sessionId = readSessionIdInCookie(context); if (sessionId != null) { sessionIdStore.remove(sessionId); } return seeOther("/?signout") .withCookie(authCookie(null)); } protected String readSessionIdInCookie(Context context) { AuthData authData = readAuthCookie(context); return (authData == null) ? null : authData.sessionId; } protected String readRedirectUrlInCookie(Context context) { AuthData authData = readAuthCookie(context); String redirectUrl = (authData == null) ? null : authData.redirectAfterLogin; redirectUrl = (redirectUrl == null) ? "/" : redirectUrl; return redirectUrl; } protected String newSessionId(String login) { String sessionId = toHexString(RANDOM.nextLong()) + toHexString(RANDOM.nextLong()); sessionIdStore.put(sessionId, login); return sessionId; } protected String buildCookie(User user, String redirectUrl) { AuthData cookie = new AuthData(); if (user != null) { cookie.login = user.login(); cookie.roles = user.roles(); cookie.sessionId = newSessionId(user.login()); } cookie.redirectAfterLogin = redirectUrl; return TypeConvert.toJson(cookie); } protected AuthData readAuthCookie(Context context) { try { return context.cookies().value(cookieName(), AuthData.class); } catch (Exception e) { // Ignore invalid cookie return null; } } protected String cookieName() { return "auth"; } protected int expiry() { return ONE_DAY; } protected String domain() { return null; } protected Cookie authCookie(String authData) { NewCookie cookie = new NewCookie(cookieName(), authData, "/", true); cookie.setExpiry(expiry()); cookie.setDomain(null); cookie.setSecure(false); return cookie; } protected String validRedirectUrl(String redirectUrl) { return redirectUrl.contains("favicon.ico") ? "/" : redirectUrl; } }