WorthMentioning
Features#
worth mentioning,In fact, log in with other IDP system accounts,The process of binding the account of ARKID。
Classic:WeChat login,Login,Flying Book Login and so on。
Implementation#
Because most of the third -party login schemes are derivatives based on the OAUTH2 protocol,Therefore, the process of the tripartite certification is based。
first,Create a third -party certification entrance on the login page,Dedication of this entrance:
- icon: get_img_url
- Third -party login request Arkid.core.extension.external_idp.ExternalIdpExtension.login, The request will redirect the entrance address of the third party login, The entrance address is get_authorize_url
When the user clicks the icon,After initiating a third -party certification request, Will return to Arkid.core.extension.external_idp.ExternalIdpExtension.callback interface,And Code
In callback,Code call get_ext_token_by_code method,Get ACCESS_token, Then via access_token call get_user_info_by_ext_token method to get user information
- 
If the third party certification returns EXT_ID is not bound to users in Arkid,The front end will jump to the binding page,Call Arkid.core.extension.external_idp.ExternalIdpExtension.bind interface, This interface will call bind_arkid_user Method binds EXT_ID to Arkid users 
- 
If the third party certification returns EXT_ID has been bound to users in Arkid,Call get_arkid_user Method obtain an arkid user that has been bound 
To complete the login at this point
Abstract method#
Foundation definition#
        
arkid.core.extension.external_idp.ExternalIdpExtension            (Extension)
        
