John-sCC / jcc_frontend

Apache License 2.0
0 stars 0 forks source link

1 26 2024 Meeting Notes #24

Open aidenhuynh opened 8 months ago

aidenhuynh commented 8 months ago

Accomplishments

Future plans

Goals

Pop quiz prep

drewreed2005 commented 8 months ago

"Show API access code and error handling, specifically redirect on 403"

I'll go over this stuff in this reply. It doesn't seem like this has anything at all to do with deployment, so I went over all of the relevant info I could find to include.

API Access Code

All API access control info can be found in ApiController files. For example, PersonApiController.java.

    /*
    GET List of People
    */
    @GetMapping("/")
    public ResponseEntity<List<Person>> getPeople() {
        return new ResponseEntity<>( repository.findAllByOrderByNameAsc(), HttpStatus.OK);
    }

    /*
    POST Aa record by Requesting Parameters from URI
     */
    @PostMapping( "/post")
    public ResponseEntity<Object> postPerson(@RequestParam("email") String email,
                                             @RequestParam("password") String password,
                                             @RequestParam("name") String name,
                                             @RequestParam("dob") String dobString) {
        Date dob;
        try {
            dob = new SimpleDateFormat("MM-dd-yyyy").parse(dobString);
        } catch (Exception e) {
            return new ResponseEntity<>(dobString +" error; try MM-dd-yyyy", HttpStatus.BAD_REQUEST);
        }
        // A person object WITHOUT ID will create a new record with default roles as student
        Person person = new Person(email, password, name, dob);
        personDetailsService.save(person);
        return new ResponseEntity<>(email +" is created successfully", HttpStatus.CREATED);
    }

These are all methods that have specific CRUD assignment (GET and POST for these) and URL mappings (/post, /{id}, etc.) that can be used to fetch the API data. This is all very basic stuff. The code within the method is what fetches/modifies the persistent data in the database on the backend according to the method specified by the URL.

Error Handling and 403 Redirect

This is where this specific topic is weird, since he says "specifically redirect on 403". A 403 error is essentially "Access Denied": it's the error that should be flagged if a user tries to make a call to a method without the proper authority to do so.

SecurityConfig Error Handler

The base Spring repository comes with specifications for error handling in CORS/authentication policy. It can be found in the SecurityConfig.java file.

The code below is from our Roles lesson backend, so it has specific roles-based authority requirements for some methods ("ROLE_ADMIN" for mvc update, delete, and API post and delete). In a non-customized backend, it just has .authenticated(), which simply requires basic user login.

    .authorizeHttpRequests(auth -> auth
    .requestMatchers("/authenticate").permitAll()
    .requestMatchers("/mvc/person/update/**", 
                                        "/mvc/person/delete/**").hasAnyAuthority("ROLE_ADMIN")
    .requestMatchers("/api/person/post/**", "/api/person/delete/**").hasAnyAuthority("ROLE_ADMIN")
    .requestMatchers("/**").permitAll()
    )

A person's role found by searching for the user associated with the username (email) stored in the JWT received after signing in. This is stuff from our lesson, so I won't go too far into it, but essentially, the user's roles are searched through when a user is loaded by username (email, for whatever reason) and then used as authorities when a request is made.

If the role requirements are not met, the error handler code is called (found below the code above and some other headers in SecurityConfig.java):

         // session won't be used to store user's state.
    .exceptionHandling(exceptions -> exceptions
        .authenticationEntryPoint(jwtAuthenticationEntryPoint)
        )

Keep the code above in mind for the following section.

403 Redirect

The base Spring repository that we all clone doesn't come with an explicit redirect system for 403 errors. The code provides an entry point for authentication by interpreting the content of the JWT with the object jwtAuthenticationEntryPoint that's imported from the JwtAuthenticationEntryPoint.java file:

@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {

        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
    }
}

If we wanted to give a specific URL redirect in response to a 403, error, we could change the SecurityConfig.java code for error handling to something like the following:

