mirror of
https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC.git
synced 2024-12-22 12:30:16 +02:00
MOB-42 Fixed token authentication issues (wrong library version, cache getting recreated every request, origin in wrong form)
This commit is contained in:
parent
9b0cb1a22d
commit
636beeb7f3
@ -90,7 +90,7 @@ class AuthFragment : Fragment() {
|
|||||||
if (args.auth) {
|
if (args.auth) {
|
||||||
val jws = Authenticator(comms).authenticate(
|
val jws = Authenticator(comms).authenticate(
|
||||||
intentParameters.challenge,
|
intentParameters.challenge,
|
||||||
intentParameters.authUrl,
|
intentParameters.origin,
|
||||||
viewModel.userPin
|
viewModel.userPin
|
||||||
)
|
)
|
||||||
intentParameters.setToken(jws)
|
intentParameters.setToken(jws)
|
||||||
|
@ -14,6 +14,7 @@ import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentHomeBinding
|
|||||||
import com.tarkvaraprojekt.mobileauthapp.model.ParametersViewModel
|
import com.tarkvaraprojekt.mobileauthapp.model.ParametersViewModel
|
||||||
import com.tarkvaraprojekt.mobileauthapp.model.SmartCardViewModel
|
import com.tarkvaraprojekt.mobileauthapp.model.SmartCardViewModel
|
||||||
import java.lang.Exception
|
import java.lang.Exception
|
||||||
|
import java.net.URLDecoder
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HomeFragment is only shown to the user when then the user launches the application. When the application
|
* HomeFragment is only shown to the user when then the user launches the application. When the application
|
||||||
@ -30,6 +31,7 @@ class HomeFragment : Fragment() {
|
|||||||
|
|
||||||
private var binding: FragmentHomeBinding? = null
|
private var binding: FragmentHomeBinding? = null
|
||||||
|
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
@ -56,11 +58,16 @@ class HomeFragment : Fragment() {
|
|||||||
// We use !! because we want an exception when something is not right.
|
// We use !! because we want an exception when something is not right.
|
||||||
intentParams.setChallenge(requireActivity().intent.getStringExtra("challenge")!!)
|
intentParams.setChallenge(requireActivity().intent.getStringExtra("challenge")!!)
|
||||||
intentParams.setAuthUrl(requireActivity().intent.getStringExtra("authUrl")!!)
|
intentParams.setAuthUrl(requireActivity().intent.getStringExtra("authUrl")!!)
|
||||||
|
intentParams.setOrigin(requireActivity().intent.getStringExtra("originUrl")!!)
|
||||||
} else { //Website
|
} else { //Website
|
||||||
// Currently the test website won't send the authUrl parameter
|
// Currently the test website won't send the authUrl parameter
|
||||||
//Log.i("intentDebugging", requireActivity().intent.data.toString())
|
//Log.i("intentDebugging", requireActivity().intent.data.toString())
|
||||||
intentParams.setChallenge(requireActivity().intent.data!!.getQueryParameter("challenge")!!)
|
var challenge = requireActivity().intent.data!!.getQueryParameter("challenge")!!
|
||||||
|
// TODO: Since due to encoding plus gets converted to space, temporary solution is to replace it back.
|
||||||
|
challenge = challenge.replace(" ", "+")
|
||||||
|
intentParams.setChallenge(challenge)
|
||||||
intentParams.setAuthUrl(requireActivity().intent.data!!.getQueryParameter("authUrl")!!)
|
intentParams.setAuthUrl(requireActivity().intent.data!!.getQueryParameter("authUrl")!!)
|
||||||
|
intentParams.setOrigin(requireActivity().intent.data!!.getQueryParameter("originUrl")!!)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// There was a problem with parameters, which means that authentication is not possible.
|
// There was a problem with parameters, which means that authentication is not possible.
|
||||||
|
@ -67,7 +67,7 @@ class ResultFragment : Fragment() {
|
|||||||
Ion.getDefault(activity).getConscryptMiddleware().enable(false)
|
Ion.getDefault(activity).getConscryptMiddleware().enable(false)
|
||||||
|
|
||||||
Ion.with(activity)
|
Ion.with(activity)
|
||||||
.load("https://6bb0-85-253-195-252.ngrok.io/auth/authentication")
|
.load(paramsModel.origin + paramsModel.authUrl)
|
||||||
.setJsonObjectBody(json)
|
.setJsonObjectBody(json)
|
||||||
.asJsonObject()
|
.asJsonObject()
|
||||||
.setCallback { e, result ->
|
.setCallback { e, result ->
|
||||||
|
@ -8,7 +8,7 @@ import java.security.MessageDigest
|
|||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.ZoneOffset
|
import java.time.ZoneOffset
|
||||||
|
|
||||||
class Authenticator(val comms : Comms) {
|
class Authenticator(val comms: Comms) {
|
||||||
|
|
||||||
val type = "JWT"
|
val type = "JWT"
|
||||||
val algorithm = "ES384"
|
val algorithm = "ES384"
|
||||||
@ -36,7 +36,7 @@ class Authenticator(val comms : Comms) {
|
|||||||
// Get header and claims.
|
// Get header and claims.
|
||||||
val header = """{"typ":"$type","alg":"$algorithm","x5c":["$base64cert"]}"""
|
val header = """{"typ":"$type","alg":"$algorithm","x5c":["$base64cert"]}"""
|
||||||
val claims =
|
val claims =
|
||||||
"""{"iat":"$epoch","exp":"$exp","aud":"$originUrl","iss":"$iss","sub":"$sub","nonce":"$challenge","cnf":{"tbh":""}}"""
|
"""{"iat":"$epoch","exp":"$exp","aud":["$originUrl"],"iss":"$iss","sub":"$sub","nonce":"$challenge","cnf":{"tbh":""}}"""
|
||||||
|
|
||||||
val jwt = base64Encode(header.toByteArray(Charsets.UTF_8)) + "." + base64Encode(
|
val jwt = base64Encode(header.toByteArray(Charsets.UTF_8)) + "." + base64Encode(
|
||||||
claims.toByteArray(Charsets.UTF_8)
|
claims.toByteArray(Charsets.UTF_8)
|
||||||
@ -51,7 +51,7 @@ class Authenticator(val comms : Comms) {
|
|||||||
return jwt + "." + base64Encode(signed)
|
return jwt + "." + base64Encode(signed)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun base64Encode(bytes: ByteArray) : String? {
|
fun base64Encode(bytes: ByteArray): String? {
|
||||||
val encoded = java.util.Base64.getUrlEncoder().encodeToString(bytes)
|
val encoded = java.util.Base64.getUrlEncoder().encodeToString(bytes)
|
||||||
return encoded.replace("=", "")
|
return encoded.replace("=", "")
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,9 @@ class ParametersViewModel: ViewModel() {
|
|||||||
private var _token: String = ""
|
private var _token: String = ""
|
||||||
val token get() = _token
|
val token get() = _token
|
||||||
|
|
||||||
|
private var _origin: String = ""
|
||||||
|
val origin get() = _origin
|
||||||
|
|
||||||
fun setChallenge(newChallenge: String) {
|
fun setChallenge(newChallenge: String) {
|
||||||
_challenge = newChallenge
|
_challenge = newChallenge
|
||||||
}
|
}
|
||||||
@ -24,4 +27,8 @@ class ParametersViewModel: ViewModel() {
|
|||||||
fun setToken(newToken: String) {
|
fun setToken(newToken: String) {
|
||||||
_token = newToken
|
_token = newToken
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setOrigin(newOrigin: String) {
|
||||||
|
_origin = newOrigin
|
||||||
|
}
|
||||||
}
|
}
|
@ -42,6 +42,11 @@
|
|||||||
<artifactId>spring-boot-starter-test</artifactId>
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.squareup.okhttp3</groupId>
|
||||||
|
<artifactId>okhttp</artifactId>
|
||||||
|
<version>4.9.0</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.webeid.security</groupId>
|
<groupId>org.webeid.security</groupId>
|
||||||
<artifactId>authtoken-validation</artifactId>
|
<artifactId>authtoken-validation</artifactId>
|
||||||
|
@ -10,7 +10,7 @@ class ApplicationConfiguration {
|
|||||||
// Endpoint for authentication
|
// Endpoint for authentication
|
||||||
val AUTHENTICATION_ENDPOINT_URL = "/auth/authentication"
|
val AUTHENTICATION_ENDPOINT_URL = "/auth/authentication"
|
||||||
// URL for application. Use ngrok for HTTPS (or a tool of your own choice) and put the HTTPS link here.
|
// URL for application. Use ngrok for HTTPS (or a tool of your own choice) and put the HTTPS link here.
|
||||||
val WEBSITE_ORIGIN_URL = "https://6bb0-85-253-195-252.ngrok.io"
|
val WEBSITE_ORIGIN_URL = "https://2c2c-85-253-195-252.ngrok.io"
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -1,6 +1,8 @@
|
|||||||
package com.tarkvaratehnika.demobackend.config
|
package com.tarkvaratehnika.demobackend.config
|
||||||
|
|
||||||
import com.github.benmanes.caffeine.jcache.spi.CaffeineCachingProvider
|
import com.github.benmanes.caffeine.jcache.spi.CaffeineCachingProvider
|
||||||
|
import org.slf4j.Logger
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.context.annotation.Bean
|
import org.springframework.context.annotation.Bean
|
||||||
import org.springframework.context.annotation.Configuration
|
import org.springframework.context.annotation.Configuration
|
||||||
import org.springframework.core.io.support.PathMatchingResourcePatternResolver
|
import org.springframework.core.io.support.PathMatchingResourcePatternResolver
|
||||||
@ -28,14 +30,25 @@ import javax.cache.configuration.MutableConfiguration
|
|||||||
import javax.cache.expiry.CreatedExpiryPolicy
|
import javax.cache.expiry.CreatedExpiryPolicy
|
||||||
import javax.cache.expiry.Duration
|
import javax.cache.expiry.Duration
|
||||||
|
|
||||||
|
import javax.cache.configuration.FactoryBuilder.factoryOf
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
class ValidationConfiguration {
|
class ValidationConfiguration {
|
||||||
|
|
||||||
|
private val LOG: Logger = LoggerFactory.getLogger(ValidationConfiguration::class.java)
|
||||||
|
|
||||||
private val NONCE_TTL_MINUTES: Long = 5
|
private val NONCE_TTL_MINUTES: Long = 5
|
||||||
private val CACHE_NAME = "nonceCache"
|
private val CACHE_NAME = "nonceCache"
|
||||||
private val CERTS_RESOURCE_PATH = "/certs/"
|
private val CERTS_RESOURCE_PATH = "/certs/"
|
||||||
private val TRUSTED_CERTIFICATES_JKS = "trusted_certificates.jks"
|
private val TRUSTED_CERTIFICATES_JKS = "trusted_certificates.jks"
|
||||||
private val TRUSTSTORE_PASSWORD = "changeit"
|
private val TRUSTSTORE_PASSWORD = "changeit"
|
||||||
|
companion object {
|
||||||
|
const val ROLE_USER : String = "ROLE_USER"
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
LOG.warn("Creating new ValidationConfiguration.")
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
fun cacheManager(): CacheManager {
|
fun cacheManager(): CacheManager {
|
||||||
@ -47,7 +60,9 @@ class ValidationConfiguration {
|
|||||||
val cacheManager: CacheManager = cacheManager()
|
val cacheManager: CacheManager = cacheManager()
|
||||||
var cache =
|
var cache =
|
||||||
cacheManager.getCache<String?, ZonedDateTime?>(CACHE_NAME)
|
cacheManager.getCache<String?, ZonedDateTime?>(CACHE_NAME)
|
||||||
|
|
||||||
if (cache == null) {
|
if (cache == null) {
|
||||||
|
LOG.warn("Creating new cache.")
|
||||||
cache = createNonceCache(cacheManager)
|
cache = createNonceCache(cacheManager)
|
||||||
}
|
}
|
||||||
return cache
|
return cache
|
||||||
@ -65,7 +80,7 @@ class ValidationConfiguration {
|
|||||||
val cacheConfig: CompleteConfiguration<String, ZonedDateTime> = MutableConfiguration<String, ZonedDateTime>()
|
val cacheConfig: CompleteConfiguration<String, ZonedDateTime> = MutableConfiguration<String, ZonedDateTime>()
|
||||||
.setTypes(String::class.java, ZonedDateTime::class.java)
|
.setTypes(String::class.java, ZonedDateTime::class.java)
|
||||||
.setExpiryPolicyFactory(
|
.setExpiryPolicyFactory(
|
||||||
FactoryBuilder.factoryOf(
|
factoryOf(
|
||||||
CreatedExpiryPolicy(
|
CreatedExpiryPolicy(
|
||||||
Duration(
|
Duration(
|
||||||
TimeUnit.MINUTES,
|
TimeUnit.MINUTES,
|
||||||
|
@ -23,6 +23,9 @@
|
|||||||
package com.tarkvaratehnika.demobackend.security
|
package com.tarkvaratehnika.demobackend.security
|
||||||
|
|
||||||
import com.tarkvaratehnika.demobackend.config.ValidationConfiguration
|
import com.tarkvaratehnika.demobackend.config.ValidationConfiguration
|
||||||
|
import com.tarkvaratehnika.demobackend.config.ValidationConfiguration.Companion.ROLE_USER
|
||||||
|
import com.tarkvaratehnika.demobackend.web.rest.AuthenticationController
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.security.authentication.AuthenticationServiceException
|
import org.springframework.security.authentication.AuthenticationServiceException
|
||||||
import org.springframework.security.core.Authentication
|
import org.springframework.security.core.Authentication
|
||||||
import org.springframework.security.core.AuthenticationException
|
import org.springframework.security.core.AuthenticationException
|
||||||
@ -37,11 +40,11 @@ import java.security.cert.X509Certificate
|
|||||||
|
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
class AuthTokenDTOAuthenticationProvider {
|
object AuthTokenDTOAuthenticationProvider {
|
||||||
|
|
||||||
|
private val LOG = LoggerFactory.getLogger(AuthTokenDTOAuthenticationProvider::class.java)
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val ROLE_USER : String = "ROLE_USER"
|
|
||||||
}
|
|
||||||
private val USER_ROLE: GrantedAuthority = SimpleGrantedAuthority(ROLE_USER)
|
private val USER_ROLE: GrantedAuthority = SimpleGrantedAuthority(ROLE_USER)
|
||||||
|
|
||||||
|
|
||||||
@ -52,7 +55,6 @@ class AuthTokenDTOAuthenticationProvider {
|
|||||||
val authentication = auth as PreAuthenticatedAuthenticationToken
|
val authentication = auth as PreAuthenticatedAuthenticationToken
|
||||||
val token = (authentication.credentials as AuthTokenDTO).token
|
val token = (authentication.credentials as AuthTokenDTO).token
|
||||||
val challenge = (authentication.credentials as AuthTokenDTO).challenge
|
val challenge = (authentication.credentials as AuthTokenDTO).challenge
|
||||||
|
|
||||||
val authorities = arrayListOf<GrantedAuthority>()
|
val authorities = arrayListOf<GrantedAuthority>()
|
||||||
authorities.add(USER_ROLE)
|
authorities.add(USER_ROLE)
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package com.tarkvaratehnika.demobackend.web
|
package com.tarkvaratehnika.demobackend.web
|
||||||
|
|
||||||
import com.tarkvaratehnika.demobackend.security.AuthTokenDTOAuthenticationProvider.Companion.ROLE_USER
|
import com.tarkvaratehnika.demobackend.config.ValidationConfiguration.Companion.ROLE_USER
|
||||||
import org.springframework.security.access.prepost.PreAuthorize
|
import org.springframework.security.access.prepost.PreAuthorize
|
||||||
import org.springframework.stereotype.Controller
|
import org.springframework.stereotype.Controller
|
||||||
import org.springframework.ui.Model
|
import org.springframework.ui.Model
|
||||||
|
@ -26,7 +26,7 @@ class AuthenticationController {
|
|||||||
val auth = PreAuthenticatedAuthenticationToken(null, authToken)
|
val auth = PreAuthenticatedAuthenticationToken(null, authToken)
|
||||||
|
|
||||||
// Return authentication object if success.
|
// Return authentication object if success.
|
||||||
return AuthTokenDTOAuthenticationProvider().authenticate(auth)
|
return AuthTokenDTOAuthenticationProvider.authenticate(auth)
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("authentication", produces = [MediaType.APPLICATION_JSON_VALUE])
|
@GetMapping("authentication", produces = [MediaType.APPLICATION_JSON_VALUE])
|
||||||
|
@ -24,6 +24,7 @@ package com.tarkvaratehnika.demobackend.web.rest
|
|||||||
|
|
||||||
import com.tarkvaratehnika.demobackend.dto.ChallengeDto
|
import com.tarkvaratehnika.demobackend.dto.ChallengeDto
|
||||||
import com.tarkvaratehnika.demobackend.security.WebEidAuthentication
|
import com.tarkvaratehnika.demobackend.security.WebEidAuthentication
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.web.bind.annotation.GetMapping
|
import org.springframework.web.bind.annotation.GetMapping
|
||||||
import org.springframework.web.bind.annotation.RequestMapping
|
import org.springframework.web.bind.annotation.RequestMapping
|
||||||
import org.springframework.web.bind.annotation.RestController
|
import org.springframework.web.bind.annotation.RestController
|
||||||
@ -33,10 +34,12 @@ import org.webeid.security.nonce.NonceGenerator
|
|||||||
@RequestMapping("auth")
|
@RequestMapping("auth")
|
||||||
class ChallengeController (val nonceGenerator: NonceGenerator) {
|
class ChallengeController (val nonceGenerator: NonceGenerator) {
|
||||||
|
|
||||||
|
private val LOG = LoggerFactory.getLogger(ChallengeController::class.java)
|
||||||
|
|
||||||
@GetMapping("challenge")
|
@GetMapping("challenge")
|
||||||
fun challenge(): ChallengeDto {
|
fun challenge(): ChallengeDto {
|
||||||
val challengeDto = ChallengeDto(nonceGenerator.generateAndStoreNonce())
|
val challengeDto = ChallengeDto(nonceGenerator.generateAndStoreNonce())
|
||||||
|
LOG.warn(challengeDto.nonce)
|
||||||
// WebEidAuthentication.addAuth(challengeDto.nonce) // For testing.
|
// WebEidAuthentication.addAuth(challengeDto.nonce) // For testing.
|
||||||
return challengeDto
|
return challengeDto
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
html {
|
||||||
|
font-size: 4vw;
|
||||||
|
}
|
||||||
|
|
||||||
.cont {
|
.cont {
|
||||||
display: grid;
|
display: grid;
|
||||||
width: 80%;
|
width: 80%;
|
||||||
|
@ -11,7 +11,7 @@ function launchAuthApp(action) {
|
|||||||
httpGetAsync(originUrl + challengeUrl, (body) => {
|
httpGetAsync(originUrl + challengeUrl, (body) => {
|
||||||
let data = JSON.parse(body);
|
let data = JSON.parse(body);
|
||||||
let challenge = data.nonce;
|
let challenge = data.nonce;
|
||||||
let intent = createParametrizedIntentUrl(challenge, action); // TODO: Error handling.
|
let intent = createParametrizedIntentUrl(challenge, action, originUrl); // TODO: Error handling.
|
||||||
console.log(intent);
|
console.log(intent);
|
||||||
window.location.href = intent;
|
window.location.href = intent;
|
||||||
pollForAuth(POLLING_INTERVAL, challenge);
|
pollForAuth(POLLING_INTERVAL, challenge);
|
||||||
@ -20,8 +20,8 @@ function launchAuthApp(action) {
|
|||||||
|
|
||||||
function pollForAuth(timeout, challenge) {
|
function pollForAuth(timeout, challenge) {
|
||||||
console.log("Polling for auth");
|
console.log("Polling for auth");
|
||||||
let requestUrl = originUrl + authenticationRequestUrl + "?challenge=" + challenge;
|
let encodedChallenge = encodeURIComponent(challenge);
|
||||||
|
let requestUrl = originUrl + authenticationRequestUrl + "?challenge=" + encodedChallenge;
|
||||||
let counter = 0;
|
let counter = 0;
|
||||||
let timer = setInterval(() => {
|
let timer = setInterval(() => {
|
||||||
// Fetch authentication object.
|
// Fetch authentication object.
|
||||||
@ -48,7 +48,7 @@ function createParametrizedIntentUrl(challenge, action) {
|
|||||||
else if (challenge == null) {
|
else if (challenge == null) {
|
||||||
console.error("Challenge missing, can't authenticate without it.")
|
console.error("Challenge missing, can't authenticate without it.")
|
||||||
} else {
|
} else {
|
||||||
return intentUrl + "?" + "action=" + action + "&challenge=" + challenge + "&authUrl=" + originUrl + authenticationRequestUrl;
|
return intentUrl + "?" + "action=" + action + "&challenge=" + encodeURIComponent(challenge) + "&authUrl=" + authenticationRequestUrl + "&originUrl=" + originUrl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<nav class="navbar navbar-dark bg-dark">
|
<nav class="navbar navbar-dark bg-dark">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<a class="navbar-brand" href="#">Auth demo web application</a>
|
<a class="navbar-brand" href="#">Auth demo webapp</a>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="cont">
|
<div class="cont">
|
||||||
@ -29,7 +29,7 @@
|
|||||||
website using your ID card by using the button below.</h4>
|
website using your ID card by using the button below.</h4>
|
||||||
<h5>Make sure you've installed the authentication app from: <a
|
<h5>Make sure you've installed the authentication app from: <a
|
||||||
href="https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC">GitHub</a></h5>
|
href="https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC">GitHub</a></h5>
|
||||||
<button type="button" class="btn btn-secondary" id="loginButton" data-action="auth">Log in</button>
|
<button type="button" class="btn btn-lg btn-secondary" id="loginButton" data-action="auth">Log in</button>
|
||||||
<div class="alert alert-danger d-none" role="alert" id="loginErrorAlert">
|
<div class="alert alert-danger d-none" role="alert" id="loginErrorAlert">
|
||||||
Login failed. Refresh the page to try again.
|
Login failed. Refresh the page to try again.
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user