feat(oidc): add advanced options (#3364)
* fix(oidc): params issue * fix: test * fix: exchangeBody * feat: add advanced settings * fix: locale * fix: bug * feat: allow to set state token manually
This commit is contained in:
parent
4d44c6e24e
commit
7d29c717eb
@ -1,5 +1,5 @@
|
||||
import { CopyOutlined } from '@ant-design/icons';
|
||||
import { ArrayItems } from '@formily/antd-v5';
|
||||
import { ArrayItems, FormTab } from '@formily/antd-v5';
|
||||
import { observer } from '@formily/react';
|
||||
import { FormItem, Input, SchemaComponent } from '@nocobase/client';
|
||||
import { Card, Space, message } from 'antd';
|
||||
@ -9,143 +9,6 @@ import { lang, useOidcTranslation } from './locale';
|
||||
const schema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
oidc: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
issuer: {
|
||||
type: 'string',
|
||||
title: '{{t("Issuer")}}',
|
||||
'x-component': 'Input',
|
||||
'x-decorator': 'FormItem',
|
||||
required: true,
|
||||
},
|
||||
clientId: {
|
||||
type: 'string',
|
||||
title: '{{t("Client ID")}}',
|
||||
'x-component': 'Input',
|
||||
'x-decorator': 'FormItem',
|
||||
required: true,
|
||||
},
|
||||
clientSecret: {
|
||||
type: 'string',
|
||||
title: '{{t("Client Secret")}}',
|
||||
'x-component': 'Input',
|
||||
'x-decorator': 'FormItem',
|
||||
required: true,
|
||||
},
|
||||
scope: {
|
||||
type: 'string',
|
||||
title: '{{t("scope")}}',
|
||||
'x-component': 'Input',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-decorator-props': {
|
||||
tooltip: '{{t("Default: openid profile email")}}',
|
||||
},
|
||||
},
|
||||
idTokenSignedResponseAlg: {
|
||||
type: 'string',
|
||||
title: '{{t("id_token signed response algorithm")}}',
|
||||
'x-component': 'Select',
|
||||
'x-decorator': 'FormItem',
|
||||
enum: [
|
||||
{ label: 'HS256', value: 'HS256' },
|
||||
{ label: 'HS384', value: 'HS384' },
|
||||
{ label: 'HS512', value: 'HS512' },
|
||||
{ label: 'RS256', value: 'RS256' },
|
||||
{ label: 'RS384', value: 'RS384' },
|
||||
{ label: 'RS512', value: 'RS512' },
|
||||
{ label: 'ES256', value: 'ES256' },
|
||||
{ label: 'ES384', value: 'ES384' },
|
||||
{ label: 'ES512', value: 'ES512' },
|
||||
{ label: 'PS256', value: 'PS256' },
|
||||
{ label: 'PS384', value: 'PS384' },
|
||||
{ label: 'PS512', value: 'PS512' },
|
||||
],
|
||||
},
|
||||
http: {
|
||||
type: 'boolean',
|
||||
title: '{{t("HTTP")}}',
|
||||
'x-component': 'Checkbox',
|
||||
'x-decorator': 'FormItem',
|
||||
},
|
||||
port: {
|
||||
type: 'number',
|
||||
title: '{{t("Port")}}',
|
||||
'x-component': 'InputNumber',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component-props': {
|
||||
style: {
|
||||
width: '15%',
|
||||
minWidth: '100px',
|
||||
},
|
||||
},
|
||||
},
|
||||
fieldMap: {
|
||||
title: '{{t("Field Map")}}',
|
||||
type: 'array',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'ArrayItems',
|
||||
items: {
|
||||
type: 'object',
|
||||
'x-decorator': 'ArrayItems.Item',
|
||||
properties: {
|
||||
space: {
|
||||
type: 'void',
|
||||
'x-component': 'Space',
|
||||
properties: {
|
||||
source: {
|
||||
type: 'string',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
'x-component-props': {
|
||||
placeholder: '{{t("source")}}',
|
||||
},
|
||||
},
|
||||
target: {
|
||||
type: 'string',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Select',
|
||||
'x-component-props': {
|
||||
placeholder: '{{t("target")}}',
|
||||
},
|
||||
enum: [
|
||||
{ label: lang('Nickname'), value: 'nickname' },
|
||||
{ label: lang('Email'), value: 'email' },
|
||||
{ label: lang('Phone'), value: 'phone' },
|
||||
{ label: lang('Username'), value: 'username' },
|
||||
],
|
||||
},
|
||||
remove: {
|
||||
type: 'void',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'ArrayItems.Remove',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
properties: {
|
||||
add: {
|
||||
type: 'void',
|
||||
title: 'Add',
|
||||
'x-component': 'ArrayItems.Addition',
|
||||
},
|
||||
},
|
||||
},
|
||||
userBindField: {
|
||||
type: 'string',
|
||||
title: '{{t("Use this field to bind the user")}}',
|
||||
'x-component': 'Select',
|
||||
'x-decorator': 'FormItem',
|
||||
default: 'email',
|
||||
enum: [
|
||||
{ label: lang('Email'), value: 'email' },
|
||||
{ label: lang('Username'), value: 'username' },
|
||||
],
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
public: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@ -158,6 +21,302 @@ const schema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
oidc: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
collapse: {
|
||||
type: 'void',
|
||||
'x-component': 'FormTab',
|
||||
properties: {
|
||||
basic: {
|
||||
type: 'void',
|
||||
'x-component': 'FormTab.TabPane',
|
||||
'x-component-props': {
|
||||
tab: lang('Basic configuration'),
|
||||
},
|
||||
properties: {
|
||||
issuer: {
|
||||
type: 'string',
|
||||
title: '{{t("Issuer")}}',
|
||||
'x-component': 'Input',
|
||||
'x-decorator': 'FormItem',
|
||||
required: true,
|
||||
},
|
||||
clientId: {
|
||||
type: 'string',
|
||||
title: '{{t("Client ID")}}',
|
||||
'x-component': 'Input',
|
||||
'x-decorator': 'FormItem',
|
||||
required: true,
|
||||
},
|
||||
clientSecret: {
|
||||
type: 'string',
|
||||
title: '{{t("Client Secret")}}',
|
||||
'x-component': 'Input',
|
||||
'x-decorator': 'FormItem',
|
||||
required: true,
|
||||
},
|
||||
scope: {
|
||||
type: 'string',
|
||||
title: '{{t("scope")}}',
|
||||
'x-component': 'Input',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-decorator-props': {
|
||||
tooltip: '{{t("Default: openid profile email")}}',
|
||||
},
|
||||
},
|
||||
idTokenSignedResponseAlg: {
|
||||
type: 'string',
|
||||
title: '{{t("id_token signed response algorithm")}}',
|
||||
'x-component': 'Select',
|
||||
'x-decorator': 'FormItem',
|
||||
enum: [
|
||||
{ label: 'HS256', value: 'HS256' },
|
||||
{ label: 'HS384', value: 'HS384' },
|
||||
{ label: 'HS512', value: 'HS512' },
|
||||
{ label: 'RS256', value: 'RS256' },
|
||||
{ label: 'RS384', value: 'RS384' },
|
||||
{ label: 'RS512', value: 'RS512' },
|
||||
{ label: 'ES256', value: 'ES256' },
|
||||
{ label: 'ES384', value: 'ES384' },
|
||||
{ label: 'ES512', value: 'ES512' },
|
||||
{ label: 'PS256', value: 'PS256' },
|
||||
{ label: 'PS384', value: 'PS384' },
|
||||
{ label: 'PS512', value: 'PS512' },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
mapping: {
|
||||
type: 'void',
|
||||
'x-component': 'FormTab.TabPane',
|
||||
'x-component-props': {
|
||||
tab: lang('Field mapping'),
|
||||
},
|
||||
properties: {
|
||||
fieldMap: {
|
||||
title: '{{t("Field Map")}}',
|
||||
type: 'array',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'ArrayItems',
|
||||
items: {
|
||||
type: 'object',
|
||||
'x-decorator': 'ArrayItems.Item',
|
||||
properties: {
|
||||
space: {
|
||||
type: 'void',
|
||||
'x-component': 'Space',
|
||||
properties: {
|
||||
source: {
|
||||
type: 'string',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
'x-component-props': {
|
||||
placeholder: '{{t("source")}}',
|
||||
},
|
||||
},
|
||||
target: {
|
||||
type: 'string',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Select',
|
||||
'x-component-props': {
|
||||
placeholder: '{{t("target")}}',
|
||||
},
|
||||
enum: [
|
||||
{ label: lang('Nickname'), value: 'nickname' },
|
||||
{ label: lang('Email'), value: 'email' },
|
||||
{ label: lang('Phone'), value: 'phone' },
|
||||
{ label: lang('Username'), value: 'username' },
|
||||
],
|
||||
},
|
||||
remove: {
|
||||
type: 'void',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'ArrayItems.Remove',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
properties: {
|
||||
add: {
|
||||
type: 'void',
|
||||
title: 'Add',
|
||||
'x-component': 'ArrayItems.Addition',
|
||||
},
|
||||
},
|
||||
},
|
||||
userBindField: {
|
||||
type: 'string',
|
||||
title: '{{t("Use this field to bind the user")}}',
|
||||
'x-component': 'Select',
|
||||
'x-decorator': 'FormItem',
|
||||
default: 'email',
|
||||
enum: [
|
||||
{ label: lang('Email'), value: 'email' },
|
||||
{ label: lang('Username'), value: 'username' },
|
||||
],
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
advanced: {
|
||||
type: 'void',
|
||||
'x-component': 'FormTab.TabPane',
|
||||
'x-component-props': {
|
||||
tab: lang('Advanced configuration'),
|
||||
},
|
||||
properties: {
|
||||
http: {
|
||||
type: 'boolean',
|
||||
title: '{{t("HTTP")}}',
|
||||
'x-component': 'Checkbox',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-decorator-props': {
|
||||
tooltip: '{{t("Check if NocoBase is running on HTTP protocol")}}',
|
||||
},
|
||||
},
|
||||
port: {
|
||||
type: 'number',
|
||||
title: '{{t("Port")}}',
|
||||
'x-component': 'InputNumber',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-decorator-props': {
|
||||
tooltip: '{{t("The port number of the NocoBase service if it is not 80 or 443")}}',
|
||||
},
|
||||
'x-component-props': {
|
||||
style: {
|
||||
width: '15%',
|
||||
minWidth: '100px',
|
||||
},
|
||||
},
|
||||
},
|
||||
stateToken: {
|
||||
type: 'string',
|
||||
title: '{{t("State token")}}',
|
||||
'x-component': 'Input',
|
||||
'x-decorator': 'FormItem',
|
||||
description: lang(
|
||||
"The state token helps prevent CSRF attacks. It's recommended to leave it blank for automatic random generation.",
|
||||
),
|
||||
},
|
||||
exchangeBodyKeys: {
|
||||
type: 'array',
|
||||
title: '{{t("Pass parameters in the authorization code grant exchange")}}',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'ArrayItems',
|
||||
default: [
|
||||
{ paramName: '', optionsKey: 'clientId' },
|
||||
{
|
||||
paramName: '',
|
||||
optionsKey: 'clientSecret',
|
||||
},
|
||||
],
|
||||
items: {
|
||||
type: 'object',
|
||||
'x-decorator': 'ArrayItems.Item',
|
||||
properties: {
|
||||
space: {
|
||||
type: 'void',
|
||||
'x-component': 'Space',
|
||||
properties: {
|
||||
enabled: {
|
||||
type: 'boolean',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Checkbox',
|
||||
},
|
||||
optionsKey: {
|
||||
type: 'string',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-decorator-props': {
|
||||
style: {
|
||||
width: '100px',
|
||||
},
|
||||
},
|
||||
'x-component': 'Select',
|
||||
'x-read-pretty': true,
|
||||
enum: [
|
||||
{ label: lang('Client ID'), value: 'clientId' },
|
||||
{ label: lang('Client Secret'), value: 'clientSecret' },
|
||||
],
|
||||
},
|
||||
paramName: {
|
||||
type: 'string',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
'x-component-props': {
|
||||
placeholder: '{{t("Parameter name")}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
userInfoMethod: {
|
||||
type: 'string',
|
||||
title: '{{t("Method to call the user info endpoint")}}',
|
||||
'x- decorator': 'FormItem',
|
||||
'x-component': 'Radio.Group',
|
||||
default: 'GET',
|
||||
enum: [
|
||||
{
|
||||
label: 'GET',
|
||||
value: 'GET',
|
||||
},
|
||||
{
|
||||
label: 'POST',
|
||||
value: 'POST',
|
||||
},
|
||||
],
|
||||
'x-reactions': [
|
||||
{
|
||||
dependencies: ['.accessTokenVia'],
|
||||
when: '{{$deps[0] === "query"}}',
|
||||
fulfill: {
|
||||
state: {
|
||||
value: 'GET',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
dependencies: ['.accessTokenVia'],
|
||||
when: '{{$deps[0] === "body"}}',
|
||||
fulfill: {
|
||||
state: {
|
||||
value: 'POST',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
accessTokenVia: {
|
||||
type: 'string',
|
||||
title: '{{t("Where to put the access token when calling the user info endpoint")}}',
|
||||
'x- decorator': 'FormItem',
|
||||
'x-component': 'Radio.Group',
|
||||
default: 'header',
|
||||
enum: [
|
||||
{
|
||||
label: lang('Header'),
|
||||
value: 'header',
|
||||
},
|
||||
{
|
||||
label: lang('Body (Use with POST method)'),
|
||||
value: 'body',
|
||||
},
|
||||
{
|
||||
label: lang('Query parameters (Use with GET method)'),
|
||||
value: 'query',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
usage: {
|
||||
type: 'void',
|
||||
'x-component': 'Usage',
|
||||
@ -187,5 +346,5 @@ const Usage = observer(() => {
|
||||
|
||||
export const Options = () => {
|
||||
const { t } = useOidcTranslation();
|
||||
return <SchemaComponent scope={{ t }} components={{ Usage, ArrayItems, Space }} schema={schema} />;
|
||||
return <SchemaComponent scope={{ t }} components={{ Usage, ArrayItems, Space, FormTab }} schema={schema} />;
|
||||
};
|
||||
|
@ -21,5 +21,20 @@
|
||||
"Use this field to bind the user": "Use this field to bind the user",
|
||||
"Sign up automatically when the user does not exist": "Sign up automatically when the user does not exist",
|
||||
"Username must be 2-16 characters in length (excluding @.<>\"'/)": "Username must be 2-16 characters in length (excluding @.<>\"'/)",
|
||||
"User not found": "User not found"
|
||||
"User not found": "User not found",
|
||||
"Basic configuration": "Basic configuration",
|
||||
"Field mapping": "Field mapping",
|
||||
"Advanced configuration": "Advanced configuration",
|
||||
"Usage": "Usage",
|
||||
"Redirect URL": "Redirect URL",
|
||||
"Check if NocoBase is running on HTTP protocol": "Check if NocoBase is running on HTTP protocol",
|
||||
"The port number of the NocoBase service if it is not 80 or 443": "The port number of the NocoBase service if it is not 80 or 443",
|
||||
"Pass parameters in the authorization code grant exchange": "Pass parameters in the authorization code grant exchange",
|
||||
"Method to call the user info endpoint": "Method to call the user info endpoint",
|
||||
"Where to put the access token when calling the user info endpoint": "Where to put the access token when calling the user info endpoint",
|
||||
"Header": "Header",
|
||||
"Body (Use with POST method)": "Body (Use with POST method)",
|
||||
"Query parameters (Use with GET method)": "Query parameters (Use with GET method)",
|
||||
"Parameter name": "Parameter name",
|
||||
"The state token helps prevent CSRF attacks. It's recommended to leave it blank for automatic random generation.": "The state token helps prevent CSRF attacks. It's recommended to leave it blank for automatic random generation."
|
||||
}
|
||||
|
@ -9,5 +9,20 @@
|
||||
"Use this field to bind the user": "使用此字段绑定用户",
|
||||
"Sign up automatically when the user does not exist": "用户不存在时自动注册",
|
||||
"Username must be 2-16 characters in length (excluding @.<>\"'/)": "用户名必须为2-16个字符并且不包含@.<>\"'/)",
|
||||
"User not found": "用户不存在"
|
||||
"User not found": "用户不存在",
|
||||
"Basic configuration": "基础配置",
|
||||
"Field mapping": "字段映射",
|
||||
"Advanced configuration": "高级配置",
|
||||
"Usage": "使用",
|
||||
"Redirect URL": "回调 URL",
|
||||
"Check if NocoBase is running on HTTP protocol": "NocoBase 应用为HTTP协议时勾选",
|
||||
"The port number of the NocoBase service if it is not 80 or 443": "NocoBase 应用端口,默认 443/80",
|
||||
"Pass parameters in the authorization code grant exchange": "使用 code 交换 token 时需要传递的参数",
|
||||
"Method to call the user info endpoint": "访问获取用户信息的 API 的 HTTP 方法",
|
||||
"Where to put the access token when calling the user info endpoint": "访问获取用户信息的 API 时 access_token 的传递方式",
|
||||
"Header": "请求头 (Header, 默认)",
|
||||
"Body (Use with POST method)": "请求体(Body, 配合 POST 方法使用)",
|
||||
"Query parameters (Use with GET method)": "请求 URL 参数(Query, 配合 GET 方法使用)",
|
||||
"Parameter name": "参数名",
|
||||
"The state token helps prevent CSRF attacks. It's recommended to leave it blank for automatic random generation.": "state token 用于防止 CSRF 攻击,建议留空使用自动生成的随机值。"
|
||||
}
|
||||
|
@ -52,7 +52,7 @@ describe('oidc', () => {
|
||||
} as any);
|
||||
const res = await agent.set('X-Authenticator', 'oidc-auth').resource('oidc').getAuthUrl();
|
||||
expect(res.body.data).toBeDefined();
|
||||
const search = new URLSearchParams(res.body.data);
|
||||
const search = new URLSearchParams(decodeURIComponent(res.body.data));
|
||||
expect(search.get('token')).toBeDefined();
|
||||
expect(search.get('name')).toBe('oidc-auth');
|
||||
expect(res.headers['set-cookie']).toBeDefined();
|
||||
|
@ -7,8 +7,8 @@ export const getAuthUrl = async (ctx: Context, next: Next) => {
|
||||
const app = ctx.app.name;
|
||||
const auth = ctx.auth as OIDCAuth;
|
||||
const client = await auth.createOIDCClient();
|
||||
const { scope } = auth.getOptions();
|
||||
const token = nanoid(15);
|
||||
const { scope, stateToken } = auth.getOptions();
|
||||
const token = stateToken || nanoid(15);
|
||||
ctx.cookies.set(cookieName, token, {
|
||||
httpOnly: true,
|
||||
domain: ctx.hostname,
|
||||
@ -17,7 +17,7 @@ export const getAuthUrl = async (ctx: Context, next: Next) => {
|
||||
response_type: 'code',
|
||||
scope: scope || 'openid email profile',
|
||||
redirect_uri: auth.getRedirectUri(),
|
||||
state: `token=${token}&name=${ctx.headers['x-authenticator']}&app=${app}`,
|
||||
state: encodeURIComponent(`token=${token}&name=${ctx.headers['x-authenticator']}&app=${app}`),
|
||||
});
|
||||
|
||||
return next();
|
||||
|
@ -6,7 +6,7 @@ export const redirect = async (ctx: Context, next: Next) => {
|
||||
const {
|
||||
params: { state },
|
||||
} = ctx.action;
|
||||
const search = new URLSearchParams(state);
|
||||
const search = new URLSearchParams(decodeURIComponent(state));
|
||||
const authenticator = search.get('name');
|
||||
const appName = search.get('app');
|
||||
let prefix = '';
|
||||
@ -21,6 +21,7 @@ export const redirect = async (ctx: Context, next: Next) => {
|
||||
const { token } = await auth.signIn();
|
||||
ctx.redirect(`${prefix}/admin?authenticator=${authenticator}&token=${token}`);
|
||||
} catch (error) {
|
||||
ctx.logger.error('OIDC auth error', { error });
|
||||
ctx.redirect(`${prefix}/signin?authenticator=${authenticator}&error=${error.message}`);
|
||||
}
|
||||
await next();
|
||||
|
@ -24,6 +24,22 @@ export class OIDCAuth extends BaseAuth {
|
||||
return this.options?.oidc || {};
|
||||
}
|
||||
|
||||
getExchangeBody() {
|
||||
const options = this.getOptions();
|
||||
const { exchangeBodyKeys } = options;
|
||||
if (!exchangeBodyKeys) {
|
||||
return {};
|
||||
}
|
||||
const body = {};
|
||||
exchangeBodyKeys
|
||||
.filter((item: { enabled: boolean }) => item.enabled)
|
||||
.forEach((item: { paramName: string; optionsKey: string }) => {
|
||||
const name = item.paramName || item.optionsKey;
|
||||
body[name] = options[item.optionsKey];
|
||||
});
|
||||
return body;
|
||||
}
|
||||
|
||||
mapField(userInfo: { [source: string]: any }) {
|
||||
const { fieldMap } = this.getOptions();
|
||||
if (!fieldMap) {
|
||||
@ -51,18 +67,33 @@ export class OIDCAuth extends BaseAuth {
|
||||
async validate() {
|
||||
const ctx = this.ctx;
|
||||
const { params: values } = ctx.action;
|
||||
const token = ctx.cookies.get(cookieName);
|
||||
const search = new URLSearchParams(values.state);
|
||||
const { userInfoMethod = 'GET', accessTokenVia = 'header', stateToken } = this.getOptions();
|
||||
const token = stateToken || ctx.cookies.get(cookieName);
|
||||
const search = new URLSearchParams(decodeURIComponent(values.state));
|
||||
if (search.get('token') !== token) {
|
||||
ctx.logger.error('nocobase_oidc state mismatch', { method: 'validate' });
|
||||
return null;
|
||||
}
|
||||
const client = await this.createOIDCClient();
|
||||
const tokens = await client.callback(this.getRedirectUri(), {
|
||||
code: values.code,
|
||||
iss: values.iss,
|
||||
const tokens = await client.callback(
|
||||
this.getRedirectUri(),
|
||||
{
|
||||
code: values.code,
|
||||
iss: values.iss,
|
||||
},
|
||||
{},
|
||||
{ exchangeBody: this.getExchangeBody() },
|
||||
);
|
||||
const userInfo: { [key: string]: any } = await client.userinfo(tokens, {
|
||||
method: userInfoMethod,
|
||||
via: accessTokenVia !== 'query' ? accessTokenVia : 'header',
|
||||
params:
|
||||
accessTokenVia === 'query'
|
||||
? {
|
||||
access_token: tokens.access_token,
|
||||
}
|
||||
: {},
|
||||
});
|
||||
const userInfo: { [key: string]: any } = await client.userinfo(tokens.access_token);
|
||||
const mappedUserInfo = this.mapField(userInfo);
|
||||
const { nickname, username, name, sub, email, phone } = mappedUserInfo;
|
||||
const authenticator = this.authenticator as AuthModel;
|
||||
|
Loading…
Reference in New Issue
Block a user