public class CustomAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response,
                       AccessDeniedException accessDeniedException) throws IOException, ServletException {
        response.sendRedirect("/access-denied"); // Redirect to a specific page for 403 errors
    }
}

This would be its own file that would be imported and added to the SecurityConfig object, as shown in the following code:

    .exceptionHandling(exceptions -> exceptions
        .accessDeniedHandler(new CustomAccessDeniedHandler())
        .authenticationEntryPoint(jwtAuthenticationEntryPoint)
    )
Toby-Leeder commented 8 months ago

Describe managing CORS policies through Nginx and Java (TOBY)

CORS stands for Cross Origin Resource Sharing. You've probably seen errors when both your backend and frontend are running locally, which are CORS errors since CORS blocks any request from the same origin. However what Mort is talking about is CORS between different origins. There are actually two ways we define CORS, and we actually can't use both of them since it breaks sometimes. The first is in our NGINX configuration file, in which we define the allowed origins.

server {
   listen 80;
    listen [::]:80;
    server_name -----.stu.nighthawkcodingsociety.com ; # Change server name to the one on R53
    # Configure CORS Headers
    location / { 
        proxy_pass http://localhost:8---; # Change port to port on docker
        # Simple requests
        if ($request_method ~* "(GET|POST|PUT|DELETE)") { # Customize Request methods based on your needs
                add_header "Access-Control-Allow-Origin"  *;
        }
        # Preflighted requests 
        if ($request_method = OPTIONS ) {
                add_header "Access-Control-Allow-Origin"  *;
                add_header "Access-Control-Allow-Methods" "GET, POST, PUT, DELETE, OPTIONS, HEAD"; # Make sure the request methods above match here
                add_header "Access-Control-Allow-Headers" "Authorization, Origin, X-Requested-With, Content-Type, Accept";
                return 200;
        }
    }
}

Here you can see that we add headers to our proxy pass which define what we can and can't do. The header "Access-Control-Allow-Origin" defines what websites can call the backend. Currently it's any website, but we will change this to only be our frontend. You can also see other headers like allowed headers and methods. We never really use these, but essentially these define both the request types that we're allowed to make and what extra header types we have like authorization, origin, etc. The other way we change the allowed origins is in our Spring backend project.

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**").allowedOrigins("https://nighthawkcoders.github.io", "http://localhost:4000");
    }

This is in our MvcConfig.java file. This method essentially does the same thing as what we did above. However, sometimes if you define this and the nginx headers together, it gives you an error saying you have duplicate headers.

raymondYsheng commented 8 months ago

Show JWT signup and/or login process The JWT Token acts as a cookie to authenticate users.

  1. User Login Form: The user provides necessary information (e.g., username, email, password) through frontend.

  2. Server-Side Validation: The server validates the user-provided information, ensuring that the data is correct and meets the required criteria (/authenticate).

  3. JWT Generation (DREW EDIT: I'm editing this to mention that, when creating the JWT, it takes the user info from the person corresponding to the one whose information was used in the /authenticate request and adds it to the JWT, specifically the email. This email is then used to fetch the Roles that act as authorities when another backend request is made. See my reply for more info about this.) After successful registration, the server generates a JWT token, in our case specifying: .httpOnly(true) (Prevent JS from accessing cookie) .secure(true) (Send only over HTTPS) .path("/") (Set cookie path to root) .maxAge(3600) (Set cookie expiration to 1 hour)

  4. JWT Sent to Frontend: The server sends the generated JWT to the frontend.

For a demo basically just start the backend, login using "/login", then open Inspect, Application, Cookies and find the cookie labelled jwt: image Then you can try CRUD-ing a user to see if you have role permissions if the backend has that set up.

Ishi-Singh commented 8 months ago

Explain security configuration rules that are required for access

Spring Security is a framework used to keep Java applications safe. It helps with authentication, authorization, and other security tasks such as securing endpoints.

Authentication: Verify the identity of users before granting access. JSON Web Token (JWT) are typically used to do this. Authorization: Define access levels and permissions for authenticated users. Assign roles and responsibilities to control what actions users can perform. eg. Teacher role having update/delete permissions while Student role can't do that.

The loadUserByUsername method is called when a user tries to authenticate during login. The purpose of this method is to load user details based on their username (in this case, email) from the database. It fetches the user details from the database, retrieves the roles associated with the user, and creates a collection of SimpleGrantedAuthority objects based on those roles. These authorities are then used to build a UserDetails object.

/* UserDetailsService Overrides and maps Person & Roles POJO into Spring Security */
@Override
public org.springframework.security.core.userdetails.UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
    Person person = personJpaRepository.findByEmail(email); // setting variable user equal to the method finding the username in the database
    if(person==null) {
        throw new UsernameNotFoundException("User not found with username: " + email);
    }
    Collection<SimpleGrantedAuthority> authorities = new ArrayList<>();
    person.getRoles().forEach(role -> { //loop through roles
        authorities.add(new SimpleGrantedAuthority(role.getName())); //create a SimpleGrantedAuthority by passed in role, adding it all to the authorities list, list of roles gets past in for spring security
    });
    // train spring security to User and Authorities
    return new org.springframework.security.core.userdetails.User(person.getEmail(), person.getPassword(), authorities);
}

