Skip to content

dsmelser/todo-security-dsmelser

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

36 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

TODO Manager w/ Security

TODO Data Model

1. text         - String
2. authorId     - int
3. isPublic     - boolean
4. createDate   - LocalDate

Roles

1. Guest/Anonymous Role
2. Author/Standard User Role
3. Administrator Role

User Stories

As a __________, I should [not] be able to ____________.

Preconditions: what must be true for the user story to be relevant.
Postconditions: what must be true after the user story ends.
  • As any user, I should be able to see all public Todos.
  • As a guest, I should not be able to see any private Todos.
  • As a guest, I should not be able to create a Todo.
  • As a guest, I should not be able to remove a Todo.
  • As a guest, I should not be able to edit a Todo.
  • As a guest, I should be able to create an account.
  • As a guest, I should be able to log into an existing account.
  • As an Author, I should be able to see my own private Todos.
  • As an Author, I should not be able to see other Author's Todos.
  • As an Author, I should be able to create a Todo.
  • As an Author, I should be able to remove my own Todos.
  • As an Author, I should not be able to remove other Author's Todos.
  • As an Author, I should be able to edit my own Todos.
  • As an Author, I should not be able to edit other Author's Todos.
  • As an Admin, I should be able to see all Todos.
  • As an Admin, I should be able to create a Todo.
  • As an Admin, I should be able to remove all Todos.
  • As an Admin, I should be able to edit all Todos.
  • As an Admin, I should be able to promote an Author to Admin.
  • As an Admin, I should be able to remove Author users.
  • As an Admin, I should be able to edit the name of Authors.
  • As an Admin, I should be able to change the password of Authors.

