Authentication
- add AuthenticationController - add AuthenticationService - add getUserByUsername to AccountService - add findByUsername to AccountRepository - add SecurityConfiguration - set up authentication
This commit is contained in:
@@ -17,6 +17,10 @@ tab_width = 2
|
||||
[*.bat]
|
||||
end_of_line = crlf
|
||||
|
||||
[*.pem]
|
||||
max_line_length = 64
|
||||
insert_final_newline = false
|
||||
|
||||
# noinspection EditorConfigKeyCorrectness
|
||||
[*.{kt,kts}]
|
||||
ij_kotlin_packages_to_use_import_on_demand = unset
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -38,3 +38,6 @@ out/
|
||||
|
||||
### Kotlin ###
|
||||
.kotlin
|
||||
|
||||
#### Hlæja ###
|
||||
/cert/
|
||||
|
||||
@@ -10,15 +10,22 @@ In twilight's hush, where mythic tales unfold, A ledger of legends, the bravest
|
||||
| spring.r2dbc.url | ✓ | Postgres host url |
|
||||
| spring.r2dbc.username | ✓ | Postgres username |
|
||||
| spring.r2dbc.password | ✗ | Postgres password |
|
||||
| jwt.private-key | ✓ | JWT private key file |
|
||||
|
||||
*Required: ✓ can be stored as text, and ✗ need to be stored as secret.*
|
||||
|
||||
## Releasing Service
|
||||
## Development Configuration
|
||||
|
||||
Run `release.sh` script from `master` branch.
|
||||
|
||||
## Development Information
|
||||
|
||||
### Private RSA Key
|
||||
|
||||
This service uses RAS keys to create identities for users. The private key is used here to generate identities, while the public key is used by **[Hlæja Registry API](https://github.com/swordsteel/hlaeja-registry-api)** to identify a users and accept data.
|
||||
|
||||
*For instructions on how to set these up, please refer to our [generate RSA key](https://github.com/swordsteel/hlaeja-development/blob/master/doc/rsa_key.md) documentation.*
|
||||
|
||||
### Global Setting
|
||||
|
||||
This services rely on a set of global settings to configure development environments. These settings, managed through Gradle properties or environment variables.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
plugins {
|
||||
alias(hlaeja.plugins.kotlin.jvm)
|
||||
alias(hlaeja.plugins.kotlin.spring)
|
||||
alias(hlaeja.plugins.ltd.hlaeja.plugin.certificate)
|
||||
alias(hlaeja.plugins.ltd.hlaeja.plugin.service)
|
||||
alias(hlaeja.plugins.spring.dependency.management)
|
||||
alias(hlaeja.plugins.springframework.boot)
|
||||
@@ -11,8 +12,10 @@ dependencies {
|
||||
implementation(hlaeja.kotlin.reflect)
|
||||
implementation(hlaeja.kotlinx.coroutines)
|
||||
implementation(hlaeja.library.hlaeja.common.messages)
|
||||
implementation(hlaeja.library.hlaeja.jwt)
|
||||
implementation(hlaeja.springboot.starter.actuator)
|
||||
implementation(hlaeja.springboot.starter.r2dbc)
|
||||
implementation(hlaeja.springboot.starter.security)
|
||||
implementation(hlaeja.springboot.starter.webflux)
|
||||
|
||||
runtimeOnly(hlaeja.postgresql)
|
||||
@@ -29,3 +32,7 @@ dependencies {
|
||||
}
|
||||
|
||||
group = "ltd.hlaeja"
|
||||
|
||||
tasks.named("processResources") {
|
||||
dependsOn("copyCertificates")
|
||||
}
|
||||
|
||||
44
http/authentication.http
Normal file
44
http/authentication.http
Normal file
@@ -0,0 +1,44 @@
|
||||
### Get admin information
|
||||
POST {{hostname}}/authenticate
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "admin",
|
||||
"password": "pass"
|
||||
}
|
||||
|
||||
### Get user information
|
||||
POST {{hostname}}/authenticate
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "user",
|
||||
"password": "pass"
|
||||
}
|
||||
|
||||
### Get bad user
|
||||
POST {{hostname}}/authenticate
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "bad user",
|
||||
"password": "pass"
|
||||
}
|
||||
|
||||
### Get bad pass
|
||||
POST {{hostname}}/authenticate
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "user",
|
||||
"password": "bad pass"
|
||||
}
|
||||
|
||||
### Get disabled user
|
||||
POST {{hostname}}/authenticate
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "disabled",
|
||||
"password": "pass"
|
||||
}
|
||||
4
sql/test_001-account.sql
Normal file
4
sql/test_001-account.sql
Normal file
@@ -0,0 +1,4 @@
|
||||
insert into public.accounts (id, created_at, updated_at, enabled, username, password, roles)
|
||||
values ('00000000-0000-7000-0000-000000000001'::uuid, '2000-01-01 00:00:00.000001 +00:00', '2000-01-01 00:00:01.000001 +00:00', true, 'admin', '$2a$12$KoXBoLOANMK11J4xeJHPA.Sy0FG.m8KWk7P4XFsMO.ZbFmFI2DckK', 'ROLE_ADMIN'),
|
||||
('00000000-0000-7000-0000-000000000002'::uuid, '2000-01-01 00:00:00.000001 +00:00', '2000-01-01 00:00:01.000001 +00:00', true, 'user', '$2a$12$KoXBoLOANMK11J4xeJHPA.Sy0FG.m8KWk7P4XFsMO.ZbFmFI2DckK', 'ROLE_USER'),
|
||||
('00000000-0000-7000-0000-000000000003'::uuid, '2000-01-01 00:00:00.000001 +00:00', '2000-01-01 00:00:01.000001 +00:00', false, 'disabled', '$2a$12$KoXBoLOANMK11J4xeJHPA.Sy0FG.m8KWk7P4XFsMO.ZbFmFI2DckK', 'ROLE_USER');
|
||||
@@ -0,0 +1,47 @@
|
||||
package ltd.hlaeja.configuration
|
||||
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
|
||||
import org.springframework.security.config.web.server.ServerHttpSecurity
|
||||
import org.springframework.security.config.web.server.ServerHttpSecurity.AuthorizeExchangeSpec
|
||||
import org.springframework.security.config.web.server.ServerHttpSecurity.CsrfSpec
|
||||
import org.springframework.security.config.web.server.ServerHttpSecurity.FormLoginSpec
|
||||
import org.springframework.security.config.web.server.ServerHttpSecurity.HttpBasicSpec
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
|
||||
import org.springframework.security.crypto.password.PasswordEncoder
|
||||
import org.springframework.security.web.server.SecurityWebFilterChain
|
||||
|
||||
@Configuration
|
||||
@EnableWebFluxSecurity
|
||||
class SecurityConfiguration {
|
||||
|
||||
@Bean
|
||||
fun passwordEncoder(): PasswordEncoder = BCryptPasswordEncoder()
|
||||
|
||||
@Bean
|
||||
fun securityWebFilterChain(
|
||||
serverHttpSecurity: ServerHttpSecurity,
|
||||
): SecurityWebFilterChain = serverHttpSecurity
|
||||
.authorizeExchange(::authorizeExchange)
|
||||
.httpBasic(::httpBasic)
|
||||
.formLogin(::formLogin)
|
||||
.csrf(::csrf)
|
||||
.build()
|
||||
|
||||
private fun csrf(
|
||||
csrf: CsrfSpec,
|
||||
) = csrf.disable()
|
||||
|
||||
private fun formLogin(
|
||||
formLogin: FormLoginSpec,
|
||||
) = formLogin.disable()
|
||||
|
||||
private fun httpBasic(
|
||||
httpBasic: HttpBasicSpec,
|
||||
) = httpBasic.disable()
|
||||
|
||||
private fun authorizeExchange(
|
||||
authorizeExchange: AuthorizeExchangeSpec,
|
||||
) = authorizeExchange.anyExchange().permitAll()
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package ltd.hlaeja.controller
|
||||
|
||||
import ltd.hlaeja.library.accountRegistry.Authentication
|
||||
import ltd.hlaeja.service.AuthenticationService
|
||||
import org.springframework.web.bind.annotation.PostMapping
|
||||
import org.springframework.web.bind.annotation.RequestBody
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import reactor.core.publisher.Mono
|
||||
|
||||
@RestController
|
||||
class AuthenticationController(
|
||||
private val authenticationService: AuthenticationService,
|
||||
) {
|
||||
|
||||
@PostMapping("/authenticate")
|
||||
fun authenticate(
|
||||
@RequestBody request: Authentication.Request,
|
||||
): Mono<Authentication.Response> = authenticationService.authenticate(request.username, request.password)
|
||||
.map { Authentication.Response(it) }
|
||||
}
|
||||
@@ -4,6 +4,9 @@ import java.util.UUID
|
||||
import ltd.hlaeja.entity.AccountEntity
|
||||
import org.springframework.data.repository.reactive.ReactiveCrudRepository
|
||||
import org.springframework.stereotype.Repository
|
||||
import reactor.core.publisher.Mono
|
||||
|
||||
@Repository
|
||||
interface AccountRepository : ReactiveCrudRepository<AccountEntity, UUID>
|
||||
interface AccountRepository : ReactiveCrudRepository<AccountEntity, UUID> {
|
||||
fun findByUsername(username: String): Mono<AccountEntity>
|
||||
}
|
||||
|
||||
@@ -21,4 +21,10 @@ class AccountService(
|
||||
): Mono<AccountEntity> = accountRepository.findById(uuid)
|
||||
.doOnNext { log.debug { "Get account ${it.id}" } }
|
||||
.switchIfEmpty(Mono.error(ResponseStatusException(HttpStatus.NOT_FOUND)))
|
||||
|
||||
fun getUserByUsername(
|
||||
username: String,
|
||||
): Mono<AccountEntity> = accountRepository.findByUsername(username)
|
||||
.doOnNext { log.debug { "Get account ${it.id} for username $username" } }
|
||||
.switchIfEmpty(Mono.error(ResponseStatusException(HttpStatus.NOT_FOUND)))
|
||||
}
|
||||
|
||||
37
src/main/kotlin/ltd/hlaeja/service/AuthenticationService.kt
Normal file
37
src/main/kotlin/ltd/hlaeja/service/AuthenticationService.kt
Normal file
@@ -0,0 +1,37 @@
|
||||
package ltd.hlaeja.service
|
||||
|
||||
import ltd.hlaeja.jwt.service.PrivateJwtService
|
||||
import org.springframework.http.HttpStatus.LOCKED
|
||||
import org.springframework.http.HttpStatus.UNAUTHORIZED
|
||||
import org.springframework.security.crypto.password.PasswordEncoder
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.web.server.ResponseStatusException
|
||||
import reactor.core.publisher.Mono
|
||||
|
||||
@Service
|
||||
class AuthenticationService(
|
||||
private val accountService: AccountService,
|
||||
private val passwordEncoder: PasswordEncoder,
|
||||
private val privateJwtService: PrivateJwtService,
|
||||
) {
|
||||
fun authenticate(
|
||||
username: String,
|
||||
password: CharSequence,
|
||||
): Mono<String> = accountService.getUserByUsername(username)
|
||||
.flatMap {
|
||||
if (!passwordEncoder.matches(password, it.password)) {
|
||||
Mono.error(ResponseStatusException(UNAUTHORIZED, "Invalid password"))
|
||||
} else if (!it.enabled) {
|
||||
Mono.error(ResponseStatusException(LOCKED, "Account disabled"))
|
||||
} else {
|
||||
Mono.just(it)
|
||||
}
|
||||
}
|
||||
.map { accountEntity ->
|
||||
privateJwtService.sign(
|
||||
"id" to accountEntity.id!!,
|
||||
"username" to accountEntity.username,
|
||||
"role" to accountEntity.roles,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,9 @@ spring:
|
||||
name: "%APP_BUILD_OS_NAME%"
|
||||
version: "%APP_BUILD_OS_VERSION%"
|
||||
|
||||
jwt:
|
||||
private-key: cert/private_key.pem
|
||||
|
||||
---
|
||||
###############################
|
||||
### Development environment ###
|
||||
|
||||
8
src/test/resources/application.yml
Normal file
8
src/test/resources/application.yml
Normal file
@@ -0,0 +1,8 @@
|
||||
jwt:
|
||||
private-key: cert/valid-private-key.pem
|
||||
|
||||
spring:
|
||||
r2dbc:
|
||||
url: r2dbc:postgresql://placeholder
|
||||
username: placeholder
|
||||
password: placeholder
|
||||
28
src/test/resources/cert/valid-private-key.pem
Normal file
28
src/test/resources/cert/valid-private-key.pem
Normal file
@@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC7VJTUt9Us8cKj
|
||||
MzEfYyjiWA4R4/M2bS1GB4t7NXp98C3SC6dVMvDuictGeurT8jNbvJZHtCSuYEvu
|
||||
NMoSfm76oqFvAp8Gy0iz5sxjZmSnXyCdPEovGhLa0VzMaQ8s+CLOyS56YyCFGeJZ
|
||||
qgtzJ6GR3eqoYSW9b9UMvkBpZODSctWSNGj3P7jRFDO5VoTwCQAWbFnOjDfH5Ulg
|
||||
p2PKSQnSJP3AJLQNFNe7br1XbrhV//eO+t51mIpGSDCUv3E0DDFcWDTH9cXDTTlR
|
||||
ZVEiR2BwpZOOkE/Z0/BVnhZYL71oZV34bKfWjQIt6V/isSMahdsAASACp4ZTGtwi
|
||||
VuNd9tybAgMBAAECggEBAKTmjaS6tkK8BlPXClTQ2vpz/N6uxDeS35mXpqasqskV
|
||||
laAidgg/sWqpjXDbXr93otIMLlWsM+X0CqMDgSXKejLS2jx4GDjI1ZTXg++0AMJ8
|
||||
sJ74pWzVDOfmCEQ/7wXs3+cbnXhKriO8Z036q92Qc1+N87SI38nkGa0ABH9CN83H
|
||||
mQqt4fB7UdHzuIRe/me2PGhIq5ZBzj6h3BpoPGzEP+x3l9YmK8t/1cN0pqI+dQwY
|
||||
dgfGjackLu/2qH80MCF7IyQaseZUOJyKrCLtSD/Iixv/hzDEUPfOCjFDgTpzf3cw
|
||||
ta8+oE4wHCo1iI1/4TlPkwmXx4qSXtmw4aQPz7IDQvECgYEA8KNThCO2gsC2I9PQ
|
||||
DM/8Cw0O983WCDY+oi+7JPiNAJwv5DYBqEZB1QYdj06YD16XlC/HAZMsMku1na2T
|
||||
N0driwenQQWzoev3g2S7gRDoS/FCJSI3jJ+kjgtaA7Qmzlgk1TxODN+G1H91HW7t
|
||||
0l7VnL27IWyYo2qRRK3jzxqUiPUCgYEAx0oQs2reBQGMVZnApD1jeq7n4MvNLcPv
|
||||
t8b/eU9iUv6Y4Mj0Suo/AU8lYZXm8ubbqAlwz2VSVunD2tOplHyMUrtCtObAfVDU
|
||||
AhCndKaA9gApgfb3xw1IKbuQ1u4IF1FJl3VtumfQn//LiH1B3rXhcdyo3/vIttEk
|
||||
48RakUKClU8CgYEAzV7W3COOlDDcQd935DdtKBFRAPRPAlspQUnzMi5eSHMD/ISL
|
||||
DY5IiQHbIH83D4bvXq0X7qQoSBSNP7Dvv3HYuqMhf0DaegrlBuJllFVVq9qPVRnK
|
||||
xt1Il2HgxOBvbhOT+9in1BzA+YJ99UzC85O0Qz06A+CmtHEy4aZ2kj5hHjECgYEA
|
||||
mNS4+A8Fkss8Js1RieK2LniBxMgmYml3pfVLKGnzmng7H2+cwPLhPIzIuwytXywh
|
||||
2bzbsYEfYx3EoEVgMEpPhoarQnYPukrJO4gwE2o5Te6T5mJSZGlQJQj9q4ZB2Dfz
|
||||
et6INsK0oG8XVGXSpQvQh3RUYekCZQkBBFcpqWpbIEsCgYAnM3DQf3FJoSnXaMhr
|
||||
VBIovic5l0xFkEHskAjFTevO86Fsz1C2aSeRKSqGFoOQ0tmJzBEs1R6KqnHInicD
|
||||
TQrKhArgLXX4v3CddjfTRJkFWDbE/CkvKZNOrcf1nhaGCPspRJj2KUkj1Fhl9Cnc
|
||||
dn/RsYEONbwQSjIfMPkvxF+8HQ==
|
||||
-----END PRIVATE KEY-----
|
||||
Reference in New Issue
Block a user