diff --git a/.project b/.project index 06912a790fe029f2a4be2508109c75e020168525..288bd03f3c7123f0cefb19910735614603b54ab4 100644 --- a/.project +++ b/.project @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="UTF-8"?> <projectDescription> - <name>dummy-service</name> + <name>authentication-service</name> <comment></comment> <projects> </projects> diff --git a/src/main/java/de/rtuni/ms/ds/Application.java b/src/main/java/de/rtuni/ms/as/Application.java similarity index 87% rename from src/main/java/de/rtuni/ms/ds/Application.java rename to src/main/java/de/rtuni/ms/as/Application.java index e41108bb0016926cb0c54ddaefe015eea29fe47b..f024e1b8ffd0bdc448b68d7f6f46589643ea9b27 100644 --- a/src/main/java/de/rtuni/ms/ds/Application.java +++ b/src/main/java/de/rtuni/ms/as/Application.java @@ -3,14 +3,14 @@ * All Rights Reserved. */ -package de.rtuni.ms.ds; +package de.rtuni.ms.as; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.EnableEurekaClient; /** - * Dummy service. + * Authentication service for microservice architecture. * * @author Julian * diff --git a/src/main/java/de/rtuni/ms/as/JwtConfig.java b/src/main/java/de/rtuni/ms/as/JwtConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..9ecde7d16b2a2e883341489d9a2cc70573a35de0 --- /dev/null +++ b/src/main/java/de/rtuni/ms/as/JwtConfig.java @@ -0,0 +1,72 @@ +/* + * Copyright 2019 (C) by Julian Horner. + * All Rights Reserved. + */ + +package de.rtuni.ms.as; + +import org.springframework.beans.factory.annotation.Value; + +/** + * Configuration class for json web token. + * + * @author Julian + * + */ +public class JwtConfig { + //---------------------------------------------------------------------------------------------- + + @Value("${security.jwt.uri:/auth/**}") + private String Uri; + + @Value("${security.jwt.header:Authorization}") + private String header; + + @Value("${security.jwt.prefix:Bearer}") + private String prefix; + + @Value("${security.jwt.expiration:#{24*60*60}}") + private int expiration; + + @Value("${security.jwt.secret:JwtSecretKey}") + private String secret; + + //---------------------------------------------------------------------------------------------- + + /** + * Get the uri. + * + * @return The uri + */ + public String getUri() { return Uri; } + + /** + * Get the header. + * + * @return The header + */ + public String getHeader() { return header; } + + /** + * Get the prefix. + * + * @return The prefix + */ + public String getPrefix() { return prefix; } + + /** + * Get the expiration. + * + * @return The expiration + */ + public int getExpiration() { return expiration; } + + /** + * Get the secret. + * + * @return The secret + */ + public String getSecret() { return secret; } + + //---------------------------------------------------------------------------------------------- +} diff --git a/src/main/java/de/rtuni/ms/as/JwtUsernameAndPasswordAuthenticationFilter.java b/src/main/java/de/rtuni/ms/as/JwtUsernameAndPasswordAuthenticationFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..4e9627fae371b730f96fa4e81e48fb2d779b117c --- /dev/null +++ b/src/main/java/de/rtuni/ms/as/JwtUsernameAndPasswordAuthenticationFilter.java @@ -0,0 +1,141 @@ +/* + * Copyright 2019 (C) by Julian Horner. + * All Rights Reserved. + */ + +package de.rtuni.ms.as; + +import java.io.IOException; +import java.util.Collections; +import java.util.Date; +import java.util.stream.Collectors; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; + +/** + * Filter class for json web token authentication of user name and password. + * + * @author Julian + */ +public class JwtUsernameAndPasswordAuthenticationFilter + extends UsernamePasswordAuthenticationFilter { + //---------------------------------------------------------------------------------------------- + + /** We use auth manager to validate the user credentials */ + private AuthenticationManager authManager; + + /** The configuration for the json web token. */ + private final JwtConfig jwtConfig; + + //---------------------------------------------------------------------------------------------- + + /** + * Creates an instance with the given <code>AuthenticationManager</code> and the given + * <code>JwtConfig</code>. + * + * @param authManager The stated manager + * @param jwtConfig The stated configuration for json web token + */ + public JwtUsernameAndPasswordAuthenticationFilter(AuthenticationManager authManager, + JwtConfig jwtConfig) { + this.authManager = authManager; + this.jwtConfig = jwtConfig; + + // By default, UsernamePasswordAuthenticationFilter listens to "/login" path. + // In our case, we use "/auth". So, we need to override the defaults. + this.setRequiresAuthenticationRequestMatcher( + new AntPathRequestMatcher(jwtConfig.getUri(), "POST")); + } + + //---------------------------------------------------------------------------------------------- + + /** + * {@inheritDoc} + */ + @Override + public Authentication attemptAuthentication(HttpServletRequest request, + HttpServletResponse response) throws AuthenticationException { + try { + // 1. Get credentials from request + UserCredentials creds = + new ObjectMapper().readValue(request.getInputStream(), UserCredentials.class); + // 2. Create auth object (contains credentials) which will be used by auth manager + UsernamePasswordAuthenticationToken authToken = + new UsernamePasswordAuthenticationToken( + creds.getUsername(), creds.getPassword(), Collections.emptyList()); + // 3. Authentication manager authenticate the user, and use + // UserDetailsServiceImpl::loadUserByUsername() method to load the user. + + return authManager.authenticate(authToken); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + //---------------------------------------------------------------------------------------------- + + /** + * Upon successful authentication, generate a token. The 'auth' passed to + * successfulAuthentication() is the current authenticated user. + * + * {@inheritDoc} + */ + @Override + protected void successfulAuthentication(HttpServletRequest request, + HttpServletResponse response, FilterChain chain, Authentication auth) + throws IOException, ServletException { + Long now = System.currentTimeMillis(); + String token = Jwts.builder().setSubject(auth.getName()) + // Convert to list of strings. This is important because it affects the way we + // get them back in the Gateway. + .claim("authorities", + auth.getAuthorities().stream().map(GrantedAuthority::getAuthority) + .collect(Collectors.toList())) + .setIssuedAt(new Date(now)) + .setExpiration(new Date(now + jwtConfig.getExpiration() * 1000)) // in milliseconds + .signWith(SignatureAlgorithm.HS512, jwtConfig.getSecret().getBytes()).compact(); + + // Add token to header + response.addHeader(jwtConfig.getHeader(), jwtConfig.getPrefix() + token); + } + + //---------------------------------------------------------------------------------------------- + + /** + * A (temporary) class just to represent the user credentials. + * + * @author Julian + * + */ + private static class UserCredentials { + private String username; + private String password; + + public String getUsername() { return username; } + + public void setUsername(final String value) { username = value; } + + public String getPassword() { return password; } + + public void setPassword(final String value) { password = value; } + + } + + //---------------------------------------------------------------------------------------------- +} diff --git a/src/main/java/de/rtuni/ms/as/SecurityConfiguration.java b/src/main/java/de/rtuni/ms/as/SecurityConfiguration.java new file mode 100644 index 0000000000000000000000000000000000000000..f7e97f08f1cb8de17da8205d72f964a12b6d4e68 --- /dev/null +++ b/src/main/java/de/rtuni/ms/as/SecurityConfiguration.java @@ -0,0 +1,104 @@ +/* + * Copyright 2019 (C) by Julian Horner. + * All Rights Reserved. + */ + +package de.rtuni.ms.as; + +import javax.servlet.http.HttpServletResponse; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +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.config.http.SessionCreationPolicy; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +/** + * Class that handles security configuration. + * + * @author Julian + * + */ +@EnableWebSecurity +public class SecurityConfiguration extends WebSecurityConfigurerAdapter { + //---------------------------------------------------------------------------------------------- + + /** A service that loads users from the database. */ + @Autowired + private UserDetailsService userDetailsService; + + /** The configuration for the json web token. */ + @Autowired + private JwtConfig jwtConfig; + + //---------------------------------------------------------------------------------------------- + + /** + * Overrides the default configuration. + */ + @Override + protected void configure(HttpSecurity http) throws Exception { + http.csrf().disable() + // make sure we use stateless session; session won't be used to store user's state. + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() + // handle an authorized attempts + .exceptionHandling().authenticationEntryPoint( + (req, rsp, e) -> rsp.sendError(HttpServletResponse.SC_UNAUTHORIZED)) + .and() + + /* + * Add a filter to validate user credentials and add token in the response header. + * + * What's the authenticationManager()? An object provided by WebSecurityConfigurerAdapter, + * used to authenticate the user passing user's credentials. The filter needs this + * authentication manager to authenticate the user. + */ + .addFilter( + new JwtUsernameAndPasswordAuthenticationFilter(authenticationManager(), jwtConfig)) + .authorizeRequests() + // allow all POST requests + .antMatchers(HttpMethod.POST, jwtConfig.getUri()).permitAll() + // any other requests must be authenticated + .anyRequest().authenticated(); + } + + /** + * Spring has <code>UserDetailsService</code> interface, which can be overridden to provide our + * implementation for fetching user from database (or any other source). + * <p> + * The UserDetailsService object is used by the authentication manager to load the user + * from database. In addition, we need to define the password encoder also. So, authentication + * manager can compare and verify passwords. + */ + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); + } + + //---------------------------------------------------------------------------------------------- + + /** + * Get a new <code>JwtConfig</code>. + * + * @return The stated configuration + */ + @Bean + public JwtConfig jwtConfig() { return new JwtConfig(); } + + //---------------------------------------------------------------------------------------------- + + /** + * Get a new <code>BCryptPasswordEncoder</code>. + * + * @return The stated encoder + */ + @Bean + public BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } + + //---------------------------------------------------------------------------------------------- +} diff --git a/src/main/java/de/rtuni/ms/as/UserDetailsServiceImpl.java b/src/main/java/de/rtuni/ms/as/UserDetailsServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..be6732fda23c3184e5046d78fb61d34cdc384392 --- /dev/null +++ b/src/main/java/de/rtuni/ms/as/UserDetailsServiceImpl.java @@ -0,0 +1,114 @@ +/* + * Copyright 2019 (C) by Julian Horner. + * All Rights Reserved. + */ + +package de.rtuni.ms.as; + +import java.util.Arrays; +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.stereotype.Service; + +/** + * Class that is able to load users. + * + * @author Julian + */ +@Service +public class UserDetailsServiceImpl implements UserDetailsService { + //---------------------------------------------------------------------------------------------- + + /** The password encoder. */ + @Autowired + private BCryptPasswordEncoder encoder; + + //---------------------------------------------------------------------------------------------- + + /** + * Loads a user by the given name. + * + * @param The stated name + */ + @Override + public UserDetails loadUserByUsername(final String username) throws UsernameNotFoundException { + // hard coding users. All passwords must be encoded. + final List<AppUser> users = Arrays.asList( + new AppUser(1, "julian", encoder.encode("12345"), "USER"), + new AppUser(2, "admin", encoder.encode("12345"), "ADMIN")); + + for (AppUser appUser : users) { + if (appUser.getUsername().equals(username)) { + /* + * Remember that Spring needs roles to be in this format: "ROLE_" + userRole + * (i.e."ROLE_ADMIN") + * So, we need to set it to that format, so we can verify and compare roles + * (i.e. hasRole("ADMIN")). + */ + List<GrantedAuthority> grantedAuthorities = + AuthorityUtils.commaSeparatedStringToAuthorityList( + "ROLE_" + appUser.getRole()); + + /* + * The "User" class is provided by Spring and represents a model class for user to + * be returned by UserDetailsService and used by authentication manager to verify + * and check use authentication. + */ + return new User(appUser.getUsername(), appUser.getPassword(), grantedAuthorities); + } + } + + // If user not found. Throw this exception. + throw new UsernameNotFoundException("Username: " + username + " not found"); + } + + //---------------------------------------------------------------------------------------------- + + /** + * A (temporary) class represent the user saved in the database. + * + * @author Julian + * + */ + private static class AppUser { + private Integer id; + private String username; + private String password; + private String role; + + public AppUser(final Integer id, final String username, final String password, + final String role) { + this.id = id; + this.username = username; + this.password = password; + this.role = role; + } + + public Integer getId() { return id; } + + public void setId(final Integer value) { id = value; } + + public String getUsername() { return username; } + + public void setUsername(final String value) { username = value; } + + public String getPassword() { return password; } + + public void setPassword(final String value) { password = value; } + + public String getRole() { return role; } + + public void setRole(final String value) { role = value; } + + } + + //---------------------------------------------------------------------------------------------- +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 1b7b187896dc697bcea80a7514805d2a87c70fc4..98429eedf14fa981799900be0a20b25f97d8b996 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,13 +1,10 @@ # Spring properties spring: application: - name: dummy-service # Identify this application - -# Map the error path to error template (for Thymeleaf) -error.path: /error + name: auth-service # Identify this application # HTTP Server -server.port: 3333 # HTTP (Tomcat) port +server.port: 4444 # HTTP (Tomcat) port eureka: client: diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html deleted file mode 100644 index ab3188e772de7271da730a4032fd98c5c508e862..0000000000000000000000000000000000000000 --- a/src/main/resources/templates/index.html +++ /dev/null @@ -1,11 +0,0 @@ -<!DOCTYPE HTML> -<html xmlns:th="http://www.thymeleaf.org"> - <head> - <title>Dummy page</title> - <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> - </head> - <body> - <h1>Page without authentication</h1> - <a href="#" th:href="@{/securePage}">geschützte Seite anfordern</a> - </body> -</html>