MOB-55 Fixed some issues with session management.

This commit is contained in:
TanelOrumaa 2021-12-12 20:06:08 +02:00
parent 13a0a9430f
commit 8b78ddf51a
17 changed files with 456 additions and 126 deletions

View File

@ -7,9 +7,10 @@
<p class="text-center">Read more from <a href="https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC">here.</a></p> <p class="text-center">Read more from <a href="https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC">here.</a></p>
</div> </div>
<div id="canvas"></div>
<div class="justify-content-center d-flex"> <div class="justify-content-center d-flex">
<div id="canvas"></div>
</div>
<div class="justify-content-center d-flex">
<button type="button" class="btn loginButton btn-dark" v-on:click="authenticate"> <button type="button" class="btn loginButton btn-dark" v-on:click="authenticate">
<div v-if="loading" class="d-flex justify-content-center"> <div v-if="loading" class="d-flex justify-content-center">
<div class="spinner-border text-light spinner-border-sm" role="status"> <div class="spinner-border text-light spinner-border-sm" role="status">
@ -20,7 +21,7 @@
</button> </button>
</div> </div>
<div class="btn-group-sm d-flex justify-content-center" role="group" aria-label="Basic radio toggle button group"> <div class="btn-group-sm d-flex justify-content-center" v-if="!isAndroidDevice" role="group" aria-label="Basic radio toggle button group">
<input type="radio" class="btn-check" name="btnradio" id="btnCardReader" autocomplete="off" v-on:click="useCardReader"> <input type="radio" class="btn-check" name="btnradio" id="btnCardReader" autocomplete="off" v-on:click="useCardReader">
<label class="btn btn-outline-secondary" for="btnCardReader">using ID-card reader</label> <label class="btn btn-outline-secondary" for="btnCardReader">using ID-card reader</label>
@ -39,14 +40,11 @@ import router from "@/router";
export default { export default {
name: 'LoginComponent', name: 'LoginComponent',
props: {
"csrftoken": String,
"csrfHeaderName": String,
},
data() { data() {
return { return {
useAndroidApp: true, useAndroidApp: true,
loading: false, loading: false,
challenge: "",
} }
}, },
methods: { methods: {
@ -58,6 +56,8 @@ export default {
this.useAndroidApp = false; this.useAndroidApp = false;
}, },
authenticate: async function () { authenticate: async function () {
this.loading = true; this.loading = true;
@ -69,7 +69,6 @@ export default {
headers: { headers: {
"sessionId": this.$store.getters.getSessionId "sessionId": this.$store.getters.getSessionId
}, },
}; };
console.log(options); console.log(options);
@ -78,7 +77,7 @@ export default {
const response = await webeid.authenticate(options); const response = await webeid.authenticate(options);
console.log("Authentication successful! Response:", response); console.log("Authentication successful! Response:", response);
this.loading = false; this.loading = false;
this.$store.commit("setLoggedIn", true); this.$store.dispatch("setLoggedIn", true);
await router.push("welcome"); await router.push("welcome");
} catch (error) { } catch (error) {
@ -95,7 +94,14 @@ export default {
}, },
loading() { loading() {
return this.loading; return this.loading;
},
isAndroidDevice() {
return this.$store.getters.getIsAndroid
} }
},
mounted() {
const isAndroid = webeid.isAndroidDevice();
this.$store.dispatch("setIsAndroid", isAndroid);
} }
} }
</script> </script>

View File

@ -31,23 +31,21 @@ export default {
fetch("/auth/logout", requestOptions) fetch("/auth/logout", requestOptions)
.then((response) => { .then((response) => {
console.log(response); console.log(response);
this.$store.commit("setLoggedIn", false); this.$store.dispatch("setLoggedIn", false);
router.push("/"); router.push("/");
} }
) )
} }
}, },
mounted() { mounted() {
if (this.$store.getters.getSessionId == null) {
const sessionId = this.$cookie.getCookie("JSESSIONID"); const sessionId = this.$cookie.getCookie("JSESSIONID");
this.$store.dispatch("fetchSessionId", sessionId); this.$store.dispatch("fetchSessionId", sessionId);
} }
} }
}
</script> </script>
<style scoped> <style scoped>
nav { nav {
height: 5vh; height: 7vh;
} }
</style> </style>

View File