JwtApiController.java contains an endpoint for user authentication (/authenticate). After successfully authenticating a JWT token is generated using the user details from the PersonDetailsService

@PostMapping("/authenticate")
    public ResponseEntity<?> createAuthenticationToken(@RequestBody Person authenticationRequest) throws Exception {
        authenticate(authenticationRequest.getEmail(), authenticationRequest.getPassword());
        final UserDetails userDetails = personDetailsService
                .loadUserByUsername(authenticationRequest.getEmail()); // HERE IT IS!!
        final String token = jwtTokenUtil.generateToken(userDetails);
        final ResponseCookie tokenCookie = ResponseCookie.from("jwt", token)
            .httpOnly(true)
            .secure(true)
            .path("/")
            .maxAge(3600)
            .sameSite("None; Secure")
            .build();
        return ResponseEntity.ok().header(HttpHeaders.SET_COOKIE, tokenCookie.toString()).build();
    }

Now we can enforce role-based access control; the update and delete endpoints are restricted to users with the "ROLE_ADMIN" role. Other endpoints have no restriction.

public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .csrf(csrf -> csrf
            .disable()
        )
        // list the requests/endpoints need to be authenticated
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/authenticate").permitAll()
            .requestMatchers("/mvc/person/update/**", "/mvc/person/delete/**").hasAnyAuthority("ROLE_ADMIN") // must be admin for these
            .requestMatchers("/api/person/post/**", "/api/person/delete/**").hasAnyAuthority("ROLE_ADMIN")
            .requestMatchers("/**").permitAll()
        )

Spring Security using Java Web Tokens Competition Spring Roles| Student | P1 & P3

Ekamjot-Kaire commented 8 months ago

Route 53

Route 53 is the built in DNS (Domain Name System) for AWS.




1) When a website's domain name is entered into your browser, the device queries a DNS recursive resolver (which finds the IP address connected to the domain name)

2) The DNS recursive resolver contacts a root nameserver (a master server that maintains a list of all top-level domains (TLDs))

- The root nameserver doesn't have the specific IP address for the requested domain, but it can direct the resolver to the appropriate TLD nameserver

- The DNS recursive resolver, with the information from the root nameserver, contacts the TLD nameserver of the domain entered by the user

- The TLD nameserver contains info about the authoritative nameservers 

3) The TLD nameserver directs the DNS recursive resolver to the authoritative nameserver for that domain

4) The authoritative nameserver returns the IP address for the domain to the DNS recursive resolver, which then sends this IP address to the user

5) The user's device, now with the correct IP address, contacts the main website server, and the website can load

Process for setting up your own domain in AWS:

