RC0: full planned functionality (+change password, +reset password)
This commit is contained in:
parent
f321a82b8c
commit
d12b392ed0
@ -44,7 +44,7 @@ kotlin {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.3")
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.3")
|
||||||
api("net.sergeych:unikrypto:1.2.2-SNAPSHOT")
|
api("net.sergeych:unikrypto:1.2.2-SNAPSHOT")
|
||||||
api("net.sergeych:parsec3:0.3.2-SNAPSHOT")
|
api("net.sergeych:parsec3:0.3.3-SNAPSHOT")
|
||||||
api("net.sergeych:boss-serialization-mp:0.2.4-SNAPSHOT")
|
api("net.sergeych:boss-serialization-mp:0.2.4-SNAPSHOT")
|
||||||
3 }
|
3 }
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ import kotlin.reflect.typeOf
|
|||||||
* To construct it please use one of:_
|
* To construct it please use one of:_
|
||||||
*
|
*
|
||||||
* - [AccessControlObject.pack] to generate new
|
* - [AccessControlObject.pack] to generate new
|
||||||
* - [AccessControlObject.unpackWithPasswordKey] to decrypt it with a password key
|
* - [AccessControlObject.unpackWithKey] to decrypt it with a password key
|
||||||
* - [AccessControlObject.unpackWithSecret] to decrypt it with a `secret`
|
* - [AccessControlObject.unpackWithSecret] to decrypt it with a `secret`
|
||||||
*
|
*
|
||||||
* @param payloadType used to properly serialize application=specific data for [payload]
|
* @param payloadType used to properly serialize application=specific data for [payload]
|
||||||
@ -125,15 +125,16 @@ class AccessControlObject<T>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unpack and decrypt ACO with a password key
|
* Unpack and decrypt ACO with a password key or secret-based key (this once can be obtained from `secret`
|
||||||
|
* with [RestoreKey.parse].
|
||||||
* @return decrypted ACO or null if the key is wrong.
|
* @return decrypted ACO or null if the key is wrong.
|
||||||
*/
|
*/
|
||||||
inline fun <reified T> unpackWithPasswordKey(packed: ByteArray, passwordKey: SymmetricKey): AccessControlObject<T>? =
|
inline fun <reified T> unpackWithKey(packed: ByteArray, key: SymmetricKey): AccessControlObject<T>? =
|
||||||
Container.decrypt<Data<T>>(packed, passwordKey)?.let {
|
Container.decrypt<Data<T>>(packed, key)?.let {
|
||||||
AccessControlObject(typeOf<Data<T>>(), packed, passwordKey, it)
|
AccessControlObject(typeOf<Data<T>>(), packed, key, it)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T>unpackWithPasswordKey(packed: ByteArray, passwordKey: SymmetricKey,payloadType: KType): AccessControlObject<T>? =
|
fun <T>unpackWithKey(packed: ByteArray, passwordKey: SymmetricKey, payloadType: KType): AccessControlObject<T>? =
|
||||||
Container.decryptAsBytes(packed, passwordKey)?.let {
|
Container.decryptAsBytes(packed, passwordKey)?.let {
|
||||||
AccessControlObject(payloadType, packed, passwordKey, BossDecoder.decodeFrom(payloadType,it))
|
AccessControlObject(payloadType, packed, passwordKey, BossDecoder.decodeFrom(payloadType,it))
|
||||||
}
|
}
|
||||||
@ -146,14 +147,11 @@ class AccessControlObject<T>(
|
|||||||
suspend inline fun <reified T> unpackWithSecret(packed: ByteArray, secret: String): AccessControlObject<T>? {
|
suspend inline fun <reified T> unpackWithSecret(packed: ByteArray, secret: String): AccessControlObject<T>? {
|
||||||
try {
|
try {
|
||||||
val (id, key) = RestoreKey.parse(secret)
|
val (id, key) = RestoreKey.parse(secret)
|
||||||
return Container.decrypt<Data<T>>(packed, key)?.let { data ->
|
return unpackWithKey(packed, key)
|
||||||
AccessControlObject(typeOf<Data<T>>(), packed, data.passwordKey, data)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch(_: RestoreKey.InvalidSecretException) {
|
catch(_: RestoreKey.InvalidSecretException) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,5 +1,6 @@
|
|||||||
package net.sergeych.superlogin
|
package net.sergeych.superlogin
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import net.sergeych.parsec3.CommandHost
|
import net.sergeych.parsec3.CommandHost
|
||||||
import net.sergeych.parsec3.WithAdapter
|
import net.sergeych.parsec3.WithAdapter
|
||||||
@ -13,13 +14,14 @@ data class RegistrationArgs(
|
|||||||
val loginPublicKey: PublicKey,
|
val loginPublicKey: PublicKey,
|
||||||
val derivationParams: PasswordDerivationParams,
|
val derivationParams: PasswordDerivationParams,
|
||||||
val restoreId: ByteArray,
|
val restoreId: ByteArray,
|
||||||
val restoreData: ByteArray,
|
val packedACO: ByteArray,
|
||||||
val extraData: ByteArray? = null
|
val extraData: ByteArray? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
sealed class AuthenticationResult {
|
sealed class AuthenticationResult {
|
||||||
@Serializable
|
@Serializable
|
||||||
|
@SerialName("Success")
|
||||||
data class Success(
|
data class Success(
|
||||||
val loginName: String,
|
val loginName: String,
|
||||||
val loginToken: ByteArray,
|
val loginToken: ByteArray,
|
||||||
@ -27,32 +29,49 @@ sealed class AuthenticationResult {
|
|||||||
): AuthenticationResult()
|
): AuthenticationResult()
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
@SerialName("LoginUnavailable")
|
||||||
object LoginUnavailable: AuthenticationResult()
|
object LoginUnavailable: AuthenticationResult()
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
@SerialName("LoginIdUnavailable")
|
||||||
object LoginIdUnavailable: AuthenticationResult()
|
object LoginIdUnavailable: AuthenticationResult()
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
@SerialName("RestoreIdUnavailable")
|
||||||
object RestoreIdUnavailable: AuthenticationResult()
|
object RestoreIdUnavailable: AuthenticationResult()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class RequestLoginDataArgs(
|
class RequestACOByLoginNameArgs(
|
||||||
val loginName: String,
|
val loginName: String,
|
||||||
val loginId: ByteArray,
|
val loginId: ByteArray,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class RequestLoginDataResult(
|
class RequestACOResult(
|
||||||
val packedACO: ByteArray,
|
val packedACO: ByteArray,
|
||||||
val nonce: ByteArray
|
val nonce: ByteArray
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class LoginByPasswordPayload(
|
class LoginByPasswordPayload(
|
||||||
val loginName: String
|
val loginName: String
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class ChangePasswordArgs(
|
||||||
|
val loginName: String,
|
||||||
|
val packedSignedRecord: ByteArray
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class ChangePasswordPayload(
|
||||||
|
val packedACO: ByteArray,
|
||||||
|
val passwordDerivationParams: PasswordDerivationParams,
|
||||||
|
val newLoginKey: PublicKey
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SuperloginServerApi<T: WithAdapter> : CommandHost<T>() {
|
class SuperloginServerApi<T: WithAdapter> : CommandHost<T>() {
|
||||||
|
|
||||||
val slGetNonce by command<Unit,ByteArray>()
|
val slGetNonce by command<Unit,ByteArray>()
|
||||||
@ -61,13 +80,11 @@ class SuperloginServerApi<T: WithAdapter> : CommandHost<T>() {
|
|||||||
val slLoginByToken by command<ByteArray,AuthenticationResult>()
|
val slLoginByToken by command<ByteArray,AuthenticationResult>()
|
||||||
|
|
||||||
val slRequestDerivationParams by command<String,PasswordDerivationParams>()
|
val slRequestDerivationParams by command<String,PasswordDerivationParams>()
|
||||||
val slRequestLoginData by command<RequestLoginDataArgs,RequestLoginDataResult>()
|
val slRequestACOByLoginName by command<RequestACOByLoginNameArgs,RequestACOResult>()
|
||||||
val slLoginByKey by command<ByteArray,AuthenticationResult>()
|
val slLoginByKey by command<ByteArray,AuthenticationResult>()
|
||||||
|
|
||||||
|
val slRequestACOBySecretId by command<ByteArray,ByteArray>()
|
||||||
|
val slChangePasswordAndLogin by command <ChangePasswordArgs,AuthenticationResult>()
|
||||||
|
|
||||||
/**
|
val slSendTestException by command<Unit,Unit>()
|
||||||
* Get resstoreData by restoreId: password reset procedure start.
|
|
||||||
*/
|
|
||||||
// val requestUserLogin by command<ByteArray,ByteArray>()
|
|
||||||
// val performLogin by command<LoginArgs
|
|
||||||
}
|
}
|
@ -1,5 +1,10 @@
|
|||||||
package net.sergeych.superlogin.client
|
package net.sergeych.superlogin.client
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Login client has a _login state_ which represents known state of the log-in protocol.
|
||||||
|
* It can properly process offline state and reconnection and report state bu mean
|
||||||
|
* of the state flow pf instanses of this class. See [SuperloginClient.state].
|
||||||
|
*/
|
||||||
sealed class LoginState(val isLoggedIn: Boolean) {
|
sealed class LoginState(val isLoggedIn: Boolean) {
|
||||||
/**
|
/**
|
||||||
* User is logged in (either connected or yet not). Client application should save
|
* User is logged in (either connected or yet not). Client application should save
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
package net.sergeych.superlogin.client
|
package net.sergeych.superlogin.client
|
||||||
|
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
@ -13,10 +11,7 @@ import net.sergeych.mp_logger.Loggable
|
|||||||
import net.sergeych.mp_logger.exception
|
import net.sergeych.mp_logger.exception
|
||||||
import net.sergeych.mp_logger.warning
|
import net.sergeych.mp_logger.warning
|
||||||
import net.sergeych.mp_tools.globalLaunch
|
import net.sergeych.mp_tools.globalLaunch
|
||||||
import net.sergeych.parsec3.Adapter
|
import net.sergeych.parsec3.*
|
||||||
import net.sergeych.parsec3.CommandDescriptor
|
|
||||||
import net.sergeych.parsec3.Parsec3Transport
|
|
||||||
import net.sergeych.parsec3.WithAdapter
|
|
||||||
import net.sergeych.superlogin.*
|
import net.sergeych.superlogin.*
|
||||||
import net.sergeych.superlogin.server.SuperloginRestoreAccessPayload
|
import net.sergeych.superlogin.server.SuperloginRestoreAccessPayload
|
||||||
import net.sergeych.unikrypto.SignedRecord
|
import net.sergeych.unikrypto.SignedRecord
|
||||||
@ -43,6 +38,7 @@ class SuperloginClient<D, S : WithAdapter>(
|
|||||||
private val transport: Parsec3Transport<S>,
|
private val transport: Parsec3Transport<S>,
|
||||||
savedData: SuperloginData<D>? = null,
|
savedData: SuperloginData<D>? = null,
|
||||||
private val dataType: KType,
|
private val dataType: KType,
|
||||||
|
override val exceptionsRegistry: ExceptionsRegistry = ExceptionsRegistry(),
|
||||||
) : Parsec3Transport<S>, Loggable by LogTag("SLCLI") {
|
) : Parsec3Transport<S>, Loggable by LogTag("SLCLI") {
|
||||||
|
|
||||||
private val _state = MutableStateFlow<LoginState>(
|
private val _state = MutableStateFlow<LoginState>(
|
||||||
@ -77,16 +73,16 @@ class SuperloginClient<D, S : WithAdapter>(
|
|||||||
|
|
||||||
private var adapterReady = CompletableDeferred<Unit>()
|
private var adapterReady = CompletableDeferred<Unit>()
|
||||||
|
|
||||||
override suspend fun adapter(): Adapter<S> {
|
override suspend fun adapter(): Adapter<S> = transport.adapter()
|
||||||
do {
|
// do {
|
||||||
try {
|
// try {
|
||||||
adapterReady.await()
|
// adapterReady.await()
|
||||||
return transport.adapter()
|
// return transport.adapter()
|
||||||
} catch (x: Throwable) {
|
// } catch (x: Throwable) {
|
||||||
exception { "failed to get adapter" to x }
|
// exception { "failed to get adapter" to x }
|
||||||
}
|
// }
|
||||||
} while (true)
|
// } while (true)
|
||||||
}
|
// }
|
||||||
|
|
||||||
suspend fun <A, R> call(ca: CommandDescriptor<A, R>, args: A): R = adapter().invokeCommand(ca, args)
|
suspend fun <A, R> call(ca: CommandDescriptor<A, R>, args: A): R = adapter().invokeCommand(ca, args)
|
||||||
suspend fun <R> call(ca: CommandDescriptor<Unit, R>): R = adapter().invokeCommand(ca)
|
suspend fun <R> call(ca: CommandDescriptor<Unit, R>): R = adapter().invokeCommand(ca)
|
||||||
@ -119,6 +115,7 @@ class SuperloginClient<D, S : WithAdapter>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
transport.registerExceptinos(SuperloginExceptionsRegistry)
|
||||||
jobs += globalLaunch {
|
jobs += globalLaunch {
|
||||||
transport.connectedFlow.collect { on ->
|
transport.connectedFlow.collect { on ->
|
||||||
if (on) tryRestoreLogin()
|
if (on) tryRestoreLogin()
|
||||||
@ -220,11 +217,11 @@ class SuperloginClient<D, S : WithAdapter>(
|
|||||||
val keys = DerivedKeys.derive(password, params)
|
val keys = DerivedKeys.derive(password, params)
|
||||||
// Request login data by derived it
|
// Request login data by derived it
|
||||||
return invoke(
|
return invoke(
|
||||||
serverApi.slRequestLoginData,
|
serverApi.slRequestACOByLoginName,
|
||||||
RequestLoginDataArgs(loginName, keys.loginId)
|
RequestACOByLoginNameArgs(loginName, keys.loginId)
|
||||||
).let { loginRequest ->
|
).let { loginRequest ->
|
||||||
try {
|
try {
|
||||||
AccessControlObject.unpackWithPasswordKey<SuperloginRestoreAccessPayload>(
|
AccessControlObject.unpackWithKey<SuperloginRestoreAccessPayload>(
|
||||||
loginRequest.packedACO,
|
loginRequest.packedACO,
|
||||||
keys.loginAccessKey
|
keys.loginAccessKey
|
||||||
)?.let { aco ->
|
)?.let { aco ->
|
||||||
@ -241,14 +238,121 @@ class SuperloginClient<D, S : WithAdapter>(
|
|||||||
.also { slData = it }
|
.also { slData = it }
|
||||||
} else null
|
} else null
|
||||||
}
|
}
|
||||||
}
|
} catch (t: Throwable) {
|
||||||
catch (t: Throwable) {
|
|
||||||
t.printStackTrace()
|
t.printStackTrace()
|
||||||
throw t
|
throw t
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets password and log in using a `secret` string (one that wwas reported on registration. __Never store
|
||||||
|
* secrt string in your app__. Always ask user to enter it just before the operation and wipe it out
|
||||||
|
* immediately after. It is a time-consuming procedure. Note that on success the client state changes
|
||||||
|
* to [LoginState.LoggedIn].
|
||||||
|
*
|
||||||
|
* @param secret the secret string as was reported when registering
|
||||||
|
* @param newPassword new password (apply strength checks, it is not checked here)
|
||||||
|
* @param loginKeyStrength desired login key strength (it will be generated there)
|
||||||
|
* @param params password derivation params: it is possible to change its strength here
|
||||||
|
* @return login data instance on success or null
|
||||||
|
*/
|
||||||
|
suspend fun resetPasswordAndLogin(
|
||||||
|
secret: String, newPassword: String,
|
||||||
|
params: PasswordDerivationParams = PasswordDerivationParams(),
|
||||||
|
loginKeyStrength: Int = 2048
|
||||||
|
): SuperloginData<D>? {
|
||||||
|
mustBeLoggedOut()
|
||||||
|
return try {
|
||||||
|
val (id, key) = RestoreKey.parse(secret)
|
||||||
|
val packedACO = invoke(serverApi.slRequestACOBySecretId, id)
|
||||||
|
AccessControlObject.unpackWithKey<SuperloginRestoreAccessPayload>(packedACO, key)?.let {
|
||||||
|
changePasswordWithACO(it, newPassword)
|
||||||
|
slData
|
||||||
|
}
|
||||||
|
} catch (x: RestoreKey.InvalidSecretException) {
|
||||||
|
null
|
||||||
|
} catch (x: Exception) {
|
||||||
|
x.printStackTrace()
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes the password (which includes generating new login key). Does not require any particular
|
||||||
|
* [state]. This is a long operation. On success, it changes (updates) [state] to [LoginState.LoggedIn]
|
||||||
|
* with new data whatever it was before. Be aware of it.
|
||||||
|
*/
|
||||||
|
protected suspend fun changePasswordWithACO(
|
||||||
|
aco: AccessControlObject<SuperloginRestoreAccessPayload>,
|
||||||
|
newPassword: String,
|
||||||
|
params: PasswordDerivationParams = PasswordDerivationParams(),
|
||||||
|
loginKeyStrength: Int = 2048,
|
||||||
|
): Boolean {
|
||||||
|
return coroutineScope {
|
||||||
|
// Get current nonce in parallel
|
||||||
|
val deferredNonce = async { invoke(serverApi.slGetNonce) }
|
||||||
|
// get login key in parallel
|
||||||
|
val newLoginKey = BackgroundKeyGenerator.getKeyAsync(loginKeyStrength)
|
||||||
|
// derive keys in main scope
|
||||||
|
val keys = DerivedKeys.derive(newPassword, params)
|
||||||
|
|
||||||
|
// new ACO payload: new login key, old data storage key and login
|
||||||
|
val newSlp = SuperloginRestoreAccessPayload(
|
||||||
|
aco.payload.login,
|
||||||
|
newLoginKey.await(),
|
||||||
|
aco.payload.dataStorageKey
|
||||||
|
)
|
||||||
|
// new ACO with a new password key and payload (but the same secret!)
|
||||||
|
var newAco = aco.updatePasswordKey(keys.loginAccessKey).updatePayload(newSlp)
|
||||||
|
// trying to update
|
||||||
|
val result = invoke(
|
||||||
|
serverApi.slChangePasswordAndLogin, ChangePasswordArgs(
|
||||||
|
aco.payload.login,
|
||||||
|
SignedRecord.pack(aco.payload.loginPrivateKey,
|
||||||
|
ChangePasswordPayload(newAco.packed,params,newLoginKey.await().publicKey),
|
||||||
|
deferredNonce.await())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
when (result) {
|
||||||
|
is AuthenticationResult.Success -> {
|
||||||
|
slData = SuperloginData(result.loginName, result.loginToken, extractData(result.applicationData))
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
warning { "Change password result: $result" }
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change password for a logged-in user using its known password. It is a long operation
|
||||||
|
* @param oldPassword existing password (re-request it from a user!)
|
||||||
|
* @param newPassword new password. we do not chek it but it should be strong - check it on your end
|
||||||
|
* for example with [net.sergeych.unikrypto.Passwords] tools
|
||||||
|
* @param passwordDerivationParams at this point derivation parameters are alwaus updated so it is possible
|
||||||
|
* to set it to desired
|
||||||
|
* @param loginKeyStrength login key is regenerateed so its strength could be updated here
|
||||||
|
* @return true if the password has been successfully changed
|
||||||
|
*/
|
||||||
|
suspend fun changePassword(oldPassword: String, newPassword: String,
|
||||||
|
passwordDerivationParams: PasswordDerivationParams = PasswordDerivationParams(),
|
||||||
|
loginKeyStrength: Int = 2048
|
||||||
|
): Boolean {
|
||||||
|
mustBeLoggedIn()
|
||||||
|
val loginName = slData?.loginName ?: throw SLInternalException("loginName should be defined here")
|
||||||
|
val dp = invoke(serverApi.slRequestDerivationParams,loginName)
|
||||||
|
val keys = DerivedKeys.derive(oldPassword,dp)
|
||||||
|
val data = invoke(serverApi.slRequestACOByLoginName,RequestACOByLoginNameArgs(loginName,keys.loginId))
|
||||||
|
return AccessControlObject.unpackWithKey<SuperloginRestoreAccessPayload>(data.packedACO, keys.loginAccessKey)?.let {
|
||||||
|
changePasswordWithACO(it, newPassword,passwordDerivationParams, loginKeyStrength)
|
||||||
|
} ?: false
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
inline operator fun <reified D, S : WithAdapter> invoke(
|
inline operator fun <reified D, S : WithAdapter> invoke(
|
||||||
t: Parsec3Transport<S>,
|
t: Parsec3Transport<S>,
|
||||||
|
12
src/commonMain/kotlin/net.sergeych.superlogin/exceptions.kt
Normal file
12
src/commonMain/kotlin/net.sergeych.superlogin/exceptions.kt
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package net.sergeych.superlogin
|
||||||
|
|
||||||
|
import net.sergeych.parsec3.ExceptionsRegistry
|
||||||
|
|
||||||
|
class SLInternalException(reason: String?="superlogin internal exception (a bug)",cause: Throwable?=null):
|
||||||
|
Exception(reason, cause)
|
||||||
|
|
||||||
|
fun addSuperloginExceptions(er: ExceptionsRegistry) {
|
||||||
|
er.register { SLInternalException(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
val SuperloginExceptionsRegistry = ExceptionsRegistry().also { addSuperloginExceptions(it) }
|
@ -1,34 +0,0 @@
|
|||||||
package net.sergeych.superlogin
|
|
||||||
|
|
||||||
import net.sergeych.unikrypto.HashAlgorithm
|
|
||||||
import net.sergeych.unikrypto.digest
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Primitive to simplify create and compare salted byte arrays
|
|
||||||
*/
|
|
||||||
data class Salted(val salt: ByteArray,val data: ByteArray) {
|
|
||||||
|
|
||||||
val salted = HashAlgorithm.SHA3_256.digest(salt, data)
|
|
||||||
|
|
||||||
fun matches(other: ByteArray): Boolean {
|
|
||||||
return Salted(salt, other) == this
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (other == null || this::class != other::class) return false
|
|
||||||
|
|
||||||
other as Salted
|
|
||||||
|
|
||||||
if (!salt.contentEquals(other.salt)) return false
|
|
||||||
if (!data.contentEquals(other.data)) return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
var result = salt.contentHashCode()
|
|
||||||
result = 31 * result + data.contentHashCode()
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,9 +2,11 @@ package superlogin
|
|||||||
|
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import net.sergeych.superlogin.AccessControlObject
|
import net.sergeych.superlogin.AccessControlObject
|
||||||
import net.sergeych.superlogin.RestoreKey
|
|
||||||
import net.sergeych.unikrypto.SymmetricKeys
|
import net.sergeych.unikrypto.SymmetricKeys
|
||||||
import kotlin.test.*
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertNotNull
|
||||||
|
import kotlin.test.assertNull
|
||||||
|
|
||||||
internal class AccessControlObjectTest {
|
internal class AccessControlObjectTest {
|
||||||
|
|
||||||
@ -14,29 +16,29 @@ internal class AccessControlObjectTest {
|
|||||||
val pk2 = SymmetricKeys.random()
|
val pk2 = SymmetricKeys.random()
|
||||||
val (rk, packed1) = AccessControlObject.pack(pk1, 117)
|
val (rk, packed1) = AccessControlObject.pack(pk1, 117)
|
||||||
println(rk.secret)
|
println(rk.secret)
|
||||||
val ac1 = AccessControlObject.unpackWithPasswordKey<Int>(packed1,pk1)
|
val ac1 = AccessControlObject.unpackWithKey<Int>(packed1,pk1)
|
||||||
assertNotNull(ac1)
|
assertNotNull(ac1)
|
||||||
assertEquals(117, ac1.payload)
|
assertEquals(117, ac1.payload)
|
||||||
val ac2 = AccessControlObject.unpackWithSecret<Int>(packed1,rk.secret)
|
val ac2 = AccessControlObject.unpackWithSecret<Int>(packed1,rk.secret)
|
||||||
assertNotNull(ac2)
|
assertNotNull(ac2)
|
||||||
assertEquals(117, ac2.payload)
|
assertEquals(117, ac2.payload)
|
||||||
assertNull(AccessControlObject.unpackWithPasswordKey<Int>(packed1,pk2))
|
assertNull(AccessControlObject.unpackWithKey<Int>(packed1,pk2))
|
||||||
assertNull(AccessControlObject.unpackWithSecret<Int>(packed1,"the_-wrong-secret-yess"))
|
assertNull(AccessControlObject.unpackWithSecret<Int>(packed1,"the_-wrong-secret-yess"))
|
||||||
|
|
||||||
val (rk2, packed2) = AccessControlObject.pack(pk2, 107)
|
val (rk2, packed2) = AccessControlObject.pack(pk2, 107)
|
||||||
assertNull(AccessControlObject.unpackWithSecret<Int>(packed1,rk2.secret))
|
assertNull(AccessControlObject.unpackWithSecret<Int>(packed1,rk2.secret))
|
||||||
var ac21 = AccessControlObject.unpackWithPasswordKey<Int>(packed2,pk2)
|
var ac21 = AccessControlObject.unpackWithKey<Int>(packed2,pk2)
|
||||||
assertNotNull(ac21)
|
assertNotNull(ac21)
|
||||||
assertEquals(107, ac21.payload)
|
assertEquals(107, ac21.payload)
|
||||||
|
|
||||||
var packed3 = ac1.updatePayload(121).packed
|
var packed3 = ac1.updatePayload(121).packed
|
||||||
ac21 = AccessControlObject.unpackWithPasswordKey(packed3,pk1)
|
ac21 = AccessControlObject.unpackWithKey(packed3,pk1)
|
||||||
assertNotNull(ac21)
|
assertNotNull(ac21)
|
||||||
assertEquals(121, ac21.payload)
|
assertEquals(121, ac21.payload)
|
||||||
|
|
||||||
packed3 = ac1.updatePasswordKey(pk2).packed
|
packed3 = ac1.updatePasswordKey(pk2).packed
|
||||||
println("-------")
|
println("-------")
|
||||||
ac21 = AccessControlObject.unpackWithPasswordKey(packed3,pk2)
|
ac21 = AccessControlObject.unpackWithKey(packed3,pk2)
|
||||||
assertNotNull(ac21)
|
assertNotNull(ac21)
|
||||||
assertEquals(117, ac21.payload)
|
assertEquals(117, ac21.payload)
|
||||||
|
|
||||||
|
@ -9,6 +9,6 @@ suspend inline fun <reified T: Throwable> assertThrowsAsync(f: suspend () -> Uni
|
|||||||
}
|
}
|
||||||
catch(x: Throwable) {
|
catch(x: Throwable) {
|
||||||
if( x !is T )
|
if( x !is T )
|
||||||
fail("${x::class.simpleName} was thrown instead of ${T::class.simpleName}")
|
fail("${x::class.simpleName} was thrown instead of ${T::class.simpleName}: $x")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -69,20 +69,35 @@ abstract class SLServerSession<T> : WithAdapter() {
|
|||||||
* Retreive exact password derivation params as were stored by registration.
|
* Retreive exact password derivation params as were stored by registration.
|
||||||
* @return derivation params for the login name or null if the name is not known
|
* @return derivation params for the login name or null if the name is not known
|
||||||
*/
|
*/
|
||||||
abstract suspend fun requestDerivationParams(login: String): PasswordDerivationParams?
|
abstract suspend fun requestDerivationParams(loginName: String): PasswordDerivationParams?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Override this method, check loginId to match loginName, and of ot os ok, return packed ACO
|
* Override this method, check loginId to match loginName, and of ot os ok, return packed ACO
|
||||||
* @return packed ACO or null if loginName is wrong, or loginId does not match it.
|
* @return packed ACO or null if loginName is wrong, or loginId does not match it.
|
||||||
*/
|
*/
|
||||||
abstract suspend fun requestLoginData(loginName: String,loginId: ByteArray): ByteArray?
|
abstract suspend fun requestACOByLoginName(loginName: String, loginId: ByteArray): ByteArray?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implement retrieving ACO object by restoreId. Return found object ot null.
|
||||||
|
*/
|
||||||
|
abstract suspend fun requestACOByRestoreId(restoreId: ByteArray): ByteArray?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implementation must: find the actual user by loginName and check the publicKey is valid (for example
|
* Implementation must: find the actual user by loginName and check the publicKey is valid (for example
|
||||||
* matches stored key id in the database, and return
|
* matches stored key id in the database, and return
|
||||||
* @return [AuthenticationResult.Success]
|
* @return [AuthenticationResult.Success]
|
||||||
*/
|
*/
|
||||||
abstract suspend fun loginByKey(loginName: String,publicKey: PublicKey): AuthenticationResult
|
abstract suspend fun loginByKey(loginName: String, publicKey: PublicKey): AuthenticationResult
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update access control object (resotre data) to the specified.
|
||||||
|
*/
|
||||||
|
abstract suspend fun updateAccessControlData(
|
||||||
|
loginName: String,
|
||||||
|
packedData: ByteArray,
|
||||||
|
passwordDerivationParams: PasswordDerivationParams,
|
||||||
|
newLoginKey: PublicKey
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun <reified D, T : SLServerSession<D>> T.setSlData(it: AuthenticationResult.Success) {
|
inline fun <reified D, T : SLServerSession<D>> T.setSlData(it: AuthenticationResult.Success) {
|
||||||
|
@ -7,19 +7,22 @@ import net.sergeych.superlogin.*
|
|||||||
import net.sergeych.unikrypto.SignedRecord
|
import net.sergeych.unikrypto.SignedRecord
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
fun randomACOLike(): ByteArray {
|
||||||
|
return Random.nextBytes(117)
|
||||||
|
}
|
||||||
inline fun <reified D, T : SLServerSession<D>, H : CommandHost<T>> AdapterBuilder<T, H>.superloginServer() {
|
inline fun <reified D, T : SLServerSession<D>, H : CommandHost<T>> AdapterBuilder<T, H>.superloginServer() {
|
||||||
|
addErrors(SuperloginExceptionsRegistry)
|
||||||
val a2 = SuperloginServerApi<WithAdapter>()
|
val a2 = SuperloginServerApi<WithAdapter>()
|
||||||
on(a2.slGetNonce) { nonce }
|
on(a2.slGetNonce) { nonce }
|
||||||
on(a2.slRegister) { packed ->
|
on(a2.slRegister) { packed ->
|
||||||
requireLoggedOut()
|
requireLoggedOut()
|
||||||
val ra = SignedRecord.unpack(packed) { sr ->
|
val ra = SignedRecord.unpack(packed) { sr ->
|
||||||
if( !(sr.nonce contentEquals nonce) )
|
if (!(sr.nonce contentEquals nonce))
|
||||||
throw IllegalArgumentException("wrong signed record nonce")
|
throw IllegalArgumentException("wrong signed record nonce")
|
||||||
}.decode<RegistrationArgs>()
|
}.decode<RegistrationArgs>()
|
||||||
|
|
||||||
register(ra).also { rr ->
|
register(ra).also { rr ->
|
||||||
if( rr is AuthenticationResult.Success) {
|
if (rr is AuthenticationResult.Success) {
|
||||||
setSlData(rr)
|
setSlData(rr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -31,7 +34,7 @@ inline fun <reified D, T : SLServerSession<D>, H : CommandHost<T>> AdapterBuilde
|
|||||||
on(a2.slLoginByToken) { token ->
|
on(a2.slLoginByToken) { token ->
|
||||||
requireLoggedOut()
|
requireLoggedOut()
|
||||||
loginByToken(token).also {
|
loginByToken(token).also {
|
||||||
if( it is AuthenticationResult.Success)
|
if (it is AuthenticationResult.Success)
|
||||||
setSlData(it)
|
setSlData(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -40,10 +43,10 @@ inline fun <reified D, T : SLServerSession<D>, H : CommandHost<T>> AdapterBuilde
|
|||||||
// slow down login scanning
|
// slow down login scanning
|
||||||
requestDerivationParams(name) ?: PasswordDerivationParams()
|
requestDerivationParams(name) ?: PasswordDerivationParams()
|
||||||
}
|
}
|
||||||
on(a2.slRequestLoginData) { args ->
|
on(a2.slRequestACOByLoginName) { args ->
|
||||||
requestLoginData(args.loginName,args.loginId)?.let {
|
requestACOByLoginName(args.loginName, args.loginId)?.let {
|
||||||
RequestLoginDataResult(it, nonce)
|
RequestACOResult(it, nonce)
|
||||||
} ?: RequestLoginDataResult(Random.nextBytes(117), nonce)
|
} ?: RequestACOResult(randomACOLike(), nonce)
|
||||||
}
|
}
|
||||||
on(a2.slLoginByKey) { packedSR ->
|
on(a2.slLoginByKey) { packedSR ->
|
||||||
try {
|
try {
|
||||||
@ -53,14 +56,47 @@ inline fun <reified D, T : SLServerSession<D>, H : CommandHost<T>> AdapterBuilde
|
|||||||
}
|
}
|
||||||
val loginName: String = sr.decode<LoginByPasswordPayload>().loginName
|
val loginName: String = sr.decode<LoginByPasswordPayload>().loginName
|
||||||
loginByKey(loginName, sr.publicKey).also {
|
loginByKey(loginName, sr.publicKey).also {
|
||||||
if( it is AuthenticationResult.Success)
|
if (it is AuthenticationResult.Success)
|
||||||
setSlData(it)
|
setSlData(it)
|
||||||
}
|
}
|
||||||
}
|
} catch (x: Exception) {
|
||||||
catch(x: Exception) {
|
|
||||||
// most likely, wrong nonce, less probable bad signature
|
// most likely, wrong nonce, less probable bad signature
|
||||||
AuthenticationResult.LoginUnavailable
|
AuthenticationResult.LoginUnavailable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
on(a2.slChangePasswordAndLogin) { args ->
|
||||||
|
val currentSlData = superloginData
|
||||||
|
try {
|
||||||
|
val sr = SignedRecord.unpack(args.packedSignedRecord) {
|
||||||
|
if (!(it.nonce contentEquals nonce)) throw IllegalArgumentException()
|
||||||
|
}
|
||||||
|
val payload = sr.decode<ChangePasswordPayload>()
|
||||||
|
val loginResult = loginByKey(args.loginName, sr.publicKey)
|
||||||
|
if (loginResult is AuthenticationResult.Success) {
|
||||||
|
setSlData(loginResult)
|
||||||
|
updateAccessControlData(
|
||||||
|
args.loginName,
|
||||||
|
payload.packedACO,
|
||||||
|
payload.passwordDerivationParams,
|
||||||
|
payload.newLoginKey
|
||||||
|
)
|
||||||
|
println(">> ${loginResult.loginToken} -- !")
|
||||||
|
}
|
||||||
|
loginResult
|
||||||
|
} catch (_: IllegalArgumentException) {
|
||||||
|
superloginData = currentSlData
|
||||||
|
AuthenticationResult.LoginUnavailable
|
||||||
|
} catch (x: Throwable) {
|
||||||
|
x.printStackTrace()
|
||||||
|
superloginData = currentSlData
|
||||||
|
AuthenticationResult.LoginUnavailable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
on(a2.slRequestACOBySecretId) {
|
||||||
|
requestACOByRestoreId(it) ?: randomACOLike()
|
||||||
|
}
|
||||||
|
on(a2.slSendTestException) {
|
||||||
|
throw SLInternalException("test")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package net.sergeych
|
package net.sergeych
|
||||||
|
|
||||||
|
import io.ktor.server.application.*
|
||||||
import io.ktor.server.engine.*
|
import io.ktor.server.engine.*
|
||||||
import io.ktor.server.netty.*
|
import io.ktor.server.netty.*
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
@ -8,9 +9,7 @@ import net.sergeych.parsec3.CommandHost
|
|||||||
import net.sergeych.parsec3.Parsec3WSClient
|
import net.sergeych.parsec3.Parsec3WSClient
|
||||||
import net.sergeych.parsec3.WithAdapter
|
import net.sergeych.parsec3.WithAdapter
|
||||||
import net.sergeych.parsec3.parsec3TransportServer
|
import net.sergeych.parsec3.parsec3TransportServer
|
||||||
import net.sergeych.superlogin.AuthenticationResult
|
import net.sergeych.superlogin.*
|
||||||
import net.sergeych.superlogin.PasswordDerivationParams
|
|
||||||
import net.sergeych.superlogin.RegistrationArgs
|
|
||||||
import net.sergeych.superlogin.client.LoginState
|
import net.sergeych.superlogin.client.LoginState
|
||||||
import net.sergeych.superlogin.client.Registration
|
import net.sergeych.superlogin.client.Registration
|
||||||
import net.sergeych.superlogin.client.SuperloginClient
|
import net.sergeych.superlogin.client.SuperloginClient
|
||||||
@ -56,11 +55,15 @@ data class TestSession(var buzz: String = "BuZZ") : SLServerSession<TestData>()
|
|||||||
?: AuthenticationResult.LoginUnavailable
|
?: AuthenticationResult.LoginUnavailable
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun requestDerivationParams(login: String): PasswordDerivationParams? =
|
override suspend fun requestDerivationParams(loginName: String): PasswordDerivationParams? =
|
||||||
byLogin[login]?.derivationParams
|
byLogin[loginName]?.derivationParams
|
||||||
|
|
||||||
override suspend fun requestLoginData(loginName: String, loginId: ByteArray): ByteArray? {
|
override suspend fun requestACOByLoginName(loginName: String, loginId: ByteArray): ByteArray? {
|
||||||
return byLogin[loginName]?.restoreData
|
return byLogin[loginName]?.packedACO
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun requestACOByRestoreId(restoreId: ByteArray): ByteArray? {
|
||||||
|
return byRestoreId[restoreId.toList()]?.packedACO
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun loginByKey(loginName: String, publicKey: PublicKey): AuthenticationResult {
|
override suspend fun loginByKey(loginName: String, publicKey: PublicKey): AuthenticationResult {
|
||||||
@ -69,6 +72,25 @@ data class TestSession(var buzz: String = "BuZZ") : SLServerSession<TestData>()
|
|||||||
AuthenticationResult.Success(ra.loginName, tokens[loginName]!!, ra.extraData)
|
AuthenticationResult.Success(ra.loginName, tokens[loginName]!!, ra.extraData)
|
||||||
else AuthenticationResult.LoginUnavailable
|
else AuthenticationResult.LoginUnavailable
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun updateAccessControlData(
|
||||||
|
loginName: String,
|
||||||
|
packedData: ByteArray,
|
||||||
|
passwordDerivationParams: PasswordDerivationParams,
|
||||||
|
newLoginKey: PublicKey,
|
||||||
|
) {
|
||||||
|
val r = byLogin[loginName]?.copy(
|
||||||
|
packedACO = packedData,
|
||||||
|
derivationParams = passwordDerivationParams,
|
||||||
|
loginPublicKey = newLoginKey
|
||||||
|
)
|
||||||
|
?: throw RuntimeException("login not found")
|
||||||
|
byLogin[loginName] = r
|
||||||
|
byLoginId[r.loginId.toList()] = r
|
||||||
|
byToken[currentLoginToken!!.toList()] = r
|
||||||
|
byRestoreId[r.restoreId.toList()] = r
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -88,17 +110,7 @@ internal class WsServerKtTest {
|
|||||||
@Test
|
@Test
|
||||||
fun testWsServer() {
|
fun testWsServer() {
|
||||||
|
|
||||||
embeddedServer(Netty, port = 8080) {
|
embeddedServer(Netty, port = 8080, module = Application::testServerModule).start(wait = false)
|
||||||
parsec3TransportServer(TestApiServer<SLServerSession<TestData>>()) {
|
|
||||||
// superloginServer(TestServerTraits,TestApiServer<SLServerSession<TestData>>()) {
|
|
||||||
newSession { TestSession() }
|
|
||||||
superloginServer()
|
|
||||||
on(api.loginName) {
|
|
||||||
println("login name called. now we have $currentLoginName : $superloginData")
|
|
||||||
currentLoginName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.start(wait = false)
|
|
||||||
|
|
||||||
val client = Parsec3WSClient("ws://localhost:8080/api/p3")
|
val client = Parsec3WSClient("ws://localhost:8080/api/p3")
|
||||||
|
|
||||||
@ -138,18 +150,75 @@ internal class WsServerKtTest {
|
|||||||
assertEquals("foo", slc.call(api.loginName))
|
assertEquals("foo", slc.call(api.loginName))
|
||||||
//
|
//
|
||||||
assertThrowsAsync<IllegalStateException> { slc.loginByToken(token) }
|
assertThrowsAsync<IllegalStateException> { slc.loginByToken(token) }
|
||||||
slc.logout()
|
}
|
||||||
|
|
||||||
assertNull(slc.loginByPassword("foo", "wrong"))
|
}
|
||||||
ar = slc.loginByPassword("foo", "passwd")
|
|
||||||
println(ar)
|
@Test
|
||||||
|
fun changePasswordTest() {
|
||||||
|
embeddedServer(Netty, port = 8081, module = Application::testServerModule).start(wait = false)
|
||||||
|
|
||||||
|
runBlocking {
|
||||||
|
val client = Parsec3WSClient("ws://localhost:8081/api/p3")
|
||||||
|
|
||||||
|
val api = TestApiServer<WithAdapter>()
|
||||||
|
val slc = SuperloginClient<TestData, WithAdapter>(client)
|
||||||
|
assertEquals(LoginState.LoggedOut, slc.state.value)
|
||||||
|
var rt = slc.register("foo", "passwd", TestData("bar!"))
|
||||||
|
assertIs<Registration.Result.Success>(rt)
|
||||||
|
val secret = rt.secret
|
||||||
|
var token = rt.loginToken
|
||||||
|
|
||||||
|
assertFalse(slc.changePassword("wrong", "new"))
|
||||||
|
assertTrue(slc.changePassword("passwd", "newpass1"))
|
||||||
|
assertTrue { slc.isLoggedIn }
|
||||||
|
assertEquals("foo", slc.call(api.loginName))
|
||||||
|
|
||||||
|
slc.logout()
|
||||||
|
assertNull(slc.loginByPassword("foo", "passwd"))
|
||||||
|
var ar = slc.loginByPassword("foo", "newpass1")
|
||||||
assertNotNull(ar)
|
assertNotNull(ar)
|
||||||
assertEquals("bar!", ar.data?.foo)
|
assertEquals("bar!", ar.data?.foo)
|
||||||
assertTrue { slc.isLoggedIn }
|
assertTrue { slc.isLoggedIn }
|
||||||
assertEquals("foo", slc.call(api.loginName))
|
assertEquals("foo", slc.call(api.loginName))
|
||||||
|
|
||||||
|
slc.logout()
|
||||||
|
println(secret)
|
||||||
|
assertNull(slc.resetPasswordAndLogin("bad_secret", "newpass2"))
|
||||||
|
assertNull(slc.resetPasswordAndLogin("3PBpp-Aris5-ogdV7-Abz36-ggGH5", "newpass2"))
|
||||||
|
ar = slc.resetPasswordAndLogin(secret,"newpass2")
|
||||||
|
assertNotNull(ar)
|
||||||
|
assertEquals("bar!", ar.data?.foo)
|
||||||
|
assertTrue { slc.isLoggedIn }
|
||||||
|
assertEquals("foo", slc.call(api.loginName))
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testExceptions() {
|
||||||
|
embeddedServer(Netty, port = 8082, module = Application::testServerModule).start(wait = false)
|
||||||
|
val client = Parsec3WSClient("ws://localhost:8082/api/p3")
|
||||||
|
runBlocking {
|
||||||
|
val slc = SuperloginClient<TestData, WithAdapter>(client)
|
||||||
|
val serverApi = SuperloginServerApi<WithAdapter>()
|
||||||
|
assertThrowsAsync<SLInternalException> {
|
||||||
|
slc.call(serverApi.slSendTestException,Unit)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Application.testServerModule() {
|
||||||
|
parsec3TransportServer(TestApiServer<SLServerSession<TestData>>()) {
|
||||||
|
// superloginServer(TestServerTraits,TestApiServer<SLServerSession<TestData>>()) {
|
||||||
|
newSession { TestSession() }
|
||||||
|
superloginServer()
|
||||||
|
on(api.loginName) {
|
||||||
|
println("login name called. now we have $currentLoginName : $superloginData")
|
||||||
|
currentLoginName
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user