Commit b12c047e authored by Filip Bugoš's avatar Filip Bugoš
Browse files

Merge branch 'OAuth_v2' into 'milestone-3'

feat: OAuth 07/05/23

See merge request !70
parents 99f9b4bd 3e9ae9cc
Loading
Loading
Loading
Loading
Loading

OAuth/Dockerfile

0 → 100644
+10 −0
Original line number Diff line number Diff line
FROM maven:3.8.3-openjdk-17 AS build

WORKDIR /app
COPY ./.. /app
RUN mvn clean install

WORKDIR ./core

EXPOSE 8082
CMD ["mvn", "spring-boot:run"]
 No newline at end of file
+1 −0
Original line number Diff line number Diff line
openapi: "3.0.3"
 No newline at end of file

OAuth/pom.xml

0 → 100644
+174 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="UTF-8"?>
<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>
    <parent>
        <artifactId>smart-energy-management-system</artifactId>
        <groupId>cz.muni.fi.pa165</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>

    <artifactId>OAuth</artifactId>
    <name>OAuth</name>
    <description>OIDC Relying Party / OAuth 2 Client implemented in Spring Security</description>

    <build>
        <defaultGoal>spring-boot:run</defaultGoal>
        <!-- name of executable JAR file -->
        <finalName>confidential_client</finalName>

        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <!-- https://docs.spring.io/spring-boot/docs/current/reference/html/deployment.html#deployment.installing -->
                    <executable>true</executable>
                </configuration>
            </plugin>
            <!-- run integration tests in "mvn verify" phase -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-failsafe-plugin</artifactId>
            </plugin>

            <plugin>
                <groupId>org.openapitools</groupId>
                <artifactId>openapi-generator-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <goals>
                            <goal>generate</goal>
                        </goals>
                        <configuration>
                            <!-- see https://github.com/OpenAPITools/openapi-generator/blob/master/modules/openapi-generator-maven-plugin/README.md -->
                            <inputSpec>${project.basedir}/../openapi.yaml</inputSpec>
                            <generatorName>java</generatorName>
                            <verbose>false</verbose>
                            <generateApiTests>false</generateApiTests>
                            <generateModelTests>false</generateModelTests>
                            <generateApiDocumentation>false</generateApiDocumentation>
                            <generateModelDocumentation>false</generateModelDocumentation>
                            <configOptions>
                                <annotationLibrary>none</annotationLibrary>
                                <!-- see https://openapi-generator.tech/docs/generators/java/ -->
                                <library>native</library>
                                <hideGenerationTimestamp>true</hideGenerationTimestamp>
                            </configOptions>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

    <dependencies>
        <!-- Spring MVC -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- Thymeleaf for HTML pages templates -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <!-- Thymeleaf layout -->
        <dependency>
            <groupId>nz.net.ultraq.thymeleaf</groupId>
            <artifactId>thymeleaf-layout-dialect</artifactId>
        </dependency>
        <!-- validation for forms -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        <!-- for Spring application.yml properties handling -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <!-- OAuth2/OIDC client -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-oauth2-client</artifactId>
        </dependency>
        <!-- web jars for Bootstrap and jQuery -->
        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>webjars-locator-core</artifactId>
        </dependency>
        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>bootstrap</artifactId>
            <version>5.2.3</version>
        </dependency>
        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>jquery</artifactId>
            <version>3.6.4</version>
        </dependency>
        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>js-cookie</artifactId>
            <version>3.0.1</version>
        </dependency>
        <dependency>
            <groupId>cz.muni.fi.pa165</groupId>
            <artifactId>models</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency><!-- OpenAPI client -->
        <dependency>
            <groupId>io.swagger</groupId>
            <artifactId>swagger-annotations</artifactId>
            <version>1.6.10</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-annotations</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.datatype</groupId>
            <artifactId>jackson-datatype-jsr310</artifactId>
        </dependency>
        <dependency>
            <groupId>com.google.code.findbugs</groupId>
            <artifactId>jsr305</artifactId>
            <version>3.0.2</version>
        </dependency>
        <dependency>
            <groupId>jakarta.annotation</groupId>
            <artifactId>jakarta.annotation-api</artifactId>
            <scope>provided</scope>
            <version>2.1.1</version>
        </dependency>
        <dependency>
            <groupId>javax.annotation</groupId>
            <artifactId>javax.annotation-api</artifactId>
            <version>1.3.2</version>
        </dependency>

        <!-- for testing -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
        </dependency>

    </dependencies>

</project>
+162 −0
Original line number Diff line number Diff line
package cz.muni.pa165.oauth2.client;