@ -1,8 +1,11 @@
<template> <template>
<div class="container container-md d-flex flex-column"> <div class="container container-md d-flex flex-column">
<div> <div>
<h3 class="text-center">Congratulations, you logged into the site. Log out to try again.</h3> <h3 class="text-center">Welcome {{ userName }}!</h3>
<p class="text-center">Read more from <a href="https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC">here.</a></p> <h4 class="text-center">{{ userIdCode }}</h4>
<p class="text-center">You've successfully logged into this site using your ID card.</p>
<p class="text-center">Read more from <a
href="https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC">here.</a></p>
</div> </div>
</div> </div>
@ -12,14 +15,46 @@
<script> <script>
export default { export default {
name: 'WelcomeComponent', name: 'WelcomeComponent',
props: { props: {},
"csrftoken": String, methods: {
getUserData: async function () {
const requestOptions = {
method: "GET",
headers: {
"sessionid": this.$store.getters.getSessionId
}
};
fetch("/auth/userData", requestOptions)
.then((response) => {
let data = response.body;
data.getReader().read().then((body) => {
let authObject = JSON.parse(new TextDecoder().decode(body.value));
this.$store.dispatch("setUserName", authObject.userData.name);
let idCode = authObject.userData.idCode.substring(6)
console.log(idCode)
this.$store.dispatch("setUserIdCode", idCode);
});
console.log(data);
}
);
},
}, },
computed: { computed: {
isLoggedIn() { isLoggedIn() {
return this.$store.getters.getAuthenticated; return this.$store.getters.getAuthenticated;
},
userName() {
return this.$store.getters.getUserName;
},
userIdCode() {
return this.$store.getters.getUserIdCode;
} }
} }
,
mounted() {
// Get user data.
this.getUserData();
}
} }
</script> </script>

View File

@ -16,6 +16,9 @@ const store = createStore({
return { return {
authenticated: false, authenticated: false,
jSessionId: null, jSessionId: null,
isAndroid: false,
userName: null,
userIdCode: null,
} }
}, },
mutations: { mutations: {
@ -24,11 +27,32 @@ const store = createStore({
}, },
setSessionId(state, sessionId) { setSessionId(state, sessionId) {
state.jSessionId = sessionId; state.jSessionId = sessionId;
},
setIsAndroid(state, isAndroid) {
state.isAndroid = isAndroid;
},
setUserName(state, userName) {
state.userName = userName;
},
setIdCode(state, idCode) {
state.userIdCode = idCode;
} }
}, },
actions: { actions: {
fetchSessionId(context, sessionId) { fetchSessionId(context, sessionId) {
context.commit("setSessionId", sessionId); context.commit("setSessionId", sessionId);
},
setLoggedIn(context, isLoggedIn) {
context.commit("setLoggedIn", isLoggedIn);
},
setIsAndroid(context, isAndroid) {
context.commit("setIsAndroid", isAndroid);
},
setUserName(context, userName) {
context.commit("setUserName", userName);
},
setUserIdCode(context, userIdCode) {
context.commit("setIdCode", userIdCode);
} }
}, },
getters: { getters: {
@ -37,7 +61,16 @@ const store = createStore({
}, },
getSessionId: state => { getSessionId: state => {
return state.jSessionId; return state.jSessionId;
} },
getIsAndroid: state => {
return state.isAndroid;
},
getUserName: state => {
return state.userName;
},
getUserIdCode: state => {
return state.userIdCode;
},
}, },
plugins: [createPersistedState()], plugins: [createPersistedState()],
}) })

View File

