All Downloads are FREE. Search and download functionalities are using the official Maven repository.

cn.authing.core.auth.AuthenticationClient.kt Maven / Gradle / Ivy

There is a newer version: 4.3.79
Show newest version
package cn.authing.core.auth

import cn.authing.core.BaseClient
import cn.authing.core.Utils
import cn.authing.core.graphql.GraphQLCall
import cn.authing.core.graphql.GraphQLResponse
import cn.authing.core.http.HttpCall
import cn.authing.core.types.*
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.reflect.TypeToken
import okhttp3.FormBody
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.IOException
import java.nio.charset.Charset
import java.util.*
import java.util.regex.Pattern
import kotlin.collections.HashMap

class AuthenticationClient : BaseClient {
    constructor(userPoolId: String) {
        this.userPoolId = userPoolId
    }

    constructor(appId: String, appHost: String) {
        this.appId = appId

        this.host = appHost
    }

    private var user: User? = null

    fun mfa(): MfaAuthenticationClient {
        return MfaAuthenticationClient(this)
    }

    /**
     * 获取当前登录的用户信息
     */
    fun getCurrentUser(): GraphQLCall {
        val param = UserParam()
        return createGraphQLCall(
            param.createRequest(),
            object : TypeToken>() {}) {
            user = it.result
            token = it.result.token ?: token
            return@createGraphQLCall it.result
        }
    }

    /**
     * 获取当前登录的用户信息
     */
    fun getGjsCurrentUser(): GraphQLCall {
        val param = UserParam()
        return createGraphQLCall(
                param.createGjsRequest(),
                object : TypeToken>() {}) {
            user = it.result
            token = it.result.token ?: token
            return@createGraphQLCall it.result
        }
    }

    /**
     * 设置当前用户
     */
    fun setCurrentUser(user: User) {
        this.user = user
        this.token = user.token
    }

    /**
     * 检测当前登录状态
     */
    fun checkLoggedIn(): Boolean {
        if (this.user == null) return false

        if (this.token == null) return false

        return true
    }

    /**
     * 通过微信登陆
     */
    @JvmOverloads
    fun loginByWechatWithConnId(
        connId: String,
        code: String
    ): HttpCall, User> {
        var url = "$host/api/v2/ecConn/wechatMobile/authByCode"
        val json = "{ \"connId\": \"$connId\", \"code\": \"$code\" }"
        return createHttpPostCall(url, json, object : TypeToken>() {}) {
            user = it.data
            token = it.data.token ?: token
            it.data
        }
    }

    /**
     * 通过微信登录,获取用户信息
     */
    @JvmOverloads
    fun loginByWechat(
        code: String,
        country: String? = null,
        lang: String? = null,
        state: String? = null
    ): HttpCall, User> {
        var url = "$host/connection/social/wechat:mobile/$userPoolId/callback?code=$code"
        url += if (country != null) "&country=$country" else ""
        url += if (lang != null) "&lang=$lang" else ""
        url += if (state != null) "&state=$state" else ""
        url += if (appId != null) "&app_id=$appId" else ""

        return createHttpGetCall(url, object : TypeToken>() {}) {
            user = it.data
            token = it.data.token ?: token
            it.data
        }
    }

    /**
     * 使用用户名登录
     */
    fun loginByUsername(input: LoginByUsernameInput): GraphQLCall {
        val param = LoginByUsernameParam(input)
        param.input.password = encrypt(param.input.password)

        return createGraphQLCall(
            param.createRequest(),
            object : TypeToken>() {}) {
            user = it.result
            token = it.result.token ?: token
            return@createGraphQLCall it.result
        }
    }

    /**
     * 使用邮箱登录
     */
    fun loginByEmail(input: LoginByEmailInput): GraphQLCall {
        val param = LoginByEmailParam(input)
        param.input.password = encrypt(param.input.password)

        return createGraphQLCall(
            param.createRequest(),
            object : TypeToken>() {}) {
            user = it.result
            token = it.result.token ?: token
            return@createGraphQLCall it.result
        }
    }

    /**
     * 使用手机号验证码登录
     */
    fun loginByPhoneCode(input: LoginByPhoneCodeInput): GraphQLCall {
        val param = LoginByPhoneCodeParam(input)

        return createGraphQLCall(
            param.createRequest(),
            object : TypeToken>() {}) {
            user = it.result
            token = it.result.token ?: token
            return@createGraphQLCall it.result
        }
    }

    /**
     * 使用手机号密码登录
     */
    fun loginByPhonePassword(input: LoginByPhonePasswordInput): GraphQLCall {
        val param = LoginByPhonePasswordParam(input)
        param.input.password = encrypt(param.input.password)

        return createGraphQLCall(
            param.createRequest(),
            object : TypeToken>() {}) {
            user = it.result
            token = it.result.token ?: token
            return@createGraphQLCall it.result
        }
    }

    /**
     * 登录子账号
     */
    fun loginBySubAccount(
        options: LoginBySubAccountParam
    ): GraphQLCall {

        options.password = encrypt(options.password)

        return createGraphQLCall(
            options.createRequest(),
            object : TypeToken>() {}
        ) {
            user = it.result
            token = it.result.token ?: token
            return@createGraphQLCall it.result
        }
    }

    /**
     * 检查密码强度
     */
    fun checkPasswordStrength(password: String): GraphQLCall {
        val param = CheckPasswordStrengthParam(password);
        return createGraphQLCall(
            param.createRequest(),
            object : TypeToken>() {}) {
            return@createGraphQLCall it.result
        }
    }

