From f67e4f4b385edaeec9dc3b75eae19f40ae68a5e6 Mon Sep 17 00:00:00 2001 From: Subhashinie Koshalya Date: Sun, 5 Apr 2020 11:12:09 +0530 Subject: [PATCH] Add endpoint to return auth token with session management --- pom.xml | 8 + .../covid19/config/WebAuthConfiguration.java | 85 ----------- .../security/SecurityConfiguration.java | 143 ++++++++++++++++++ .../SessionAuthenticationSuccessHandler.java | 25 +++ .../gov/govtech/covid19/util/Constants.java | 3 + src/main/resources/application.yml | 7 + src/main/resources/session/schema-mysql.sql | 22 +++ src/main/resources/templates/login.html | 2 +- 8 files changed, 209 insertions(+), 86 deletions(-) delete mode 100644 src/main/java/lk/gov/govtech/covid19/config/WebAuthConfiguration.java create mode 100644 src/main/java/lk/gov/govtech/covid19/security/SecurityConfiguration.java create mode 100644 src/main/java/lk/gov/govtech/covid19/security/SessionAuthenticationSuccessHandler.java create mode 100644 src/main/resources/session/schema-mysql.sql diff --git a/pom.xml b/pom.xml index 0081dfc..ca6479e 100644 --- a/pom.xml +++ b/pom.xml @@ -102,6 +102,14 @@ org.springframework.boot spring-boot-starter-security + + org.springframework.session + spring-session-core + + + org.springframework.session + spring-session-jdbc + diff --git a/src/main/java/lk/gov/govtech/covid19/config/WebAuthConfiguration.java b/src/main/java/lk/gov/govtech/covid19/config/WebAuthConfiguration.java deleted file mode 100644 index 887d2a0..0000000 --- a/src/main/java/lk/gov/govtech/covid19/config/WebAuthConfiguration.java +++ /dev/null @@ -1,85 +0,0 @@ -package lk.gov.govtech.covid19.config; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; -import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; - -import static lk.gov.govtech.covid19.util.Constants.*; - -@Configuration -@EnableWebSecurity -public class WebAuthConfiguration extends WebSecurityConfigurerAdapter { - - @Autowired - PortalUserConfiguration users; - - @Bean - public BCryptPasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } - - @Override - protected void configure(final HttpSecurity http) throws Exception { - /* - * Endpoints without auth - * - /application/** (all were GETs) - * - /dhis/** (includes both POSTS and GETs) - * - * Endpoints with auth: either http basic auth or login (/portal) based can be used - * - /notification/alert/add - * - /notification/case/add - * - /portal/** - * - * */ - http - .authorizeRequests() - .mvcMatchers( // to exclude auth for GETs - APPLICATION_API_CONTEXT +"/**", - DHIS_API_CONTEXT + "/**", - DOCUMENTS_API_CONTEXT + "/**") - .permitAll() - .and() - .authorizeRequests() - .antMatchers(HttpMethod.POST, // to exclude auth for POSTs - DHIS_API_CONTEXT + "/**") - .permitAll() - .and() - .csrf().disable() - - .authorizeRequests() - .mvcMatchers( - "/notification/alert/add", - "/notification/case/add", - PORTAL_API_CONTEXT + "/**") - .hasRole("USER") - .and() - .httpBasic() - .and() - .formLogin() - .loginPage(PORTAL_API_CONTEXT) - .permitAll() - .defaultSuccessUrl(PORTAL_API_CONTEXT + DASHBOARD_PATH) //redirects once successful - .and() - .logout() - .logoutRequestMatcher(new AntPathRequestMatcher(PORTAL_API_CONTEXT + "/logout")) //logs out with a GET - .permitAll() - .logoutSuccessUrl(PORTAL_API_CONTEXT); //redirects once successful - } - - @Override - protected void configure(AuthenticationManagerBuilder auth) throws Exception { - for (String[] aUser : users.getUserCredentials()) { - auth.inMemoryAuthentication() - .withUser(aUser[0]) - .password(passwordEncoder().encode(aUser[1])) - .roles("USER"); - } - } -} diff --git a/src/main/java/lk/gov/govtech/covid19/security/SecurityConfiguration.java b/src/main/java/lk/gov/govtech/covid19/security/SecurityConfiguration.java new file mode 100644 index 0000000..e93a1c5 --- /dev/null +++ b/src/main/java/lk/gov/govtech/covid19/security/SecurityConfiguration.java @@ -0,0 +1,143 @@ +package lk.gov.govtech.covid19.security; + +import lk.gov.govtech.covid19.config.PortalUserConfiguration; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; +import org.springframework.security.web.savedrequest.NullRequestCache; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.session.web.http.HeaderHttpSessionIdResolver; +import org.springframework.session.web.http.HttpSessionIdResolver; + +import javax.servlet.http.HttpServletRequest; + +import static lk.gov.govtech.covid19.util.Constants.*; + +@Configuration +@EnableWebSecurity +public class SecurityConfiguration { + /* + * Endpoints without auth + * - /application/** (all were GETs) + * - /dhis/** (includes both POSTS and GETs) + * + * Endpoints with auth: either http basic auth or login (/portal) based can be used + * - /notification/alert/add + * - /notification/case/add + * - /portal/** + * + * */ + + @Autowired + PortalUserConfiguration users; + + @Bean + public BCryptPasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public HttpSessionIdResolver httpSessionIdResolver() { + return HeaderHttpSessionIdResolver.xAuthToken(); + } + + @Bean //An exposed user details bean which both the following WebSecurityConfigurerAdapters use + public UserDetailsService userDetailsService() throws Exception { + InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager(); + for (String[] aUser : users.getUserCredentials()) { + manager.createUser(User.builder() + .username(aUser[0]) + .password(passwordEncoder().encode(aUser[1])) + .authorities(AUTHORITY_NOTIFICATION).build()); + } + return manager; + } + + @Configuration + @Order(1) + public static class StatelessSecurityConfig extends WebSecurityConfigurerAdapter { + /* + * This section + * - excludes /application/** and /dhis/** from auth + * - allows requests with http-basic-auth to be STATELESS + * */ + @Override + protected void configure(final HttpSecurity http) throws Exception { + + http + .authorizeRequests() + .mvcMatchers( // to exclude auth for GETs + APPLICATION_API_CONTEXT + "/**", + DHIS_API_CONTEXT + "/**") + .permitAll() + .and() + .authorizeRequests() + .antMatchers(HttpMethod.POST, // to exclude auth for POSTs + DHIS_API_CONTEXT + "/**") + .permitAll() + .and() + .csrf().disable() + .requestMatcher(new RequestMatcher() { + @Override + public boolean matches(HttpServletRequest request) { + return request.getHeader("Authorization") != null; + } + }) + .httpBasic() + .and() + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); + } + } + + @Configuration + @Order(2) + public static class StatefulSecurityConfig extends WebSecurityConfigurerAdapter { + /* + * This section + * - adds auth to some paths, that are common to both basic-auth & auth-token (i.e. stateless and stateful) + * - specifies where the login-page is + * - creates a POST endpoint at path /auth (for form login) + * - created a GET endpoint at path /auth/logout (to logout) + * - stops saving anonymous requests in sessions + * */ + @Override + protected void configure(final HttpSecurity http) throws Exception { + + http + .csrf().disable() + .authorizeRequests() // Common to both: stateless & stateful. Only the paths and the authority matters + .mvcMatchers( + "/notification/alert/add", + "/notification/case/add", + PORTAL_API_CONTEXT + "/**") + .hasAuthority(AUTHORITY_NOTIFICATION) + .and() + .formLogin() + .loginPage(PORTAL_API_CONTEXT) //While specifying the login-page, this also creates a POST endpoint at path /portal (to send username password) + .loginProcessingUrl(AUTH_API_CONTEXT) + .successHandler(new SessionAuthenticationSuccessHandler()) //overriding the default handler to avoid redirect + .failureHandler(new SimpleUrlAuthenticationFailureHandler()) + .permitAll() + .and() + .requestCache() // stops saving anonymous requests in sessions + .requestCache(new NullRequestCache()) + .and() + .logout() + .logoutRequestMatcher(new AntPathRequestMatcher(AUTH_API_CONTEXT + "/logout")) //logs out with a GET + .permitAll() + .logoutSuccessUrl(PORTAL_API_CONTEXT); //redirects once successful + } + } +} diff --git a/src/main/java/lk/gov/govtech/covid19/security/SessionAuthenticationSuccessHandler.java b/src/main/java/lk/gov/govtech/covid19/security/SessionAuthenticationSuccessHandler.java new file mode 100644 index 0000000..9a908be --- /dev/null +++ b/src/main/java/lk/gov/govtech/covid19/security/SessionAuthenticationSuccessHandler.java @@ -0,0 +1,25 @@ +package lk.gov.govtech.covid19.security; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.gson.Gson; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +public class SessionAuthenticationSuccessHandler implements AuthenticationSuccessHandler { + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { + Map scope = new HashMap<>(); + scope.put("scope", authentication.getAuthorities().toString()); + + response.setContentType("application/json;charset=UTF-8"); + response.setStatus(HttpServletResponse.SC_OK); + response.getWriter().write(String.valueOf(new Gson().toJson(scope))); + } +} diff --git a/src/main/java/lk/gov/govtech/covid19/util/Constants.java b/src/main/java/lk/gov/govtech/covid19/util/Constants.java index bb31073..804881b 100644 --- a/src/main/java/lk/gov/govtech/covid19/util/Constants.java +++ b/src/main/java/lk/gov/govtech/covid19/util/Constants.java @@ -10,6 +10,7 @@ public class Constants { public static final String APPLICATION_API_CONTEXT = "/application"; public static final String NOTIFICATION_API_CONTEXT = "/notification"; public static final String DOCUMENTS_API_CONTEXT = "/documents"; + public static final String AUTH_API_CONTEXT = "/auth"; public static final String NEWS_PATH = "/news"; public static final String CASES_PATH = "/cases"; @@ -17,4 +18,6 @@ public class Constants { public static final String PUSH_NOTIFICATION_MESSAGE_TYPE_ALERT = "alert"; public static final String PUSH_NOTIFICATION_MESSAGE_TYPE_CASE = "case"; + + public static final String AUTHORITY_NOTIFICATION = "admin_notification"; } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 2876cbc..50bcf15 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -26,6 +26,13 @@ spring: - [dev2@aaa.com, dev] googleapi: mapKey: abc + session: + timeout: 3600 + jdbc: + initialize-schema: always + schema: classpath:session/schema-mysql.sql + initializer: + enabled: true firebase: config-path: credentials/covid-19-lk-dev-firebase-adminsdk.json diff --git a/src/main/resources/session/schema-mysql.sql b/src/main/resources/session/schema-mysql.sql new file mode 100644 index 0000000..0b99c80 --- /dev/null +++ b/src/main/resources/session/schema-mysql.sql @@ -0,0 +1,22 @@ +CREATE TABLE SPRING_SESSION ( + PRIMARY_ID CHAR(36) NOT NULL, + SESSION_ID CHAR(36) NOT NULL, + CREATION_TIME BIGINT NOT NULL, + LAST_ACCESS_TIME BIGINT NOT NULL, + MAX_INACTIVE_INTERVAL INT NOT NULL, + EXPIRY_TIME BIGINT NOT NULL, + PRINCIPAL_NAME VARCHAR(100), + CONSTRAINT SPRING_SESSION_PK PRIMARY KEY (PRIMARY_ID) +) ENGINE=InnoDB ROW_FORMAT=DYNAMIC; + +CREATE UNIQUE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (SESSION_ID); +CREATE INDEX SPRING_SESSION_IX2 ON SPRING_SESSION (EXPIRY_TIME); +CREATE INDEX SPRING_SESSION_IX3 ON SPRING_SESSION (PRINCIPAL_NAME); + +CREATE TABLE SPRING_SESSION_ATTRIBUTES ( + SESSION_PRIMARY_ID CHAR(36) NOT NULL, + ATTRIBUTE_NAME VARCHAR(200) NOT NULL, + ATTRIBUTE_BYTES BLOB NOT NULL, + CONSTRAINT SPRING_SESSION_ATTRIBUTES_PK PRIMARY KEY (SESSION_PRIMARY_ID, ATTRIBUTE_NAME), + CONSTRAINT SPRING_SESSION_ATTRIBUTES_FK FOREIGN KEY (SESSION_PRIMARY_ID) REFERENCES SPRING_SESSION(PRIMARY_ID) ON DELETE CASCADE +) ENGINE=InnoDB ROW_FORMAT=DYNAMIC; \ No newline at end of file diff --git a/src/main/resources/templates/login.html b/src/main/resources/templates/login.html index 5fb1d9e..cd6cc01 100644 --- a/src/main/resources/templates/login.html +++ b/src/main/resources/templates/login.html @@ -35,7 +35,7 @@

-
+