Skip to content

Commit 1e3e150

Browse files
committed
feat: add 2FA auth
1 parent 72f7a68 commit 1e3e150

File tree

11 files changed

+134
-23
lines changed

11 files changed

+134
-23
lines changed

contributed/requests/address.http

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ Content-Type: application/json
1212

1313
{
1414
"username": "admin.admin",
15-
"password": "test1234"
15+
"password": "test1234",
16+
"verificationCode": ""
1617
}
1718

1819
> {%
@@ -22,6 +23,23 @@ Content-Type: application/json
2223
client.global.set("auth_token", response.headers.valueOf("Authorization").replace("Bearer ", ""));
2324
%}
2425

26+
### Login as user
27+
POST http://localhost:{{port}}/starter-test/login
28+
Content-Type: application/json
29+
30+
{
31+
"username": "john.doe",
32+
"password": "test1234",
33+
"verificationCode": "479346"
34+
}
35+
36+
> {%
37+
client.test("Request executed successfully", function() {
38+
client.assert(response.status === 200, "Response status is not 200");
39+
});
40+
%}
41+
42+
2543

2644
### Add / Update
2745
POST http://localhost:{{port}}/starter-test/api/v1/address

pom.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
<jasypt.version>1.9.3</jasypt.version>
3333
<opencsv.version>5.7.1</opencsv.version>
3434
<commons-io.version>2.11.0</commons-io.version>
35+
<aerogear-otp-java.version>1.0.0</aerogear-otp-java.version>
3536
</properties>
3637

3738
<dependencies>
@@ -72,6 +73,12 @@
7273
<version>${kotlin.version}</version>
7374
</dependency>
7475

76+
<dependency>
77+
<groupId>org.jboss.aerogear</groupId>
78+
<artifactId>aerogear-otp-java</artifactId>
79+
<version>${aerogear-otp-java.version}</version>
80+
</dependency>
81+
7582
<dependency>
7683
<groupId>io.jsonwebtoken</groupId>
7784
<artifactId>jjwt-api</artifactId>

src/main/kotlin/osahner/config/WebConfig.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,10 @@ import org.springframework.security.web.SecurityFilterChain
1111
import org.springframework.web.cors.CorsConfiguration
1212
import org.springframework.web.cors.CorsConfigurationSource
1313
import org.springframework.web.cors.UrlBasedCorsConfigurationSource
14-
import osahner.security.JWTAuthenticationFilter
15-
import osahner.security.JWTAuthorizationFilter
16-
import osahner.security.TokenProvider
14+
import osahner.security.*
1715
import osahner.service.AppAuthenticationManager
1816

17+
1918
@Configuration
2019
@EnableWebSecurity
2120
@EnableMethodSecurity(prePostEnabled = true)
@@ -65,4 +64,5 @@ class WebConfig(
6564
cors.registerCorsConfiguration("/**", this)
6665
}
6766
}
67+
6868
}

src/main/kotlin/osahner/domain/User.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package osahner.domain
55
import jakarta.persistence.*
66
import org.hibernate.annotations.GenericGenerator
77
import org.hibernate.annotations.Parameter
8+
import org.jboss.aerogear.security.otp.api.Base32
89

