[Vulcan/Web] Update web login to work with FSLogin Realms.

This commit is contained in:
Kuba Szczodrzyński 2021-02-25 19:29:06 +01:00
parent 459bbf78b2
commit 3ad9e5da1f
10 changed files with 65 additions and 94 deletions

View File

@ -211,7 +211,7 @@ dependencies {
implementation 'com.qifan.powerpermission:powerpermission:1.3.0' implementation 'com.qifan.powerpermission:powerpermission:1.3.0'
implementation 'com.qifan.powerpermission:powerpermission-coroutines:1.3.0' implementation 'com.qifan.powerpermission:powerpermission-coroutines:1.3.0'
implementation 'com.github.kuba2k2.FSLogin:lib:master-SNAPSHOT' implementation 'com.github.kuba2k2.FSLogin:lib:2.0.0'
implementation 'pl.droidsonroids:jspoon:1.3.2' implementation 'pl.droidsonroids:jspoon:1.3.2'
implementation "com.squareup.retrofit2:converter-scalars:2.8.1" implementation "com.squareup.retrofit2:converter-scalars:2.8.1"
implementation "pl.droidsonroids.retrofit2:converter-jspoon:1.3.2" implementation "pl.droidsonroids.retrofit2:converter-jspoon:1.3.2"

View File

@ -43,10 +43,9 @@ import androidx.viewpager.widget.ViewPager
import com.google.android.gms.security.ProviderInstaller import com.google.android.gms.security.ProviderInstaller
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.gson.*
import com.google.gson.JsonArray import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonObject import com.google.gson.JsonObject
import com.google.gson.JsonParser
import im.wangchao.mhttp.Response import im.wangchao.mhttp.Response
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@ -703,6 +702,21 @@ fun JsonObject(vararg properties: Pair<String, Any?>): JsonObject {
} }
} }
fun JsonObject.toBundle(): Bundle {
return Bundle().also {
for ((key, value) in this.entrySet()) {
when (value) {
is JsonObject -> it.putBundle(key, value.toBundle())
is JsonPrimitive -> when {
value.isString -> it.putString(key, value.asString)
value.isBoolean -> it.putBoolean(key, value.asBoolean)
value.isNumber -> it.putInt(key, value.asInt)
}
}
}
}
}
fun JsonArray(vararg properties: Any?): JsonArray { fun JsonArray(vararg properties: Any?): JsonArray {
return JsonArray().apply { return JsonArray().apply {
for (property in properties) { for (property in properties) {

View File

@ -4,6 +4,7 @@
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan
import com.google.gson.JsonObject
import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_VULCAN_API import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_VULCAN_API
import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_VULCAN_HEBE import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_VULCAN_HEBE
@ -14,13 +15,13 @@ import pl.szczodrzynski.edziennik.data.db.entity.LoginStore
import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.data.db.entity.Team import pl.szczodrzynski.edziennik.data.db.entity.Team
import pl.szczodrzynski.edziennik.utils.Utils import pl.szczodrzynski.edziennik.utils.Utils
import pl.szczodrzynski.fslogin.realm.RealmData
class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app, profile, loginStore) { class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app, profile, loginStore) {
fun isWebMainLoginValid() = symbol.isNotNullNorEmpty() fun isWebMainLoginValid() = symbol.isNotNullNorEmpty()
&& (webExpiryTime[symbol]?.toLongOrNull() ?: 0) - 30 > currentTimeUnix() && (webExpiryTime[symbol]?.toLongOrNull() ?: 0) - 30 > currentTimeUnix()
&& webAuthCookie[symbol].isNotNullNorEmpty() && webAuthCookie[symbol].isNotNullNorEmpty()
&& webHost.isNotNullNorEmpty() && webRealmData != null
&& webType.isNotNullNorEmpty()
fun isApiLoginValid() = currentSemesterEndDate-30 > currentTimeUnix() fun isApiLoginValid() = currentSemesterEndDate-30 > currentTimeUnix()
&& apiFingerprint[symbol].isNotNullNorEmpty() && apiFingerprint[symbol].isNotNullNorEmpty()
&& apiPrivateKey[symbol].isNotNullNorEmpty() && apiPrivateKey[symbol].isNotNullNorEmpty()
@ -296,49 +297,15 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
\/ \/ \___|_.__/ |_| |_____/ |______\___/ \__, |_|_| |_| \/ \/ \___|_.__/ |_| |_____/ |______\___/ \__, |_|_| |_|
__/ | __/ |
|__*/ |__*/
/** var webRealmData: RealmData?
* Federation Services login type. get() { mWebRealmData = mWebRealmData ?: loginStore.getLoginData("webRealmData", JsonObject()).let {
* This might be one of: cufs, adfs, adfslight. app.gson.fromJson(it, RealmData::class.java)
*/ }; return mWebRealmData }
var webType: String? set(value) { loginStore.putLoginData("webRealmData", app.gson.toJsonTree(value) as JsonObject); mWebRealmData = value }
get() { mWebType = mWebType ?: loginStore.getLoginData("webType", null); return mWebType } private var mWebRealmData: RealmData? = null
set(value) { loginStore.putLoginData("webType", value); mWebType = value }
private var mWebType: String? = null
/** val webHost
* Web server providing the federation services login. get() = webRealmData?.host
* If this is present, WEB_MAIN login is considered as available.
*/
var webHost: String?
get() { mWebHost = mWebHost ?: loginStore.getLoginData("webHost", null); return mWebHost }
set(value) { loginStore.putLoginData("webHost", value); mWebHost = value }
private var mWebHost: String? = null
/**
* An ID used in ADFS & ADFSLight login types.
*/
var webAdfsId: String?
get() { mWebAdfsId = mWebAdfsId ?: loginStore.getLoginData("webAdfsId", null); return mWebAdfsId }
set(value) { loginStore.putLoginData("webAdfsId", value); mWebAdfsId = value }
private var mWebAdfsId: String? = null
/**
* A domain override for ADFS Light.
*/
var webAdfsDomain: String?
get() { mWebAdfsDomain = mWebAdfsDomain ?: loginStore.getLoginData("webAdfsDomain", null); return mWebAdfsDomain }
set(value) { loginStore.putLoginData("webAdfsDomain", value); mWebAdfsDomain = value }
private var mWebAdfsDomain: String? = null
var webIsHttpCufs: Boolean
get() { mWebIsHttpCufs = mWebIsHttpCufs ?: loginStore.getLoginData("webIsHttpCufs", false); return mWebIsHttpCufs ?: false }
set(value) { loginStore.putLoginData("webIsHttpCufs", value); mWebIsHttpCufs = value }
private var mWebIsHttpCufs: Boolean? = null
var webIsScopedAdfs: Boolean
get() { mWebIsScopedAdfs = mWebIsScopedAdfs ?: loginStore.getLoginData("webIsScopedAdfs", false); return mWebIsScopedAdfs ?: false }
set(value) { loginStore.putLoginData("webIsScopedAdfs", value); mWebIsScopedAdfs = value }
private var mWebIsScopedAdfs: Boolean? = null
var webEmail: String? var webEmail: String?
get() { mWebEmail = mWebEmail ?: loginStore.getLoginData("webEmail", null); return mWebEmail } get() { mWebEmail = mWebEmail ?: loginStore.getLoginData("webEmail", null); return mWebEmail }

View File

@ -13,7 +13,7 @@ import pl.szczodrzynski.edziennik.getString
import pl.szczodrzynski.edziennik.isNotNullNorEmpty import pl.szczodrzynski.edziennik.isNotNullNorEmpty
import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.fslogin.FSLogin import pl.szczodrzynski.fslogin.FSLogin
import pl.szczodrzynski.fslogin.realm.CufsRealm import pl.szczodrzynski.fslogin.realm.toRealm
class VulcanLoginWebMain(val data: DataVulcan, val onSuccess: () -> Unit) { class VulcanLoginWebMain(val data: DataVulcan, val onSuccess: () -> Unit) {
companion object { companion object {
@ -30,8 +30,7 @@ class VulcanLoginWebMain(val data: DataVulcan, val onSuccess: () -> Unit) {
} }
else { else {
if (data.symbol.isNotNullNorEmpty() if (data.symbol.isNotNullNorEmpty()
&& data.webType.isNotNullNorEmpty() && data.webRealmData != null
&& data.webHost.isNotNullNorEmpty()
&& (data.webEmail.isNotNullNorEmpty() || data.webUsername.isNotNullNorEmpty()) && (data.webEmail.isNotNullNorEmpty() || data.webUsername.isNotNullNorEmpty())
&& data.webPassword.isNotNullNorEmpty()) { && data.webPassword.isNotNullNorEmpty()) {
try { try {
@ -56,32 +55,28 @@ class VulcanLoginWebMain(val data: DataVulcan, val onSuccess: () -> Unit) {
data.symbol = getString("symbol") data.symbol = getString("symbol")
remove("symbol") remove("symbol")
} }
// 4.6 - form inputs renamed
if (has("email")) {
data.webEmail = getString("email")
remove("email")
}
if (has("username")) {
data.webUsername = getString("username")
remove("username")
}
if (has("password")) {
data.webPassword = getString("password")
remove("password")
}
}
if (data.symbol == null && data.webRealmData != null) {
data.symbol = data.webRealmData?.symbol
} }
} }
private fun loginWithCredentials(): Boolean { private fun loginWithCredentials(): Boolean {
val realm = when (data.webType) { val realm = data.webRealmData?.toRealm() ?: return false
"cufs" -> CufsRealm(
host = data.webHost ?: return false,
symbol = data.symbol ?: "default",
httpCufs = data.webIsHttpCufs
)
"adfs" -> CufsRealm(
host = data.webHost ?: return false,
symbol = data.symbol ?: "default",
httpCufs = data.webIsHttpCufs
).toAdfsRealm(id = data.webAdfsId ?: return false)
"adfslight" -> CufsRealm(
host = data.webHost ?: return false,
symbol = data.symbol ?: "default",
httpCufs = data.webIsHttpCufs
).toAdfsLightRealm(
id = data.webAdfsId ?: return false,
domain = data.webAdfsDomain ?: "adfslight",
isScoped = data.webIsScopedAdfs
)
else -> return false
}
val certificate = web.readCertificate()?.let { web.parseCertificate(it) } val certificate = web.readCertificate()?.let { web.parseCertificate(it) }
if (certificate != null && Date.fromIso(certificate.expiryDate) > System.currentTimeMillis()) { if (certificate != null && Date.fromIso(certificate.expiryDate) > System.currentTimeMillis()) {

View File

@ -348,8 +348,8 @@ class SzkolnyApi(val app: App) : CoroutineScope {
} }
@Throws(Exception::class) @Throws(Exception::class)
fun getPlatforms(registerName: String): List<LoginInfo.Platform> { fun getRealms(registerName: String): List<LoginInfo.Platform> {
val response = api.appLoginPlatforms(registerName).execute() val response = api.fsLoginRealms(registerName).execute()
return parseResponse(response) return parseResponse(response)
} }

View File

@ -33,12 +33,12 @@ interface SzkolnyService {
@POST("feedbackMessage") @POST("feedbackMessage")
fun feedbackMessage(@Body request: FeedbackMessageRequest): Call<ApiResponse<FeedbackMessageResponse>> fun feedbackMessage(@Body request: FeedbackMessageRequest): Call<ApiResponse<FeedbackMessageResponse>>
@GET("appLogin/platforms/{registerName}")
fun appLoginPlatforms(@Path("registerName") registerName: String): Call<ApiResponse<List<LoginInfo.Platform>>>
@GET("firebase/token/{registerName}") @GET("firebase/token/{registerName}")
fun firebaseToken(@Path("registerName") registerName: String): Call<ApiResponse<String>> fun firebaseToken(@Path("registerName") registerName: String): Call<ApiResponse<String>>
@GET("registerAvailability") @GET("registerAvailability")
fun registerAvailability(): Call<ApiResponse<Map<String, RegisterAvailabilityStatus>>> fun registerAvailability(): Call<ApiResponse<Map<String, RegisterAvailabilityStatus>>>
@GET("fsLogin/{registerName}")
fun fsLoginRealms(@Path("registerName") registerName: String): Call<ApiResponse<List<LoginInfo.Platform>>>
} }

View File

@ -59,6 +59,7 @@ class LoginStore(
is Long -> putLoginData(key, o) is Long -> putLoginData(key, o)
is Float -> putLoginData(key, o) is Float -> putLoginData(key, o)
is Boolean -> putLoginData(key, o) is Boolean -> putLoginData(key, o)
is Bundle -> putLoginData(key, o.toJsonObject())
} }
} }
} }

View File

@ -14,7 +14,6 @@ import androidx.core.view.isVisible
import androidx.core.widget.addTextChangedListener import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import com.google.gson.JsonParser
import com.mikepenz.iconics.IconicsDrawable import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.utils.paddingDp import com.mikepenz.iconics.utils.paddingDp
import com.mikepenz.iconics.utils.sizeDp import com.mikepenz.iconics.utils.sizeDp
@ -72,7 +71,7 @@ class LoginFormFragment : Fragment(), CoroutineScope {
val platformGuideText = arguments?.getString("platformGuideText") val platformGuideText = arguments?.getString("platformGuideText")
val platformDescription = arguments?.getString("platformDescription") val platformDescription = arguments?.getString("platformDescription")
val platformFormFields = arguments?.getString("platformFormFields")?.split(";") val platformFormFields = arguments?.getString("platformFormFields")?.split(";")
val platformApiData = arguments?.getString("platformApiData")?.let { JsonParser().parse(it)?.asJsonObject } val platformRealmData = arguments?.getString("platformRealmData")?.toJsonObject()
b.title.setText(R.string.login_form_title_format, app.getString(register.registerName)) b.title.setText(R.string.login_form_title_format, app.getString(register.registerName))
b.subTitle.text = platformName ?: app.getString(mode.name) b.subTitle.text = platformName ?: app.getString(mode.name)
@ -159,9 +158,7 @@ class LoginFormFragment : Fragment(), CoroutineScope {
payload.putBoolean("fakeLogin", true) payload.putBoolean("fakeLogin", true)
} }
platformApiData?.entrySet()?.forEach { payload.putBundle("webRealmData", platformRealmData?.toBundle())
payload.putString(it.key, it.value.asString)
}
var hasErrors = false var hasErrors = false
credentials.forEach { (credential, b) -> credentials.forEach { (credential, b) ->

View File

@ -6,12 +6,12 @@ package pl.szczodrzynski.edziennik.ui.modules.login
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import com.google.gson.JsonObject
import com.mikepenz.iconics.typeface.IIcon import com.mikepenz.iconics.typeface.IIcon
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.api.* import pl.szczodrzynski.edziennik.data.api.*
import pl.szczodrzynski.edziennik.ui.modules.grades.models.ExpandableItemModel import pl.szczodrzynski.edziennik.ui.modules.grades.models.ExpandableItemModel
import pl.szczodrzynski.fslogin.realm.RealmData
object LoginInfo { object LoginInfo {
@ -191,9 +191,9 @@ object LoginInfo {
isDevOnly = true, isDevOnly = true,
isPlatformSelection = true, isPlatformSelection = true,
credentials = listOf( credentials = listOf(
getEmailCredential("webEmail"), getEmailCredential("email"),
FormField( FormField(
keyName = "webUsername", keyName = "username",
name = R.string.login_hint_username, name = R.string.login_hint_username,
icon = CommunityMaterial.Icon.cmd_account_outline, icon = CommunityMaterial.Icon.cmd_account_outline,
emptyText = R.string.login_error_no_username, emptyText = R.string.login_error_no_username,
@ -203,7 +203,7 @@ object LoginInfo {
validationRegex = "[A-Z]{7}[0-9]+", validationRegex = "[A-Z]{7}[0-9]+",
caseMode = FormField.CaseMode.UPPER_CASE caseMode = FormField.CaseMode.UPPER_CASE
), ),
getPasswordCredential("webPassword") getPasswordCredential("password")
), ),
errorCodes = mapOf() errorCodes = mapOf()
) )
@ -407,15 +407,12 @@ object LoginInfo {
data class Platform( data class Platform(
val id: Int, val id: Int,
val loginType: Int,
val loginMode: Int,
val name: String, val name: String,
val description: String?, val description: String?,
val guideText: String?,
val icon: String, val icon: String,
val screenshot: String?, val screenshot: String?,
val formFields: List<String>, val formFields: List<String>,
val apiData: JsonObject val realmData: RealmData
) )
open class BaseCredential( open class BaseCredential(

View File

@ -60,12 +60,12 @@ class LoginPlatformListFragment : Fragment(), CoroutineScope {
adapter = LoginPlatformAdapter(activity) { platform -> adapter = LoginPlatformAdapter(activity) { platform ->
nav.navigate(R.id.loginFormFragment, Bundle( nav.navigate(R.id.loginFormFragment, Bundle(
"loginType" to platform.loginType, "loginType" to loginType,
"loginMode" to platform.loginMode, "loginMode" to loginMode,
"platformName" to platform.name, "platformName" to platform.name,
"platformDescription" to platform.description, "platformDescription" to platform.description,
"platformFormFields" to platform.formFields.joinToString(";"), "platformFormFields" to platform.formFields.joinToString(";"),
"platformApiData" to platform.apiData.toString() "platformRealmData" to app.gson.toJson(platform.realmData)
), activity.navOptions) ), activity.navOptions)
} }
@ -96,7 +96,7 @@ class LoginPlatformListFragment : Fragment(), CoroutineScope {
val platforms = LoginInfo.platformList[mode.name] val platforms = LoginInfo.platformList[mode.name]
?: run { ?: run {
api.runCatching(activity) { api.runCatching(activity) {
getPlatforms(register.internalName) getRealms(register.internalName)
} ?: run { } ?: run {
nav.navigateUp() nav.navigateUp()
return@launch return@launch