@ -8658,16 +8658,33 @@ class WebExtensionService {
} }
publishMessage(message, timeout) { publishMessage(message, timeout) {
if (message.useAuthApp && message.useAuthApp == true) { if (message.useAuthApp && message.useAuthApp == true) {
if (this.isAndroidDevice()) { if (isAndroidDevice()) {
// Launch auth app. // Launch auth app.
console.log("Launching auth app");
this.launchAuthApp(message); this.launchAuthApp(message);
} }
else { else {
// Display QR code. // Display QR code.
this.displayQRCode(message); this.displayQRCode(message);
} }
this.pollForLoginSuccess(message, timeout).then((res) => { console.log("Polling for success.");
console.log(res); this.pollForLoginSuccess(message, timeout).then((req) => {
req.on("response", (res) => {
if (res.statusCode == 200) {
console.log(res.statusCode);
window.postMessage({ action: this.getRelevantSuccessAction(message) }, location.origin);
res.on("data", (data) => {
console.log("HERE WE GOOO:" + data);
});
}
else {
this.removeFromQueue(message.action);
return Promise.reject(new ServerRejectedError("Server rejected the authentication."));
}
}).on("error", () => {
this.removeFromQueue(message.action);
return Promise.reject(new ServerRejectedError("Server unreachable."));
});
}); });
} }
else { else {
@ -8703,35 +8720,29 @@ class WebExtensionService {
if (!message.getAuthSuccessUrl.startsWith("https://")) { if (!message.getAuthSuccessUrl.startsWith("https://")) {
throw new ProtocolInsecureError(`HTTPS required for getAuthSuccessUrl ${message.getAuthSuccessUrl}`); throw new ProtocolInsecureError(`HTTPS required for getAuthSuccessUrl ${message.getAuthSuccessUrl}`);
} }
console.log("Polling for success.");
const headers = message.headers; const headers = message.headers;
const url = new URL(message.getAuthSuccessUrl);
const host = url.hostname;
const port = url.port;
const path = url.pathname;
const options = { const options = {
host: host,
port: port,
path: path,
method: "GET", method: "GET",
headers: headers, headers: headers,
timeout: timeout, timeout: timeout,
}; };
return get(message.getAuthSuccessUrl, options).on("error", (e) => { return get(options, (res) => {
console.error(e); console.log("Polling request answered.");
}).on("data", (data) => {
console.log("DATA: " + data);
}).on("error", () => {
throw new ServerRejectedError("Authentication failed.");
}).on("timeout", () => { }).on("timeout", () => {
console.error("Timeout"); throw new ServerTimeoutError("Server didn't respond in time");
}); });
// return await Promise.race([
// https.get(message.getAuthSuccessUrl, options, (res) => {
// if (res.statusCode < 200 || res.statusCode > 299) {
// return reject(new Error(`HTTP status code ${res.statusCode}`))
// }
//
// const body = []
// res.on('data', (chunk) => body.push(chunk))
// res.on('end', () => {
// const resString = Buffer.concat(body).toString()
// resolve(resString)
// })
//
// this.throwAfterTimeout(
// timeout,
// new ServerTimeoutError(`server failed to respond in time - GET ${message.getAuthSuccessUrl}`),
// ),
// ]) as HttpResponse;
} }
else { else {
throw new MissingParameterError("getAuthSuccessUrl missing for Android auth app authentication option."); throw new MissingParameterError("getAuthSuccessUrl missing for Android auth app authentication option.");
@ -8746,9 +8757,6 @@ class WebExtensionService {
setTimeout(() => resolve(), milliseconds); setTimeout(() => resolve(), milliseconds);
}); });
} }
isAndroidDevice() {
return navigator.userAgent.toLowerCase().indexOf("android") > -1;
}
getRelevantAckAction(message) { getRelevantAckAction(message) {
let ackAction; let ackAction;
switch (message.action) { switch (message.action) {
@ -8767,6 +8775,24 @@ class WebExtensionService {
} }
return ackAction; return ackAction;
} }
getRelevantSuccessAction(message) {
let ackAction;
switch (message.action) {
case Action$1.AUTHENTICATE:
ackAction = Action$1.AUTHENTICATE_SUCCESS;
break;
case Action$1.SIGN:
ackAction = Action$1.SIGN_SUCCESS;
break;
case Action$1.STATUS:
ackAction = Action$1.STATUS_SUCCESS;
break;
default:
ackAction = Action$1.STATUS_SUCCESS;
break;
}
return ackAction;
}
onReplyTimeout(pending) { onReplyTimeout(pending) {
var _a; var _a;
console.log("onReplyTimeout", pending.message.action); console.log("onReplyTimeout", pending.message.action);
@ -8776,14 +8802,13 @@ class WebExtensionService {
onAckTimeout(pending) { onAckTimeout(pending) {
var _a, _b; var _a, _b;
console.log("onAckTimeout", pending.message.action); console.log("onAckTimeout", pending.message.action);
console.log("Pending message");
console.log(pending.message.authApp);
if (pending.message.useAuthApp && pending.message.useAuthApp == true) { if (pending.message.useAuthApp && pending.message.useAuthApp == true) {
(_a = pending.reject) === null || _a === void 0 ? void 0 : _a.call(pending, new AuthAppNotInstalledError()); (_a = pending.reject) === null || _a === void 0 ? void 0 : _a.call(pending, new AuthAppNotInstalledError());
} }
else { else {
(_b = pending.reject) === null || _b === void 0 ? void 0 : _b.call(pending, new ExtensionUnavailableError()); (_b = pending.reject) === null || _b === void 0 ? void 0 : _b.call(pending, new ExtensionUnavailableError());
} }
this.removeFromQueue(pending.message.action);
clearTimeout(pending.replyTimer); clearTimeout(pending.replyTimer);
} }
getPendingMessage(action) { getPendingMessage(action) {
@ -9021,5 +9046,8 @@ async function sign(options) {
const result = await webExtensionService.send(message, timeout); const result = await webExtensionService.send(message, timeout);
return result.response; return result.response;
} }
function isAndroidDevice() {
return navigator.userAgent.toLowerCase().indexOf("android") > -1;
}
export { Action$1 as Action, ErrorCode$1 as ErrorCode, authenticate, config$1 as config, hasVersionProperties, sign, status }; export { Action$1 as Action, ErrorCode$1 as ErrorCode, authenticate, config$1 as config, hasVersionProperties, isAndroidDevice, sign, status };

View File

@ -3,14 +3,13 @@ package com.tarkvaratehnika.demobackend.config
class ApplicationConfiguration { class ApplicationConfiguration {
companion object { companion object {
// URL for intent, do not edit.
val AUTH_APP_LAUNCH_INTENT = "authapp://start/"
// Endpoint for challenge.
val CHALLENGE_ENDPOINT_URL = "/auth/challenge"
// Endpoint for 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://6b9f-85-253-195-195.ngrok.io" val WEBSITE_ORIGIN_URL = "https://5a0b-85-253-195-195.ngrok.io"
// Authentication request timeout in seconds.
val AUTH_REQUEST_TIMEOUT_MS = 120000
val USER_ROLE = "USER"
} }
} }

View File

@ -1,4 +1,4 @@
package com.tarkvaratehnika.demobackend.security package com.tarkvaratehnika.demobackend.config
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder
import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.builders.HttpSecurity
@ -15,9 +15,10 @@ class SecurityConfiguration : WebSecurityConfigurerAdapter() {
} }
override fun configure(http: HttpSecurity) { override fun configure(http: HttpSecurity) {
http.authorizeRequests()?.antMatchers("/**")?.permitAll() http.authorizeRequests()
?.antMatchers("/auth/**")?.permitAll() ?.antMatchers("/welcome")?.hasRole("USER")
http.sessionManagement()?.sessionCreationPolicy(SessionCreationPolicy.ALWAYS) ?.and()
http.csrf().disable() ?.sessionManagement()?.sessionCreationPolicy(SessionCreationPolicy.ALWAYS)
?.and()?.csrf()?.disable()
} }
} }

View File

@ -0,0 +1,107 @@
package com.tarkvaratehnika.demobackend.config
import com.tarkvaratehnika.demobackend.dto.AuthDto
import org.slf4j.LoggerFactory
import org.springframework.context.annotation.Configuration
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.web.authentication.WebAuthenticationDetails
@Configuration
class SessionManager {
companion object {
private val LOG = LoggerFactory.getLogger(SessionManager::class.java)
private val sessionRegistry = HashMap<String, AuthDto>()
fun registerSession(sessionId: String) {
LOG.warn("REGISTERING SESSION $sessionId")
if (sessionRegistry.containsKey(sessionId)) {
LOG.debug("Session already exists.")
} else {
sessionRegistry[sessionId] = AuthDto(arrayListOf(), hashMapOf())
}
}
fun addRoleToSession(sessionId: String, role: GrantedAuthority): AuthDto {
if (sessionRegistry.containsKey(sessionId)) {
val session = sessionRegistry[sessionId]
session!!.roles.add(role)
return session
} else {
throw Exception("Session with sessionId: $sessionId does not exist.")
}
}
/**
* Function adds role and userdata specified in authDto to the current session.
*/
fun addRoleToCurrentSession(authDto: AuthDto) {
val securityContext = SecurityContextHolder.getContext()
var sessionId = getSessionId()
if (sessionId == null) {
// No sessionId attached to the session, get one from credentials.
sessionId = securityContext.authentication.credentials.toString()
}
val authentication = UsernamePasswordAuthenticationToken(authDto.userData, sessionId, authDto.roles)
securityContext.authentication = authentication
}
fun removeRoleFromCurrentSession(headers: Map<String, String>) {
val securityContext = SecurityContextHolder.getContext()
var sessionId = securityContext.authentication.credentials
if (sessionId == null) {
// Fallback to when for some reason session object doesn't have sessionId attached.
sessionId = getSessionId(headers)
}
sessionRegistry[sessionId]!!.roles = arrayListOf()
val authentication = UsernamePasswordAuthenticationToken(null, sessionId, listOf())
securityContext.authentication = authentication
}
fun addUserDataToSession(sessionId: String, name: String, idCode: String): AuthDto {
if (sessionRegistry.containsKey(sessionId)) {
val session = sessionRegistry[sessionId]
session!!.userData["name"] = name
session.userData["idCode"] = idCode
return session
} else {
throw Exception("Session with sessionId: $sessionId does not exist.")
}
}
fun getSessionHasRole(sessionId: String, role: String): Boolean {
if (sessionRegistry.containsKey(sessionId)) {
if (sessionRegistry[sessionId]!!.roles.contains(SimpleGrantedAuthority(role))) {
return true
}
}
return false
}
fun getSessionAuth(sessionId: String?): AuthDto? {
if (sessionId == null) {
return null
}
return sessionRegistry[sessionId]
}
fun getSessionId(headers: Map<String, String>): String? {
return headers["sessionid"]
}
fun getSessionId(): String? {
val context = SecurityContextHolder.getContext()
if (context.authentication != null && context.authentication.details != null) {
return (context.authentication.details as WebAuthenticationDetails).sessionId
}
return null
}
}
}

View File

@ -3,9 +3,12 @@ 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.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
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
import org.springframework.security.core.session.SessionRegistry
import org.springframework.security.core.session.SessionRegistryImpl
import org.webeid.security.exceptions.JceException import org.webeid.security.exceptions.JceException
import org.webeid.security.nonce.NonceGenerator import org.webeid.security.nonce.NonceGenerator
import org.webeid.security.nonce.NonceGeneratorBuilder import org.webeid.security.nonce.NonceGeneratorBuilder
@ -13,7 +16,6 @@ import org.webeid.security.validator.AuthTokenValidator
import org.webeid.security.validator.AuthTokenValidatorBuilder import org.webeid.security.validator.AuthTokenValidatorBuilder
import java.io.IOException import java.io.IOException
import java.net.URI import java.net.URI
import java.net.URL
import java.security.KeyStore import java.security.KeyStore
import java.security.KeyStoreException import java.security.KeyStoreException
import java.security.NoSuchAlgorithmException import java.security.NoSuchAlgorithmException
@ -26,12 +28,11 @@ import javax.cache.Cache
import javax.cache.CacheManager import javax.cache.CacheManager
import javax.cache.Caching import javax.cache.Caching
import javax.cache.configuration.CompleteConfiguration import javax.cache.configuration.CompleteConfiguration
import javax.cache.configuration.FactoryBuilder import javax.cache.configuration.FactoryBuilder.factoryOf
import javax.cache.configuration.MutableConfiguration 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 {
@ -43,9 +44,8 @@ class ValidationConfiguration {
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 { init {
LOG.warn("Creating new ValidationConfiguration.") LOG.warn("Creating new ValidationConfiguration.")
@ -56,6 +56,8 @@ class ValidationConfiguration {
return Caching.getCachingProvider(CaffeineCachingProvider::class.java.name).cacheManager return Caching.getCachingProvider(CaffeineCachingProvider::class.java.name).cacheManager
} }
@Bean @Bean
fun nonceCache(): Cache<String, ZonedDateTime>? { fun nonceCache(): Cache<String, ZonedDateTime>? {
val cacheManager: CacheManager = cacheManager() val cacheManager: CacheManager = cacheManager()

View File

@ -0,0 +1,5 @@
package com.tarkvaratehnika.demobackend.dto
import org.springframework.security.core.GrantedAuthority
data class AuthDto(var roles: ArrayList<GrantedAuthority>, var userData: HashMap<String, String>)

View File

@ -0,0 +1,5 @@
package com.tarkvaratehnika.demobackend.dto
import com.fasterxml.jackson.annotation.JsonProperty
class AuthTokenDTO (@JsonProperty("auth-token") val token : String)

View File

@ -1,4 +0,0 @@
package com.tarkvaratehnika.demobackend.security
class AuthTokenDTO (val token : String, val xsrfToken : String) {
}

View File

@ -23,14 +23,12 @@
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.dto.AuthDto
import com.tarkvaratehnika.demobackend.web.rest.AuthenticationController import com.tarkvaratehnika.demobackend.dto.AuthTokenDTO
import org.slf4j.LoggerFactory 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
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
import org.webeid.security.exceptions.TokenValidationException import org.webeid.security.exceptions.TokenValidationException
@ -44,23 +42,15 @@ object AuthTokenDTOAuthenticationProvider {
private val LOG = LoggerFactory.getLogger(AuthTokenDTOAuthenticationProvider::class.java) private val LOG = LoggerFactory.getLogger(AuthTokenDTOAuthenticationProvider::class.java)
private val USER_ROLE: GrantedAuthority = SimpleGrantedAuthority(ROLE_USER)
val tokenValidator: AuthTokenValidator = ValidationConfiguration().validator() val tokenValidator: AuthTokenValidator = ValidationConfiguration().validator()
@Throws(AuthenticationException::class) @Throws(AuthenticationException::class)
fun authenticate(auth : Authentication) : Authentication { fun authenticate(auth : Authentication, sessionId: String?) : AuthDto {
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)
val authorities = arrayListOf<GrantedAuthority>()
authorities.add(USER_ROLE)
try { try {
val userCertificate: X509Certificate = tokenValidator.validate(token) val userCertificate: X509Certificate = tokenValidator.validate(token)
return WebEidAuthentication.fromCertificate(userCertificate, authorities, "as") return WebEidAuthentication.fromCertificate(userCertificate, sessionId)
} catch (e : TokenValidationException) { } catch (e : TokenValidationException) {
// Validation failed. // Validation failed.
throw AuthenticationServiceException("Token validation failed. " + e.message) throw AuthenticationServiceException("Token validation failed. " + e.message)

View File

@ -22,17 +22,27 @@
package com.tarkvaratehnika.demobackend.security package com.tarkvaratehnika.demobackend.security
import com.tarkvaratehnika.demobackend.config.ApplicationConfiguration
import com.tarkvaratehnika.demobackend.config.ApplicationConfiguration.Companion.USER_ROLE
import com.tarkvaratehnika.demobackend.config.SessionManager
import com.tarkvaratehnika.demobackend.dto.AuthDto
import org.slf4j.LoggerFactory
import org.springframework.http.HttpStatus
import org.webeid.security.certificate.CertificateData import org.webeid.security.certificate.CertificateData
import org.springframework.security.core.Authentication import org.springframework.security.core.Authentication
import org.springframework.security.core.GrantedAuthority import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.authority.SimpleGrantedAuthority import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.core.context.SecurityContext
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.web.authentication.WebAuthenticationDetails
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken
import org.springframework.web.server.ResponseStatusException
import java.io.Serializable
import java.security.cert.X509Certificate import java.security.cert.X509Certificate
import java.util.* import java.util.*
import java.util.concurrent.ThreadLocalRandom
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
import kotlin.math.log import kotlin.collections.HashMap
class WebEidAuthentication( class WebEidAuthentication(
private val principalName: String, private val principalName: String,
@ -40,39 +50,71 @@ class WebEidAuthentication(
private val authorities: ArrayList<GrantedAuthority> private val authorities: ArrayList<GrantedAuthority>
) : PreAuthenticatedAuthenticationToken(principalName, idCode, authorities), Authentication { ) : PreAuthenticatedAuthenticationToken(principalName, idCode, authorities), Authentication {
// Companion object is for static functions. // Companion object is for static functions.
companion object { companion object {
private val LOG = LoggerFactory.getLogger(WebEidAuthentication::class.java)
private val loggedInUsers = HashMap<String, Authentication>()
fun fromCertificate( fun fromCertificate(
userCertificate: X509Certificate, userCertificate: X509Certificate,
authorities: ArrayList<GrantedAuthority>, sessionId: String?,
challenge: String ): AuthDto {
): Authentication { // Get user data.
val principalName = getPrincipalNameFromCertificate(userCertificate) val name = getPrincipalNameFromCertificate(userCertificate)
val idCode = Objects.requireNonNull(CertificateData.getSubjectIdCode(userCertificate)) val idCode = Objects.requireNonNull(CertificateData.getSubjectIdCode(userCertificate))
val authentication = WebEidAuthentication(principalName, idCode, authorities)
loggedInUsers[challenge] = authentication // Fetch valid sessionId.
return authentication var methodIndependentSessionId = sessionId
if (methodIndependentSessionId == null) {
methodIndependentSessionId = SessionManager.getSessionId()
if (methodIndependentSessionId == null) {
throw Exception("No session")
}
}
// Add role and user data to the AuthDto and return it.
SessionManager.addRoleToSession(methodIndependentSessionId, SimpleGrantedAuthority(USER_ROLE))
return SessionManager.addUserDataToSession(methodIndependentSessionId, name, idCode)
} }
/** /**
* Function for getting a Spring authentication object by supplying a challenge. * Function for getting a Spring authentication object by supplying a challenge.
* TODO: Figure out a more secure solution in the future. * TODO: Figure out a more secure solution in the future.
*/ */
fun fromChallenge(challenge: String): Authentication? { fun fromSession(headers: HashMap<String, String>): AuthDto {
val currentTime = Date()
// Get sessionId for current session.
var sessionId = SessionManager.getSessionId()
if (sessionId == null) {
LOG.warn("SESSION IS NULL")
sessionId = SessionManager.getSessionId(headers)
if (sessionId == null) {
LOG.warn("SESSION IS STILL NULL")
throw ResponseStatusException(HttpStatus.FORBIDDEN, "Session ID not found.")
}
LOG.warn("SESSION IS NOW: " + sessionId)
}
while (currentTime.time + ApplicationConfiguration.AUTH_REQUEST_TIMEOUT_MS > Date().time) {
Thread.sleep(1000)
if (SessionManager.getSessionHasRole(sessionId, USER_ROLE)) {
// Get AuthDto
val auth = SessionManager.getSessionAuth(sessionId)
// Set role and user data to current session.
SessionManager.addRoleToCurrentSession(auth!!)
LOG.warn("ROLE ADDED AND LOGGING IN.")
return auth
}
}
// if (ThreadLocalRandom.current().nextFloat() < 0.5f) { // TODO: For testing. // if (ThreadLocalRandom.current().nextFloat() < 0.5f) { // TODO: For testing.
// return null // return null
// } // }
val auth = loggedInUsers[challenge] throw ResponseStatusException(HttpStatus.REQUEST_TIMEOUT, "Token not received in time.")
if (auth != null) {
// If challenge is valid, delete the authentication object from the map (so this can only be fetched once).
loggedInUsers.remove(challenge)
} else {
return null
}
return auth
} }
// // TODO: DELETE // // TODO: DELETE

View File

@ -1,15 +1,15 @@
package com.tarkvaratehnika.demobackend.web.rest package com.tarkvaratehnika.demobackend.web.rest
import com.tarkvaratehnika.demobackend.security.AuthTokenDTO import com.tarkvaratehnika.demobackend.config.SessionManager
import com.tarkvaratehnika.demobackend.dto.AuthDto
import com.tarkvaratehnika.demobackend.dto.AuthTokenDTO
import com.tarkvaratehnika.demobackend.security.AuthTokenDTOAuthenticationProvider import com.tarkvaratehnika.demobackend.security.AuthTokenDTOAuthenticationProvider
import com.tarkvaratehnika.demobackend.security.WebEidAuthentication import com.tarkvaratehnika.demobackend.security.WebEidAuthentication
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.http.HttpStatus import org.springframework.http.HttpStatus
import org.springframework.http.MediaType import org.springframework.http.MediaType
import org.springframework.security.core.Authentication
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken
import org.springframework.web.bind.annotation.* import org.springframework.web.bind.annotation.*
import org.springframework.web.server.ResponseStatusException
@RestController @RestController
@RequestMapping("auth") @RequestMapping("auth")
@ -19,30 +19,31 @@ class AuthenticationController {
@PostMapping("login", consumes = [MediaType.APPLICATION_JSON_VALUE], produces = [MediaType.APPLICATION_JSON_VALUE]) @PostMapping("login", consumes = [MediaType.APPLICATION_JSON_VALUE], produces = [MediaType.APPLICATION_JSON_VALUE])
fun authenticate(@RequestBody body : String): Authentication { fun authenticate(@RequestHeader headers: Map<String, String>, @RequestBody body : AuthTokenDTO): AuthDto {
val parts = body.split("\"")
val authToken = AuthTokenDTO(parts[3], parts[7]) val sessionId = SessionManager.getSessionId(headers)
// Create Spring Security Authentication object with supplied token as credentials. // Create Spring Security Authentication object with supplied token as credentials.
val auth = PreAuthenticatedAuthenticationToken(null, authToken) val auth = PreAuthenticatedAuthenticationToken(null, body)
// Return authentication object if success. // Return authentication object if success.
return AuthTokenDTOAuthenticationProvider.authenticate(auth) return AuthTokenDTOAuthenticationProvider.authenticate(auth, sessionId)
} }
@GetMapping("login", produces = [MediaType.APPLICATION_JSON_VALUE]) @GetMapping("login", produces = [MediaType.APPLICATION_JSON_VALUE])
fun getAuthenticated(headers: String) : Authentication? { fun getAuthenticated(@RequestHeader headers: HashMap<String, String>) : AuthDto {
val auth = WebEidAuthentication.fromChallenge("as") return WebEidAuthentication.fromSession(headers)
if (auth == null) {
throw ResponseStatusException(HttpStatus.FORBIDDEN, "Not allowed.")
} }
return auth
@GetMapping("userData", produces = [MediaType.APPLICATION_JSON_VALUE])
fun getUserData(@RequestHeader headers: Map<String, String>) : AuthDto? {
return SessionManager.getSessionAuth(SessionManager.getSessionId(headers))
} }
@PostMapping("logout", consumes = [MediaType.APPLICATION_JSON_VALUE]) @PostMapping("logout", consumes = [MediaType.APPLICATION_JSON_VALUE])
fun logOut(@RequestBody body: String) : HttpStatus? { fun logOut(@RequestHeader headers: Map<String, String>, @RequestBody body: String) : HttpStatus? {
LOG.warn("I WAS HERE") SessionManager.removeRoleFromCurrentSession(headers)
LOG.warn(body)
return HttpStatus.ACCEPTED return HttpStatus.ACCEPTED
} }

View File

@ -22,14 +22,24 @@
package com.tarkvaratehnika.demobackend.web.rest package com.tarkvaratehnika.demobackend.web.rest
import com.tarkvaratehnika.demobackend.config.SessionManager
import com.tarkvaratehnika.demobackend.dto.ChallengeDto import com.tarkvaratehnika.demobackend.dto.ChallengeDto
import com.tarkvaratehnika.demobackend.security.WebEidAuthentication
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.http.HttpStatus
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.Authentication
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestHeader
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
import org.springframework.web.client.HttpClientErrorException
import org.springframework.web.server.ResponseStatusException
import org.webeid.security.nonce.NonceGenerator import org.webeid.security.nonce.NonceGenerator
@RestController @RestController
@RequestMapping("auth") @RequestMapping("auth")
class ChallengeController (val nonceGenerator: NonceGenerator) { class ChallengeController (val nonceGenerator: NonceGenerator) {
@ -37,10 +47,30 @@ class ChallengeController (val nonceGenerator: NonceGenerator) {
private val LOG = LoggerFactory.getLogger(ChallengeController::class.java) private val LOG = LoggerFactory.getLogger(ChallengeController::class.java)
@GetMapping("challenge") @GetMapping("challenge")
fun challenge(): ChallengeDto { fun challenge(@RequestHeader headers: Map<String, String>): ChallengeDto {
val sessionId = SessionManager.getSessionId(headers)
if (sessionId == null) {
LOG.warn("SESSION ID MISSING FOR CHALLENGE")
throw ResponseStatusException(HttpStatus.FORBIDDEN, "SessionId missing.")
}
SessionManager.registerSession(sessionId)
// val context = SecurityContextHolder.getContext()
// val authorities = arrayListOf<GrantedAuthority>()
// authorities.add(SimpleGrantedAuthority("USER"))
// authorities.add(SimpleGrantedAuthority("ROLE_USER"))
// val auth = context.authentication
//
// val newAuth: Authentication =
// UsernamePasswordAuthenticationToken(auth.principal, auth.credentials, authorities)
// SecurityContextHolder.getContext().authentication = newAuth;
// SessionManager.createSession(SessionManager.getSessionId(headers))
val challengeDto = ChallengeDto(nonceGenerator.generateAndStoreNonce()) val challengeDto = ChallengeDto(nonceGenerator.generateAndStoreNonce())
LOG.warn(challengeDto.nonce) LOG.warn(challengeDto.nonce)
// WebEidAuthentication.addAuth(challengeDto.nonce) // For testing.
return challengeDto return challengeDto
} }

View File

@ -0,0 +1,52 @@
/*
* Copyright (c) 2020, 2021 The Web eID Project
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.tarkvaratehnika.demobackend.web.rest
import com.tarkvaratehnika.demobackend.config.SessionManager
import com.tarkvaratehnika.demobackend.dto.ChallengeDto
import com.tarkvaratehnika.demobackend.security.WebEidAuthentication
import org.slf4j.LoggerFactory
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestHeader
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import org.webeid.security.nonce.NonceGenerator
@RestController
@RequestMapping("auth")
class Test (val nonceGenerator: NonceGenerator) {
private val LOG = LoggerFactory.getLogger(ChallengeController::class.java)
@GetMapping("test")
fun test(@RequestHeader headers: Map<String, String>): String {
return "<h1>JOUUUUUUUU</h1>"
}
@GetMapping("test2")
fun test2(@RequestHeader headers: Map<String, String>): String {
return "<h1>JOUUUUUUUU22222222222222222</h1>"
}
}