#
    Source code in arkid/core/extension/external_idp.py
          class ExternalIdpExtension(Extension):
    TYPE = "external_idp"
    composite_schema_map = {}
    created_composite_schema_list = []
    composite_key = 'type'
    composite_model = TenantExtensionConfig
    @property
    def type(self):
        return ExternalIdpExtension.TYPE
    def load(self):
        urls = [
            re_path(
                rf'^idp/{self.pname}/(?P<config_id>[\w-]+)/login$',
                self.login,
                name=f'{self.pname}_login',
            ),
            re_path(
                rf'^idp/{self.pname}/(?P<config_id>[\w-]+)/callback$',
                self.callback,
                name=f'{self.pname}_callback',
            ),
            re_path(
                rf'^idp/{self.pname}/(?P<config_id>[\w-]+)/bind$',
                self.bind,
                name=f'{self.pname}_bind',
            ),
        ]
        self.register_routers(urls, False)
        self.listen_event(
            core_event.CREATE_LOGIN_PAGE_AUTH_FACTOR, self.add_idp_login_buttons
        )
        self.listen_event(
            core_event.ACCOUNT_UNBIND, self.account_unbind
        )
        super().load()
    @abstractmethod
    def get_authorize_url(self, config, callback_url, next_url):
        """
        抽象方法
        Args:
            config (arkid.extension.models.TenantExtensionConfig): 第三方认证提供的Client_ID,
            callback_url (str): 由ArkID提供的回调地址
            next_url (str): 前端传来的跳转地址
        Returns:
            str: 第三方登录提供的认证URL
        """
        pass
    def login(self, request, config_id):
        """
        重定向到第三方登录的入口地址, 该入口地址由get_authorize_url提供
        """
        config = self.get_config_by_id(config_id)
        if not config:
            return JsonResponse({"error_msg": "没有找到登录配置"})
        callback_url = config.config.get("callback_url")
        # callback_url = callback_url.replace(
        #     "localhost:8000", "xxxx.vaiwan.com"
        # )  # 内网穿透测试用
        next_url = request.GET.get("next", None)
        if next_url is not None:
            next_url = "?next=" + next_url
        else:
            next_url = ""
        url = self.get_authorize_url(config, callback_url, next_url)
        return HttpResponseRedirect(url)
    @abstractmethod
    def get_ext_token(self, config, code):
        """
        抽象方法
        Args:
            code (str): 第三方认证返回的code
            config (arkid.core.extension.TenantExtensionConfig): 第三方登录的插件运行时配置
        Returns:
            str: 返回第三方认证提供的token
        """
        pass
    @abstractmethod
    def get_ext_user_info(self, config, code, token):
        """
        抽象方法
        Args:
            config (arkid.core.extension.TenantExtensionConfig): 第三方登录的插件运行时配置
            code (str): 第三方认证返回的code
            token (str): 第三方认证返回的token
        Returns:
            dict: 返回第三方认证提供的用户信息
        """
        pass
    @abstractmethod
    def get_arkid_user(self, ext_id):
        """
        抽象方法
        Args:
            ext_id (str): 第三方认证返回的用户标识
        Returns:
            arkid.core.models.User: ArkID用户
        """
        pass
    def get_arkid_token(self, ext_id, ext_name, ext_icon, config):
        arkid_user = self.get_arkid_user(ext_id)
        if arkid_user:
            from arkid.core.models import ExpiringToken
            et = ExpiringToken.objects.filter(
                user=arkid_user
            ).first()
            if et and et.expired(config.tenant) is False:
                token = et.token
            else:
                token = refresh_token(arkid_user)
            context = {"token": token}
        else:
            context = {
                "token": "",
                "ext_id": ext_id,
                "ext_name": ext_name,
                "ext_icon": ext_icon,
                "tenant_id": config.tenant.id.hex,
                "bind": config.config.get('bind_url'),
            }
        return context
    @abstractmethod
    def get_auth_code_from_request(self, request):
        """
        抽象方法
        Args:
            request (HTTPRequest): 第三方认证返回的用户标识
        Returns:
            str: 授权码
        """
        pass
    def callback(self, request, config_id):
        """
        拿到请求中携带的code,调用get_ext_token_by_code获取第三方认证的token,
        调用get_user_info_by_ext_token获取第三方认证提供的用户信息,
        拿到ext_id后,判断该ext_id是否已经和ArkID中的用户绑定,如果绑定直接返回绑定用户的Token,
        如果没有,返回重定向到前端绑定页面
        """
        code = self.get_auth_code_from_request(request)
        next_url = request.GET.get("next", '')
        config = self.get_config_by_id(config_id)
        frontend_host = (
            get_app_config()
            .get_frontend_host()
            .replace('http://', '')
            .replace('https://', '')
        )
        # if next_url and (
        #     "third_part_callback" not in next_url or frontend_host not in next_url
        # ):
        #     return JsonResponse({'error_msg': '错误的跳转页面'})
        if next_url and (
            frontend_host not in next_url
        ):
            return JsonResponse({'error_msg': '错误的跳转页面'})
        if code:
            try:
                ext_token = self.get_ext_token(config, code)
                ext_id, ext_name, ext_icon, ext_info = self.get_ext_user_info(
                    config, code, ext_token
                )
            except Exception as e:
                logger.error(e)
                args = e.args
                if len(args) == 1 and isinstance(args[0], dict):
                    return JsonResponse(e.args[0], json_dumps_params={'ensure_ascii': False})
                else:
                    return JsonResponse({"error_msg": str(e)}, json_dumps_params={'ensure_ascii': False})
        else:
            return JsonResponse({"error_msg": "授权码丢失", "code": ["required"]}, json_dumps_params={'ensure_ascii': False})
        context = self.get_arkid_token(ext_id, ext_name, ext_icon, config)
        query_string = urlencode(context)
        if next_url:
            url = f"{next_url}?{query_string}"
            url = unquote(url)
            return HttpResponseRedirect(url)
        else:
            frontend_host = get_app_config().get_frontend_host()
            frontend_callback = f'{frontend_host}/third_part_callback'
            url = f"{frontend_callback}?{query_string}"
            url = unquote(url)
            return HttpResponseRedirect(url)
    @abstractmethod
    def bind_arkid_user(self, ext_id, user, data):
        """
        Args:
            ext_id (str): 第三方登录返回的用户标识
            user (arkid.core.models.User): ArkID的用户
            data (dict) request数据
        Returns:
            {"token":xxx}: 返回token
        """
        pass
    @csrf_exempt
    def bind(self, request, config_id):
        """
        处理第三方身份源返回的user_id和ArkID的user之间的绑定
        """
        from urllib.parse import unquote
        from arkid.core.event import SAVE_FILE, dispatch_event, Event
        from arkid.extension.models import Extension
        ext_id = request.POST.get("ext_id")
        user = verify_token(request)
        if not user:
            return JsonResponse({"error_msg": "Token验证失败", "code": ["token invalid"]})
        ext_icon = request.POST.get('ext_icon', '')
        ext_name = request.POST.get('ext_name', '')
        config = self.get_config_by_id(config_id)
        extension = Extension.active_objects.filter(
            type="storage"
        ).first()
        if ext_name:
            ext_name = unquote(ext_name)
            request.POST['ext_name'] = ext_name
        if ext_icon:
            data = {
                'fileurl': ext_icon
            }
            responses = dispatch_event(Event(tag=SAVE_FILE, tenant=config.tenant, request=request, packages=extension.package, data=data))
            useless, (fileinfo, extension) = responses[0]
            request.POST['ext_icon'] = fileinfo
        self.bind_arkid_user(ext_id, user, request.POST)
        # token = refresh_token(user)
        # data = {"token": token}
        data = {}
        return JsonResponse(self.success())
    @abstractmethod
    def get_img_url(self):
        """
        抽象方法
        Returns:
            url str: 返回第三方登录按钮的图标
        """
        pass
    def register_external_idp_schema(self, idp_type, schema):
        self.register_config_schema(schema, self.package + '_' + idp_type)
        self.register_composite_config_schema(schema, idp_type, exclude=['extension'])
    def create_tenant_config(self, tenant, config, name, type):
        config_created = super().create_tenant_config(tenant, config, name, type)
        server_host = get_app_config().get_host()
        login_url = server_host + reverse(
            f'api:{self.pname}:{self.pname}_login',
            args=[config_created.id],
        )
        callback_url = server_host + reverse(
            f'api:{self.pname}:{self.pname}_callback',
            args=[config_created.id],
        )
        bind_url = server_host + reverse(
            f'api:{self.pname}:{self.pname}_bind',
            args=[config_created.id],
        )
        # img_url = self.get_img_url()
        config["login_url"] = login_url
        config["callback_url"] = callback_url
        config["bind_url"] = bind_url
        # config["img_url"] = img_url
        config_created.config = config
        config_created.save()
        return config_created
    def update_tenant_config(self, id, config, name, type):
        super().update_tenant_config(id, config, name, type)
        config_created = TenantExtensionConfig.valid_objects.filter(id=id).first()
        server_host = get_app_config().get_host()
        login_url = server_host + reverse(
            f'api:{self.pname}:{self.pname}_login',
            args=[config_created.id],
        )
        callback_url = server_host + reverse(
            f'api:{self.pname}:{self.pname}_callback',
            args=[config_created.id],
        )
        bind_url = server_host + reverse(
            f'api:{self.pname}:{self.pname}_bind',
            args=[config_created.id],
        )
        # img_url = self.get_img_url()
        config["login_url"] = login_url
        config["callback_url"] = callback_url
        config["bind_url"] = bind_url
        # config["img_url"] = img_url
        config_created.config = config
        config_created.save()
        return config_created
    def add_idp_login_buttons(self, event, **kwargs):
        logger.info(f'{self.package} add idp login buttons start')
        data = {}
        configs = self.get_tenant_configs(event.tenant)
        for config in configs:
            img_url, redirect_url = self.get_img_and_redirect_url(config)
            if img_url and redirect_url:
                buttons = [{"img": config.config.get('img_url', img_url), "redirect": {"url": redirect_url}, "tooltip":config.name}]
                data[config.id.hex] = {"login": {'extend': {"buttons": buttons}}}
        logger.info(f'{self.package} add idp login buttions end')
        return data
    def account_unbind(self, event, **kwargs):
        '''
        在账户解绑的时候会调用此方法,开发者可以根据需要重写此方法
        Params:
            event: 事件参数
                data: 数据
                    user_id: 用户id
        '''
        data = event.data
        pass
    @abstractmethod
    def get_img_and_redirect_url(self, config):
        """
        返回前端渲染第三方登录的按钮
        抽象方法
        Args:
            config (arkid.extension.models.TenantExtensionConfig): 第三方认证提供的Client_ID,
        Returns:
            tuple: 返回图片url和跳转地址('image_url', 'redirect_url')
        """
        pass
        
