From e6e82189d23dc9c47de432703d01efd7d23b05c0 Mon Sep 17 00:00:00 2001 From: "nise.moe" Date: Sat, 17 Feb 2024 20:54:56 +0100 Subject: [PATCH] Integrating osu!auth --- nise-backend/pom.xml | 10 +++ .../com/nisemoe/nise/config/SecurityConfig.kt | 74 ++++++++++++++++ .../com/nisemoe/nise/config/WebConfig.kt | 1 + .../nisemoe/nise/controller/AuthController.kt | 23 +++++ .../com/nisemoe/nise/service/AuthService.kt | 35 ++++++++ .../src/main/resources/application.properties | 15 +++- nise-frontend/src/app/app.component.html | 9 +- nise-frontend/src/app/app.component.ts | 7 +- .../src/corelib/service/user.service.ts | 84 +++++++++++++++++++ 9 files changed, 253 insertions(+), 5 deletions(-) create mode 100644 nise-backend/src/main/kotlin/com/nisemoe/nise/config/SecurityConfig.kt create mode 100644 nise-backend/src/main/kotlin/com/nisemoe/nise/controller/AuthController.kt create mode 100644 nise-backend/src/main/kotlin/com/nisemoe/nise/service/AuthService.kt create mode 100644 nise-frontend/src/corelib/service/user.service.ts diff --git a/nise-backend/pom.xml b/nise-backend/pom.xml index a56e1bb..15314e4 100644 --- a/nise-backend/pom.xml +++ b/nise-backend/pom.xml @@ -23,6 +23,16 @@ + + + org.springframework.boot + spring-boot-starter-oauth2-client + + + org.springframework.boot + spring-boot-starter-security + + org.testcontainers diff --git a/nise-backend/src/main/kotlin/com/nisemoe/nise/config/SecurityConfig.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/config/SecurityConfig.kt new file mode 100644 index 0000000..b24a4f0 --- /dev/null +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/config/SecurityConfig.kt @@ -0,0 +1,74 @@ +package com.nisemoe.nise.config + +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.core.Authentication +import org.springframework.security.web.SecurityFilterChain +import org.springframework.security.web.authentication.AuthenticationSuccessHandler +import org.springframework.security.web.authentication.logout.LogoutSuccessHandler +import org.springframework.session.web.http.CookieSerializer +import org.springframework.session.web.http.DefaultCookieSerializer +import org.springframework.stereotype.Component +import java.util.concurrent.TimeUnit + + +@Configuration +@EnableWebSecurity +class SecurityConfig { + + @Value("\${COOKIE_DOMAIN_PATTERN:^.+?\\.nise\\.moe\$}") + private lateinit var domainNamePattern: String + + @Value("\${COOKIE_SECURE:false}") + private var cookieSecure: Boolean = false + + @Bean + fun cookieSerializer(): CookieSerializer? { + val serializer = DefaultCookieSerializer() + serializer.setCookieName("SESSION") + serializer.setCookiePath("/") + serializer.setDomainNamePattern(domainNamePattern) + serializer.setCookieMaxAge(TimeUnit.DAYS.toSeconds(30).toInt()) + serializer.setUseHttpOnlyCookie(cookieSecure) + serializer.setUseSecureCookie(cookieSecure) + + return serializer + } + + @Component + class CustomAuthenticationSuccessHandler : AuthenticationSuccessHandler { + + override fun onAuthenticationSuccess(request: HttpServletRequest?, response: HttpServletResponse?, authentication: Authentication?) { + response?.sendRedirect(System.getenv("ORIGIN")) + } + + } + + @Component + class CustomLogoutSuccessHandler : LogoutSuccessHandler { + + override fun onLogoutSuccess(request: HttpServletRequest?, response: HttpServletResponse?, authentication: Authentication?) { + response?.sendRedirect(System.getenv("ORIGIN")) + } + + } + + @Bean + fun filterChain(http: HttpSecurity): SecurityFilterChain? { + http + .csrf { csrf -> csrf.disable() } + .oauth2Login { oauthLogin -> + oauthLogin.successHandler(CustomAuthenticationSuccessHandler()) + } + .logout { logout -> + logout.logoutSuccessHandler(CustomLogoutSuccessHandler()) + } + return http.build() + } + +} diff --git a/nise-backend/src/main/kotlin/com/nisemoe/nise/config/WebConfig.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/config/WebConfig.kt index f47d7ad..8ddef8b 100644 --- a/nise-backend/src/main/kotlin/com/nisemoe/nise/config/WebConfig.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/config/WebConfig.kt @@ -17,6 +17,7 @@ class WebConfig: WebMvcConfigurer { override fun addCorsMappings(registry: CorsRegistry) { registry.addMapping("/**") .allowedOrigins(origin) + .allowCredentials(true) } } \ No newline at end of file diff --git a/nise-backend/src/main/kotlin/com/nisemoe/nise/controller/AuthController.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/controller/AuthController.kt new file mode 100644 index 0000000..d7d9086 --- /dev/null +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/controller/AuthController.kt @@ -0,0 +1,23 @@ +package com.nisemoe.nise.controller + +import com.nisemoe.nise.service.AuthService +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +class AuthController( + private val authService: AuthService +) { + + @GetMapping("/auth", produces = [MediaType.APPLICATION_JSON_VALUE]) + fun getUser(): ResponseEntity { + if(!this.authService.isLoggedIn()) + return ResponseEntity.status(401).build() + + val currentUser = this.authService.getCurrentUser() + return ResponseEntity.ok(currentUser) + } + +} diff --git a/nise-backend/src/main/kotlin/com/nisemoe/nise/service/AuthService.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/service/AuthService.kt new file mode 100644 index 0000000..8afe05e --- /dev/null +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/service/AuthService.kt @@ -0,0 +1,35 @@ +package com.nisemoe.nise.service + +import org.springframework.security.authentication.AnonymousAuthenticationToken +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.security.oauth2.core.user.DefaultOAuth2User +import org.springframework.stereotype.Service + +@Service +class AuthService { + + data class UserInfo( + val userId: Int, + val username: String + ) + + fun isLoggedIn(): Boolean { + val authentication = SecurityContextHolder.getContext().authentication + return !(authentication == null || authentication is AnonymousAuthenticationToken || authentication.principal == null) + } + + fun getCurrentUser(): UserInfo { + if(!this.isLoggedIn()) + throw IllegalStateException("User is not logged in") + + val authentication = SecurityContextHolder.getContext().authentication + val userDetails = authentication.principal as DefaultOAuth2User + + return UserInfo( + userId = userDetails.attributes["id"] as Int, + username = userDetails.attributes["username"] as String + ) + + } + +} \ No newline at end of file diff --git a/nise-backend/src/main/resources/application.properties b/nise-backend/src/main/resources/application.properties index 902aec0..b707a3e 100644 --- a/nise-backend/src/main/resources/application.properties +++ b/nise-backend/src/main/resources/application.properties @@ -15,4 +15,17 @@ server.http2.enabled=true spring.data.redis.host=${REDIS_HOST:redis} spring.data.redis.port=${REDIS_PORT:6379} spring.data.redis.repositories.enabled=false -spring.data.redis.database=${REDIS_DB:2} \ No newline at end of file +spring.data.redis.database=${REDIS_DB:2} + +# osu!auth + +spring.security.oauth2.client.registration.osu.clientId=${OSU_CLIENT_ID} +spring.security.oauth2.client.registration.osu.clientSecret=${OSU_CLIENT_SECRET} +spring.security.oauth2.client.registration.osu.redirect-uri=http://localhost:8080/login/oauth2/code/osu +spring.security.oauth2.client.registration.osu.authorization-grant-type=authorization_code +spring.security.oauth2.client.registration.osu.provider=osu + +spring.security.oauth2.client.provider.osu.authorization-uri=https://osu.ppy.sh/oauth/authorize +spring.security.oauth2.client.provider.osu.token-uri=https://osu.ppy.sh/oauth/token +spring.security.oauth2.client.provider.osu.user-info-uri=https://osu.ppy.sh/api/v2/me/osu +spring.security.oauth2.client.provider.osu.user-name-attribute=username \ No newline at end of file diff --git a/nise-frontend/src/app/app.component.html b/nise-frontend/src/app/app.component.html index 1e11def..bbe8bd4 100644 --- a/nise-frontend/src/app/app.component.html +++ b/nise-frontend/src/app/app.component.html @@ -14,7 +14,14 @@
- +
+ + hi, {{this.userService.currentUser?.username}} Logout + + + Login + +
diff --git a/nise-frontend/src/app/app.component.ts b/nise-frontend/src/app/app.component.ts index abaf6af..df51986 100644 --- a/nise-frontend/src/app/app.component.ts +++ b/nise-frontend/src/app/app.component.ts @@ -1,5 +1,6 @@ import {Component} from '@angular/core'; import {Router} from "@angular/router"; +import {UserService} from "../corelib/service/user.service"; @Component({ @@ -11,9 +12,9 @@ export class AppComponent { term: string = ''; - constructor(private router: Router) { - } - + constructor(private router: Router, + public userService: UserService + ) { } onSubmit(): void { this.router.navigate(['/u/' + this.term]) diff --git a/nise-frontend/src/corelib/service/user.service.ts b/nise-frontend/src/corelib/service/user.service.ts new file mode 100644 index 0000000..25119cc --- /dev/null +++ b/nise-frontend/src/corelib/service/user.service.ts @@ -0,0 +1,84 @@ +import {Injectable} from '@angular/core'; +import {HttpBackend, HttpClient} from "@angular/common/http"; +import {environment} from "../../environments/environment"; + +interface UserInfo { + userId: number; + username: string; +} + +@Injectable({ + providedIn: 'root' +}) +export class UserService { + + private httpClient: HttpClient; + + currentUser: UserInfo | null = null; + + loginCallback: () => void = () => {}; + logoutCallback: () => void = () => {}; + + constructor(private httpBackend: HttpBackend) { + this.httpClient = new HttpClient(httpBackend); + this.currentUser = this.loadCurrentUserFromLocalStorage(); + this.updateUser() + .catch(reason => console.debug(reason)); + } + + public doLogout(): void { + this.currentUser = null; + this.clearCurrentUserFromLocalStorage(); + this.updateUser().then( + () => this.logoutCallback() + ) + } + + isUserLoggedIn(): boolean { + return this.currentUser !== null; + } + + public updateUser(): Promise { + return new Promise((resolve, reject) => { + this.httpClient.get(`${environment.apiUrl}/auth`, {withCredentials: true}) + .subscribe({ + next: (user) => { + this.currentUser = user; + this.saveCurrentUserToLocalStorage(user); + this.loginCallback(); + resolve(user); + }, + error: (err) => { + if (err.status === 401) { + this.currentUser = null; + this.clearCurrentUserFromLocalStorage(); + } + reject(err); + } + }); + }); + } + + public getLoginUrl(): string { + return `${environment.apiUrl}/oauth2/authorization/osu` + } + + public getLogoutUrl(): string { + return `${environment.apiUrl}/logout` + } + + saveCurrentUserToLocalStorage(user: UserInfo) { + localStorage.setItem('currentUser', JSON.stringify(user)); + } + + loadCurrentUserFromLocalStorage(): UserInfo { + const savedUser = localStorage.getItem('currentUser'); + return savedUser ? JSON.parse(savedUser) : null; + } + + clearCurrentUserFromLocalStorage() { + localStorage.clear(); + document.cookie = 'SESSION=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; + } + +}