    /**
     * 使用用户名注册
     */
    fun registerByUsername(input: RegisterByUsernameInput): GraphQLCall {
        val param = RegisterByUsernameParam(input)
        param.input.password = encrypt(param.input.password)

        return createGraphQLCall(
            param.createRequest(),
            object : TypeToken>() {}) {
            user = it.result
            token = it.result.token ?: token
            return@createGraphQLCall it.result
        }
    }

    /**
     * 使用邮箱注册
     */
    fun registerByEmail(input: RegisterByEmailInput): GraphQLCall {
        val param = RegisterByEmailParam(input)
        param.input.password = encrypt(param.input.password)

        return createGraphQLCall(
            param.createRequest(),
            object : TypeToken>() {}) {
            user = it.result
            token = it.result.token ?: token
            return@createGraphQLCall it.result
        }
    }

    /**
     * 使用手机号及验证码注册
     */
    fun registerByPhoneCode(input: RegisterByPhoneCodeInput): GraphQLCall {
        val param = RegisterByPhoneCodeParam(input)
        if (param.input.password !== null) {
            param.input.password = encrypt(param.input.password!!)
        }

        return createGraphQLCall(
            param.createRequest(),
            object : TypeToken>() {}) {
            user = it.result
            token = it.result.token ?: token
            return@createGraphQLCall it.result
        }
    }

    /**
     * 发送短信验证码
     */
    fun sendSmsCode(phone: String): HttpCall {
        val url = "$host/api/v2/sms/send"
        val json = "{ \"phone\": \"$phone\" }"

        return createHttpPostCall(url, json, object : TypeToken() {}) {
            if (it.code != 200) {
                throw IOException(it.message)
            } else {
                it
            }
        }
    }

    /**
     * 检测 Token 登录状态
     */
    fun checkLoginStatus(): GraphQLCall {
        val param = CheckLoginStatusParam(token = token)
        return createGraphQLCall(
            param.createRequest(),
            object : TypeToken>() {}) {
            it.result
        }
    }

    /**
     * 发送邮件
     */
    fun sendEmail(email: String, scene: EmailScene): GraphQLCall {
        val param = SendEmailParam(email, scene)
        return createGraphQLCall(
            param.createRequest(),
            object : TypeToken>() {}) {
            it.result
        }
    }

    /**
     * 通过邮件验证码重置密码
     */
    fun resetPasswordByEmailCode(
        email: String,
        code: String,
        newPassword: String
    ): GraphQLCall {
        val param = ResetPasswordParam(null, email, code, encrypt(newPassword))

        return createGraphQLCall(
            param.createRequest(),
            object : TypeToken>() {}) {
            it.result
        }
    }

    /**
     * 通过短信验证码重置密码
     */
    fun resetPasswordByPhoneCode(
        phone: String,
        code: String,
        newPassword: String
    ): GraphQLCall {
        val param = ResetPasswordParam(phone, null, code, encrypt(newPassword))

        return createGraphQLCall(
            param.createRequest(),
            object : TypeToken>() {}) {
            it.result
        }
    }

    /**
     * 修改用户资料
     */
    fun updateProfile(input: UpdateUserInput): GraphQLCall {
        val param = UpdateUserParam(input = input)
        return createGraphQLCall(
            param.createRequest(),
            object : TypeToken>() {}) {
            user = it.result
            token = it.result.token ?: token
            return@createGraphQLCall it.result
        }
    }

    /**
     * 更新用户密码
     */
    @JvmOverloads
    fun updatePassword(newPassword: String, oldPassword: String? = null): GraphQLCall {
        val param = UpdatePasswordParam(encrypt(newPassword), encrypt(oldPassword))

        return createGraphQLCall(
            param.createRequest(),
            object : TypeToken>() {}) {
            user = it.result
            token = it.result.token ?: token
            return@createGraphQLCall it.result
        }
    }

    /**
     * 更新用户手机号
     */
    @JvmOverloads
    fun updatePhone(
        phone: String,
        phoneCode: String,
        oldPhone: String? = null,
        oldPhoneCode: String? = null
    ): GraphQLCall {
        val param = UpdatePhoneParam(phone, phoneCode, oldPhone, oldPhoneCode)

        return createGraphQLCall(
            param.createRequest(),
            object : TypeToken>() {}) {
            user = it.result
            token = it.result.token ?: token
            return@createGraphQLCall it.result
        }
    }

    /**
     * 更新用户邮箱
     */
    @JvmOverloads
    fun updateEmail(
        email: String,
        emailCode: String,
        oldEmail: String? = null,
        oldEmailCode: String? = null
    ): GraphQLCall {
        val param = UpdateEmailParam(email, emailCode, oldEmail, oldEmailCode)

        return createGraphQLCall(
            param.createRequest(),
            object : TypeToken>() {}) {
            user = it.result
            token = it.result.token ?: token
            return@createGraphQLCall it.result
        }
    }

    /**
     * 绑定邮箱号
     */
    fun bindEmail(email: String, emailCode: String): GraphQLCall {
        val param = BindEmailParam(email, emailCode)
        return createGraphQLCall(
            param.createRequest(),
            object : TypeToken>() {}) {
            user = it.result
            token = it.result.token ?: token
            return@createGraphQLCall it.result
        }
    }

    /**
     * 解绑邮箱号
     */
    fun unbindEmail(): GraphQLCall {
        val param = UnbindEmailParam()
        return createGraphQLCall(
            param.createRequest(),
            object : TypeToken>() {}) {
            user = it.result
            token = it.result.token ?: token
            return@createGraphQLCall it.result
        }
    }