import com.nimbusds.jose.JWSObject;
import com.nimbusds.jose.Payload;
import cz.muni.fi.pa165.model.dto.user.UserCreateDto;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.GetMapping;
import org.apache.http.entity.StringEntity;

import java.io.IOException;
import java.net.http.HttpClient;
import java.text.ParseException;
import java.util.Map;

import static org.hibernate.validator.internal.util.Contracts.assertTrue;

/**
 * Spring MVC Controller.
 * Handles HTTP requests by preparing data in model and passing it to Thymeleaf HTML templates.
 */
@Controller
public class MainController {

    private static final Logger log = LoggerFactory.getLogger(MainController.class);

    /**
     * Home page accessible even to non-authenticated users. Displays user personal data.
     */
    @GetMapping("/")
    public String index(Model model, @AuthenticationPrincipal OidcUser user) {
        log.debug("********************************************************");
        log.debug("* index() called                                       *");
        log.debug("********************************************************");
        log.debug("user {}", user == null ? "is anonymous" : user.getSubject());
        log.debug("token {}", user == null ? "is anonymous" : user.getIdToken().getTokenValue());

        // put obtained user data into a model attribute named "user"
        model.addAttribute("user", user);

        // put issuer name into a model attribute named "issuerName"
        if (user != null) {
            model.addAttribute("issuerName",
                    "https://oidc.muni.cz/oidc/".equals(user.getIssuer().toString()) ? "MUNI" : "Google");

            model.addAttribute("token", user.getIdToken().getTokenValue());
        }

        // return the name of a Thymeleaf HTML template that
        // will be searched in src/main/resources/templates with .html suffix
        return "index";
    }

    /**
     * Home page accessible even to non-authenticated users. Displays user personal data.
     */
    @GetMapping("/register")
    public String register(Model model, @AuthenticationPrincipal OidcUser user) {
        log.debug("********************************************************");
        log.debug("* register() called                                       *");
        log.debug("********************************************************");
        log.debug("user {}", user == null ? "is anonymous" : user.getSubject());
        log.debug("token {}", user == null ? "is anonymous" : user.getIdToken().getTokenValue());

        // put obtained user data into a model attribute named "user"
        model.addAttribute("user", user);

        // put issuer name into a model attribute named "issuerName"
        if (user != null) {
            if (!Registrate(user.getIdToken().getTokenValue())){
                return "error";
            };

            model.addAttribute("issuerName",
                    "https://oidc.muni.cz/oidc/".equals(user.getIssuer().toString()) ? "MUNI" : "Google");

            model.addAttribute("token", user.getIdToken().getTokenValue());
        }

        // return the name of a Thymeleaf HTML template that
        // will be searched in src/main/resources/templates with .html suffix
        return "index";
    }

    private Boolean Registrate(String token) {
        String payload = CreatePayload(token);
        StringEntity entity = new StringEntity(payload,
                ContentType.APPLICATION_JSON);

        CloseableHttpClient httpClient = HttpClientBuilder.create().build();
        HttpPost request = new HttpPost("http://localhost:8080/api/user");
        request.setEntity(entity);
        request.setHeader("Content-Type", "application/json");
        request.setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + token);

        HttpResponse response = null;
        try{
            response = httpClient.execute(request);
            // assert 201
        }
        catch (Exception e){
            return false;
        }

        return true;
    }

    private String CreatePayload(String token){
        UserCreateDto userCreateDto = GetUserCreateDto(token);

        String payload = String.format("""
                {
                    "username": "%s",
                    "email": "%s",
                    "firstName": "%s",
                    "lastName": "%s"
                }
                """, userCreateDto.getUsername(),
                userCreateDto.getEmail(),
                userCreateDto.getFirstName(),
                userCreateDto.getLastName());

        return payload;
    }

    private UserCreateDto GetUserCreateDto(String token){

        JWSObject jwsObject = null;
        try {
            jwsObject = JWSObject.parse(token);
        } catch (ParseException e) {
            throw new RuntimeException(e);
        }
        Payload payload = jwsObject.getPayload();
        Map<String, Object> jsonObject = payload.toJSONObject();

        String email = (String) jsonObject.get("email");
        String userName = (String) jsonObject.get("name"); // or preferred_username
        String firstName = (String) jsonObject.get("given_name");
        String lastName = (String) jsonObject.get("family_name");

        UserCreateDto userCreateDto = new UserCreateDto();
        userCreateDto.setEmail(email);
        userCreateDto.setUsername(userName);
        userCreateDto.setFirstName(firstName);
        userCreateDto.setLastName(lastName);

        return userCreateDto;
    }

}
 No newline at end of file
+136 −0
Original line number Diff line number Diff line
package cz.muni.pa165.oauth2.client;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.context.ServletWebServerInitializedEvent;
import org.springframework.context.annotation.Bean;
import org.springframework.context.event.EventListener;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.client.oidc.web.logout.OidcClientInitiatedLogoutSuccessHandler;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;

import java.io.IOException;

@SpringBootApplication
public class MyWebApp {

    private static final Logger log = LoggerFactory.getLogger(MyWebApp.class);

    public static void main(String[] args) {
        SpringApplication.run(MyWebApp.class, args);
    }

    /**
     * Configuration of Spring Security. Sets up OAuth2/OIDC authentication
     * for all URLS except a list of public ones.
     */
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                .authorizeHttpRequests(x -> x
                        // allow anonymous access to listed URLs
                        .requestMatchers("/", "/error", "/robots.txt", "/style.css", "/favicon.ico", "/webjars/**").permitAll()
                        // all other requests must be authenticated
                        .anyRequest().authenticated()
                )
                .oauth2Login(x -> x
                        // our custom handler for successful logins
                        .successHandler(authenticationSuccessHandler())
                )
                .logout(x -> x
                        // After we log out, redirect to the root page, by default Spring will send you to /login?logout
                         .logoutSuccessUrl("/")
                        // after local logout, do also remote logout at the OIDC Provider too
                        .logoutSuccessHandler(oidcLogoutSuccessHandler())
                )
                .csrf(c -> c
                        //set CSRF token cookie "XSRF-TOKEN" with httpOnly=false that can be read by JavaScript
                        .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
                        //replace the default XorCsrfTokenRequestAttributeHandler with one that can use value from the cookie
                        .csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler())
                )
        ;
        return httpSecurity.build();
    }

    /**
     * Handler called when OIDC login successfully completes.
     * It extends the default SavedRequestAwareAuthenticationSuccessHandler that saves the access token
     * to the session.
     * This handler just prints the available info about user to the log and calls its parent implementation.
     * @see SavedRequestAwareAuthenticationSuccessHandler
     */
    @Bean
    public AuthenticationSuccessHandler authenticationSuccessHandler() {
        return new SavedRequestAwareAuthenticationSuccessHandler() {
            @Override
            public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse res, Authentication auth) throws ServletException, IOException {
                if (auth instanceof OAuth2AuthenticationToken token
                        && token.getPrincipal() instanceof OidcUser user) {
                    log.debug("********************************************************");
                    log.debug("* user successfully logged in                          *");
                    log.debug("********************************************************");
                    log.info("user.issuer: {}", user.getIssuer());
                    log.info("user.subject: {}", user.getSubject());
                    log.info("user.fullName: {}", user.getFullName());
                    log.info("user.givenName: {}", user.getGivenName());
                    log.info("user.familyName: {}", user.getFamilyName());
                    log.info("user.gender: {}", user.getGender());
                    log.info("user.email: {}", user.getEmail());
                    log.info("user.locale: {}", user.getLocale());
                    log.info("user.zoneInfo: {}", user.getZoneInfo());
                    log.info("user.preferredUsername: {}", user.getPreferredUsername());
                    log.info("user.issuedAt: {}", user.getIssuedAt());
                    log.info("user.authenticatedAt: {}", user.getAuthenticatedAt());
                    log.info("user.claimAsListString(\"eduperson_scoped_affiliation\"): {}", user.getClaimAsStringList("eduperson_scoped_affiliation"));
                    log.info("user.attributes.acr: {}", user.<String>getAttribute("acr"));
                    log.info("user.attributes: {}", user.getAttributes());
                    log.info("user.authorities: {}", user.getAuthorities());
                }
                super.onAuthenticationSuccess(req, res, auth);
            }
        };
    }


    /**
     * Handler called when local logout successfully completes.
     * It initiates also a complete remote logout at the Authorization Server.
     * @see OidcClientInitiatedLogoutSuccessHandler
     */
    private OidcClientInitiatedLogoutSuccessHandler oidcLogoutSuccessHandler() {
        OidcClientInitiatedLogoutSuccessHandler successHandler =
                new OidcClientInitiatedLogoutSuccessHandler(clientRegistrationRepository);
        successHandler.setPostLogoutRedirectUri("http://localhost:8080/");
        return successHandler;
    }

    @Autowired
    private ClientRegistrationRepository clientRegistrationRepository;


    /**
     * Display a hint in the log.
     */
    @EventListener
    public void onApplicationEvent(final ServletWebServerInitializedEvent event) {
        log.info("**************************");
        log.info("visit http://localhost:{}/", event.getWebServer().getPort());
        log.info("**************************");
    }

}
Loading