composite_model            (BaseModel)
        
  
      django-model
  
#
    TenantExtensionConfig(id, is_del, is_active, updated, created, tenant, extension, config, name, type)
Source code in arkid/core/extension/external_idp.py
          class TenantExtensionConfig(BaseModel):
    class Meta(object):
        verbose_name = _("插件运行时配置")
        verbose_name_plural = _("插件运行时配置")
    tenant = models.ForeignKey('core.Tenant', blank=False, on_delete=models.PROTECT, verbose_name=_('租户'))
    extension = models.ForeignKey('Extension', blank=False, on_delete=models.PROTECT, verbose_name=_('插件'))
    config = models.JSONField(blank=True, default=dict, verbose_name=_('Runtime Config','运行时配置'))
    name = models.CharField(max_length=128, default='', verbose_name=_('名称'))
    type = models.CharField(max_length=128, default='', verbose_name=_('类型'))
config: JSONField
  
      blank
      django-field
  
#
    Runtime Config
created: DateTimeField
  
      blank
      django-field
      nullable
  
#
    创建时间
extension: ForeignKey
  
      django-field
  
#
    插件
id: UUIDField
  
      django-field
  
#
    ID
is_active: BooleanField
  
      django-field
  
#
    是否可用
is_del: BooleanField
  
      django-field
  