    /**
     * 关联账号,将社交账号绑定到主账号(手机号、邮箱账号)
     * primaryUserToken: 主账号 Token
     * secondaryUserToken: 社交账号 Token
     */
    fun linkAccount(primaryUserToken: String, secondaryUserToken: String): HttpCall {
        val url = "$host/api/v2/users/link"
        val json = "{ \"primaryUserToken\": \"$primaryUserToken\", \"secondaryUserToken\": \"$secondaryUserToken\" }"

        return createHttpPostCall(url, json, object : TypeToken() {}) {
            if (it.code != 200) {
                throw IOException(it.message)
            } else {
                it
            }
        }
    }

    /**
     * 主账号解绑社会化登录账号
     */
    fun unLinkAccount(options: UnLinkAccountParam): HttpCall, Boolean> {
        val url = "$host/api/v2/users/unlink"

        return createHttpPostCall(
            url,
            Gson().toJson(options),
            object : TypeToken>() {}
        ) { it.code == 200 }
    }

    /**
     * 刷新当前用户的 token
     */
    fun refreshToken(): GraphQLCall {
        val param = RefreshTokenParam()
        return createGraphQLCall(
            param.createRequest(),
            object : TypeToken>() {}) {
            token = it.result.token
            return@createGraphQLCall it.result
        }
    }

    /**
     * 绑定手机号
     */
    fun bindPhone(phone: String, phoneCode: String): GraphQLCall {
        val param = BindPhoneParam(phone, phoneCode)
        return createGraphQLCall(
            param.createRequest(),
            object : TypeToken>() {}) {
            user = it.result
            token = it.result.token ?: token
            return@createGraphQLCall it.result
        }
    }

    /**
     * 解绑手机号
     */
    fun unbindPhone(): GraphQLCall {
        val param = UnbindPhoneParam()
        return createGraphQLCall(
            param.createRequest(),
            object : TypeToken>() {}) {
            user = it.result
            token = it.result.token ?: token
            return@createGraphQLCall it.result
        }
    }

    /**
     * 退出登录
     * 注销当前用户
     */
    fun logout(): HttpCall, Unit> {
        if (appId == null) {
            throw Exception("appId cannot be null")
        }
        val url = "$host/logout?appId=$appId&userId=${user?.id}"
        return createHttpGetCall(
            url,
            object : TypeToken>() {}
        ) {
            user = null
            token = null
        }
    }


    /**
     * 获取当前用户的自定义数据列表
     */
    fun listUdv(): GraphQLCall> {
        if (user == null) {
            throw Exception("login first")
        }
        val param = UdvParam(UdfTargetType.USER, user!!.id)
        return createGraphQLCall(param.createRequest(), object : TypeToken>() {}) {
            it.result
        }
    }

    /**
     * 添加自定义数据
     */
    fun setUdv(key: String, value: Any): GraphQLCall> {
        if (user == null) {
            throw Exception("login first")
        }
        val json = Gson()
        val param = SetUdvParam(UdfTargetType.USER, user!!.id, key, json.toJson(value))
        return createGraphQLCall(
            param.createRequest(),
            object : TypeToken>() {}) {
            it.result
        }
    }

    /**
     * 添加自定义数据
     */
    fun setUdvValue(param: SetUdfValueBatchParam): GraphQLCall> {
        return createGraphQLCall(
            param.createRequest(),
            object : TypeToken>() {}
        ) { it.result }
    }

    /**
     * 删除自定义数据
     */
    private fun removeUdv(key: String): GraphQLCall> {
        if (user == null) {
            throw Exception("login first")
        }
        val param = RemoveUdvParam(UdfTargetType.USER, user!!.id, key)
        return createGraphQLCall(
            param.createRequest(),
            object : TypeToken>() {}) {
            it.result
        }
    }

    /**
     * 获取用户所在组织机构
     */
    fun listOrgs(): HttpCall>>, List>> {
        return this.createHttpGetCall(
            "${this.host}/api/v2/users/me/orgs",
            object : TypeToken>>>() {}) {
            it.data
        }
    }

    /**
     * 使用 LDAP 用户名登录,如果你的用户池配置了登录失败检测,当同一  IP 下登录多次失败的时候会要求用户输入图形验证码(code 为 2000)。
     */
    fun loginByLdap(param: LoginByLdapParam): HttpCall, User> {
        return this.createHttpPostCall(
            "${this.host}/api/v2/ldap/verify-user",
            GsonBuilder().create().toJson(param),
            object : TypeToken>() {}) {
            user = it.data
            token = it.data.token ?: token
            it.data
        }
    }

    /**
     * 使用 AD 用户名登录
     */
    fun loginByAd(username: String, password: String): HttpCall, User> {
        return this.createHttpPostCall(
            "${this.host}/api/v2/ad/verify-user",
            "{ \"username\": \"$username\", \"password\": \"$password\" }",
            object : TypeToken>() {}) {
            user = it.data
            token = it.data.token ?: token
            it.data
        }
    }

    /**
     * 获取用户被授权的所有资源
     */
    fun listAuthorizedResources(namespace: String): GraphQLCall {
        if (user == null) {
            throw Exception("login first")
        }
        val param = ListUserAuthorizedResourcesParam(user!!.id, namespace)
        return createGraphQLCall(
            param.createRequest(),
            object : TypeToken>() {}) {
            it.result.authorizedResources!!
        }
    }

