Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#79 Register user with Yandex ID and get token #108

Merged
merged 14 commits into from
May 8, 2024
6 changes: 6 additions & 0 deletions botalka/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@ plugins {
val basePackage = "$group.lms.botalka"

dependencies {
val jwtVersion = "0.12.5"

implementation("org.apache.commons:commons-lang3:3.14.0")

implementation("io.jsonwebtoken:jjwt-api:$jwtVersion")
runtimeOnly("io.jsonwebtoken:jjwt-impl:$jwtVersion")
runtimeOnly("io.jsonwebtoken:jjwt-jackson:$jwtVersion")
}

val jooqPackageName = "$basePackage.storage.jooq"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,22 @@
package ru.vityaman.lms.botalka.app.spring.api.http.endpoint

import org.slf4j.LoggerFactory
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.RestController
import ru.vityaman.lms.botalka.app.spring.api.http.server.Auth2ViaCodeMessage
import ru.vityaman.lms.botalka.app.spring.api.http.server.TokensMessage
import ru.vityaman.lms.botalka.app.spring.api.http.server.apis.AuthApi
import ru.vityaman.lms.botalka.core.external.yandex.Yandex
import ru.vityaman.lms.botalka.core.external.yandex.YandexVerificationCode
import ru.vityaman.lms.botalka.app.spring.security.SpringYandexOAuthService
import ru.vityaman.lms.botalka.core.external.yandex.YandexOAuthCode

@RestController
class AuthHttpApi(private val yandex: Yandex) : AuthApi {
private val log = LoggerFactory.getLogger(this::class.java)

override suspend fun authCodeYandexPost(
class AuthHttpApi(
private val yandex: SpringYandexOAuthService,
) : AuthApi {
override suspend fun authYandexCodePut(
auth2ViaCodeMessage: Auth2ViaCodeMessage,
): ResponseEntity<Unit> {
val code = YandexVerificationCode(auth2ViaCodeMessage.code)
val token = yandex.token(code)
val user = yandex.user(token)
log.info("Got $user")
return ResponseEntity.ok(Unit)
): ResponseEntity<TokensMessage> {
val code = YandexOAuthCode(auth2ViaCodeMessage.code)
val token = yandex.signIn(code)
return ResponseEntity.ok(TokensMessage(access = token.text))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,20 @@ import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.RestController
import ru.vityaman.lms.botalka.app.spring.api.http.message.toMessage
import ru.vityaman.lms.botalka.app.spring.api.http.message.toModel
import ru.vityaman.lms.botalka.app.spring.api.http.server.UserDraftMessage
import ru.vityaman.lms.botalka.app.spring.api.http.server.Auth2ViaCodeMessage
import ru.vityaman.lms.botalka.app.spring.api.http.server.PostUserRequestMessage
import ru.vityaman.lms.botalka.app.spring.api.http.server.PostUserResponseMessage
import ru.vityaman.lms.botalka.app.spring.api.http.server.UserMessage
import ru.vityaman.lms.botalka.app.spring.api.http.server.apis.UserApi
import ru.vityaman.lms.botalka.app.spring.security.SpringYandexOAuthService
import ru.vityaman.lms.botalka.core.exception.orNotFound
import ru.vityaman.lms.botalka.core.logic.UserService
import ru.vityaman.lms.botalka.core.model.User

@RestController
class UserHttpApi(
@Autowired private val userService: UserService,
@Autowired private val auth: SpringYandexOAuthService,
) : UserApi {
override suspend fun userIdGet(id: Int): ResponseEntity<UserMessage> {
val userId = User.Id(id)
Expand All @@ -24,10 +28,15 @@ class UserHttpApi(
}

override suspend fun userPost(
userDraftMessage: UserDraftMessage,
): ResponseEntity<UserMessage> {
val draft = userDraftMessage.toModel()
val user = userService.create(draft)
return ResponseEntity.ok(user.toMessage())
postUserRequestMessage: PostUserRequestMessage,
): ResponseEntity<PostUserResponseMessage> {
val draft = postUserRequestMessage.user.toModel()
val credentialMessage = postUserRequestMessage.credential
val credential = (credentialMessage as? Auth2ViaCodeMessage)
?.toModel()
?: TODO("Unexpected credential $credentialMessage")
return auth.signUp(draft, credential)
.toMessage()
.let { ResponseEntity.ok(it) }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package ru.vityaman.lms.botalka.app.spring.api.http.message

import ru.vityaman.lms.botalka.app.spring.api.http.server.Auth2ViaCodeMessage
import ru.vityaman.lms.botalka.app.spring.api.http.server.PostUserResponseMessage
import ru.vityaman.lms.botalka.core.external.yandex.YandexOAuthCode
import ru.vityaman.lms.botalka.core.model.AuthUser

fun Auth2ViaCodeMessage.toModel(): YandexOAuthCode =
YandexOAuthCode(this.code)

fun AuthUser.toMessage(): PostUserResponseMessage =
PostUserResponseMessage(
user = this.user.toMessage(),
token = this.token.text,
)
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import org.springframework.web.reactive.function.client.WebClient
import org.springframework.web.reactive.function.client.awaitBody
import ru.vityaman.lms.botalka.core.external.yandex.Yandex
import ru.vityaman.lms.botalka.core.external.yandex.YandexAppCredentials
import ru.vityaman.lms.botalka.core.external.yandex.YandexOAuthCode
import ru.vityaman.lms.botalka.core.external.yandex.YandexOAuthToken
import ru.vityaman.lms.botalka.core.external.yandex.YandexUser
import ru.vityaman.lms.botalka.core.external.yandex.YandexVerificationCode
import java.util.Base64

@Service
Expand Down Expand Up @@ -46,12 +46,12 @@ class SpringYandexClient(
.awaitBody<YandexUserInfo>()
.let {
YandexUser(
YandexUser.Id(it.id.toLong()),
YandexUser.Id(it.id.toInt()),
YandexUser.Login(it.login),
)
}

override suspend fun token(code: YandexVerificationCode): YandexOAuthToken =
override suspend fun token(code: YandexOAuthCode): YandexOAuthToken =
oauth.post()
.uri("/token")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package ru.vityaman.lms.botalka.app.spring.logic

import org.springframework.stereotype.Service
import ru.vityaman.lms.botalka.core.external.yandex.YandexOAuthCode
import ru.vityaman.lms.botalka.core.logic.AuthService
import ru.vityaman.lms.botalka.core.logic.UserService
import ru.vityaman.lms.botalka.core.logic.basic.BasicAuthService
import ru.vityaman.lms.botalka.core.security.auth.AuthMethod
import ru.vityaman.lms.botalka.core.tx.TxEnv

@Service
class SpringYandexAuthService(
method: AuthMethod<YandexOAuthCode>,
users: UserService,
txEnv: TxEnv,
) : AuthService<YandexOAuthCode> by BasicAuthService(
method,
users,
txEnv,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package ru.vityaman.lms.botalka.app.spring.security

import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Service
import ru.vityaman.lms.botalka.core.security.auth.JwtTokenService
import ru.vityaman.lms.botalka.core.security.auth.TokenService
import java.time.Clock
import java.time.Duration

@Service
class SpringTokenService(
@Value("\${security.token.signing.secret}")
signingKey: String,

@Value("\${security.token.duration}")
duration: Duration,

clock: Clock,
) : TokenService by JwtTokenService(
JwtTokenService.Config(signingKey, duration),
clock,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package ru.vityaman.lms.botalka.app.spring.security

import org.springframework.stereotype.Service
import ru.vityaman.lms.botalka.core.external.yandex.Yandex
import ru.vityaman.lms.botalka.core.external.yandex.YandexCodeAuthMethod
import ru.vityaman.lms.botalka.core.external.yandex.YandexOAuthCode
import ru.vityaman.lms.botalka.core.security.auth.AuthMethod
import ru.vityaman.lms.botalka.core.security.auth.TokenService
import ru.vityaman.lms.botalka.core.storage.YandexAuthStorage

@Service
class SpringYandexOAuthMethod(
yandex: Yandex,
storage: YandexAuthStorage,
tokens: TokenService,
) : AuthMethod<YandexOAuthCode> by YandexCodeAuthMethod(
yandex,
storage,
tokens,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package ru.vityaman.lms.botalka.app.spring.security

import org.springframework.stereotype.Service
import ru.vityaman.lms.botalka.core.external.yandex.YandexOAuthCode
import ru.vityaman.lms.botalka.core.logic.AuthService
import ru.vityaman.lms.botalka.core.logic.UserService
import ru.vityaman.lms.botalka.core.logic.basic.BasicAuthService
import ru.vityaman.lms.botalka.core.tx.TxEnv

@Service
class SpringYandexOAuthService(
method: SpringYandexOAuthMethod,
users: UserService,
txEnv: TxEnv,
) : AuthService<YandexOAuthCode> by BasicAuthService(
method,
users,
txEnv,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package ru.vityaman.lms.botalka.app.spring.storage

import org.springframework.stereotype.Repository
import ru.vityaman.lms.botalka.core.storage.YandexAuthStorage
import ru.vityaman.lms.botalka.storage.jooq.JooqDatabase
import ru.vityaman.lms.botalka.storage.jooq.JooqYandexAuthStorage

@Repository
class SpringYandexAuthStorage(database: JooqDatabase) :
YandexAuthStorage by JooqYandexAuthStorage(database)
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ package ru.vityaman.lms.botalka.core.external.yandex

interface Yandex {
suspend fun user(token: YandexOAuthToken): YandexUser
suspend fun token(code: YandexVerificationCode): YandexOAuthToken
suspend fun token(code: YandexOAuthCode): YandexOAuthToken
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package ru.vityaman.lms.botalka.core.external.yandex

import ru.vityaman.lms.botalka.core.exception.orNotFound
import ru.vityaman.lms.botalka.core.model.User
import ru.vityaman.lms.botalka.core.security.auth.AccessToken
import ru.vityaman.lms.botalka.core.security.auth.AuthMethod
import ru.vityaman.lms.botalka.core.security.auth.TokenService
import ru.vityaman.lms.botalka.core.storage.YandexAuthStorage

class YandexCodeAuthMethod(
private val yandex: Yandex,
private val storage: YandexAuthStorage,
private val tokens: TokenService,
) : AuthMethod<YandexOAuthCode> {
override suspend fun connect(
credential: YandexOAuthCode,
userId: User.Id,
): AccessToken {
val yandexoid = yandex.user(yandex.token(credential))
storage.create(userId, yandexoid)
return tokens.issue(AccessToken.Payload(userId))
}

override suspend fun authenticate(
credential: YandexOAuthCode,
): AccessToken {
val yandexoid = yandex.user(yandex.token(credential))
val userId = storage.corresponding(yandexoid.id)
.orNotFound("user with yandex id ${yandexoid.id.number}")
return tokens.issue(AccessToken.Payload(userId))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package ru.vityaman.lms.botalka.core.external.yandex

import ru.vityaman.lms.botalka.core.expectId

@JvmInline
value class YandexOAuthCode(val number: Int) {
init {
expectId(number)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ data class YandexUser(
val login: Login,
) {
@JvmInline
value class Id(val number: Long)
value class Id(val number: Int)

@JvmInline
value class Login(val text: String)
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package ru.vityaman.lms.botalka.core.logic

import ru.vityaman.lms.botalka.core.model.AuthUser
import ru.vityaman.lms.botalka.core.model.User
import ru.vityaman.lms.botalka.core.security.auth.AccessToken

interface AuthService<T> {
suspend fun signUp(draft: User.Draft, credentials: T): AuthUser
suspend fun signIn(credentials: T): AccessToken
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package ru.vityaman.lms.botalka.core.logic
import ru.vityaman.lms.botalka.core.model.User

interface UserService {
suspend fun getById(id: User.Id): User?
suspend fun create(user: User.Draft): User
suspend fun getById(id: User.Id): User?
suspend fun promote(id: User.Id, role: User.Role)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package ru.vityaman.lms.botalka.core.logic.basic

import ru.vityaman.lms.botalka.core.logic.AuthService
import ru.vityaman.lms.botalka.core.logic.UserService
import ru.vityaman.lms.botalka.core.model.AuthUser
import ru.vityaman.lms.botalka.core.model.User
import ru.vityaman.lms.botalka.core.security.auth.AccessToken
import ru.vityaman.lms.botalka.core.security.auth.AuthMethod
import ru.vityaman.lms.botalka.core.tx.TxEnv

class BasicAuthService<T>(
private val auth: AuthMethod<T>,
private val users: UserService,
private val txEnv: TxEnv,
) : AuthService<T> {
override suspend fun signUp(draft: User.Draft, credentials: T): AuthUser =
txEnv.transactional {
val user = users.create(draft)
val token = auth.connect(credentials, user.id)
AuthUser(user, token)
}

override suspend fun signIn(credentials: T): AccessToken =
auth.authenticate(credentials)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package ru.vityaman.lms.botalka.core.model

import ru.vityaman.lms.botalka.core.security.auth.AccessToken

data class AuthUser(val user: User, val token: AccessToken)
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package ru.vityaman.lms.botalka.core.security.auth

import ru.vityaman.lms.botalka.core.model.User

@JvmInline
value class AccessToken(val text: String) {
data class Payload(val userId: User.Id)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package ru.vityaman.lms.botalka.core.security.auth

import ru.vityaman.lms.botalka.core.model.User

interface AuthMethod<T> {
suspend fun connect(credential: T, userId: User.Id): AccessToken
suspend fun authenticate(credential: T): AccessToken
}
Loading
Loading