#
    是否删除
name: CharField
  
      django-field
  
#
    名称
tenant: ForeignKey
  
      django-field
  
#
    租户
type: CharField
  
      django-field
  
#
    类型
updated: DateTimeField
  
      blank
      django-field
      nullable
  
#
    更新时间
account_unbind(self, event, **kwargs)
#
    在账户解绑的时候会调用此方法,开发者可以根据需要重写此方法
Parameters:
| Name | Type | Description | Default | 
|---|---|---|---|
| event | 事件参数 data: 数据 user_id: 用户id | required | 
bind(self, request, config_id)
#
    处理第三方身份源返回的user_id和ArkID的user之间的绑定
Source code in arkid/core/extension/external_idp.py
          @csrf_exempt
def bind(self, request, config_id):
    """
    处理第三方身份源返回的user_id和ArkID的user之间的绑定
    """
    from urllib.parse import unquote
    from arkid.core.event import SAVE_FILE, dispatch_event, Event
    from arkid.extension.models import Extension
    ext_id = request.POST.get("ext_id")
    user = verify_token(request)
    if not user:
        return JsonResponse({"error_msg": "Token验证失败", "code": ["token invalid"]})
    ext_icon = request.POST.get('ext_icon', '')
    ext_name = request.POST.get('ext_name', '')
    config = self.get_config_by_id(config_id)
    extension = Extension.active_objects.filter(
        type="storage"
    ).first()
    if ext_name:
        ext_name = unquote(ext_name)
        request.POST['ext_name'] = ext_name
    if ext_icon:
        data = {
            'fileurl': ext_icon
        }
        responses = dispatch_event(Event(tag=SAVE_FILE, tenant=config.tenant, request=request, packages=extension.package, data=data))
        useless, (fileinfo, extension) = responses[0]
        request.POST['ext_icon'] = fileinfo
    self.bind_arkid_user(ext_id, user, request.POST)
    # token = refresh_token(user)
    # data = {"token": token}
    data = {}
    return JsonResponse(self.success())
bind_arkid_user(self, ext_id, user, data)
#
    Parameters:
| Name | Type | Description | Default | 
|---|---|---|---|
| ext_id | str | 第三方登录返回的用户标识 | required | 
| user | arkid.core.models.User | ArkID的用户 | required | 
Returns:
| Type | Description | 
|---|---|
| {"token" | xxx}: 返回token | 
callback(self, request, config_id)
#
    拿到请求中携带的code,调用get_ext_token_by_code获取第三方认证的token, 调用get_user_info_by_ext_token获取第三方认证提供的用户信息, 拿到ext_id后,判断该ext_id是否已经和ArkID中的用户绑定,如果绑定直接返回绑定用户的Token, 如果没有,返回重定向到前端绑定页面
Source code in arkid/core/extension/external_idp.py
          def callback(self, request, config_id):
    """
    拿到请求中携带的code,调用get_ext_token_by_code获取第三方认证的token,
    调用get_user_info_by_ext_token获取第三方认证提供的用户信息,
    拿到ext_id后,判断该ext_id是否已经和ArkID中的用户绑定,如果绑定直接返回绑定用户的Token,
    如果没有,返回重定向到前端绑定页面
    """
    code = self.get_auth_code_from_request(request)
    next_url = request.GET.get("next", '')
    config = self.get_config_by_id(config_id)
    frontend_host = (
        get_app_config()
        .get_frontend_host()
        .replace('http://', '')
        .replace('https://', '')
    )
    # if next_url and (
    #     "third_part_callback" not in next_url or frontend_host not in next_url
    # ):
    #     return JsonResponse({'error_msg': '错误的跳转页面'})
    if next_url and (
        frontend_host not in next_url
    ):
        return JsonResponse({'error_msg': '错误的跳转页面'})
    if code:
        try:
            ext_token = self.get_ext_token(config, code)
            ext_id, ext_name, ext_icon, ext_info = self.get_ext_user_info(
                config, code, ext_token
            )
        except Exception as e:
            logger.error(e)
            args = e.args
            if len(args) == 1 and isinstance(args[0], dict):
                return JsonResponse(e.args[0], json_dumps_params={'ensure_ascii': False})
            else:
                return JsonResponse({"error_msg": str(e)}, json_dumps_params={'ensure_ascii': False})
    else:
        return JsonResponse({"error_msg": "授权码丢失", "code": ["required"]}, json_dumps_params={'ensure_ascii': False})
    context = self.get_arkid_token(ext_id, ext_name, ext_icon, config)
    query_string = urlencode(context)
    if next_url:
        url = f"{next_url}?{query_string}"
        url = unquote(url)
        return HttpResponseRedirect(url)
    else:
        frontend_host = get_app_config().get_frontend_host()
        frontend_callback = f'{frontend_host}/third_part_callback'
        url = f"{frontend_callback}?{query_string}"
        url = unquote(url)
        return HttpResponseRedirect(url)
create_tenant_config(self, tenant, config, name, type)
#
    创建运行时配置
Parameters:
| Name | Type | Description | Default | 
|---|---|---|---|
| tenant | Tenant | 租户 | required | 
| config | dict | config | required | 
| name | str | 运行时配置名字 | required | 
| type | str | 配置类型 | required | 
Returns:
| Type | Description | 
|---|---|
| TenantExtensionConfig | 创建的对象 | 
Source code in arkid/core/extension/external_idp.py
          def create_tenant_config(self, tenant, config, name, type):
    config_created = super().create_tenant_config(tenant, config, name, type)
    server_host = get_app_config().get_host()
    login_url = server_host + reverse(
        f'api:{self.pname}:{self.pname}_login',
        args=[config_created.id],
    )
    callback_url = server_host + reverse(
        f'api:{self.pname}:{self.pname}_callback',
        args=[config_created.id],
    )
    bind_url = server_host + reverse(
        f'api:{self.pname}:{self.pname}_bind',
        args=[config_created.id],
    )
    # img_url = self.get_img_url()
    config["login_url"] = login_url
    config["callback_url"] = callback_url
    config["bind_url"] = bind_url
    # config["img_url"] = img_url
    config_created.config = config
    config_created.save()
    return config_created
get_arkid_user(self, ext_id)
#
    抽象方法
Parameters:
| Name | Type | Description | Default | 
|---|---|---|---|
| ext_id | str | 第三方认证返回的用户标识 | required | 
Returns:
| Type | Description | 
|---|---|
| arkid.core.models.User | ArkID用户 | 
get_auth_code_from_request(self, request)
#
    抽象方法
Parameters:
| Name | Type | Description | Default | 
|---|---|---|---|
| request | HTTPRequest | 第三方认证返回的用户标识 | required | 
Returns:
| Type | Description | 
|---|---|
| str | 授权码 | 
get_authorize_url(self, config, callback_url, next_url)
#
    抽象方法
Parameters:
| Name | Type | Description | Default | 
|---|---|---|---|
| config | arkid.extension.models.TenantExtensionConfig | 第三方认证提供的Client_ID, | required | 
| callback_url | str | 由ArkID提供的回调地址 | required | 
| next_url | str | 前端传来的跳转地址 | required | 
Returns:
| Type | Description | 
|---|---|
| str | 第三方登录提供的认证URL | 
Source code in arkid/core/extension/external_idp.py
          
        
get_ext_token(self, config, code)
#
    抽象方法