1) Log in to AWS 2) Go to the "Route 53" service. 3) Create a Hosted Zone:

Click on "Create Hosted Zone." Enter your domain name and click "Create." Make suer to note down the name servers provided by Route 53, which will be used in the next step

4) Update Name Servers:

Find the option to manage DNS or name servers on the website you registered a domain from Replace the existing name servers with the ones provided by Route 53 when you created the hosted zone.

5) Configure DNS Records

In the AWS Route 53 console, go to your hosted zone. Add DNS Records: Click on "Create Record Set." Add necessary DNS records like A (IPv4 address), CNAME (canonical name), etc.

Creating the NGINX file:

Navigate to /etc/nginx/sites-available directory in your AWS terminal


Create the NGINX config file using the command sudo nano *uniquename*

This is what the file should look like:

server {
   listen 80;
    listen [::]:80;
    server_name -----.duckdns.org; # CHANGE SERVER NAME TO YOUR REGISTERED DOMAIN
    location / {
        proxy_pass http://localhost:8---; # CHANGE PORT TO YOUR UNIQUE PORT
        # Simple requests
        if ($request_method ~* "(GET|POST|PUT|DELETE)") { # Customize Request methods based on your needs
                add_header "Access-Control-Allow-Origin"  *;
        }
        # Preflighted requests
        if ($request_method = OPTIONS ) {
                add_header "Access-Control-Allow-Origin"  *;
                add_header "Access-Control-Allow-Methods" "GET, POST, PUT, DELETE, OPTIONS, HEAD"; # Make sure the request methods above match here
                add_header "Access-Control-Allow-Headers" "Authorization, Origin, X-Requested-With, Content-Type, Accept";
                return 200;
        }
    }
}

To save changes, ctl X or cmd X, then y, then enter


Create a symbolic link: cd /etc/nginx/sites-enabled, then sudo ln -s /etc/nginx/sites-available/uniquename /etc/nginx/sites-enabled (change uniquename to your nginx config file name)


Validate by running: sudo nginx -t

Restart nginx by running sudo systemctl restart nginx

Test in Browser

KKcbal commented 8 months ago

POJO - Plain Old Java Object

changes:

aidenhuynh commented 8 months ago

Describe Docker

Docker is an operating system for servers, essentially acting like a virtual machine but for servers. It is widely used because of its simplicity.

Commands

Updating Docker

Get latest docker curl -fsSL https://get.docker.com/ | sh

Check version docker -v

Process

  1. Pull the latest version of the Docker image from the container registry.
  2. Stop the running container(s) of the old version. (docker-compose down)
  3. Remove the old container(s) if necessary.
  4. Run a new container with the updated image, using the appropriate configurations. (docker-compose up -d)
  5. Ensure any necessary data or configurations are migrated or applied.
Toby-Leeder commented 8 months ago
server {
   listen 80;
    listen [::]:80;
    server_name -----.stu.nighthawkcodingsociety.com ; # Change server name to the one on R53
    # Configure CORS Headers
    location / { 
        proxy_pass http://localhost:8---; # Change port to port on docker
        # Simple requests
        if ($request_method ~* "(GET|POST|PUT|DELETE)") { # Customize Request methods based on your needs
                add_header "Access-Control-Allow-Origin"  *;
        }
        # Preflighted requests 
        if ($request_method = OPTIONS ) {
                add_header "Access-Control-Allow-Origin"  
                add_header "Access-Control-Allow-Methods" "GET, POST, PUT, DELETE, OPTIONS, HEAD"; # Make sure the request methods above match here
                add_header "Access-Control-Allow-Headers" "Authorization, Origin, X-Requested-With, Content-Type, Accept";
                return 200;
        }
    }
}

Here is where we set up the reverse proxy. Basically a reverse proxy intercepts the web browser requests to redirect to the backend. Server_name defines what the name of the server is, we use this to define the domain name that we want to use. the proxy_pass then forwards the request to this server to the local backend, which is where the backend is actually running.