Integrating osu!auth

This commit is contained in:
nise.moe 2024-02-17 20:54:56 +01:00
parent 8b88277a9d
commit e6e82189d2
9 changed files with 253 additions and 5 deletions

View File

@ -23,6 +23,16 @@
</properties> </properties>
<dependencies> <dependencies>
<!-- Security / Login -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Test containers --> <!-- Test containers -->
<dependency> <dependency>
<groupId>org.testcontainers</groupId> <groupId>org.testcontainers</groupId>

View File

@ -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()
}
}

View File

@ -17,6 +17,7 @@ class WebConfig: WebMvcConfigurer {
override fun addCorsMappings(registry: CorsRegistry) { override fun addCorsMappings(registry: CorsRegistry) {
registry.addMapping("/**") registry.addMapping("/**")
.allowedOrigins(origin) .allowedOrigins(origin)
.allowCredentials(true)
} }
} }

View File

@ -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<AuthService.UserInfo> {
if(!this.authService.isLoggedIn())
return ResponseEntity.status(401).build()
val currentUser = this.authService.getCurrentUser()
return ResponseEntity.ok(currentUser)
}
}

View File

@ -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
)
}
}

View File

@ -15,4 +15,17 @@ server.http2.enabled=true
spring.data.redis.host=${REDIS_HOST:redis} spring.data.redis.host=${REDIS_HOST:redis}
spring.data.redis.port=${REDIS_PORT:6379} spring.data.redis.port=${REDIS_PORT:6379}
spring.data.redis.repositories.enabled=false spring.data.redis.repositories.enabled=false
spring.data.redis.database=${REDIS_DB:2} 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

View File

@ -14,7 +14,14 @@
<form (ngSubmit)="onSubmit()"> <form (ngSubmit)="onSubmit()">
<input type="text" [(ngModel)]="term" [ngModelOptions]="{standalone: true}" id="nise-osu-username" required minlength="2" maxlength="50" placeholder="Search for users..."> <input type="text" [(ngModel)]="term" [ngModelOptions]="{standalone: true}" id="nise-osu-username" required minlength="2" maxlength="50" placeholder="Search for users...">
</form> </form>
<div style="margin-top: 8px">
<ng-container *ngIf="this.userService.isUserLoggedIn()">
hi, {{this.userService.currentUser?.username}} <a [href]="this.userService.getLogoutUrl()">Logout</a>
</ng-container>
<ng-container *ngIf="!this.userService.isUserLoggedIn()">
<a [href]="this.userService.getLoginUrl()">Login</a>
</ng-container>
</div>
</div> </div>
</div> </div>
<router-outlet></router-outlet> <router-outlet></router-outlet>

View File

@ -1,5 +1,6 @@
import {Component} from '@angular/core'; import {Component} from '@angular/core';
import {Router} from "@angular/router"; import {Router} from "@angular/router";
import {UserService} from "../corelib/service/user.service";
@Component({ @Component({
@ -11,9 +12,9 @@ export class AppComponent {
term: string = ''; term: string = '';
constructor(private router: Router) { constructor(private router: Router,
} public userService: UserService
) { }
onSubmit(): void { onSubmit(): void {
this.router.navigate(['/u/' + this.term]) this.router.navigate(['/u/' + this.term])

View File

@ -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<any> {
return new Promise((resolve, reject) => {
this.httpClient.get<UserInfo>(`${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=/;';
}
}