Parameters:
| Name | Type | Description | Default | 
|---|---|---|---|
| code | str | 第三方认证返回的code | required | 
| config | arkid.core.extension.TenantExtensionConfig | 第三方登录的插件运行时配置 | required | 
Returns:
| Type | Description | 
|---|---|
| str | 返回第三方认证提供的token | 
get_ext_user_info(self, config, code, token)
#
    抽象方法
Parameters:
| Name | Type | Description | Default | 
|---|---|---|---|
| config | arkid.core.extension.TenantExtensionConfig | 第三方登录的插件运行时配置 | required | 
| code | str | 第三方认证返回的code | required | 
| token | str | 第三方认证返回的token | required | 
Returns:
| Type | Description | 
|---|---|
| dict | 返回第三方认证提供的用户信息 | 
get_img_and_redirect_url(self, config)
#
    返回前端渲染第三方登录的按钮 抽象方法
Parameters:
| Name | Type | Description | Default | 
|---|---|---|---|
| config | arkid.extension.models.TenantExtensionConfig | 第三方认证提供的Client_ID, | required | 
Returns:
| Type | Description | 
|---|---|
| tuple | 返回图片url和跳转地址('image_url', 'redirect_url') | 
get_img_url(self)
#
    
  
load(self)
#
    抽象方法,插件加载的入口方法
Source code in arkid/core/extension/external_idp.py
          def load(self):
    urls = [
        re_path(
            rf'^idp/{self.pname}/(?P<config_id>[\w-]+)/login$',
            self.login,
            name=f'{self.pname}_login',
        ),
        re_path(
            rf'^idp/{self.pname}/(?P<config_id>[\w-]+)/callback$',
            self.callback,
            name=f'{self.pname}_callback',
        ),
        re_path(
            rf'^idp/{self.pname}/(?P<config_id>[\w-]+)/bind$',
            self.bind,
            name=f'{self.pname}_bind',
        ),
    ]
    self.register_routers(urls, False)
    self.listen_event(
        core_event.CREATE_LOGIN_PAGE_AUTH_FACTOR, self.add_idp_login_buttons
    )
    self.listen_event(
        core_event.ACCOUNT_UNBIND, self.account_unbind
    )
    super().load()
login(self, request, config_id)
#
    重定向到第三方登录的入口地址, 该入口地址由get_authorize_url提供
Source code in arkid/core/extension/external_idp.py
          def login(self, request, config_id):
    """
    重定向到第三方登录的入口地址, 该入口地址由get_authorize_url提供
    """
    config = self.get_config_by_id(config_id)
    if not config:
        return JsonResponse({"error_msg": "没有找到登录配置"})
    callback_url = config.config.get("callback_url")
    # callback_url = callback_url.replace(
    #     "localhost:8000", "xxxx.vaiwan.com"
    # )  # 内网穿透测试用
    next_url = request.GET.get("next", None)
    if next_url is not None:
        next_url = "?next=" + next_url
    else:
        next_url = ""
    url = self.get_authorize_url(config, callback_url, next_url)
    return HttpResponseRedirect(url)
update_tenant_config(self, id, config, name, type)
#
    更新运行时配置
Parameters:
| Name | Type | Description | Default | 
|---|---|---|---|
| id | str | config_id | required | 
| config | dict | config | required | 
| name | str | 运行时配置名字 | required | 
| type | str | 配置类型 | required | 
Returns:
| Type | Description | 
|---|---|
| bool | 更新成功True,没有找到该配置返回False | 
Source code in arkid/core/extension/external_idp.py
          def update_tenant_config(self, id, config, name, type):
    super().update_tenant_config(id, config, name, type)
    config_created = TenantExtensionConfig.valid_objects.filter(id=id).first()
    server_host = get_app_config().get_host()
    login_url = server_host + reverse(
        f'api:{self.pname}:{self.pname}_login',
        args=[config_created.id],
    )
    callback_url = server_host + reverse(
        f'api:{self.pname}:{self.pname}_callback',
        args=[config_created.id],
    )
    bind_url = server_host + reverse(
        f'api:{self.pname}:{self.pname}_bind',
        args=[config_created.id],
    )
    # img_url = self.get_img_url()
    config["login_url"] = login_url
    config["callback_url"] = callback_url
    config["bind_url"] = bind_url
    # config["img_url"] = img_url
    config_created.config = config
    config_created.save()
    return config_created
Exemplary#
        
extension_root.com_longgui_external_idp_github.ExternalIdpGithubExtension            (ExternalIdpExtension)
        
#
    Source code in extension_root/com_longgui_external_idp_github/__init__.py
          class ExternalIdpGithubExtension(ExternalIdpExtension):
    def load(self):
        super().load()
        self.register_extend_field(GithubUser, "github_user_id")
        self.register_extend_field(GithubUser, "github_nickname")
        self.register_extend_field(GithubUser, "github_avatar")
        self.register_external_idp_schema("github", GithubConfigSchema)
    def get_img_url(self):
        """
        返回Github的图标URL
        """
        return IMG_URL
    def get_auth_code_from_request(self, request):
        code = request.GET.get("code", '')
        return code
    def get_authorize_url(self, config, callback_url, next_url):
        """
        Args:
            config (arkid.core.extension.TenantExtensionConfig): 第三方登录的插件运行时配置
            redirect_uri (str): 在ArkID中创建Github登录配置后返回的回调地址
        Returns:
            str: 返回用于向Github发起认证的URL
        """
        redirect_uri = "{}{}".format(callback_url, next_url)
        redirect_uri = quote(redirect_uri)
        url = "{}?client_id={}&redirect_uri={}".format(
            AUTHORIZE_URL,
            config.config.get("client_id"),
            redirect_uri,
        )
        return url
    def get_ext_token(self, config, code):
        """
        Args:
            config (arkid.core.extension.TenantExtensionConfig): 第三方登录的插件运行时配置
            code (str): Github返回的授权码
        Returns:
            str: 返回Github返回的access_token
        """
        response = requests.post(
            GET_TOKEN_URL,
            params={
                "code": code,
                "client_id": config.config.get("client_id"),
                "client_secret": config.config.get("client_secret"),
                "grant_type": "authorization_code",
            },
        ).__getattribute__("_content")
        result = dict(
            [(k, v[0]) for k, v in parse_qs(response.decode()).items()]
        )  # 将响应信息转换为字典
        return result["access_token"]
    def get_ext_user_info(self, config, code, token):
        """
        Args:
            config (arkid.core.extension.TenantExtensionConfig): 第三方登录的插件运行时配置
            code (str): Github返回的授权码
            token (str): Github返回的access_token
        Returns:
            tuple: 返回Github中用户信息中的id, login,avatar_url和所有用户信息
        """
        headers = {"Authorization": "token " + token}
        response = requests.get(
            GET_USERINFO_URL,
            params={"access_token": token},
            headers=headers,
        ).json()
        return response["id"], response["login"], response["avatar_url"], response
    def get_arkid_user(self, ext_id):
        """
        Args:
            ext_id (str): 从Github用户信息接口获取的用户标识
        Returns:
            arkid.core.models.User: 返回ext_id绑定的ArkID用户
        """
        github_user = GithubUser.valid_objects.filter(github_user_id=ext_id).first()
        if github_user:
            return github_user.target
        else:
            return None
    def bind_arkid_user(self, ext_id, user, data):
        """
        Args:
            ext_id (str): 从Github用户信息接口获取的用户标识
            user (arkid.core.models.User): 用于绑定的ArkID用户
            data (dict) request数据
        """
        user.github_user_id = ext_id
        user.github_nickname = data.get('ext_name', '')
        user.github_avatar = data.get('ext_icon', '')
        user.save()
    def get_img_and_redirect_url(self, config):
        return config.config.get("img_url", ""), config.config.get("login_url", "")
bind_arkid_user(self, ext_id, user, data)
#
    Parameters:
| Name | Type | Description | Default | 
|---|---|---|---|
| ext_id | str | 从Github用户信息接口获取的用户标识 | required | 
| user | arkid.core.models.User | 用于绑定的ArkID用户 | required | 
Source code in extension_root/com_longgui_external_idp_github/__init__.py
          
        
get_arkid_user(self, ext_id)
#
    Parameters:
