From a6b2d15e3da71779dfce9c4fe43e3be64e392041 Mon Sep 17 00:00:00 2001 From: Julian Horner <julianhorner@web.de> Date: Tue, 17 Dec 2019 17:38:52 +0100 Subject: [PATCH] Correct files --- launchConfigurations/dummy-service.launch | 17 +++ pom.xml | 82 ++++++++++ src/main/java/de/rtuni/ms/as/Application.java | 32 ++++ src/main/java/de/rtuni/ms/as/JwtConfig.java | 73 +++++++++ ...ernameAndPasswordAuthenticationFilter.java | 142 ++++++++++++++++++ .../de/rtuni/ms/as/SecurityConfiguration.java | 105 +++++++++++++ .../rtuni/ms/as/UserDetailsServiceImpl.java | 115 ++++++++++++++ src/main/resources/application.yml | 12 ++ 8 files changed, 578 insertions(+) create mode 100644 launchConfigurations/dummy-service.launch create mode 100644 pom.xml create mode 100644 src/main/java/de/rtuni/ms/as/Application.java create mode 100644 src/main/java/de/rtuni/ms/as/JwtConfig.java create mode 100644 src/main/java/de/rtuni/ms/as/JwtUsernameAndPasswordAuthenticationFilter.java create mode 100644 src/main/java/de/rtuni/ms/as/SecurityConfiguration.java create mode 100644 src/main/java/de/rtuni/ms/as/UserDetailsServiceImpl.java create mode 100644 src/main/resources/application.yml diff --git a/launchConfigurations/dummy-service.launch b/launchConfigurations/dummy-service.launch new file mode 100644 index 0000000..615918c --- /dev/null +++ b/launchConfigurations/dummy-service.launch @@ -0,0 +1,17 @@ +{ + "ServiceId": "com.ibm.cloudoe.orion.client.deploy", + "Params": { + "Target": { + "Url": "https://api.eu-gb.cf.cloud.ibm.com", + "Org": "organisation one", + "Space": "dev" + }, + "Name": "dummy-service", + "Instrumentation": { + "domain": "eu-gb.mybluemix.net", + "instances": "1" + } + }, + "Path": "manifest.yml", + "Type": "Cloud Foundry" +} \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..100227e --- /dev/null +++ b/pom.xml @@ -0,0 +1,82 @@ +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <groupId>de.rtuni.ms.as</groupId> + <artifactId>authentication-service</artifactId> + <version>1.0.0</version> + <packaging>war</packaging> + + <name>authentication-service</name> + <description>Authentication service for microservice architecture</description> + + <properties> + <java.version>1.8</java.version> + </properties> + + <parent> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-parent</artifactId> + <version>2.0.0.RELEASE</version> + </parent> + + <dependencies> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter</artifactId> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-web</artifactId> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-security</artifactId> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-tomcat</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-devtools</artifactId> + <scope>runtime</scope> + <optional>true</optional> + </dependency> + <dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> + </dependency> + <dependency> + <groupId>io.jsonwebtoken</groupId> + <artifactId>jjwt</artifactId> + <version>0.9.0</version> + </dependency> + </dependencies> + + <dependencyManagement> + <dependencies> + <dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-dependencies</artifactId> + <version>Finchley.RELEASE</version> + <type>pom</type> + <scope>import</scope> + </dependency> + </dependencies> + </dependencyManagement> + + <build> + <plugins> + <plugin> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-maven-plugin</artifactId> + <configuration> + <mainClass>de.rtuni.ms.as.Application</mainClass> + </configuration> + </plugin> + </plugins> + </build> +</project> \ No newline at end of file diff --git a/src/main/java/de/rtuni/ms/as/Application.java b/src/main/java/de/rtuni/ms/as/Application.java new file mode 100644 index 0000000..7e0aff1 --- /dev/null +++ b/src/main/java/de/rtuni/ms/as/Application.java @@ -0,0 +1,32 @@ +package de.rtuni.ms.as; +/* + * Copyright 2019 (C) by Julian Horner. + * All Rights Reserved. + */ + + + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.netflix.eureka.EnableEurekaClient; + +/** + * Authentication service for microservice architecture. + * + * @author Julian + * + */ +@SpringBootApplication +@EnableEurekaClient +public class Application { + //--------------------------------------------------------------------------------------------- + + /** + * Starts the application. + * + * @param args The arguments + */ + public static void main(final String[] args) { SpringApplication.run(Application.class, args); } + + //--------------------------------------------------------------------------------------------- +} 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 0000000..459660d --- /dev/null +++ b/src/main/java/de/rtuni/ms/as/JwtConfig.java @@ -0,0 +1,73 @@ +package de.rtuni.ms.as; +/* + * Copyright 2019 (C) by Julian Horner. + * All Rights Reserved. + */ + + + +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 0000000..b9036c9 --- /dev/null +++ b/src/main/java/de/rtuni/ms/as/JwtUsernameAndPasswordAuthenticationFilter.java @@ -0,0 +1,142 @@ +package de.rtuni.ms.as; +/* + * Copyright 2019 (C) by Julian Horner. + * All Rights Reserved. + */ + + + +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 0000000..ef648e0 --- /dev/null +++ b/src/main/java/de/rtuni/ms/as/SecurityConfiguration.java @@ -0,0 +1,105 @@ +package de.rtuni.ms.as; +/* + * Copyright 2019 (C) by Julian Horner. + * All Rights Reserved. + */ + + + +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 0000000..0d0a174 --- /dev/null +++ b/src/main/java/de/rtuni/ms/as/UserDetailsServiceImpl.java @@ -0,0 +1,115 @@ +package de.rtuni.ms.as; +/* + * Copyright 2019 (C) by Julian Horner. + * All Rights Reserved. + */ + + + +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 new file mode 100644 index 0000000..98429ee --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,12 @@ +# Spring properties +spring: + application: + name: auth-service # Identify this application + +# HTTP Server +server.port: 4444 # HTTP (Tomcat) port + +eureka: + client: + serviceUrl: + defaultZone: http://localhost:8761/eureka/ \ No newline at end of file -- GitLab