    /**
     * 用户安全等级
     * 获取用户的安全等级评分
     * 此接口需要登录后才可以调用
     */
    fun getSecurityLevel(): HttpCall, SecurityLevel> {
        if (user == null) {
            throw Exception("login first")
        }

        return this.createHttpGetCall(
            "${this.host}/api/v2/users/me/security-level",
            object : TypeToken>() {}) {
            it.data
        }
    }

    /**
     * 获取当前用户的所有自定义数据
     *
     */
    fun getUdfValue(): GraphQLCall> {
        if (user == null) {
            throw Exception("login first")
        }
        val param = UdvParam(UdfTargetType.USER, user!!.id)
        return createGraphQLCall(
            param.createRequest(),
            object : TypeToken>() {}) {
            convertUdvToKeyValuePair(it.result)
        }
    }

    /**
     * 设置自定义字段值
     */
    fun setUdfValue(data: Map): GraphQLCall> {
        if (user == null) {
            throw Exception("login first")
        }

        val udvList = data.entries.map { UserDefinedDataInput(it.key, Gson().toJson(it.value)) }
        val param = SetUdvBatchParam(UdfTargetType.USER, user!!.id, udvList)
        return createGraphQLCall(
            param.createRequest(),
            object : TypeToken>() {}) {
            it.result
        }
    }

    /**
     * 删除用户自定义数据
     */
    fun removeUdfValue(key: String): GraphQLCall> {
        return removeUdv(key)
    }

    /**
     * code 换取 accessToken
     */
    fun getAccessTokenByCode(code: String): HttpCall {

        if (this.secret.isNullOrBlank()
            && this.tokenEndPointAuthMethod != AuthMethodEnum.NONE
        ) {
            throw Exception("请在初始化 AuthenticationClient 时传入 appId 和 secret 参数")
        }

        var url = "${this.host}/oidc/token"
        if(this.protocol == ProtocolEnum.OAUTH) {
            url = "${this.host}/oauth/token"
        }
        when (this.tokenEndPointAuthMethod) {
            AuthMethodEnum.CLIENT_SECRET_POST -> {
                return HttpCall(
                    okHttpClient.newCall(
                        Request.Builder()
                            .url(url)
                            .addHeader("Content-Type", "application/x-www-form-urlencoded")
                            .post(
                                FormBody.Builder().add("client_id", this.appId!!)
                                    .add("client_secret", this.secret!!)
                                    .add("grant_type", "authorization_code")
                                    .add("code", code)
                                    .add("redirect_uri", this.redirectUri!!).build()
                            )
                            .build()
                    ), json.getAdapter(object : TypeToken() {})
                ) {
                    it
                }
            }
            AuthMethodEnum.CLIENT_SECRET_BASIC -> {
                val basic64Str =
                    "Basic " + Base64.getEncoder().encodeToString(("${this.appId}:${this.secret}").toByteArray())

                return HttpCall(
                    okHttpClient.newCall(
                        Request.Builder()
                            .url(url)
                            .addHeader("Authorization", basic64Str)
                            .addHeader("Content-Type", "application/x-www-form-urlencoded")
                            .post(
                                FormBody.Builder()
                                    .add("grant_type", "authorization_code")
                                    .add("code", code)
                                    .add("redirect_uri", this.redirectUri!!).build()
                            )
                            .build()
                    ), json.getAdapter(object : TypeToken() {})
                ) {
                    it
                }
            }
            else -> {//AuthMethodEnum.NONE
                return HttpCall(
                    okHttpClient.newCall(
                        Request.Builder()
                            .url(url)
                            .addHeader("Content-Type", "application/x-www-form-urlencoded")
                            .post(
                                FormBody.Builder().add("client_id", this.appId!!)
                                    .add("grant_type", "authorization_code")
                                    .add("code", code)
                                    .add("redirect_uri", this.redirectUri!!).build()
                            )
                            .build()
                    ), json.getAdapter(object : TypeToken() {})
                ) {
                    it
                }
            }
        }
    }

    /**
     * Client Credentials 模式获取 Access Token
     */
    fun getAccessTokenByClientCredentials(scope: String, options: ClientCredentialInput?): HttpCall {
        if (scope.isBlank()) {
            throw Exception("请传入 scope 参数,请看文档:https://docs.authing.cn/v2/guides/authorization/m2m-authz.html")
        }

        if (options == null) {
            throw Exception("请在调用本方法时传入 { accessKey: string, accessSecret: string },请看文档:https://docs.authing.cn/v2/guides/authorization/m2m-authz.html")
        }

        return HttpCall(
            okHttpClient.newCall(
                Request.Builder()
                    .url("${this.host}/oidc/token")
                    .addHeader("Content-Type", "application/x-www-form-urlencoded")
                    .post(
                        FormBody.Builder().add("client_id", options.accessKey)
                            .add("client_secret", options.accessSecret)
                            .add("grant_type", "client_credentials")
                            .add("scope", scope).build()
                    )
                    .build()
            ), json.getAdapter(object : TypeToken() {})
        ) {
            it
        }
    }

    /**
     * accessToken 换取用户信息
     */
    fun getUserInfoByAccessToken(accessToken: String): HttpCall {
        if (this.protocol == ProtocolEnum.OAUTH) {
            return HttpCall(
                okHttpClient.newCall(
                    Request.Builder()
                        .url("${this.host}/oauth/me?access_token=${accessToken}")
                        .post("{}".toRequestBody(mediaTypeJson))
                        .build()
                ), json.getAdapter(object : TypeToken() {})
            ) {
                it
            }
        }
        return HttpCall(
            okHttpClient.newCall(
                Request.Builder()
                    .url("${this.host}/oidc/me?access_token=${accessToken}")
                    .get()
                    .build()
            ), json.getAdapter(object : TypeToken() {})
        ) {
            it
        }
    }