910
@Entity
1011
@Table(name = "app_user")
@@ -34,6 +35,10 @@ class User(
3435
@Column(name = "last_name")
3536
var lastName: String? = null,
3637

38+
var isUsing2FA: Boolean = false,
39+
40+
var secret: String = Base32.random(),
41+
3742
@ManyToMany(fetch = FetchType.EAGER, cascade = [CascadeType.MERGE])
3843
@JoinTable(
3944
name = "user_role",

src/main/kotlin/osahner/security/JWTAuthenticationFilter.kt

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,17 @@ import jakarta.servlet.FilterChain
66
import jakarta.servlet.ServletException
77
import jakarta.servlet.http.HttpServletRequest
88
import jakarta.servlet.http.HttpServletResponse
9-
import org.springframework.security.authentication.AuthenticationManager
109
import org.springframework.security.authentication.AuthenticationServiceException
1110
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
1211
import org.springframework.security.core.Authentication
1312
import org.springframework.security.core.AuthenticationException
1413
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
1514
import osahner.config.SecurityProperties
15+
import osahner.service.AppAuthenticationManager
1616
import java.io.IOException
1717

1818
class JWTAuthenticationFilter(
19-
private val authManager: AuthenticationManager,
19+
private val authManager: AppAuthenticationManager,
2020
private val securityProperties: SecurityProperties,
2121
private val tokenProvider: TokenProvider
2222
) : UsernamePasswordAuthenticationFilter() {
@@ -29,15 +29,16 @@ class JWTAuthenticationFilter(
2929
return try {
3030
val mapper = jacksonObjectMapper()
3131

32-
val creds = mapper
33-
.readValue<osahner.domain.User>(req.inputStream)
32+
val creds = mapper.readValue<UserLoginDTO>(req.inputStream)
3433

3534
authManager.authenticate(
3635
UsernamePasswordAuthenticationToken(
3736
creds.username,
3837
creds.password,
3938
ArrayList()
40-
)
39+
).apply {
40+
details = mapOf("verificationCode" to creds.verificationCode)
41+
}
4142
)
4243
} catch (e: IOException) {
4344
throw AuthenticationServiceException(e.message)
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package osahner.security
2+
3+
data class UserLoginDTO(
4+
val username: String,
5+
val password: String,
6+
val verificationCode: String? = null
7+
)
Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,54 @@
11
package osahner.service
22

3+
import org.jboss.aerogear.security.otp.Totp
34
import org.springframework.security.authentication.AuthenticationManager
45
import org.springframework.security.authentication.BadCredentialsException
56
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
67
import org.springframework.security.core.Authentication
78
import org.springframework.security.core.AuthenticationException
9+
import org.springframework.security.core.GrantedAuthority
10+
import org.springframework.security.core.authority.SimpleGrantedAuthority
11+
import org.springframework.security.core.userdetails.UsernameNotFoundException
812
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
913
import org.springframework.stereotype.Component
14+
import osahner.domain.User
1015

1116

1217
@Component
1318
class AppAuthenticationManager(
14-
private val userService: AppUserDetailsService, val bCryptPasswordEncoder: BCryptPasswordEncoder,
19+
private val userRepository: UserRepository,
20+
val bCryptPasswordEncoder: BCryptPasswordEncoder,
1521
) : AuthenticationManager {
1622
@Throws(AuthenticationException::class)
17-
override fun authenticate(authentication: Authentication): Authentication? {
23+
override fun authenticate(authentication: Authentication): Authentication {
1824
val password = authentication.credentials.toString()
19-
val user = userService.loadUserByUsername(authentication.name)
25+
val user: User = userRepository.findByUsername(authentication.name).orElseThrow {
26+
UsernameNotFoundException("The username ${authentication.name} doesn't exist")
27+
}
2028
if (!bCryptPasswordEncoder.matches(password, user.password)) {
2129
throw BadCredentialsException("Bad credentials")
2230
}
23-
return UsernamePasswordAuthenticationToken(user.username, user.password, user.authorities)
31+
if (user.isUsing2FA) {
32+
val verificationCode: String = (authentication.details as Map<*, *>)["verificationCode"].toString()
33+
val totp = Totp(user.secret)
34+
when {
35+
!isValidLong(verificationCode) -> throw BadCredentialsException("Invalid verfication code")
36+
!totp.verify(verificationCode) -> throw BadCredentialsException("Invalid verfication code")
37+
}
38+
}
39+
val authorities = ArrayList<GrantedAuthority>()
40+
if (user.roles != null) {
41+
user.roles!!.forEach { authorities.add(SimpleGrantedAuthority(it.roleName)) }
42+
}
43+
return UsernamePasswordAuthenticationToken(user.username, user.password, authorities)
44+
}
45+
46+
private fun isValidLong(code: String): Boolean {
47+
try {
48+
code.toLong()
49+
} catch (e: NumberFormatException) {
50+
return false
51+
}
52+
return true
2453
}
2554
}

src/main/kotlin/osahner/service/AppUserDetailsService.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import org.springframework.security.core.userdetails.UserDetailsService
88
import org.springframework.security.core.userdetails.UsernameNotFoundException
99
import org.springframework.stereotype.Component
1010

11+
1112
@Component
1213
class AppUserDetailsService(private val userRepository: UserRepository) : UserDetailsService {
1314

src/main/resources/application.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ management:
3939
spring:
4040
jpa:
4141
hibernate:
42-
ddl-auto: update
42+
ddl-auto: create-drop
4343
properties:
4444
hibernate.show_sql: true
4545
config:

src/main/resources/data.sql

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ VALUES (2, 'STANDARD_USER', 'Standard User - Has no admin rights');
88

99
TRUNCATE TABLE app_user;
1010
-- password test1234
11-
INSERT INTO app_user (id, first_name, last_name, password, username)
12-
VALUES (1, 'Admin', 'Admin', '$2a$10$5AWyzymSnNypg9BkMOyKE.zA05GtRKHCoWimh.q2w.KAO5koBYPM6', 'admin.admin');
11+
INSERT INTO app_user (id, first_name, last_name, password, username, isUsing2FA, secret)
12+
VALUES (1, 'Admin', 'Admin', '$2a$10$5AWyzymSnNypg9BkMOyKE.zA05GtRKHCoWimh.q2w.KAO5koBYPM6', 'admin.admin', false, '5TPFV3VGX3EKYAET');
1313
-- password test1234
14-
INSERT INTO app_user (id, first_name, last_name, password, username)
15-
VALUES (2, 'John', 'Doe', '$2a$10$5AWyzymSnNypg9BkMOyKE.zA05GtRKHCoWimh.q2w.KAO5koBYPM6', 'john.doe');
14+
INSERT INTO app_user (id, first_name, last_name, password, username, isUsing2FA, secret)
15+
VALUES (2, 'John', 'Doe', '$2a$10$5AWyzymSnNypg9BkMOyKE.zA05GtRKHCoWimh.q2w.KAO5koBYPM6', 'john.doe', true, '5TPFV3VGX3EKYAET');
1616

1717
TRUNCATE TABLE user_role;
1818
INSERT INTO user_role(user_id, role_id)

0 commit comments

Comments
 (0)