diff --git a/src/main/kotlin/dev/usbharu/hideout/Application.kt b/src/main/kotlin/dev/usbharu/hideout/Application.kt index 4a6d4f68..e94ec8a5 100644 --- a/src/main/kotlin/dev/usbharu/hideout/Application.kt +++ b/src/main/kotlin/dev/usbharu/hideout/Application.kt @@ -49,19 +49,19 @@ val Application.property: Application.(propertyName: String) -> String @Suppress("unused", "LongMethod") fun Application.parent() { Config.configData = ConfigData( - url = property("hideout.url"), - objectMapper = jacksonObjectMapper().enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY) - .setSerializationInclusion(JsonInclude.Include.NON_EMPTY) - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + url = property("hideout.url"), + objectMapper = jacksonObjectMapper().enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY) + .setSerializationInclusion(JsonInclude.Include.NON_EMPTY) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) ) val module = org.koin.dsl.module { single { Database.connect( - url = property("hideout.database.url"), - driver = property("hideout.database.driver"), - user = property("hideout.database.username"), - password = property("hideout.database.password") + url = property("hideout.database.url"), + driver = property("hideout.database.driver"), + user = property("hideout.database.username"), + password = property("hideout.database.password") ) } single { @@ -84,11 +84,11 @@ fun Application.parent() { single { TwitterSnowflakeIdGenerateService } single { JwkProviderBuilder(Config.configData.url).cached( - 10, - 24, - TimeUnit.HOURS + 10, + 24, + TimeUnit.HOURS ) - .rateLimited(10, 1, TimeUnit.MINUTES).build() + .rateLimited(10, 1, TimeUnit.MINUTES).build() } } configureKoin(module, HideoutModule().module) @@ -102,19 +102,20 @@ fun Application.parent() { configureSerialization() register(inject().value) configureSecurity( - inject().value, - inject().value, - inject().value, - inject().value, - inject().value, + inject().value, + inject().value ) configureRouting( - httpSignatureVerifyService = inject().value, - activityPubService = inject().value, - userService = inject().value, - activityPubUserService = inject().value, - postService = inject().value, - userApiService = inject().value, + httpSignatureVerifyService = inject().value, + activityPubService = inject().value, + userService = inject().value, + activityPubUserService = inject().value, + postService = inject().value, + userApiService = inject().value, + userAuthService = inject().value, + userRepository = inject().value, + jwtService = inject().value, + metaService = inject().value ) } diff --git a/src/main/kotlin/dev/usbharu/hideout/plugins/Routing.kt b/src/main/kotlin/dev/usbharu/hideout/plugins/Routing.kt index 5931ad53..b45fc1a7 100644 --- a/src/main/kotlin/dev/usbharu/hideout/plugins/Routing.kt +++ b/src/main/kotlin/dev/usbharu/hideout/plugins/Routing.kt @@ -1,8 +1,10 @@ package dev.usbharu.hideout.plugins +import dev.usbharu.hideout.repository.IUserRepository import dev.usbharu.hideout.routing.activitypub.inbox import dev.usbharu.hideout.routing.activitypub.outbox import dev.usbharu.hideout.routing.activitypub.usersAP +import dev.usbharu.hideout.routing.api.internal.v1.auth import dev.usbharu.hideout.routing.api.internal.v1.posts import dev.usbharu.hideout.routing.api.internal.v1.users import dev.usbharu.hideout.routing.wellknown.webfinger @@ -11,6 +13,9 @@ import dev.usbharu.hideout.service.activitypub.ActivityPubUserService import dev.usbharu.hideout.service.api.IPostApiService import dev.usbharu.hideout.service.api.IUserApiService import dev.usbharu.hideout.service.auth.HttpSignatureVerifyService +import dev.usbharu.hideout.service.auth.IJwtService +import dev.usbharu.hideout.service.core.IMetaService +import dev.usbharu.hideout.service.user.IUserAuthService import dev.usbharu.hideout.service.user.IUserService import io.ktor.server.application.* import io.ktor.server.plugins.autohead.* @@ -18,12 +23,16 @@ import io.ktor.server.routing.* @Suppress("LongParameterList") fun Application.configureRouting( - httpSignatureVerifyService: HttpSignatureVerifyService, - activityPubService: ActivityPubService, - userService: IUserService, - activityPubUserService: ActivityPubUserService, - postService: IPostApiService, - userApiService: IUserApiService + httpSignatureVerifyService: HttpSignatureVerifyService, + activityPubService: ActivityPubService, + userService: IUserService, + activityPubUserService: ActivityPubUserService, + postService: IPostApiService, + userApiService: IUserApiService, + userAuthService: IUserAuthService, + userRepository: IUserRepository, + jwtService: IJwtService, + metaService: IMetaService ) { install(AutoHeadResponse) routing { @@ -34,6 +43,7 @@ fun Application.configureRouting( route("/api/internal/v1") { posts(postService) users(userService, userApiService) + auth(userAuthService, userRepository, jwtService, metaService) } } } diff --git a/src/main/kotlin/dev/usbharu/hideout/plugins/Security.kt b/src/main/kotlin/dev/usbharu/hideout/plugins/Security.kt index a98eee18..2bba7f6c 100644 --- a/src/main/kotlin/dev/usbharu/hideout/plugins/Security.kt +++ b/src/main/kotlin/dev/usbharu/hideout/plugins/Security.kt @@ -2,19 +2,12 @@ package dev.usbharu.hideout.plugins import com.auth0.jwk.JwkProvider import dev.usbharu.hideout.config.Config -import dev.usbharu.hideout.domain.model.hideout.form.RefreshToken -import dev.usbharu.hideout.domain.model.hideout.form.UserLogin -import dev.usbharu.hideout.exception.UserNotFoundException -import dev.usbharu.hideout.repository.IUserRepository -import dev.usbharu.hideout.service.auth.IJwtService import dev.usbharu.hideout.service.core.IMetaService -import dev.usbharu.hideout.service.user.IUserAuthService import dev.usbharu.hideout.util.JsonWebKeyUtil import io.ktor.http.* import io.ktor.server.application.* import io.ktor.server.auth.* import io.ktor.server.auth.jwt.* -import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* @@ -22,11 +15,8 @@ const val TOKEN_AUTH = "jwt-auth" @Suppress("MagicNumber") fun Application.configureSecurity( - userAuthService: IUserAuthService, - metaService: IMetaService, - userRepository: IUserRepository, - jwtService: IJwtService, - jwkProvider: JwkProvider + jwkProvider: JwkProvider, + metaService: IMetaService ) { val issuer = Config.configData.url install(Authentication) { @@ -48,38 +38,13 @@ fun Application.configureSecurity( } routing { - post("/login") { - val loginUser = call.receive() - val check = userAuthService.verifyAccount(loginUser.username, loginUser.password) - if (check.not()) { - return@post call.respond(HttpStatusCode.Unauthorized) - } - - val user = userRepository.findByNameAndDomain(loginUser.username, Config.configData.domain) - ?: throw UserNotFoundException("${loginUser.username} was not found.") - - return@post call.respond(jwtService.createToken(user)) - } - - post("/refresh-token") { - val refreshToken = call.receive() - return@post call.respond(jwtService.refreshToken(refreshToken)) - } - get("/.well-known/jwks.json") { //language=JSON val jwt = metaService.getJwtMeta() call.respondText( - contentType = ContentType.Application.Json, - text = JsonWebKeyUtil.publicKeyToJwk(jwt.publicKey, jwt.kid.toString()) + contentType = ContentType.Application.Json, + text = JsonWebKeyUtil.publicKeyToJwk(jwt.publicKey, jwt.kid.toString()) ) } - authenticate(TOKEN_AUTH) { - get("/auth-check") { - val principal = call.principal() ?: throw IllegalStateException("no principal") - val username = principal.payload.getClaim("uid") - call.respondText("Hello $username") - } - } } } diff --git a/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Auth.kt b/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Auth.kt new file mode 100644 index 00000000..8a1e344a --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Auth.kt @@ -0,0 +1,49 @@ +package dev.usbharu.hideout.routing.api.internal.v1 + +import dev.usbharu.hideout.config.Config +import dev.usbharu.hideout.domain.model.hideout.form.RefreshToken +import dev.usbharu.hideout.domain.model.hideout.form.UserLogin +import dev.usbharu.hideout.exception.UserNotFoundException +import dev.usbharu.hideout.plugins.TOKEN_AUTH +import dev.usbharu.hideout.repository.IUserRepository +import dev.usbharu.hideout.service.auth.IJwtService +import dev.usbharu.hideout.service.core.IMetaService +import dev.usbharu.hideout.service.user.IUserAuthService +import dev.usbharu.hideout.util.JsonWebKeyUtil +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.auth.* +import io.ktor.server.auth.jwt.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* + +fun Route.auth(userAuthService: IUserAuthService, + userRepository: IUserRepository, + jwtService: IJwtService, + metaService: IMetaService) { + post("/login") { + val loginUser = call.receive() + val check = userAuthService.verifyAccount(loginUser.username, loginUser.password) + if (check.not()) { + return@post call.respond(HttpStatusCode.Unauthorized) + } + + val user = userRepository.findByNameAndDomain(loginUser.username, Config.configData.domain) + ?: throw UserNotFoundException("${loginUser.username} was not found.") + + return@post call.respond(jwtService.createToken(user)) + } + + post("/refresh-token") { + val refreshToken = call.receive() + return@post call.respond(jwtService.refreshToken(refreshToken)) + } + authenticate(TOKEN_AUTH) { + get("/auth-check") { + val principal = call.principal() ?: throw IllegalStateException("no principal") + val username = principal.payload.getClaim("uid") + call.respondText("Hello $username") + } + } +} diff --git a/src/main/resources/openapi/api.yaml b/src/main/resources/openapi/api.yaml index 17efe775..3cc79aab 100644 --- a/src/main/resources/openapi/api.yaml +++ b/src/main/resources/openapi/api.yaml @@ -425,3 +425,4 @@ components: BearerAuth: type: http scheme: bearer + bearerFormat: JWT diff --git a/src/main/web/App.tsx b/src/main/web/App.tsx index 8114059a..3a833754 100644 --- a/src/main/web/App.tsx +++ b/src/main/web/App.tsx @@ -1,16 +1,28 @@ -import {Component, createSignal, lazy} from "solid-js"; +import {Component, createEffect, createSignal} from "solid-js"; import {Route, Router, Routes} from "@solidjs/router"; import {TopPage} from "./pages/TopPage"; import {createTheme, CssBaseline, ThemeProvider, useMediaQuery} from "@suid/material"; import {createCookieStorage} from "@solid-primitives/storage"; -import {ApiProvider, useApi} from "./lib/ApiProvider"; +import {ApiProvider} from "./lib/ApiProvider"; import {Configuration, DefaultApi} from "./generated"; import {LoginPage} from "./pages/LoginPage"; export const App: Component = () => { const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)'); - const [cookie,setCookie] = createCookieStorage() - const [api,setApi] = createSignal(new DefaultApi(new Configuration({basePath:window.location.origin+"/api/internal/v1",apiKey:cookie.key as string}))) + const [cookie, setCookie] = createCookieStorage() + const [api, setApi] = createSignal(new DefaultApi(new Configuration({ + basePath: window.location.origin + "/api/internal/v1", + accessToken: cookie.token as string + }))) + + createEffect(() => { + setApi( + new DefaultApi(new Configuration({ + basePath: window.location.origin + "/api/internal/v1", + accessToken : cookie.token as string + }))) + }) + const theme = createTheme({ palette: { mode: prefersDarkMode() ? 'dark' : 'light', diff --git a/src/main/web/pages/LoginPage.tsx b/src/main/web/pages/LoginPage.tsx index a2d1b1ff..decb6990 100644 --- a/src/main/web/pages/LoginPage.tsx +++ b/src/main/web/pages/LoginPage.tsx @@ -2,6 +2,7 @@ import {Button, Card, CardContent, CardHeader, Modal, Stack, TextField} from "@s import {Component, createSignal} from "solid-js"; import {createCookieStorage} from "@solid-primitives/storage"; import {useApi} from "../lib/ApiProvider"; +import {useNavigate} from "@solidjs/router"; export const LoginPage: Component = () => { const [username, setUsername] = createSignal("") @@ -9,12 +10,15 @@ export const LoginPage: Component = () => { const [cookie, setCookie] = createCookieStorage(); + const navigator = useNavigate(); + const api = useApi(); const onSubmit: () => void = () => { api().loginPost({password: password(), username: username()}).then(value => { setCookie("token", value.token); setCookie("refresh-token", value.refreshToken) + navigator("/") }).catch(reason => { console.log(reason); setPassword("") diff --git a/src/test/kotlin/dev/usbharu/hideout/plugins/SecurityKtTest.kt b/src/test/kotlin/dev/usbharu/hideout/plugins/SecurityKtTest.kt index 9da64ff0..113e9b38 100644 --- a/src/test/kotlin/dev/usbharu/hideout/plugins/SecurityKtTest.kt +++ b/src/test/kotlin/dev/usbharu/hideout/plugins/SecurityKtTest.kt @@ -70,7 +70,7 @@ class SecurityKtTest { val jwkProvider = mock() application { configureSerialization() - configureSecurity(userAuthService, metaService, userRepository, jwtService, jwkProvider) + configureSecurity(jwkProvider) } client.post("/login") { @@ -97,7 +97,7 @@ class SecurityKtTest { val jwkProvider = mock() application { configureSerialization() - configureSecurity(userAuthService, metaService, userRepository, jwtService, jwkProvider) + configureSecurity(jwkProvider) } client.post("/login") { contentType(ContentType.Application.Json) @@ -122,7 +122,7 @@ class SecurityKtTest { val jwkProvider = mock() application { configureSerialization() - configureSecurity(userAuthService, metaService, userRepository, jwtService, jwkProvider) + configureSecurity(jwkProvider) } client.post("/login") { contentType(ContentType.Application.Json) @@ -140,7 +140,7 @@ class SecurityKtTest { Config.configData = ConfigData(url = "http://example.com", objectMapper = jacksonObjectMapper()) application { configureSerialization() - configureSecurity(mock(), mock(), mock(), mock(), mock()) + configureSecurity(mock()) } client.get("/auth-check").apply { assertEquals(HttpStatusCode.Unauthorized, call.response.status) @@ -155,7 +155,7 @@ class SecurityKtTest { Config.configData = ConfigData(url = "http://example.com", objectMapper = jacksonObjectMapper()) application { configureSerialization() - configureSecurity(mock(), mock(), mock(), mock(), mock()) + configureSecurity(mock()) } client.get("/auth-check") { header("Authorization", "Digest dsfjjhogalkjdfmlhaog") @@ -172,7 +172,7 @@ class SecurityKtTest { Config.configData = ConfigData(url = "http://example.com", objectMapper = jacksonObjectMapper()) application { configureSerialization() - configureSecurity(mock(), mock(), mock(), mock(), mock()) + configureSecurity(mock()) } client.get("/auth-check") { header("Authorization", "") @@ -190,7 +190,7 @@ class SecurityKtTest { application { configureSerialization() - configureSecurity(mock(), mock(), mock(), mock(), mock()) + configureSecurity(mock()) } client.get("/auth-check") { header("Authorization", "Bearer ") @@ -248,7 +248,7 @@ class SecurityKtTest { val jwtService = mock() application { configureSerialization() - configureSecurity(mock(), metaService, userRepository, jwtService, jwkProvider) + configureSecurity(jwkProvider) } client.get("/auth-check") { @@ -308,7 +308,7 @@ class SecurityKtTest { val jwtService = mock() application { configureSerialization() - configureSecurity(mock(), metaService, userRepository, jwtService, jwkProvider) + configureSecurity(jwkProvider) } client.get("/auth-check") { header("Authorization", "Bearer $token") @@ -366,7 +366,7 @@ class SecurityKtTest { val jwtService = mock() application { configureSerialization() - configureSecurity(mock(), metaService, userRepository, jwtService, jwkProvider) + configureSecurity(jwkProvider) } client.get("/auth-check") { header("Authorization", "Bearer $token") @@ -424,7 +424,7 @@ class SecurityKtTest { val jwtService = mock() application { configureSerialization() - configureSecurity(mock(), metaService, userRepository, jwtService, jwkProvider) + configureSecurity(jwkProvider) } client.get("/auth-check") { header("Authorization", "Bearer $token") @@ -481,7 +481,7 @@ class SecurityKtTest { val jwtService = mock() application { configureSerialization() - configureSecurity(mock(), metaService, userRepository, jwtService, jwkProvider) + configureSecurity(jwkProvider) } client.get("/auth-check") { header("Authorization", "Bearer $token") @@ -501,7 +501,7 @@ class SecurityKtTest { } application { configureSerialization() - configureSecurity(mock(), mock(), mock(), jwtService, mock()) + configureSecurity(mock()) } client.post("/refresh-token") { header("Content-Type", "application/json") @@ -523,7 +523,7 @@ class SecurityKtTest { application { configureStatusPages() configureSerialization() - configureSecurity(mock(), mock(), mock(), jwtService, mock()) + configureSecurity(mock()) } client.post("/refresh-token") { header("Content-Type", "application/json") diff --git a/src/test/kotlin/dev/usbharu/hideout/routing/api/internal/v1/PostsTest.kt b/src/test/kotlin/dev/usbharu/hideout/routing/api/internal/v1/PostsTest.kt index 0f2c1627..c9842020 100644 --- a/src/test/kotlin/dev/usbharu/hideout/routing/api/internal/v1/PostsTest.kt +++ b/src/test/kotlin/dev/usbharu/hideout/routing/api/internal/v1/PostsTest.kt @@ -64,7 +64,7 @@ class PostsTest { } application { configureSerialization() - configureSecurity(mock(), mock(), mock(), mock(), mock()) + configureSecurity(mock()) routing { route("/api/internal/v1") { posts(postService) @@ -169,7 +169,7 @@ class PostsTest { } application { configureSerialization() - configureSecurity(mock(), mock(), mock(), mock(), mock()) + configureSecurity(mock()) routing { route("/api/internal/v1") { posts(postService) @@ -323,7 +323,7 @@ class PostsTest { } application { configureSerialization() - configureSecurity(mock(), mock(), mock(), mock(), mock()) + configureSecurity(mock()) routing { route("/api/internal/v1") { posts(postService) @@ -375,7 +375,7 @@ class PostsTest { } application { configureSerialization() - configureSecurity(mock(), mock(), mock(), mock(), mock()) + configureSecurity(mock()) routing { route("/api/internal/v1") { posts(postService) @@ -427,7 +427,7 @@ class PostsTest { } application { configureSerialization() - configureSecurity(mock(), mock(), mock(), mock(), mock()) + configureSecurity(mock()) routing { route("/api/internal/v1") { posts(postService) @@ -479,7 +479,7 @@ class PostsTest { } application { configureSerialization() - configureSecurity(mock(), mock(), mock(), mock(), mock()) + configureSecurity(mock()) routing { route("/api/internal/v1") { posts(postService) @@ -511,7 +511,7 @@ class PostsTest { } application { configureSerialization() - configureSecurity(mock(), mock(), mock(), mock(), mock()) + configureSecurity(mock()) routing { route("/api/internal/v1") { posts(postService) @@ -543,7 +543,7 @@ class PostsTest { } application { configureSerialization() - configureSecurity(mock(), mock(), mock(), mock(), mock()) + configureSecurity(mock()) routing { route("/api/internal/v1") { posts(postService) @@ -575,7 +575,7 @@ class PostsTest { } application { configureSerialization() - configureSecurity(mock(), mock(), mock(), mock(), mock()) + configureSecurity(mock()) routing { route("/api/internal/v1") { posts(postService) @@ -607,7 +607,7 @@ class PostsTest { } application { configureSerialization() - configureSecurity(mock(), mock(), mock(), mock(), mock()) + configureSecurity(mock()) routing { route("/api/internal/v1") { posts(postService) diff --git a/src/test/kotlin/dev/usbharu/hideout/routing/api/internal/v1/UsersTest.kt b/src/test/kotlin/dev/usbharu/hideout/routing/api/internal/v1/UsersTest.kt index ea9d703b..e49752d1 100644 --- a/src/test/kotlin/dev/usbharu/hideout/routing/api/internal/v1/UsersTest.kt +++ b/src/test/kotlin/dev/usbharu/hideout/routing/api/internal/v1/UsersTest.kt @@ -58,7 +58,7 @@ class UsersTest { } application { configureSerialization() - configureSecurity(mock(), mock(), mock(), mock(), mock()) + configureSecurity(mock()) routing { route("/api/internal/v1") { users(mock(), userService) @@ -96,7 +96,7 @@ class UsersTest { } application { configureSerialization() - configureSecurity(mock(), mock(), mock(), mock(), mock()) + configureSecurity(mock()) routing { route("/api/internal/v1") { users(userService, mock()) @@ -127,7 +127,7 @@ class UsersTest { } application { configureSerialization() - configureSecurity(mock(), mock(), mock(), mock(), mock()) + configureSecurity(mock()) routing { route("/api/internal/v1") { users(userService, mock()) @@ -162,7 +162,7 @@ class UsersTest { } application { configureSerialization() - configureSecurity(mock(), mock(), mock(), mock(), mock()) + configureSecurity(mock()) routing { route("/api/internal/v1") { users(mock(), userApiService) @@ -195,7 +195,7 @@ class UsersTest { } application { configureSerialization() - configureSecurity(mock(), mock(), mock(), mock(), mock()) + configureSecurity(mock()) routing { route("/api/internal/v1") { users(mock(), userApiService) @@ -228,7 +228,7 @@ class UsersTest { } application { configureSerialization() - configureSecurity(mock(), mock(), mock(), mock(), mock()) + configureSecurity(mock()) routing { route("/api/internal/v1") { users(mock(), userApiService) @@ -261,7 +261,7 @@ class UsersTest { } application { configureSerialization() - configureSecurity(mock(), mock(), mock(), mock(), mock()) + configureSecurity(mock()) routing { route("/api/internal/v1") { users(mock(), userApiService) @@ -306,7 +306,7 @@ class UsersTest { } application { configureSerialization() - configureSecurity(mock(), mock(), mock(), mock(), mock()) + configureSecurity(mock()) routing { route("/api/internal/v1") { users(mock(), userApiService) @@ -351,7 +351,7 @@ class UsersTest { } application { configureSerialization() - configureSecurity(mock(), mock(), mock(), mock(), mock()) + configureSecurity(mock()) routing { route("/api/internal/v1") { users(mock(), userApiService) @@ -396,7 +396,7 @@ class UsersTest { } application { configureSerialization() - configureSecurity(mock(), mock(), mock(), mock(), mock()) + configureSecurity(mock()) routing { route("/api/internal/v1") { users(mock(), userApiService) @@ -591,7 +591,7 @@ class UsersTest { } application { configureSerialization() - configureSecurity(mock(), mock(), mock(), mock(), mock()) + configureSecurity(mock()) routing { route("/api/internal/v1") { users(mock(), userApiService) @@ -636,7 +636,7 @@ class UsersTest { } application { configureSerialization() - configureSecurity(mock(), mock(), mock(), mock(), mock()) + configureSecurity(mock()) routing { route("/api/internal/v1") { users(mock(), userApiService) @@ -681,7 +681,7 @@ class UsersTest { } application { configureSerialization() - configureSecurity(mock(), mock(), mock(), mock(), mock()) + configureSecurity(mock()) routing { route("/api/internal/v1") { users(mock(), userApiService)