    /**
     * 获取当前用户能够访问的应用
     */
    @JvmOverloads
    fun listApplications(
        page: Int = 1,
        limit: Int = 10
    ): HttpCall>, Pagination> {
        val url = "$host/api/v2/users/me/applications/allowed?page=$page&limit=$limit"

        return createHttpGetCall(
            url,
            object : TypeToken>>() {}) { it.data }
    }

    /**
     * 计算密码安全等级
     */
    fun computedPasswordSecurityLevel(
        password: String
    ): PasswordSecurityLevel {
        val highLevel = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)[\\^]{12,}\$"
        val middleLevel = "^(?=.*[a-zA-Z])(?=.*\\d)[\\^]{8,}\$"

        if (Pattern.matches(highLevel, password)) {
            return PasswordSecurityLevel.HIGH;
        }
        if (Pattern.matches(middleLevel, password)) {
            return PasswordSecurityLevel.MIDDLE;
        }
        return PasswordSecurityLevel.LOW;
    }

    /**
     * 拼接 OIDC、OAuth 2.0、SAML、CAS 协议授权链接
     */
    fun buildAuthorizeUrl(): String {
        if (this.appId == null)
            throw Exception("请在初始化 AuthenticationClient 时传入 appId")

        if (this.protocol != ProtocolEnum.SAML)
            throw Exception("初始化 AuthenticationClient 传入的 protocol 应为 ProtocolEnum.SAML 不应该为 $protocol")

        return "$host/api/v2/saml-idp/$appId"
    }

    /**
     * 拼接 OIDC、OAuth 2.0、SAML、CAS 协议授权链接
     */
    fun buildAuthorizeUrl(param: IOidcParams): String {
        if (this.appId == null)
            throw Exception("请在初始化 AuthenticationClient 时传入 appId")

        if (this.protocol != ProtocolEnum.OIDC)
            throw Exception("初始化 AuthenticationClient 传入的 protocol 应为 ProtocolEnum.OIDC 不应该为 $protocol")

        if (this.redirectUri == "" && param.redirectUri == null)
            throw Exception("redirectUri 不应该为空 解决方法:请在 AuthenticationClient 初始化时传入 redirectUri,或者调用 buildAuthorizeUrl 时传入 redirectUri")

        val map = mutableMapOf(
            "client_id" to
                    if (param.appId != null) param.appId
                    else this.appId,
            "scope" to
                    if (param.scope != null) param.scope
                    else "openid profile email phone address",
            "state" to
                    if (param.state != null) param.state
                    else Utils().randomString(12),
            "nonce" to
                    if (param.nonce != null) param.nonce
                    else Utils().randomString(12),
            "response_mode" to
                    if (param.responseMode != null) param.responseMode.toString()
                    else null,
            "response_type" to
                    if (param.responseType != null) param.responseType
                    else "code",
            "redirect_uri" to
                    if (param.redirectUri != null) param.redirectUri
                    else this.redirectUri,
            "prompt" to
                    if (param.scope?.contains("offline_access") == true) "consent"
                    else null
        )

        val params = Utils().getQueryUrl(map.filter { (_, value) -> value != null })

        return "$host/oidc/auth$params"
    }

    /**
     * 拼接 OIDC、OAuth 2.0、SAML、CAS 协议授权链接
     */
    fun buildAuthorizeUrl(param: IOauthParams): String {
        if (this.appId == null)
            throw Exception("请在初始化 AuthenticationClient 时传入 appId")

        if (this.protocol != ProtocolEnum.OAUTH)
            throw Exception("初始化 AuthenticationClient 传入的 protocol 应为 ProtocolEnum.OAUTH 不应该为 $protocol")


        if (this.redirectUri == "" && param.redirectUri == null)
            throw Exception("redirectUri 不应该为空 解决方法:请在 AuthenticationClient 初始化时传入 redirectUri,或者调用 buildAuthorizeUrl 时传入 redirectUri")


        val paramsMap = mutableMapOf(
            "client_id" to
                    if (param.appId != null) param.appId
                    else this.appId,
            "scope" to
                    if (param.scope != null) param.scope
                    else "user",
            "state" to
                    if (param.state != null) param.state
                    else Utils().randomString(12),
            "response_type" to
                    if (param.responseType != null) param.responseType
                    else "code",
            "redirect_uri" to
                    if (param.redirectUri != null) param.redirectUri
                    else this.redirectUri
        )

        val params = Utils().getQueryUrl(paramsMap.filter { (_, value) -> value != null })

        return "$host/oauth/auth$params"
    }

    /**
     * 拼接 OIDC、OAuth 2.0、SAML、CAS 协议授权链接
     */
    fun buildAuthorizeUrl(param: ICasParams): String {
        if (this.appId == null)
            throw Exception("请在初始化 AuthenticationClient 时传入 appId")

        if (this.protocol != ProtocolEnum.CAS)
            throw Exception("初始化 AuthenticationClient 传入的 protocol 应为 ProtocolEnum.CAS 不应该为 $protocol")

        if (this.appId == null) {
            throw Exception("请在初始化 AuthenticationClient 时传入 appId")
        }

        return if (param.service != null)
            "$host/cas-idp/$appId?service=${param.service}"
        else
            "$host/cas-idp/$appId"
    }

    /**
     * 使用 Refresh token 获取新的 Access token
     */
    fun getNewAccessTokenByRefreshToken(refreshToken: String): HttpCall {
        this.verificationProtocol()

        return when (this.tokenEndPointAuthMethod) {
            AuthMethodEnum.CLIENT_SECRET_POST -> {
                getNewAccessTokenByRefreshTokenWithClientSecretPost(refreshToken)
            }
            AuthMethodEnum.CLIENT_SECRET_BASIC -> {
                getNewAccessTokenByRefreshTokenWithClientSecretBasic(refreshToken)
            }
            else -> {
                getNewAccessTokenByRefreshTokenWithNone(refreshToken)
            }
        }
    }

    private fun getNewAccessTokenByRefreshTokenWithClientSecretPost(refreshToken: String): HttpCall {
        return HttpCall(
            okHttpClient.newCall(
                Request.Builder()
                    .url(
                        if (this.protocol == ProtocolEnum.OIDC) "$host/oidc/token"
                        else "$host/oauth/token"
                    )
                    .addHeader("Content-Type", "application/x-www-form-urlencoded")
                    .post(
                        FormBody.Builder()
                            .add("client_id", this.appId!!)
                            .add("client_secret", this.secret!!)
                            .add("grant_type", "refresh_token")
                            .add("refresh_token", refreshToken)
                            .build()
                    )
                    .build()
            ), json.getAdapter(object : TypeToken() {})
        ) {
            it
        }
    }

    private fun getNewAccessTokenByRefreshTokenWithClientSecretBasic(refreshToken: String): HttpCall {
        val basic64Str =
            "Basic " + Base64.getEncoder().encodeToString(("${this.appId}:${this.secret}").toByteArray())
        return HttpCall(
            okHttpClient.newCall(
                Request.Builder()
                    .url(
                        if (this.protocol == ProtocolEnum.OIDC) "$host/oidc/token"
                        else "$host/oauth/token"
                    )
                    .addHeader("Authorization", basic64Str)
                    .addHeader("Content-Type", "application/x-www-form-urlencoded")
                    .post(
                        FormBody.Builder()
                            .add("grant_type", "refresh_token")
                            .add("refresh_token", refreshToken)
                            .build()
                    )
                    .build()
            ), json.getAdapter(object : TypeToken() {})
        ) {
            it
        }
    }

    private fun getNewAccessTokenByRefreshTokenWithNone(refreshToken: String): HttpCall {
        return HttpCall(
            okHttpClient.newCall(
                Request.Builder()
                    .url(
                        if (this.protocol == ProtocolEnum.OIDC) "$host/oidc/token"
                        else "$host/oauth/token"
                    )
                    .addHeader("Content-Type", "application/x-www-form-urlencoded")
                    .post(
                        FormBody.Builder()
                            .add("client_id", this.appId!!)
                            .add("grant_type", "refresh_token")
                            .add("refresh_token", refreshToken)
                            .build()
                    )
                    .build()
            ), json.getAdapter(object : TypeToken() {})
        ) {
            it
        }
    }

    /**
     * 检查 Access token 或 Refresh token 的状态
     */
    fun introspectToken(token: String): HttpCall {
        this.verificationProtocol()

        return when (this.introspectionEndPointAuthMethod) {
            AuthMethodEnum.CLIENT_SECRET_POST -> introspectTokenWithClientSecretPost(token)
            AuthMethodEnum.CLIENT_SECRET_BASIC -> introspectTokenWithClientSecretBasic(token)
            else -> introspectTokenWithNone(token)

        }
    }

    private fun introspectTokenWithClientSecretPost(token: String): HttpCall {
        return HttpCall(
            okHttpClient.newCall(
                Request.Builder()
                    .url(
                        if (this.protocol == ProtocolEnum.OIDC) "$host/oidc/token/introspection"
                        else "$host/oauth/token/introspection"
                    )
                    .addHeader("Content-Type", "application/x-www-form-urlencoded")
                    .post(
                        FormBody.Builder()
                            .add("client_id", this.appId!!)
                            .add("client_secret", this.secret!!)
                            .add("token", token)
                            .build()
                    )
                    .build()
            ), json.getAdapter(object : TypeToken() {})
        ) {
            it
        }
    }

    private fun introspectTokenWithClientSecretBasic(token: String): HttpCall {
        val basic64Str =
            "Basic " + Base64.getEncoder().encodeToString(("${this.appId}:${this.secret}").toByteArray())
        return HttpCall(
            okHttpClient.newCall(
                Request.Builder()
                    .url(
                        if (this.protocol == ProtocolEnum.OIDC) "$host/oidc/token/introspection"
                        else "$host/oauth/token/introspection"
                    )
                    .addHeader("Authorization", basic64Str)
                    .addHeader("Content-Type", "application/x-www-form-urlencoded")
                    .post(
                        FormBody.Builder()
                            .add("token", token)
                            .build()
                    )
                    .build()
            ), json.getAdapter(object : TypeToken() {})
        ) {
            it
        }
    }

    private fun introspectTokenWithNone(token: String): HttpCall {
        return HttpCall(
            okHttpClient.newCall(
                Request.Builder()
                    .url(
                        if (this.protocol == ProtocolEnum.OIDC) "$host/oidc/token/introspection"
                        else "$host/oauth/token/introspection"
                    )
                    .addHeader("Content-Type", "application/x-www-form-urlencoded")
                    .post(
                        FormBody.Builder()
                            .add("client_id", this.appId!!)
                            .add("token", token)
                            .build()
                    )
                    .build()
            ), json.getAdapter(object : TypeToken() {})
        ) {
            it
        }
    }

    /**
     * 效验Token合法性
     */
    fun validateToken(param: ValidateTokenParams): HttpCall {
        val (accessToken, idToken) = param
        if (accessToken == null && idToken == null)
            throw Exception("请在传入的参数对象中包含 accessToken 或 idToken 字段")
        if (accessToken != null && idToken != null)
            throw Exception("accessToken 和 idToken 只能传入一个,不能同时传入")

        var url = "$host/api/v2/oidc/validate_token"
        url += if (accessToken != null) "?access_token=$accessToken" else "?id_token=$idToken"

        return HttpCall(
            okHttpClient.newCall(
                Request.Builder()
                    .url(url)
                    .addHeader("Content-Type", "application/x-www-form-urlencoded")
                    .get()
                    .build()
            ), json.getAdapter(object : TypeToken() {})
        ) {
            it
        }
    }

    /**
     * 撤回 Access token 或 Refresh token
     */
    fun revokeToken(token: String): HttpCall {
        this.verificationProtocol()

        return when (this.introspectionEndPointAuthMethod) {
            AuthMethodEnum.CLIENT_SECRET_POST -> revokeTokenWithClientSecretPost(token)
            AuthMethodEnum.CLIENT_SECRET_BASIC -> revokeTokenWithClientSecretBasic(token)
            else -> revokeTokenWithNone(token)

        }
    }

    private fun verificationProtocol(){
        if (!listOf(ProtocolEnum.OAUTH, ProtocolEnum.OIDC).contains(this.protocol)) {
            throw Exception("初始化 AuthenticationClient 时传入的 protocol 参数必须为 ProtocolEnum.OAUTH 或 ProtocolEnum.OIDC,请检查参数")
        }

        if (this.secret.isNullOrBlank()
            && this.tokenEndPointAuthMethod != AuthMethodEnum.NONE
        ) {
            throw Exception("请在初始化 AuthenticationClient 时传入 appId 和 secret 参数")
        }
    }

    private fun revokeTokenWithClientSecretPost(token: String): HttpCall {
        return HttpCall(
            okHttpClient.newCall(
                Request.Builder()
                    .url(
                        if (this.protocol == ProtocolEnum.OIDC) "$host/oidc/token/revocation"
                        else "$host/oauth/token/revocation"
                    )
                    .addHeader("Content-Type", "application/x-www-form-urlencoded")
                    .post(
                        FormBody.Builder()
                            .add("token", token)
                            .add("client_id", this.appId!!)
                            .add("client_secret", this.secret!!)
                            .build()
                    )
                    .build()
            ), json.getAdapter(object : TypeToken() {})
        ) {
            it != null
        }
    }

    private fun revokeTokenWithClientSecretBasic(token: String): HttpCall {
        if (this.protocol == ProtocolEnum.OAUTH)
            throw Exception("OAuth 2.0 暂不支持用 client_secret_basic 模式身份验证撤回 Token")

        val basic64Str =
            "Basic " + Base64.getEncoder().encodeToString(("${this.appId}:${this.secret}").toByteArray())
        return HttpCall(
            okHttpClient.newCall(
                Request.Builder()
                    .url("$host/oidc/token/revocation")
                    .addHeader("Content-Type", "application/x-www-form-urlencoded")
                    .addHeader("Authorization", basic64Str)
                    .post(
                        FormBody.Builder()
                            .add("token", token)
                            .build()
                    )
                    .build()
            ), json.getAdapter(object : TypeToken() {})
        ) {
            it != null
        }
    }

    private fun revokeTokenWithNone(token: String): HttpCall {
        return HttpCall(
            okHttpClient.newCall(
                Request.Builder()
                    .url(
                        if (this.protocol == ProtocolEnum.OIDC) "$host/oidc/token/revocation"
                        else "$host/oauth/token/revocation"
                    )
                    .addHeader("Content-Type", "application/x-www-form-urlencoded")
                    .post(
                        FormBody.Builder()
                            .add("token", token)
                            .add("client_id", this.appId!!)
                            .build()
                    )
                    .build()
            ), json.getAdapter(object : TypeToken() {})
        ) {
            it != null
        }
    }

    /**
     * 拼接登出 URL
     */
    fun buildLogoutUrl(options: ILogoutParams): String {
        if (this.protocol == ProtocolEnum.OAUTH)
            return this.buildCasLogoutUrl(options)

        if (this.protocol == ProtocolEnum.OIDC && options.expert != null)
            return this.buildOidcLogoutUrl(options)

        return buildEasyLogoutUrl(options)
    }

    private fun buildCasLogoutUrl(options: ILogoutParams): String {
        return if (options.redirectUri != null)
            "$host/cas-idp/logout?url=${options.redirectUri}"
        else
            "$host/cas-idp/logout"
    }

    private fun buildOidcLogoutUrl(options: ILogoutParams): String {
        if ((options.redirectUri != null && options.idToken != null) || (options.redirectUri == null && options.idToken == null))
            throw Exception("必须同时传入 idToken 和 redirectUri 参数,或者同时都不传入")

        return if (options.redirectUri != null)
            "${host}/oidc/session/end?id_token_hint=${options.idToken}&post_logout_redirect_uri=${options.redirectUri}"
        else
            "${host}/oidc/session/end"
    }

    private fun buildEasyLogoutUrl(options: ILogoutParams): String {
        return if (options.redirectUri != null)
            "${host}/login/profile/logout?redirect_uri=${options.redirectUri}"
        else
            "${host}/login/profile/logout"
    }

    /**
     * 生成一个 PKCE 校验码,长度必须大于等于 43。
     */
    fun generateCodeChallenge(): String {
        return Utils().randomString(43)
    }

    /**
     * 生成一个 PKCE 校验码摘要值
     */
    fun getCodeChallengeDigest(options: CodeChallengeDigestParam): String{
        val codeChallenge = options.codeChallenge;
        val method = options.method;
        if(codeChallenge == "") {
            throw Exception(
                    "请提供 options.codeChallenge,值为一个长度大于等于 43 的字符串")
        }

        if (method == "S256" || method == "") {
            val encode = Base64.getEncoder().encode(Utils().sha256(codeChallenge))
            return String(encode, Charset.forName("UTF-8")).replace("+", "-")
                .replace("/", "_").replace("=", "")
        }
        if (method == "plain") {
            return codeChallenge
        }
        throw Exception("不支持的 options.method,可选值为 S256、plain")
    }

    /**
     * 判断当前用户是否有某个角色
     */
    fun hasRole(roleCode: String,  namespace: String? = null): GraphQLCall {
        if (user == null) {
            throw Exception("login first")
        }
        val param = GetUserRolesParam(user!!.id)
        return createGraphQLCall(
            param.createRequest(),
            object : TypeToken>() {}) {
            val u = it.result
            var have = false
            if (u.roles != null) {
                val roleListPage = u.roles
                for (role in roleListPage?.list!!) {
                    if (role.code == roleCode) {
                        have = true
                    }
                }
            }
            have
        }
    }

    /**
     * 判断用户是否存在
     */
    fun isUserExists(username: String? = null,
                     email: String? = null,
                     phone: String? = null,
                     externalId: String? = null): GraphQLCall {
        val param = IsUserExistsParam(email, phone, username,externalId)
        return createGraphQLCall(
            param.createRequest(),
            object : TypeToken>() {}) {
            it.result
        }
    }

    /**
     * 获取用户所有部门
     */
    fun listDepartments(): GraphQLCall {
        if (user == null) {
            throw Exception("login first")
        }
        val param = GetUserDepartmentsParam(user!!.id)
        return createGraphQLCall(
            param.createRequest(),
            object : TypeToken>() {}) {
            it.result.departments
        }
    }

    /**
     * 通过首次登录的 Token 重置密码
     */
    fun resetPasswordByFirstLoginToken(token: String, password: String)
    : GraphQLCall {
        val param = ResetPasswordByFirstLoginTokenParam(token, password)
        return createGraphQLCall(
            param.createRequest(),
            object : TypeToken>() {}) {
            it.resetPasswordByFirstLoginToken
        }
    }

    /**
     * 通过密码强制更新临时 Token 修改密码
     */
    fun resetPasswordByForceResetToken(token: String, oldPassword: String, newPassword: String):
        GraphQLCall {
        val param = ResetPasswordByForceResetTokenParam(token, oldPassword, newPassword)
        return createGraphQLCall(
            param.createRequest(),
            object : TypeToken>() {}) {
            it.resetPasswordByFirstLoginToken
        }
    }

    /**
     * sso 检测登录态
     */
    fun trackSession(): HttpCall, User> {
        val url = "${this.host}/cas/session"
        return createHttpGetCall(
            url,
            object : TypeToken>() {}
        ) { it.data }
    }

    /**
     * 检验 CAS 1.0 Ticket 合法性
     */
    fun validateTicketV1(ticket: String, service: String): ValidateTicketV1Response {
        var url = "${this.host}/cas-idp/${this.appId}/validate"
        val map =  HashMap()
        map["ticket"] = ticket
        map["service"] = service
        url = Utils().getQueryUrl(url, map)

        val response = okHttpClient.newCall(
            Request.Builder()
                .url(url)
                .addHeader("Authorization", "Bearer " + this.token)
                .addHeader("x-authing-userpool-id", "" + userPoolId)
                .addHeader("x-authing-request-from", sdkType)
                .addHeader("x-authing-sdk-version", sdkVersion)
                .addHeader("x-authing-app-id", "" + this.appId)
                .get()
                .build()
        ).execute()
        if (response.isSuccessful) {
            val body = response.body?.string()
            val valid = body?.split("\n")?.contains("yes")
            if (valid == true) {
                return ValidateTicketV1Response(valid)
            } else {
                return ValidateTicketV1Response(valid, "ticket 不合法")
            }
        }
        throw Exception(response.message)
    }

    /**
     * 通过远端服务验证票据合法性
     */
    fun validateTicketV2(ticket: String, service: String, format: String): String? {
        if (format != "XML" && format != "JSON") {
            throw Exception("format 参数可选值为 XML、JSON,请检查输入");
        }
        var url = "${this.host}/cas-idp/${this.appId}/serviceValidate"
        val map =  HashMap()
        map["ticket"] = ticket
        map["service"] = service
        map["format"] = format
        url = Utils().getQueryUrl(url, map)
        val response = okHttpClient.newCall(
            Request.Builder()
                .url(url)
                .addHeader("Authorization", "Bearer " + this.token)
                .addHeader("x-authing-userpool-id", "" + userPoolId)
                .addHeader("x-authing-request-from", sdkType)
                .addHeader("x-authing-sdk-version", sdkVersion)
                .addHeader("x-authing-app-id", "" + this.appId)
                .get()
                .build()
        ).execute()
        if (response.isSuccessful) {
            val body = response.body?.string()
            return body
        }
        throw Exception(response.message)
    }


    /**
     * 检测密码是否合法
     */
    fun isPasswordValid(password: String): HttpCall  {
        val url = "${this.host}/api/v2/users/password/check?password=" + encrypt(password)
        return createHttpGetCall(
            url,
            object : TypeToken() {}) {
            it
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy