Integrating osu!auth
This commit is contained in:
parent
8b88277a9d
commit
e6e82189d2
@ -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>
|
||||||
|
|||||||
@ -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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -16,3 +16,16 @@ 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
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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])
|
||||||
|
|||||||
84
nise-frontend/src/corelib/service/user.service.ts
Normal file
84
nise-frontend/src/corelib/service/user.service.ts
Normal 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=/;';
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user