You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tailchat/server/services/openapi/oidc/adapter.ts

184 lines
4.5 KiB
TypeScript

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import type { Adapter, AdapterPayload } from 'oidc-provider';
import { config } from 'tailchat-server-sdk';
import RedisClient from 'ioredis';
import _ from 'lodash';
import { OpenApp } from './model';
const client = new RedisClient(config.redisUrl, {
keyPrefix: 'tailchat:oidc:',
});
const grantable = new Set([
'AccessToken',
'AuthorizationCode',
'RefreshToken',
'DeviceCode',
'BackchannelAuthenticationRequest',
]);
const consumable = new Set([
'AuthorizationCode',
'RefreshToken',
'DeviceCode',
'BackchannelAuthenticationRequest',
]);
function grantKeyFor(id) {
return `grant:${id}`;
}
function userCodeKeyFor(userCode) {
return `userCode:${userCode}`;
}
function uidKeyFor(uid) {
return `uid:${uid}`;
}
function parseImageUrl(input: string | undefined) {
if (typeof input === 'string') {
return input.replace('{BACKEND}', config.apiUrl); // 因为/open接口是在服务端的所以该标识直接移除即可
}
return input;
}
/**
* Reference: https://github.com/panva/node-oidc-provider/blob/main/example/my_adapter.js
*/
export class TcOIDCAdapter implements Adapter {
constructor(public name: string) {}
async upsert(
id: string,
payload: AdapterPayload,
expiresIn: number
): Promise<undefined | void> {
const key = this.key(id);
const multi = client.multi();
if (consumable.has(this.name)) {
multi['hmset'](key, { payload: JSON.stringify(payload) });
} else {
multi['set'](key, JSON.stringify(payload));
}
if (expiresIn) {
multi.expire(key, expiresIn);
}
if (grantable.has(this.name) && payload.grantId) {
const grantKey = grantKeyFor(payload.grantId);
multi.rpush(grantKey, key);
// if you're seeing grant key lists growing out of acceptable proportions consider using LTRIM
// here to trim the list to an appropriate length
const ttl = await client.ttl(grantKey);
if (expiresIn > ttl) {
multi.expire(grantKey, expiresIn);
}
}
if (payload.userCode) {
const userCodeKey = userCodeKeyFor(payload.userCode);
multi.set(userCodeKey, id);
multi.expire(userCodeKey, expiresIn);
}
if (payload.uid) {
const uidKey = uidKeyFor(payload.uid);
multi.set(uidKey, id);
multi.expire(uidKey, expiresIn);
}
await multi.exec();
}
async find(id: string): Promise<AdapterPayload | undefined | void> {
if (this.name === 'Client') {
return this.findClient(id);
}
const data = consumable.has(this.name)
? await client.hgetall(this.key(id))
: await client.get(this.key(id));
if (_.isEmpty(data)) {
return undefined;
}
if (typeof data === 'string') {
return JSON.parse(data);
}
const { payload, ...rest } = data;
return {
...rest,
...JSON.parse(payload),
};
}
async findByUid(uid: string): Promise<AdapterPayload | undefined | void> {
const id = await client.get(uidKeyFor(uid));
return this.find(id);
}
async findByUserCode(
userCode: string
): Promise<AdapterPayload | undefined | void> {
const id = await client.get(userCodeKeyFor(userCode));
return this.find(id);
}
async destroy(id: string): Promise<undefined | void> {
const key = this.key(id);
await client.del(key);
}
async revokeByGrantId(grantId: string): Promise<undefined | void> {
// eslint-disable-line class-methods-use-this
const multi = client.multi();
const tokens = await client.lrange(grantKeyFor(grantId), 0, -1);
tokens.forEach((token) => multi.del(token));
multi.del(grantKeyFor(grantId));
await multi.exec();
}
async consume(id: string) {
await client.hset(this.key(id), 'consumed', Math.floor(Date.now() / 1000));
}
/**
* 查询客户端
*/
private async findClient(clientId: string): Promise<AdapterPayload | void> {
const app = await OpenApp.findOne({
appId: clientId,
}).exec();
if (!app) {
return;
}
if (!app.capability.includes('oauth')) {
return;
}
const clientPayload: AdapterPayload = {
client_id: app.appId,
client_secret: app.appSecret,
client_name: app.appName,
application_type: 'web',
grant_types: ['refresh_token', 'authorization_code'],
redirect_uris: [...(app.oauth?.redirectUrls ?? [])],
};
if (app.appIcon) {
clientPayload.logo_uri = parseImageUrl(app.appIcon);
}
return clientPayload;
}
key(id: string) {
return `${this.name}:${id}`;
}
}