Tasks

  • Shift+Alt+F is awesome in VSCode

  • Create Java API

    • Create Java Project (todo-with-security)
    • Modify pom.xml to include the parent tag (spring-boot-starter-parent)
    • Modify pom.xml to include the following dependencies
      • spring-boot-starter-security
      • jjwt-api
      • jjwt-impl
      • jjwt-jackson
      • mysql-connector-java
      • spring-boot-starter-jdbc
      • spring-boot-starter-web
    • Create base package (todo)
      • Create App class
        • @SpringBootApplication
        • main
          • SpringApplication.run( App.class, args );
    • Create application.properties file
      • spring.datasource.url=jdbc:mysql://localhost:3306/todo_prod
      • spring.datasource.username=root
      • spring.datasource.password=top-secret-password
    • Create models package
      • Create AppUser class
        • Extend from the User (org.springframework.security.core.userdetails)
        • Add Set<String> roles field variable
        • Add Integer userId field variable
        • Generate getters/setters
        • Generate hashCode/equals
        • Add constructor which takes Integer userId, String username, String password, and Set<String> roles
          • call super(username, password, roles.stream().map( r -> new SimpleGrantedAuthority( "ROLE_" + r )).collect( Collectors.toList() ) )
          • assign to this.userId
          • assign to this.roles
      • Create Todo class
        • Create Integer todoId field variable
        • Create String text field variable
        • Create Integer userId field variable
        • Create Boolean isPublic field variable
        • Create LocalDate createDate field variable
        • Generate getters/setters
        • Generate hashCode/equals
    • Create data package
      • Create TodoRepo interface
        • List<Todo> findAllPublic()
        • List<Todo> findByUserId(Integer userId)
        • Todo findById( Integer todoId )
        • Todo add(Todo toAdd)
        • boolean remove(Integer todoId)
        • void edit( Todo updated )
      • Create UserRepo interface
        • AppUser findByUsername( String username )
        • AppUser add( AppUser toAdd )
        • boolean remove( Integer userId )
        • void edit( User updated )
      • Create TodoMapper class
        • implements RowMapper<Todo>
        • Generate interface method
          • Todo toReturn = new Todo();
          • toReturn.setTodoId( rs.getInt("todoId") );
          • toReturn.setText( rs.getString("todoText"));
          • toReturn.setUserId( rs.getInt("authorId") );
          • toReturn.setPublic( rs.getBoolean("isPublic"));
          • toReturn.setCreateDate( LocalDate.parse( rs.getString("createDate") ) );
          • return toReturn;
      • Create TodoDbRepo class
        • Add @Repository
        • add @Autowired JdbcTemplate template field variable
        • implements TodoRepo
          • generate functions automatically
          • implement findAllPublic()
            • String sql = "SELECT * FROM todos where isPublic = 1;"
            • return template.query( sql, new TodoMapper());
          • implement findById()
            • String sql = return template.query("select * from todos where todoId = ?", new TodoMapper(), todoId).stream().findAny().orElse(null);
            • implement boolean remove(Integer todoId) {
              • return template.update( "delete from todos where todoId = ?", todoId) == 1;
      • Create UserMapper class
        • create Set<String> roles field variable
        • create UserMapper constructor which takes in the Set of roles and sets the field variable
        • implements RowMapper<AppUser>
        • auto-generate methods
          • AppUser toBuild = new AppUser(userId, username, password, roles);
      • Create UserDbRepo class
        • Add @Repository
        • implements UserRepo
          • Add @Autowired JdbcTemplate template field variable
          • generate functions automatically
          • create private Set<String> findRolesByUsername(String username)
            • String sql = "SELECT roleName FROM users u inner join userroles ur on ur.userId = u.userId inner join roles r on ur.roleId = r.roleId where username = ?"
            • return template.query( sql, (rowData, rowNum)->rowData.getString("roleName"), username).stream().collect(Collectors.toSet())
          • implement findByUsername(String username)
            • String sql = "select userId, username, password from users where username = ?"
            • return template.query( sql, new UserMapper(findRolesByUsername(username)), username).stream().findAny().orElse(null);
    • Create domain package
      • Create InvalidUserException
        • create constructor that takes in String message, call super(message)
        • create constructor that takes in String message, Throwable innerException calls super( message, innerException )
      • Create UserService class
        • mark with @Service
        • implements UserDetailsService
        • add UserRepo field variable
        • add PasswordEncoder field variable
        • add constructor which takes in a UserRepo & PasswordEncoder
        • @Override loadUserByUsername (can return AppUser as a UserDetails object)
          • use the repo to pass along the user
          • add //TODO: validate (later we'll check to make sure username isn't null/empty/etc)
          • if user is not found (we get a null) throw new UsernameNotFoundException(username + " not found")
          • otherwise, return the user
        • add AppUser create( String username, String password )
          • for now just return null
      • Create TodoService class
        • mark as @Service
        • add @Autowired TodoRepo tRepo field variable
        • add @Autowired UserRepo uRepo field variable
        • -- should have autogenerated getPublicTodos method from controller --
          • return repo.findAllPublic();
        • create public void deleteById(Integer todoId, Principal user) throws InvalidUserException {
          • Todo toDelete = tRepo.findById(todoId);
          • AppUser requester = uRepo.findByUsername(user.getName());
          • if( requester.getRoles().contains("ADMIN") || requester.getUserId().intValue() == toDelete.getUserId().intValue() ){
            • tRepo.remove(todoId);
          • } else { throw new InvalidUserException("Only admins and the author of the todo may delete it."); }
    • Create security package
      • create SecurityConfig class
        • @EnableWebSecurity
        • extends WebSecurityConfigurerAdapter
        • @Override protected void configure( HttpSecurity http) throws Exception
          • http.csrf().disable()
          • http.cors()
          • http.authorizeRequests()
            • .antMatchers( HttpMethod.POST, "/api/security/login").permitAll()
            • .antMatchers( HttpMethod.GET, "/api/todo/public" ).permitAll()
            • .antMatchers( HttpMethod.DELETE, "/api/todo/*").hasAnyRole("AUTHOR", "ADMIN")
            • .antMatchers("/**").denyAll()
            • .and()
            • .sessionManagement()
              • .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        • public PasswordEncoder getEncoder(){ return new BCryptPasswordEncoder(); }
          • mark with @Bean
        • @Override protected AuthenticationManager authenticationManager() throws Exception
          • just return super.authenticationManager();
          • mark with @Bean
      • Create JwtConverter class
        • Mark as @Component
        • add a Key field variable (secretKey) assign Keys.secretKeyFor(SignatureAlgorithm.HS256)
        • add public String getTokenFromUser( User toConvert )
          • generate comma separated string of authorities granted to the user (retrieve those with .getAuthorities() )
          • return Jwts.builder()
            • .setIssuer("todo-app")
            • .setSubject(toConvert.getUsername())
            • .claim("authorties", commaSeparatedString)
            • .setExpiration( new Date(System.currentTimeMillis() + 15 * 60 * 1000 ) )
            • .signWithKey( secretKey )
            • .compact();
        • add public User getUserFromToken( String token )
          • try/catch (JwtException)
            • JwtParser parser = Jwts.parserBuilder().requireIssuer("todo-app").setSigningKey( secretKey ).build();
            • Jws<Claims> claims = parser.parseClaimsJws( token.substring(7) );
            • String username = claims.getBody().getSubject();
            • String authorities = (String)claims.getBody().get("authorities");
            • String [] authSplit = authorities.split(",");
            • List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
            • for( String auth : authSplit ){ grantedAuthorities.add(new SimpleGrantedAuthority(auth)); }
            • return new User( username, username, grantedAuthorities );
            • catch( JwtException ex ) {
              • ex.printStackTrace( System.err );
              • return null; }
      • Create JwtRequestFilter class
        • extends BasicAuthenticationFilter
        • Add a JwtConverter field
        • Add a constructor that takes in a JwtConvert and AuthenticationManager
          • super( authManager )
          • store the JwtConverter in the field variable
        • @Override protected void doFilterInternal( HttpServletRequest request, HttpServletResponse response, FilterChain chain)
          • String authHeader = request.getHeader( "Authorization");
          • if( authHeader != null && authHeader.startsWith( "Bearer ")){
            • User converted = converter.getUserFromToken( authHeader );
            • if( converted != null ){
              • UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( converted.getUsername(), null, convertedUser.getAuthorities() );
              • SecurityContextHolder.getContext().setAuthentication( token );
            • } else {
              • response.setStatus( 403 ); }
          • chain.doFilter( request, response );
        • IN SecurityConfig.java
          • add @Autowired JwtConverter field variable
          • right after the .and() call .addFilter( new JwtReqestFilter() )
    • Create controllers package
      • Add AuthController class
        • mark as @RestController
        • add @RequestMapping( "/api/security" )
        • add AuthenticationManager field variable
        • add JwtConverter field variable
        • add UserService field variable
        • add a constructor that takes in all field variables and sets them
        • add ResponseEntity login( @RequestBody Map<String,String> credentials )
          • mark as @PostMapping("/login")
          • create UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( credentials.get("username"), credentials.get("password") );
          • in a try/catch( AuthenticationException ex) block...
            • Authentication authResult = authManager.authenticate( token );
            • if( authResult.isAuthenticated() ){
              • String jwt = converter.getTokenFromUser( (User)authResult.getPrincipal());
              • Map<String,String> tokenWrapper = new HashMap<>();
              • tokenWrapper.put( "jwt_token", jwt);
              • return ResponseEntity.ok( tokenWrapper );
            • }
            • catch( AuthenticationException ex ){
              • ex.printStackTrace( System.err ); }
            • return new ResponseEntity( HttpStatus.FORBIDDEN );
      • Add TodoController class
        • mark as @RestController
        • @RequestMapping( "/api/todo" )
        • add @Autowired TodoService field variable (service)
        • add a GET endpoint ("/public") for retrieving all todos
          • List<Todo> pubTodos = service.getPublicTodos() (doesn't exist yet...)
          • generate TodoService.getPublicTodos()
          • return ResponseEntity.ok(pubTodos);
        • add a DELETE endpoint ("/{todoId}")
          • public ResponseEntity delete( @PathVariable Integer todoId, Principal user ){
            • service.deleteById( todoId, user );
            • generate TodoService.deleteById()
            • return ResponseEntity.ok().build();
  • Create mysql schemas (test/prod)

    • create sql folder in project folder
    • create todo-test.sql
    • create todo-prod.sql
    • drop database if exists todo_X
    • create database todo_X
    • use todo_X
    • create table users
      • userId int primary key auto_increment
      • username varchar(300) not null unique
      • password varchar(2048) not null,
    • create table todos
      • todoId int primary key auto_increment
      • todoText text not null
      • authorId int not null
      • isPublic bit(1) not null
      • createDate date not null
      • constraint fk_todos_users foreign key (authorId) references users(userId)
    • create table roles
      • roleId int primay key auto_increment
      • roleName varchar(20) not null unique
    • create table userroles
      • userId int not null,
      • roleId int not null,
      • constraint pk_userroles (userId, roleId),
      • constraint fk_users_userroles foreign key (userId) references users(userId)
      • constraint fk_roles_userroles foreign key (roleId) references roles(roleId)
    • insert into users (username, password) values ('bob', '$2a$12$HqaU3VlN09ufZ60R8VrLHuIX8H6b1iFDA9AG./vzThpIzhxEIF8nC'); -- pw is password
    • insert into users (username, password) values ('june', '$2a$12$k2TB.cQ1TLHLOYn.pbbiTuQ5HoUxozWkl.ZgFZ.9eioAeMxndT5AS'); -- pw is admin-password
    • insert into roles (roleName) VALUES ('AUTHOR'), ('ADMIN');
    • insert into userroles (userId, roleId) VALUES (1,1), (2,2);
    • insert into todos (todoText, authorId, isPublic, createDate) values ('this is a private todo', 1, 0, '2020-04-06'), ('this is a public todo', 2, 1, '2020-04-05');
    • generate reset stored procedure in db (set_known_good_state)
      • delete from userroles;
      • delete from users;
      • alter table users auto_increment = 1;
      • delete from roles;
      • alter table roles auto_increment = 1;
      • delete from todos;
      • alter table todos auto_increment = 1;
      • (copy all inserts from prod)
    • at end of test schema call set_known_good_state();
  • Create React Front-End

    • From the terminal, inside of your Java application
      • npx create-react-app client
      • cd client
      • code . [optional - open in VSCode]
    • Delete cruft
      • ./public/favicon.ico
      • ./public/logo192.png
      • ./public/logo512.png
      • ./public/manifest.json
      • ./public/robots.txt
      • ./src/App.css
      • ./src/App.test.js
      • ./src/logo.svg
      • ./src/reportWebVitals.js
      • ./src/setupTests.js
      • Update ./public/index.html
        • From default file, delete:
          • Lines 4-26
          • Change Title to Todo App
          • Delete any additional comments here
      • Update ./src/App.js
        • From default file, delete:
          • Lines 7-20
          • Lines 1-2
      • Update ./src/index.css\
        • Trashcan it all
      • Update ./src/index.js
      • From default file, delete:
        • Lines 14-17
        • Lines 5
    • Add additional dependencies
      • npm i react-router-dom
    • Create components (indents below indicate parent-child relations)
      • Nav Component
      • Login Component
      • Home Component - welcoming and showing all pub todos
        • Welcome Component - nested inside Home
        • Todos (container) Component
          • Todo Component
          • Delete Component
      • AddTodo Component
    • Add react-router to our project
      • At the top of index.js
        • import { BrowserRouter } from 'react-router-dom';
        • Change <React.StrictMode> to <BrowserRouter>
        • Change </React.StrictMode> to </BrowserRouter>
    • Build out base Home component
      • Functional component, don't forget to export!
    • Build out base Welcome component
      • Functional component, don't forget to export!
    • Add <Home /> to App.js
      • import Home from "./Home";
      • Add flavor-text to ground ourselves
    • Add <Welcome /> to Home.js
      • import Welcome from "./Welcome";
      • Add flavor-text to ground ourselves
    • Add <Nav /> to App.js
      • import Nav from "./Nav";
      • Add flavor-text to ground ourselves
    • Begin implementing Routes in App.js
      • import { Routes, Route } from 'react-router-dom';
      • <Routes>
        • <Route path="/" element={<Home />} />
        • // ^^ Home Page Route, at base dot-com URL
      • </Routes>
    • Begin implementing Links in Nav.js
      • import { Link } from 'react-router-dom';
      • <Link to="/">Home</Link>
    • Add <Todos /> component to Home.js
      • import Todos from "./Todos";
    • In Todos.js...
      • Create State to store public todos
        • import { useState } from 'react';
          • const [pubTodos, setPubTodos] = useState([]);
      • Implement useEffect() hook for setting state on fetch
        • import { useState, useEffect } from 'react';
        • useEffect(() => {
          • Use Fetch API to retrieve our public todos
            • Verify CORS is open in your TodoController (Java)
              • @CrossOrigin(origins = {"http://localhost:3000"})
            • fetch("http://localhost:8080/api/todo/public")
            • .then(response => {
              • if (response.status === 200) {
                • return response.json()
              • } else {
                • alert("Something went wrong when fetching")
              • }
            • })
            • .then(todosData => setPubTodos(todosData))
            • .catch(rejection => alert("Failure: " + rejection.status))
        • }, [])
      • import Todo from './Todo'
      • Implement a <Todo /> factory function
        • function todoFactory() {
          • return pubTodos.map(todo => <Todo key={todo.todoId} todoObj={todo} />);
        • }
        • Call function inside of the return for <Todos />
          • return (
            • <>
              • {todoFactory()}
            • </>
          • )
      • Build out the base <Todo /> component
        • Functional component, don't forget to export
      • Use props.todoObj to access the todo and display in Todo.js
        • Destructure the properties of my todoObj into variables
          • const { text, userId, createDate } = props.todoObj;
          • Build HTML/JSX structure to display data to return
            • <div className="todo-item">
              • <h3>User Id: {userId}</h3>
              • <p>Created: {createDate}</p>
              • <p>Text: {text}</p>
            • </div>
      • Update index.css with Dev-CSS to help visualize
        • .todo-item {
          • border: 1px black solid;
          • padding: 20px;
          • margin-bottom: 30px;
        • }
    • Implement useContext hook
      • Create AuthContext.js
        • import { createContext } from 'react';
        • const AuthContext = createContext();
        • export default AuthContext
    • In App.js, implement Context
      • import { useState } from 'react';
      • import AuthContext from "./AuthContext";
      • const [user, setUser] = useState(null);
      • Inside of the return
        • Before rendering any other components, encapusulate with:
          • <AuthContext.Provider value={[user, setUser]}>
            • (everything else you already had here)
          • </AuthContext.Provider>
    • Verify CORS is handled in AuthController (Java)
      • @CrossOrigin(origins = {"http://localhost:3000"})
    • Build out Login component
      • In terminal: npm i jwt-decode
      • import { useState, useContext } from "react";
      • import { useNavigate } from "react-router-dom";
      • import jwtDecode from "jwt-decode";
      • import AuthContext from "./AuthContext";
      • const [username, setUsername] = useState("");
      • const [password, setPassword] = useState("");
      • const [user, setUser] = useContext(AuthContext);
      • const navigate = useNavigate();
      • Build out a form for logging in
        • <form onSubmit={submitHandler}>
          • <label>Username:</label><br />
          • <input onChange={event => setUsername(event.target.value)}></input><br /><br />
          • <label>Password:</label><br />
          • <input type="password" onChange={event => setPassword(event.target.value)}></input><br /><br />
          • <button>Submit</button>
        • </form>
      • Create submit handler for form
        • function submitHandler(event) {
          • event.preventDefault()
          • fetch("http://localhost:8080/api/security/login", {
            • method: "POST",
            • headers: {
              • "Content-Type": "application/json"
            • },
            • body: JSON.stringify({
              • username, password
            • })
          • })
          • .then(response => {
            • if (response.status === 200) {
              • const { jwt_token } = response.json()
              • localStorage.setItem("token", jwt_token)
              • setUser({user: jwtDecode(jwt_token)})
              • navigate("/")
            • } else {
              • alert("Something bad");
            • }
          • })
          • .catch(rejection => alert(rejection))
        • }
      • Update App.js with new Login route
        • import Login from "./Login";
        • <Route path="/login" element={<Login />} />
      • Update Nav.js with new Login link
        • import { useConte xt } from 'react';
        • import AuthContext from './AuthContext';
        • const [userStatus, setUserStatus] = useContext(AuthContext);

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages