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]
|
[*.bat]
|
||||||
end_of_line = crlf
|
end_of_line = crlf
|
||||||
|
|
||||||
|
[*.pem]
|
||||||
|
max_line_length = 64
|
||||||
|
insert_final_newline = false
|
||||||
|
|
||||||
# noinspection EditorConfigKeyCorrectness
|
# noinspection EditorConfigKeyCorrectness
|
||||||
[*.{kt,kts}]
|
[*.{kt,kts}]
|
||||||
ij_kotlin_packages_to_use_import_on_demand = unset
|
ij_kotlin_packages_to_use_import_on_demand = unset
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -38,3 +38,6 @@ out/
|
|||||||
|
|
||||||
### Kotlin ###
|
### Kotlin ###
|
||||||
.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.url | ✓ | Postgres host url |
|
||||||
| spring.r2dbc.username | ✓ | Postgres username |
|
| spring.r2dbc.username | ✓ | Postgres username |
|
||||||
| spring.r2dbc.password | ✗ | Postgres password |
|
| 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.*
|
*Required: ✓ can be stored as text, and ✗ need to be stored as secret.*
|
||||||
|
|
||||||
## Releasing Service
|
## Development Configuration
|
||||||
|
|
||||||
Run `release.sh` script from `master` branch.
|
Run `release.sh` script from `master` branch.
|
||||||
|
|
||||||
## Development Information
|
## 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
|
### Global Setting
|
||||||
|
|
||||||
This services rely on a set of global settings to configure development environments. These settings, managed through Gradle properties or environment variables.
|
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 {
|
plugins {
|
||||||
alias(hlaeja.plugins.kotlin.jvm)
|
alias(hlaeja.plugins.kotlin.jvm)
|
||||||
alias(hlaeja.plugins.kotlin.spring)
|
alias(hlaeja.plugins.kotlin.spring)
|
||||||
|
alias(hlaeja.plugins.ltd.hlaeja.plugin.certificate)
|
||||||
alias(hlaeja.plugins.ltd.hlaeja.plugin.service)
|
alias(hlaeja.plugins.ltd.hlaeja.plugin.service)
|
||||||
alias(hlaeja.plugins.spring.dependency.management)
|
alias(hlaeja.plugins.spring.dependency.management)
|
||||||
alias(hlaeja.plugins.springframework.boot)
|
alias(hlaeja.plugins.springframework.boot)
|
||||||
@@ -11,8 +12,10 @@ dependencies {
|
|||||||
implementation(hlaeja.kotlin.reflect)
|
implementation(hlaeja.kotlin.reflect)
|
||||||
implementation(hlaeja.kotlinx.coroutines)
|
implementation(hlaeja.kotlinx.coroutines)
|
||||||
implementation(hlaeja.library.hlaeja.common.messages)
|
implementation(hlaeja.library.hlaeja.common.messages)
|
||||||
|
implementation(hlaeja.library.hlaeja.jwt)
|
||||||
implementation(hlaeja.springboot.starter.actuator)
|
implementation(hlaeja.springboot.starter.actuator)
|
||||||
implementation(hlaeja.springboot.starter.r2dbc)
|
implementation(hlaeja.springboot.starter.r2dbc)
|
||||||
|
implementation(hlaeja.springboot.starter.security)
|
||||||
implementation(hlaeja.springboot.starter.webflux)
|
implementation(hlaeja.springboot.starter.webflux)
|
||||||
|
|
||||||
runtimeOnly(hlaeja.postgresql)
|
runtimeOnly(hlaeja.postgresql)
|
||||||
@@ -29,3 +32,7 @@ dependencies {
|
|||||||
}
|
}
|
||||||
|
|
||||||
group = "ltd.hlaeja"
|
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 ltd.hlaeja.entity.AccountEntity
|
||||||
import org.springframework.data.repository.reactive.ReactiveCrudRepository
|
import org.springframework.data.repository.reactive.ReactiveCrudRepository
|
||||||
import org.springframework.stereotype.Repository
|
import org.springframework.stereotype.Repository
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
|
||||||
@Repository
|
@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)
|
): Mono<AccountEntity> = accountRepository.findById(uuid)
|
||||||
.doOnNext { log.debug { "Get account ${it.id}" } }
|
.doOnNext { log.debug { "Get account ${it.id}" } }
|
||||||
.switchIfEmpty(Mono.error(ResponseStatusException(HttpStatus.NOT_FOUND)))
|
.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%"
|
name: "%APP_BUILD_OS_NAME%"
|
||||||
version: "%APP_BUILD_OS_VERSION%"
|
version: "%APP_BUILD_OS_VERSION%"
|
||||||
|
|
||||||
|
jwt:
|
||||||
|
private-key: cert/private_key.pem
|
||||||
|
|
||||||
---
|
---
|
||||||
###############################
|
###############################
|
||||||
### Development environment ###
|
### 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