| Name | Type | Description | Default | 
|---|---|---|---|
| ext_id | str | 从Github用户信息接口获取的用户标识 | required | 
Returns:
| Type | Description | 
|---|---|
| arkid.core.models.User | 返回ext_id绑定的ArkID用户 | 
Source code in extension_root/com_longgui_external_idp_github/__init__.py
          
        
get_auth_code_from_request(self, request)
#
    
  
get_authorize_url(self, config, callback_url, next_url)
#
    Parameters:
| Name | Type | Description | Default | 
|---|---|---|---|
| config | arkid.core.extension.TenantExtensionConfig | 第三方登录的插件运行时配置 | required | 
| redirect_uri | str | 在ArkID中创建Github登录配置后返回的回调地址 | required | 
Returns:
| Type | Description | 
|---|---|
| str | 返回用于向Github发起认证的URL | 
Source code in extension_root/com_longgui_external_idp_github/__init__.py
          def get_authorize_url(self, config, callback_url, next_url):
    """
    Args:
        config (arkid.core.extension.TenantExtensionConfig): 第三方登录的插件运行时配置
        redirect_uri (str): 在ArkID中创建Github登录配置后返回的回调地址
    Returns:
        str: 返回用于向Github发起认证的URL
    """
    redirect_uri = "{}{}".format(callback_url, next_url)
    redirect_uri = quote(redirect_uri)
    url = "{}?client_id={}&redirect_uri={}".format(
        AUTHORIZE_URL,
        config.config.get("client_id"),
        redirect_uri,
    )
    return url
get_ext_token(self, config, code)
#
    Parameters:
| Name | Type | Description | Default | 
|---|---|---|---|
| config | arkid.core.extension.TenantExtensionConfig | 第三方登录的插件运行时配置 | required | 
| code | str | Github返回的授权码 | required | 
Returns:
| Type | Description | 
|---|---|
| str | 返回Github返回的access_token | 
Source code in extension_root/com_longgui_external_idp_github/__init__.py
          def get_ext_token(self, config, code):
    """
    Args:
        config (arkid.core.extension.TenantExtensionConfig): 第三方登录的插件运行时配置
        code (str): Github返回的授权码
    Returns:
        str: 返回Github返回的access_token
    """
    response = requests.post(
        GET_TOKEN_URL,
        params={
            "code": code,
            "client_id": config.config.get("client_id"),
            "client_secret": config.config.get("client_secret"),
            "grant_type": "authorization_code",
        },
    ).__getattribute__("_content")
    result = dict(
        [(k, v[0]) for k, v in parse_qs(response.decode()).items()]
    )  # 将响应信息转换为字典
    return result["access_token"]
get_ext_user_info(self, config, code, token)
#
    Parameters:
| Name | Type | Description | Default | 
|---|---|---|---|
| config | arkid.core.extension.TenantExtensionConfig | 第三方登录的插件运行时配置 | required | 
| code | str | Github返回的授权码 | required | 
| token | str | Github返回的access_token | required | 
Returns:
| Type | Description | 
|---|---|
| tuple | 返回Github中用户信息中的id, login,avatar_url和所有用户信息 | 
Source code in extension_root/com_longgui_external_idp_github/__init__.py
          def get_ext_user_info(self, config, code, token):
    """
    Args:
        config (arkid.core.extension.TenantExtensionConfig): 第三方登录的插件运行时配置
        code (str): Github返回的授权码
        token (str): Github返回的access_token
    Returns:
        tuple: 返回Github中用户信息中的id, login,avatar_url和所有用户信息
    """
    headers = {"Authorization": "token " + token}
    response = requests.get(
        GET_USERINFO_URL,
        params={"access_token": token},
        headers=headers,
    ).json()
    return response["id"], response["login"], response["avatar_url"], response
get_img_and_redirect_url(self, config)
#
    返回前端渲染第三方登录的按钮 抽象方法
Parameters:
| Name | Type | Description | Default | 
|---|---|---|---|
| config | arkid.extension.models.TenantExtensionConfig | 第三方认证提供的Client_ID, | required | 
Returns:
| Type | Description | 
|---|---|
| tuple | 返回图片url和跳转地址('image_url', 'redirect_url') | 
get_img_url(self)
#
    
  
load(self)
#
    抽象方法,插件加载的入口方法